如何用Java Socket實現一個簡單的Redis客戶端

卡卡西村長發表於2021-05-26

Redis是最常見的快取服務中介軟體,在java開發中,一般使用 jedis 來實現。

如果不想依賴第三方元件,自己實現一個簡單的redis客戶端工具,該如何實現呢?本文就是介紹這樣一種方法。

 

Redis的協議非常簡單,而且輸入資料和輸出資料都遵循統一的協議,具體規則參考這裡:

http://redisdoc.com/topic/protocol.html

 

Redis的命令協議

$引數數量n

$引數1的值的位元組陣列長度

$引數1的值的字串表示

$引數2的值的位元組陣列長度

$引數2的值的字串表示

...

$引數n的值的位元組陣列長度

$引數n的值的字串表示

 

Redis的返回協議

1、狀態回覆(status reply)的第一個位元組是 "+",單行字串;
2、錯誤回覆(error reply)的第一個位元組是 "-";
3、整數回覆(integer reply)的第一個位元組是 ":";
4、批次回復(bulk reply)的第一個位元組是 "$";
5、多條批次回復(multi bulk reply)的第一個位元組是 "*";
6、所有的命令都是以 \r\n 結尾。

 

Java程式碼說明

 

針對上述規則,我們用兩個類來實現:

1、SimpleRedisClient類,主要用於傳送請求,並讀取響應結果(字串);

    整體比較簡單,稍微複雜點的地方就是讀取流資料,遇到兩種情況就該結束迴圈,一是返回長度為-1,二是返回字串以 \r\n 結尾。

    如果處理不當,可能會導致 read 阻塞,Socket卡住。

2、SimpleRedisData類,用於解析響應結果,把redis統一協議的字串,解析為具體的物件。

    這部分程式碼完全是按照協議規則來實現的,透過一個遊標 pos 來向前移動,在移動過程中識別不同格式的資料。

    最複雜的是 list 型別的資料,以 * 開頭,後面跟著一個整數,表示列表中所有元素的數量,然後就是每一個列表元素的值,迴圈解析即可。

 

package demo;

import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.util.List;

public class SimpleRedisClient implements Closeable {

    private String host;
    private int port;
    private String auth;
    private Socket socket = null;

    public SimpleRedisClient(String host, int port, String auth) {
        this.host = host;
        this.port = port;
        this.auth = auth;

        try {
            socket = new Socket(this.host, this.port);
            socket.setSoTimeout(8 * 1000);//8秒
        } catch (Exception ex) {
            socket = null;
            ex.printStackTrace();
        }
    }

    public boolean connect() throws IOException {
        if (socket == null || auth == null || auth.length() <= 0) {
            return false;
        }
        String response = execute("AUTH", auth);
        if (response == null || response.length() <= 0) {
            return false;
        }
        String res = new SimpleRedisData(response).getString();
        return "OK".compareTo(res) == 0;
    }

    @Override
    public void close()  {
        try {
            if (socket != null) {
                socket.shutdownOutput();
                socket.close();
            }
            //System.out.println("closed");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public String getString(String key) {
        if (socket == null || key == null || key.isEmpty()) {
            return null;
        }
        try {
            String response = execute("GET", key);
            return new SimpleRedisData(response).getString();
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public String setString(String key, String value) {
        if (socket == null || key == null || key.isEmpty()) {
            return null;
        }
        try {
            String response = execute("SET", key, value);
            return new SimpleRedisData(response).getString();
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public String deleteKey(String key) throws IOException {
        if (socket == null || key == null || key.isEmpty()) {
            return null;
        }
        String response = execute("DEL", key);
        return new SimpleRedisData(response).getString();
    }

    public List<String> getKeys(String pattern) throws IOException {
        if (socket == null || pattern == null || pattern.isEmpty()) {
            return null;
        }

        String response = execute("KEYS", pattern);
        return new SimpleRedisData(response).getStringList();
    }

    public String execute(String... args) throws IOException {
        if (socket == null || args == null || args.length <= 0) {
            return null;
        }

        //System.out.println(StringUtil.join(args, " "));

        StringBuilder request = new StringBuilder();
        request.append("*" + args.length).append("\r\n");//引數的數量

        for (int i = 0; i < args.length; i++) {
            request.append("$" + args[i].getBytes("utf8").length).append("\r\n");//引數的長度
            request.append(args[i]).append("\r\n");//引數的內容
        }

        socket.getOutputStream().write(request.toString().getBytes());
        socket.getOutputStream().flush();

        StringBuilder reply = new StringBuilder();
        int bufSize = 1024;
        while (true) {
            byte[] buf = new byte[bufSize];
            int len = socket.getInputStream().read(buf);
            if (len < 0) {
                break;
            }
            String str = new String(buf, 0, len);
            reply.append(str);
            if (str.endsWith("\r\n")) {
                break;
            }
        }

        String response = reply.toString();
        //System.out.println("response: " + response);
        return response;
    }


}

 

package demo;

import java.util.ArrayList;
import java.util.List;

public class SimpleRedisData {

    public SimpleRedisData(String rawData) {
        this.rawData = rawData;
        //System.out.println(rawData);
    }

    private int pos;
    private String rawData;

    public String getString() {
        if (rawData == null || rawData.length() <= 0) {
            return null;
        }
        int i = rawData.indexOf("\r\n", pos);
        if (i <= 0) {
            return null;
        }
        char c = rawData.charAt(pos);
        if (c == '+') {
            int from = pos + 1;
            int to = i;
            String v = rawData.substring(from, to);
            pos = to + 2;
            return v;
        } else if (c == '-') {
            int from = pos + 1;
            int to = i;
            String v = rawData.substring(from, to);
            pos = to + 2;
            return v;
        } else if (c == ':') {
            int from = pos + 1;
            int to = i;
            String v = rawData.substring(from, to);
            pos = to + 2;
            return v;
        } else if (c == '$') {
            int from = pos + 1;
            int to = i;
            int bulkSize = Integer.parseInt(rawData.substring(from, to));
            pos = to + 2;

            from = pos;
            to = pos + bulkSize;
            try {
                //$符號後面的數值是指內容的位元組長度,而不是字元數量,所以要轉換為二進位制位元組陣列,再取指定長度的資料
                byte[] buf = rawData.substring(from).getBytes("utf-8");
                String v = new String(buf, 0, bulkSize);
                pos = to + 2;
                return v;
            } catch (Exception ex) {
                ex.printStackTrace();
                return null;
            }
        } else {
            return null;
        }
    }

    public List<String> getStringList() {
        if (rawData == null || rawData.length() <= 0) {
            return null;
        }
        int i = rawData.indexOf("\r\n", pos);
        if (i <= 0) {
            return null;
        }
        char c = rawData.charAt(pos);
        if (c == '*') {
            List<String> values = new ArrayList<>();
            int from = pos + 1;
            int to = i;
            int multSize = Integer.parseInt(rawData.substring(from, to));
            pos = to + 2;
            for (int index = 0; index < multSize; index++) {
                values.add(getString());
            }
            return values;
        } else {
            return null;
        }
    }

}

 

package demo;

import org.junit.jupiter.api.Test;

import java.util.List;

public class RedisTest {

    @Test
    public void test() {
        SimpleRedisClient client = null;
        try {
            client = new SimpleRedisClient("127.0.0.1", 6379, "123456");
            System.out.println("connected: " + client.connect());

            List<String> keyList = client.getKeys("api_*");

            for (int i = 0; i < keyList.size(); i++) {
                System.out.println((i + 1) + "\t" + keyList.get(i));
            }

           System.out.println("keys: " + keyList != null ? keyList.size() : "null");

           System.out.println(client.getString("api_getCustomerName"));

        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            if (client != null) {
                client.close();
            }
        }
    }

}

 

優點:

1、不依賴任何第三方元件,可以順利編譯透過;

2、程式碼極其簡單。

 

不足之處:

1、未考慮併發訪問;

2、未提供更多的資料型別,以及讀寫方法,大家可以在此基礎上包裝一下。

 

相關文章