Java - 简单理解Redis客户端连接工具

Itachi 2019年12月16日 149次浏览

手写一个简单的Redis连接工具

既然要连接Redis服务端,那么我们就要先知道客户端与redis服务端的通信协议protocol是怎么约定的.

通过查阅redis官方相关资料后发现 

文章所涉及的代码:

https://gitee.com/isidea/example-redis-client.git

  • 网络层

    1. 客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
    2. 客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。
  • 新版统一请求协议

    新版统一请求协议在 Redis 1.2 版本中引入, 并最终在 Redis 2.0 版本成为 Redis 服务器通信的标准方式。

    你的 Redis 客户端应该按照这个新版协议来进行实现。

    在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。

    以下是这个协议的一般形式:

    *<参数数量> CR LF
    $<参数 1 的字节数量> CR LF
    <参数 1 的数据> CR LF
    ...
    $<参数 N 的字节数量> CR LF
    <参数 N 的数据> CR LF
    

    命令本身也作为协议的其中一个参数来发送。

    举个例子: set "mykey" "myvalue" 以下是这个命令协议的打印版本:

    *3
    $3
    SET
    $5
    mykey
    $7
    myvalue
    

    简单分析一下:

    *3 : 参数数量 SET命令本身是一个参数 mykey 是一个参数 myvalue也是一个参数一共3个参数.所以就是3

    $3 : 代表你这个参数的字节数量 看 $3 下面的SET一共是3个字符所以就是$3$5下面的mykey数一下一共是5个字符所以也就是$5.要注意,每行结束后后面都会跟一个\r\n换行

    这个命令的实际协议值如下:

    "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
    

代码结构

. |____test | |____java |____main | |____resources | |____java | | |____com | | | |____example | | | | |____redis | | | | | |____client | | | | | | |____Protocol.java | | | | | | |____Command.java | | | | | | |____RClient.java | | | | | | |____Test.java | | | | | | |____RSocket.java

时序调用图

image.png

理论了解完后,开始上代码

最终结果图

image.png

image.png

既然是网络连接,必然离不开Socket

/**
 * 连接通讯层
 *
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-16 10:05
 */
public class RSocket {

    private Socket socket;

    private InputStream inputStream;

    private OutputStream outputStream;

    @SneakyThrows
    public RSocket(String host, Integer port) {
        socket = new Socket(host, port);
        inputStream = socket.getInputStream();
        outputStream = socket.getOutputStream();
    }

    /**
     * 向Redis服务发送命令
     * str:     "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
     */
    @SneakyThrows
    public void send(String str) {
        outputStream.write(str.getBytes(Charset.defaultCharset()));
    }

    /**
     * 读取Redis返回的数据
     */
    @SneakyThrows
    public String read() {
        byte[] bytes = new byte[1024];
        int read = inputStream.read(bytes);
        return new String(bytes, 0, read, Charset.defaultCharset());
    }

}

@SneakyThrows : 只是lombook的一个注解,我是为了代码里不必要写太多的try(){}catch{}不美观加一个注解而已.

构造器内通过TCP连接到redis服务端

这段代码内主要的还是send方法,参数怎么来???

既然要连接redis那么就要按照redis定义的通信协议标准来,那我我们就去定义一下这个标准

/**
 * Redis协议 @link http://doc.redisfans.com/topic/protocol.html
 *
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-16 10:22
 */
public class Protocol {

    static final String LINE = "\r\n";

    static final String STRINGLINGTH = "$";

    static final String STAR = "*";

    public static String reversion(Command command,byte[]... bytes){
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(STAR).append(1 + bytes.length).append(LINE);
        stringBuffer.append(STRINGLINGTH).append(command.toString().length()).append(LINE);
        stringBuffer.append(command.toString()).append(LINE);
        for (byte[] aByte : bytes) {
            stringBuffer.append(STRINGLINGTH).append(aByte.length).append(LINE);
            stringBuffer.append(new String(aByte, Charset.defaultCharset())).append(LINE);
        }
        return stringBuffer.toString();
    }

}
/**
 * 命令类型
 *
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-16 10:19
 */
public enum Command {

    GET,
    SET,
    INCR,
    ;
}

标准定义完毕后编写API层

/**
 * API层
 *
 * @author Itachi is.xianglei@gmail.com
 * @Date 2019-12-16 00:34
 */
public class RClient {

    private RSocket socket;

    public RClient(String host, Integer port) {
        this.socket = new RSocket(host, port);
    }


    public String set(String key,String value){
        String reversion = Protocol.reversion(Command.SET, key.getBytes(),value.getBytes());
        socket.send(reversion);
        return socket.read();
    }

    public String get(String key){
        socket.send(Protocol.reversion(Command.GET,key.getBytes()));
        return socket.read();
    }


}

测试

image.png

image.png

为什么会返回 +OK 呢?

这是因为Redis 命令会返回多种不同类型的回复

通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:

  • 状态回复(status reply)的第一个字节是 "+"
  • 错误回复(error reply)的第一个字节是 "-"
  • 整数回复(integer reply)的第一个字节是 ":"
  • 批量回复(bulk reply)的第一个字节是 "$"
  • 多条批量回复(multi bulk reply)的第一个字节是 "*"

客户端库应该返回 "+" 号之后的所有内容。 比如在在上面的这个例子中, 客户端就应该返回字符串 "OK"

状态回复通常由那些不需要返回数据的命令返回,这种回复不是二进制安全的,它也不能包含新行。

状态回复的额外开销非常少,只需要三个字节(开头的 "+" 和结尾的 CRLF)。

image.png