手写仿 Jedis

此篇文章,帮助你理解 Jedis Client是如何与 Redis Server通信的。
源码参考:jedis-demo

客户端的三层架构

1、操作层(api):发送哪些命令,及数据
2、消息处理层(protocol):编码格式
3、传输层(transfer):通过建立 Socket,发送数据,接收数据
在这里插入图片描述

操作 Jedis

1、使用 Docker 启动 Redis

2、创建一个 maven 项目,导入 Jedis 依赖

1
2
3
4
5
6
7
8
<dependencies>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.5.2</version>
</dependency>
</dependencies>

3、测试 Jedis 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import redis.clients.jedis.Jedis;

import java.time.LocalDateTime;
import java.util.Set;

/**
* 测试
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午5:29
*/
public class MyTest {

//2021/3/29 下午5:30 测试 jedis
public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("127.0.0.1", 6379);

Set<String> keys = jedis.keys("*");
System.out.println(keys);// [k1, name]

jedis.set("k1", LocalDateTime.now().toString());

String k1 = jedis.get("k1");
System.out.println(k1);// 2021-03-30T09:00:58.932
}
}

伪造 Hack拦截数据

在这里插入图片描述

1、编写 Hack.java 类,并启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
* 黑客:拦截数据
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午5:47
*/
public class Hack {

//2021/3/29 下午5:47 黑客:拦截数据(先启动自己,监听着,再启动 MyTest 18019)
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(18019);// 使用一个本地未被占用的端口
Socket socket = serverSocket.accept();

InputStream in = socket.getInputStream();

byte[] bytes = new byte[1024];
in.read(bytes);

System.out.println(new String(bytes));
// *3
// $3
// SET
// $2
// k1
// $23
// 2021-03-29T17:55:12.291
}
}

2、使用 Jedis 发送数据。启动之前,先启动 Hack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import redis.clients.jedis.Jedis;

import java.time.LocalDateTime;

/**
* 测试
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午5:29
*/
public class MyTest {

//2021/3/29 下午5:30 测试 jedis
/*public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("127.0.0.1", 6379);

Set<String> keys = jedis.keys("*");
System.out.println(keys);// [k1, name]

jedis.set("k1", LocalDateTime.now().toString());

String k1 = jedis.get("k1");
System.out.println(k1);// 2021-03-30T09:00:58.932
}*/

//2021/3/29 下午5:30 发送数据给 Hack,先启动 Hack
public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("127.0.0.1", 18019);

jedis.set("k1", LocalDateTime.now().toString());

String k1 = jedis.get("k1");
System.out.println(k1);
}
}

3、Hack 拦截到数据

1
2
3
4
5
6
7
*3
$3
SET
$2
k1
$23
2021-03-29T17:55:12.291

分析拦截数据

1、打开 Redis 官方文档:https://redis.io/documentation

2、搜索 protocol

3、找到 Specifications ===> Redis Protocol specification ===> https://redis.io/topics/protocol
在这里插入图片描述

4、先看一下简介:
在这里插入图片描述

1
2
3
4
5
6
7
** Redis 协议规范 **
Redis客户端使用一种称为RESP(Redis序列化协议)的协议与Redis服务器通信。
虽然该协议是专门为Redis设计的,但它也可以用于其他客户端-服务器软件项目。
RESP是以下事项之间的折衷:
简单的实现
快速解析
人类可读

5、使用规则
在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RESP协议描述
RESP协议在Redis 1.2中引入,但它成为Redis 2.0中与Redis服务器对话的标准方式。这是您应该在Redis客户端中实现的协议。
RESP实际上是一种序列化协议,支持以下数据类型:简单字符串、错误、整数、散装字符串和数组。
RESP在Redis中用作请求响应协议的方式如下:
+ 客户端将命令作为散装字符串的RESP数组发送到Redis服务器。
+ 服务器根据命令实现使用RESP类型之一进行响应。

在RESP中,某些数据的类型取决于第一个字节:
+ 对于简单字符串,回复的第一个字节是“+”
+ 对于错误,回复的第一个字节是“-”
+ 对于整数来说,回复的第一个字节是“:”
+ 对于散装字符串,回复的第一个字节是“$”
+ 对于数组,回复的第一个字节是“*”

此外,RESP可以使用稍后指定的批量字符串或数组的特殊变体表示空值。
在RESP中,协议的不同部分总是以“\r\n”(CRLF)结束。

6、把我们的翻译一下:

1
2
3
4
5
6
7
*3 ===> 数组有 3个【($3,SET),($2,k1),($23,2021-03-29T17:55:12.291)】
$3 ===> 字符串 3个【(S),(E),(T)】
SET
$2 ===> 字符串 2个【(k),(1)】
k1
$23 ===> 字符串 23个
2021-03-29T17:55:12.291

手写 Jedis

一共三个类:MyJedis,MyConnection,MyProtocol

1、MyJedis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.taopanfeng.connection.MyConnection;
import com.taopanfeng.protocol.MyProtocol;

/**
* api 操作层
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午5:36
*/
public class MyJedis {
private MyConnection myConnection;

public MyJedis(String host, int port) {
myConnection = new MyConnection(host, port);
}

public String set(final String key, final String value) {
myConnection.sendCommand(MyProtocol.Command.SET, key.getBytes(), value.getBytes());
return null;
}

public String get(final String key) {
myConnection.sendCommand(MyProtocol.Command.GET, key.getBytes());
return myConnection.getReply();
}
}

2、MyConnection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/**
* 连接层(连接 Redis 数据库)
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午5:37
*/
public class MyConnection {
private String host;
private int port;

private Socket socket;

private InputStream in;
private OutputStream out;

public MyConnection(String host, int port) {
this.host = host;
this.port = port;
}

private void connect() {
try {
socket = new Socket(host, port);
in = socket.getInputStream();
out = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}


public MyConnection sendCommand(MyProtocol.Command command, byte[]... bytes) {
// 连接
connect();
// 发送
MyProtocol.sendCommand(command, out, bytes);

return this;
}

/**
* 从 socket 读取数据
*
* @author 陶攀峰
* @date 2021/3/30 上午8:29
*/
public String getReply() {
byte[] bytes = new byte[1024];

try {
socket.getInputStream().read(bytes);
} catch (IOException e) {
e.printStackTrace();
}

return new String(bytes);
}
}

3、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.io.IOException;
import java.io.OutputStream;

/**
* 协议层
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午6:05
*/
public class MyProtocol {

public static final String X = "*";// 数组
public static final String S = "$";// 字符串
public static final String CRLF = "\r\n";// 换行

/**
* 组装发送的数据
*
* @author 陶攀峰
* @date 2021/3/29 下午6:07
*/
public static void sendCommand(Command command, OutputStream out, byte[]... bytes) {
StringBuffer sb = new StringBuffer();

sb.append(X).append(bytes.length + 1).append(CRLF);// *3

sb.append(S).append(command.name().length()).append(CRLF);// $3
sb.append(command).append(CRLF);// SET

for (byte[] b : bytes) {
sb.append(S).append(b.length).append(CRLF);// $2 $23
sb.append(new String(b)).append(CRLF);// k1 2021-03-29T17:55:12.291
}

try {
out.write(sb.toString().getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 使用枚举,规定哪些命令可用
*
* @author 陶攀峰
* @date 2021/3/30 上午9:24
*/
public enum Command {
SET, GET
}

}

4、测试 MyTest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import com.taopanfeng.api.MyJedis;

/**
* 测试
*
* @author 陶攀峰
* @version 1.0
* @date 2021/3/29 下午5:29
*/
public class MyTest {

//2021/3/29 下午5:30 测试 jedis
/*public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("127.0.0.1", 6379);

Set<String> keys = jedis.keys("*");
System.out.println(keys);// [k1, name]

jedis.set("k1", LocalDateTime.now().toString());

String k1 = jedis.get("k1");
System.out.println(k1);// 2021-03-30T09:00:58.932
}*/

//2021/3/29 下午5:30 发送数据给 Hack,先启动 Hack
/*public static void main(String[] args) throws Exception {
Jedis jedis = new Jedis("127.0.0.1", 18019);

jedis.set("k1", LocalDateTime.now().toString());

String k1 = jedis.get("k1");
System.out.println(k1);
}*/

//2021/3/29 下午6:20 测试 MyJedis set
/*public static void main(String[] args) throws Exception {
MyJedis myJedis = new MyJedis("127.0.0.1", 6379);
myJedis.set("name", "陶攀峰");
}*/

//2021/3/30 上午8:30 测试 MyJedis get
public static void main(String[] args) throws Exception {
MyJedis myJedis = new MyJedis("127.0.0.1", 6379);
String k1 = myJedis.get("name");
System.out.println(k1);
//$9
//陶攀峰
}
}