Redis 5通信协议解析以及手写一个Jedis客户端
Redis系统介绍:
Redis的基础介绍与安装使用步骤:https://www.jianshu.com/p/2a23257af57b
Redis的基础数据结构与使用:https://www.jianshu.com/p/c95c8450c5b6
Redis核心原理:https://www.jianshu.com/p/4e6b7809e10a
Redis 5 之后版本的高可用集群搭建:https://www.jianshu.com/p/8045b92fafb2
Redis 5 版本的高可用集群的水平扩展:https://www.jianshu.com/p/6355d0827aea
Redis 5 集群选举原理分析:https://www.jianshu.com/p/e6894713a6d5
Redis 5 通信协议解析以及手写一个Jedis客户端:https://www.jianshu.com/p/575544f68615
优秀博客:
Redis Protocol specification:https://redis.io/topics/protocol
通信协议(protocol):http://doc.redisfans.com/topic/protocol.html
redis的通信协议是什么?我的理解是双方约定了一种编码方式,客户端将要发送的命令进行编码,然后服务端收到后,使用同样的协议进行解码,服务端处理完成后,再次编码返回给客户端,客户端解码拿到返回结果,这样就完成了一次通信。如下图:
Redis 协议在以下三个目标之间进行折中:
- 易于实现
- 可以高效地被计算机分析(parse)
- 可以很容易地被人类读懂
简单来说:简单,高效,易读。
看一下redis的通信协议:
- 客户端和服务器通过 TCP 连接来进行数据交互, 服务器默认的端口号为 6379 。
- 客户端和服务器发送的命令或数据一律以 \r\n (CRLF)结尾。
- 在这个协议中, 所有发送至 Redis 服务器的参数都是二进制安全(binary safe)的。
请求协议:
*<参数数量> CR LF
$<参数 1 的字节数量> CR LF
<参数 1 的数据> CR LF
...
$<参数 N 的字节数量> CR LF
<参数 N 的数据> CR LF
举个例子, 以下是一个命令协议的打印版本:
*3
$3
SET
$5
mykey
$7
myvalue
这个命令的实际协议值如下:
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
返回协议:
Redis 命令会返回多种不同类型的回复。
通过检查服务器发回数据的第一个字节, 可以确定这个回复是什么类型:
状态回复(status reply)的第一个字节是 "+"
错误回复(error reply)的第一个字节是 "-"
整数回复(integer reply)的第一个字节是 ":"
批量回复(bulk reply)的第一个字节是 "$"
多条批量回复(multi bulk reply)的第一个字节是 "*"
状态回复
一个状态回复(或者单行回复,single line reply)是一段以 "+" 开始、 "\r\n" 结尾的单行字符串。
例如:
+OK
引用:http://doc.redisfans.com/topic/protocol.html
具体其他的也可以看下官网的介绍
我们看下Jedis是如何连接后台redis服务的
启动后台redis服务
[root@localhost redis-5.0.2]# src/redis-server redis.conf
2800:C 17 Dec 2018 22:53:50.981 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2800:C 17 Dec 2018 22:53:50.982 # Redis version=5.0.2, bits=64, commit=00000000, modified=0, pid=2800, just started
2800:C 17 Dec 2018 22:53:50.982 # Configuration loaded
[root@localhost redis-5.0.2]# ps -ef|grep redis
root 2801 1 0 22:53 ? 00:00:00 src/redis-server *:6379
root 2806 2674 0 22:53 pts/0 00:00:00 grep --color=auto redis
[root@localhost redis-5.0.2]#
注意:
1、如果出现下面这种异常:
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketTimeoutException: connect timed out
linux执行下面命令,开放6379端口:
/sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
2、关闭redis的保护模式
vim redis.conf
修改:
protected-mode no
Jedis代码:
pom依赖,我们目前使用jedis-2.9.0,可以连接单台redis,也可以连接集群,也可以开监控:
redis.clients
jedis
2.9.0
代码很简单:
package com.demo.redis.client;
import redis.clients.jedis.Jedis;
public class RedisClient {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.5.100",6379);
System.out.println(jedis.set("name","xxx"));
System.out.println(jedis.get("name"));
}
}
返回:
OK
xxx
先看下Jedis的类图:
大家可以自己点进去看一下,其实很清晰。
具体Jedis是怎么调用的,如果我们点进去看一下:
set方法:
> redis.clients.jedis.Jedis#set(java.lang.String, java.lang.String)
>redis.clients.jedis.Client#set(java.lang.String, java.lang.String)
>redis.clients.jedis.BinaryClient#set(byte[], byte[])
>redis.clients.jedis.Connection#sendCommand(redis.clients.jedis.Protocol.Command, byte[]...)
>redis.clients.jedis.Protocol#sendCommand(redis.clients.util.RedisOutputStream, redis.clients.jedis.Protocol.Command, byte[]...)
>redis.clients.jedis.Protocol#sendCommand(redis.clients.util.RedisOutputStream, byte[], byte[]...)
就大致这么几步调用,我们尝试自己写一个试试看
核心代码如下:
package com.demo.redis.client;
import com.demo.redis.connection.Connection;
import com.demo.redis.protocol.Protocol;
/**
* 提供api服务
* @author zyy
* @date 2018年12月17日
* */
public class Client {
private Connection connection;
public Client(String host, int port) {
connection = new Connection(host, port);
}
public String set(String key, String value) {
set(SafeEncoder.encode(key), SafeEncoder.encode(value));
return connection.getStatusReply();
}
public void set(byte[] key, byte[] value) {
this.connection.sendCommand(Protocol.Command.SET,new byte[][]{key,value});
}
public String get(String key) {
this.connection.sendCommand(Protocol.Command.GET,SafeEncoder.encode(key));
return connection.getStatusReply();
}
}
package com.demo.redis.client;
import redis.clients.jedis.exceptions.JedisDataException;
import redis.clients.jedis.exceptions.JedisException;
import java.io.UnsupportedEncodingException;
/**
* 编码
* @author zyy
* @date 2018年12月17日
* */
public class SafeEncoder {
public static byte[] encode(String str) {
try {
if (str == null) {
throw new JedisDataException("value sent to redis cannot be null");
} else {
return str.getBytes("UTF-8");
}
} catch (UnsupportedEncodingException var2) {
throw new JedisException(var2);
}
}
}
package com.demo.redis.connection;
import com.demo.redis.protocol.Protocol;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* 建立连接
*
* @author zyy
* @date 2018年12月17日
*/
public class Connection {
private Socket socket;
private String host;
private int port;
private OutputStream outputStream;
private InputStream inputStream;
public Connection(String host, int port) {
this.host = host;
this.port = port;
}
//发送命令
public Connection sendCommand(Protocol.Command cmd, byte[]... args) {
try {
this.connect();
Protocol.sendCommand(this.outputStream, cmd, args);
//++this.pipelinedCommands;
return this;
} catch (JedisConnectionException var6) {
throw var6;
}
}
//如果未建立连接,则scoket 连接
public void connect() {
try {
if (!isConnected()) {
socket = new Socket(host, port);
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//判断是否已建立连接
public boolean isConnected() {
return socket != null && socket.isBound() && !socket.isClosed() && socket.isConnected()
&& !socket.isInputShutdown() && !socket.isOutputShutdown();
}
//获取返回信息
public String getStatusReply() {
byte b[] = new byte[1024];
try {
socket.getInputStream().read(b);
} catch (IOException e) {
e.printStackTrace();
}
return new String(b);
}
}
package com.demo.redis.protocol;
import java.io.IOException;
import java.io.OutputStream;
/**
* 进行协议编码
* @author zyy
* @date 2018年12月17日
* */
public class Protocol {
/**
* * <参数数量> CR LF
* $ <参数 1 的字节数量> CR LF
* <参数 1 的数据> CR LF
* ...
* $ <参数 N 的字节数量> CR LF
* <参数 N 的数据> CR LF
* */
public static final String PARAM_BYTE_NUM = "$";
public static final String PARAM_NUM = "*";
public static final String TERMINATION = "\r\n";
public static void sendCommand(OutputStream outputStream, Command command, byte[]... b) {
/*
照着 SET mykey myvalue 的格式进行编码:
*3
$3
SET
$5
mykey
$7
myvalue
最终如下:
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
*/
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(PARAM_NUM).append(b.length + 1).append(TERMINATION);
stringBuffer.append(PARAM_BYTE_NUM).append(command.name().length()).append(TERMINATION);
stringBuffer.append(command).append(TERMINATION);
for (byte[] arg : b) {
stringBuffer.append(PARAM_BYTE_NUM).append(arg.length).append(TERMINATION);
stringBuffer.append(new String(arg)).append(TERMINATION);
}
try {
outputStream.write(stringBuffer.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
public static enum Command {
SET,
GET;
}
}
ok,我们调用下自己写的client,试试能否成功。
package com.demo.redis.client;
public class Jedis {
public static void main(String[] args) {
Client client = new Client("192.168.5.100",6379);
System.out.println(client.set("name","xxxx"));
System.out.println(client.get("name"));
}
}
返回结果:
+OK
$4
xxxx