乐趣区

关于redis:Redis我是如何与客户端进行通信的

江湖上说,天下文治,无坚不摧,唯快不破,这句话几乎是为我量身定制。

我是一个 Redis 服务,最引以为傲的就是我的速度,我的 QPS 能达到 10 万级别。

在我的手下有数不清的小弟,他们会时不时到我这来寄存或者取走一些数据,我管他们叫做客户端,还给他们起了英文名叫 Redis-client。

有时候一个小弟会来的十分频繁,有时候一堆小弟会同时过去,然而,即便再多的小弟我也能治理的东倒西歪。

有一天,小弟们问我。

想当年,为了不让小弟们拖垮我傲人的速度,在设计和他们的通信协议时,我搜索枯肠,制订了上面的三条准则:

  • 实现简略
  • 针对计算机来说,解析速度快
  • 针对人类来说,可读性强

为什么这么设计呢?先来看看一条指令收回的过程,首先在客户端须要对指令操作进行封装,应用网络进行传输,最初在服务端进行相应的解析、执行。

这一过程如果设计成一种非常复杂的协定,那么封装、解析、传输的过程都将十分耗时,无疑会升高我的速度。什么,你问我为什么要遵循最初一条规定?算是对于程序员们的馈赠吧,我真是太凶恶了。

我把发明进去的这种协定称为 RESP (REdis Serialization Protocol)协定,它工作在 TCP 协定的下层,作为我和客户端之间进行通信的规范模式。

说到这,我曾经有点急不可待想让你们看看我设计进去的杰作了,但我好歹也是个大哥,得摆点架子,不能我被动拿来给你们看。

所以我倡议你间接应用客户端收回一条向服务器的命令,而后取出这条命令对应的报文来直观的看一下。话虽如此,不过我曾经被封装的很严实了,失常状况下你是看不到我外部进行通信的具体报文的,所以,你能够 假装 成一个 Redis 的服务端,来截获小弟们发给我的音讯。

实现起来也很简略,我和小弟之间是基于 Socket 进行通信,所以在本地先启动一个ServerSocket,用来监听 Redis 服务的 6379 端口:

public static void server() throws IOException {ServerSocket serverSocket = new ServerSocket(6379);
    Socket socket = serverSocket.accept();
    byte[] bytes = new byte[1024];
    InputStream input = socket.getInputStream();
    while(input.read(bytes)!=0){System.out.println(new String(bytes));
    }
}

而后启动 redis-cli 客户端,发送一条命令:

set key1 value1

这时,假装的服务端就会收到报文了,在控制台打印了:

*3
$3
set
$4
key1
$6
value1

看到这里,模摸糊糊看到了方才输出的几个关键字,然而还有一些其余的字符,要怎么解释呢,是时候让我对协定报文中的格局进行一下揭秘了。

我对小弟们说了,对大哥谈话的时候得按规矩来,这样吧,你们在申请的时候要遵循上面的规定:

*< 参数数量 > CRLF
$< 参数 1 的字节长度 > CRLF
< 参数 1 的数据 > CRLF
$< 参数 2 的字节长度 > CRLF
< 参数 2 的数据 > CRLF
...
$< 参数 N 的字节长度 > CRLF
< 参数 N 的数据 > CRLF

首先解释一下每行开端的CRLF,转换成程序语言就是\r\n,也就是回车加换行。看到这里,你也就可能明确为什么控制台打印出的指令是竖向排列了吧。

在命令的解析过程中,setkey1value1会被认为是 3 个参数,因而参数数量为 3,对应第一行的*3

第一个参数set,长度为 3 对应$3;第二个参数key1,长度为 4 对应$4;第三个参数value1,长度为 6 对应$6。在每个参数长度的下一行对应真正的参数数据。

看到这,一条指令被转换为协定报文的过程是不是就很好了解了?

当小弟对我发送完申请后,作为大哥,我就要对小弟的申请进行 指令回复 了,而且我得依据回复内容进行一下分类,要不然小弟该搞不清我的批示了。

简略字符串

简略字符串回复只有一行回复,回复的内容以 + 作为结尾,不容许换行,并以 \r\n 完结。有很多指令在执行胜利后只会回复一个OK,应用的就是这种格局,可能无效的将传输、解析的开销降到最低。

谬误回复

在 RESP 协定中,谬误回复能够当做简略字符串回复的变种模式,它们之间的格局也十分相似,区别只有第一个字符是以 - 作为结尾,谬误回复的内容通常是谬误类型及对谬误形容的字符串。

谬误回复呈现在一些异样的场景,例如当发送了谬误的指令、操作数的数量不对时,都会进行谬误回复。在客户端收到谬误回复后,会将它与简略字符串回复进行辨别,视为异样。

整数回复

整数回复的利用也十分宽泛,它以 : 作为结尾,以 \r\n 完结,用于返回一个整数。例如当执行 incr 后返回自增后的值,执行 llen 返回数组的长度,或者应用 exists 命令返回的 0 或 1 作为判断一个 key 是否存在的根据,这些都应用了整数回复。

批量回复

批量回复,就是多行字符串的回复。它以 $ 作为结尾,前面是发送的字节长度,而后是 \r\n,而后发送理论的数据,最终以\r\n 完结。如果要回复的数据不存在,那么回复长度为 -1。

多条批量回复

当服务端要返回多个值时,例如返回一些元素的汇合时,就会应用多条批量回复。它以 * 作为结尾,前面是返回元素的个数,之后再追随多个下面讲到过的批量回复。

到这里,基本上我和小弟之间的通信协定就介绍完了。方才你尝试了伪装成一个服务端,这会再来试一试间接写一个客户端来间接和我进行交互吧。

private static void client() throws IOException {
    String CRLF="\r\n";

    Socket socket=new Socket("localhost", 6379);
    try (OutputStream out = socket.getOutputStream()) {StringBuffer sb=new StringBuffer();
        sb.append("*3").append(CRLF)
                .append("$3").append(CRLF).append("set").append(CRLF)
                .append("$4").append(CRLF).append("key1").append(CRLF)
                .append("$6").append(CRLF).append("value1").append(CRLF);
        out.write(sb.toString().getBytes());
        out.flush();

        try (InputStream inputStream = socket.getInputStream()) {byte[] buff = new byte[1024];
            int len = inputStream.read(buff);
            if (len > 0) {String ret = new String(buff, 0, len);
                System.out.println("Recv:" + ret);
            }
        }
    }
}

运行下面的代码,控制台输入:

Recv:+OK

下面模拟了客户端收回 set 命令的过程,并收到了回复。依此类推,你也能够本人封装其余的命令,来实现一个本人的 Redis 客户端来和我进行通信。

不过记住,要叫我大哥。


如果文章对您有所帮忙,欢送关注公众号 码农参上

退出移动版