Redis客户端和服务端交互是通过tcp协议,在通讯的报文格式使用的是RESP协议规范,也就是意味只要和Redis服务端建立Scoket连接,通过RESP报文格式传输数据就可以实现Redis客户端和服务端的交互。看起来是很简单的,但是实际上的确是这么简单,RESP报文格式的可读性也是很高的。
1. RESP协议介绍
1.1 RESP协议特点
RESP是Redis通讯的协议规范,有以下几种特点:
- 简单的实现,人工也就可以写的出来
- 快速的被计算机解析
- 简单的可以被人工解析
- 基于网络层,Redis在tcp端口6379(默认)上监听到来的连接(本质是Socket),客户端连接到来时,Redis服务器为此建立一个tcp连接。
1.2 RESP格式规范
RESP中涉及到主要的两个符号,分别是*
和$
,其中*
表示此报文里面有几个$
符,准确的说是几组。$
表示本组数据所占的字符数。文字干巴巴,直接看例子:
*3
$3
SET
$3
key
$5
joker
- 报文总共有三组
$
数据组成,所以开头的*
标明的值是3 - 第一组数据是
SET
,占用3个字符,所以$
标明的值为3,下面的两组以此类推
为了可阅读性上面写成是一列,但是实际上他们是组成一个字符串发送,需要注意的是,每一行都是独立的一行,需要在字符串中加入\r\n
换行才行,压缩后如下:
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\njoker\r\n
注意压缩成字符串后末尾也是需要\r\n
的。(Redis的AOF持久化文件中也是这样保存的)
了解RESP协议规范的通讯以及报文格式,接下来就可以根据这些知识来写一个属于自己的Redis客户端啦。
2. 手写Redis客户端
2.1 Jedis源码是怎么实现的
手写客户端代码其实不是自创的,在Jedis中就有的,先看一下Jedis内部的实现源码:
protected Connection sendCommand(Protocol.Command cmd, byte[]... args) {
try {
this.connect();//建立Socket连接
Protocol.sendCommand(this.outputStream, cmd, args);//封装报文并将报文写入流中
++this.pipelinedCommands;
return this;
} catch (JedisConnectionException var6) {
JedisConnectionException ex = var6;
try {
String errorMessage = Protocol.readErrorLineIfPossible(this.inputStream);
if (errorMessage != null && errorMessage.length() > 0) {
ex = new JedisConnectionException(errorMessage, ex.getCause());
}
} catch (Exception var5) {
}
this.broken = true;
throw ex;
}
}
这段源码并不难找,使用Jedis的set方法,然后一直跟进去就可以。最终方法的位置是redis.clients.jedis.Connection.sebdCommand()
。
从这个方法的内部实现就可以看出来其实就是通过Socket建立tcp连接,然后将命令和数据转换成RESP协议规范的报文格式,最后通过Socket将数据传入过去。知道这些对于自己写一个Jedis客户端是不是就有思路啦。
2.2 自己实现一个
基于对源码的借鉴,简易的Jedis实现如下:
public class CustomJedis {
public static void main(String[] args) throws IOException {
//建立socket连接
Socket socket = new Socket();
InetSocketAddress socketAddress = new InetSocketAddress("106.12.75.86", 6379);
socket.connect(socketAddress, 10000);
//获取scoket输出流,将报文转换成byte[]传入流中
OutputStream outputStream = socket.getOutputStream();
outputStream.write(command());
//获取返回的输出流,并打印输出数据
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
inputStream.read(buffer);
System.out.println("返回执行结果:" + new String(buffer));
}
//组装报文信息
private static byte[] command() {
return "*3\r\n$3\r\nSET\r\n$9\r\nuser:name\r\n$6\r\nitcrud\r\n".getBytes();
}
}
但是这里需要注意,上面的实现方式是直接建立socket连接,Redis很多时候是设置密码认证的,如果这样的话上面的代码就需要改动啦。
改动后如下:
public class CustomJedis {
public static void main(String[] args) throws IOException {
Socket socket = new Socket();
InetSocketAddress socketAddress = new InetSocketAddress("106.12.75.86", 6379);
socket.connect(socketAddress, 10000);
OutputStream outputStream = socket.getOutputStream();
//验证密码
outputStream.write(auth());
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
inputStream.read(buffer);
System.out.println("返回执行结果:" + new String(buffer));
//发送数据
outputStream.write(command());
inputStream.read(buffer);
System.out.println("返回执行结果:" + new String(buffer));
inputStream.close();
outputStream.close();
}
//验证
private static byte[] auth(){
return "*2\r\n$4\r\nAUTH\r\n$12\r\nitcrud_redis\r\n".getBytes();
}
//组装报文信息
private static byte[] command() {
return "*3\r\n$3\r\nSET\r\n$9\r\nuser:name\r\n$6\r\nitcrud\r\n".getBytes();
}
}