文章目录
- 前言
- 一、Redis 的通信过程:
- 二、RESP 协议:
- 三、客户端模拟RESP 通信:
- 总结
前言
在我们知道redis 的网络模型后,继续看下 redis 的通信协议。
一、Redis 的通信过程:
Redis是一个CS架构的软件,通信一般分两步 (不包括pipeline和PubSub):
- 客户端 (client) 向服务端 (server) 发送一条命令
- 服务端解析并执行命令,返回响应结果给客户端
- 因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。
而在Redis中采用的是RESP (Redis Serialization Protocol) 协议:
- Redis 1.2版本引入了RESP协议
- Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2
- Redis6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型并且支持6.0的新特性–客户端缓存
但目前,默认使用的依然是RESP2协议
二、RESP 协议:
客户端 (client) 向服务端 (server) 发送指令,服务端完成对应操作后返回结果 ,那么服务端怎么知道客户端传入过来的数据类型是什么呢,因为只有知道了数据类型,服务端才能去正确的包装Redisobject 对象 进行存储;
在RESP中,通过首字节的字符来区分不同数据类型,常用的数据类型包括5种:
- 单行字符串::首字节是“+,后面跟上单行字符串,以CRLF(“\r\n”) 结尾。例如返回”OK”:“+OK\r\n”
此种字符中不能包含 \r\n, 这个通常用于服务端返回消息,如 ping ->pong 和 执行客户端命令成果返回ok; - 错误(Errors):首字节是“-“,与单行字符串格式一样,只是字符串是异常信息,
例如:”-Error message\r\n";
只出现在 服务端接到客户端的命令,在执行命令过程中出现错误; - 数值:首字节是“:’,后面跟上数字格式的字符串,以CRLF结尾。例如:“:10\r\n”
- 多行字符串:首字节是$’,表示二进制安全的字符串,最大支持512MB:
- 数组:首字节是"*“,后面跟上数组元素个数,再跟上元素,元素数据类型不限:可以是字符串,数值,数组;事实上客户端在给服务端发送命令是 使用的就是这个格式:
这样Redis 就可以来发送各种各样的数据给服务端了,现在还有一个问题,服务端怎么知道 发送的命令是正确的读完了呢, 答案是客户端在发送命令之前给每中数据 都标记上 命令的长度:$ 跟上长度,如发送的数据是字符串:
对于服务端返回的数据结果也会将数据类型及长度进行标注,在服务端的返回 长度有如下含义:
- 如果是正常代表字符串长度;
- 如果是0 代表空字符串 “$0\r\n\r\n”
- 如果是-1 在代表字符串不存在 “$-1\r\n\r\n”,如根据key没有找到值;
客户端在给服务端发送命令时使用的就是数组的格式
- *3 表示数组中有3个元素;
- $3 表示数据类型是多行字符串,长度是3个字节;
- $4 表示数据类型是多行字符串,长度是4个字节;
- $6 表示数据类型是多行字符串,长度是6个字节;
- 最后服务端就可以解析出来正确的命令 set name 虎哥
三、客户端模拟RESP 通信:
package com.example.springredisms.mock.redis;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class RedicClient {
static Socket s;
static PrintWriter writer;
static BufferedReader reader;
public static void main(String[] args) {
try {
// 和服务的建立socket 连接
s = new Socket("localhost", 6379);
// 获取写数据流
writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
// 获取读数据流
reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
// 密码认证命令
sendRequest("auth", "123456");
// 解析请求
Object o1 = handleResponse();
System.out.println("o1 = " + o1);
sendRequest("set", "name", "张三");
Object o2 = handleResponse();
System.out.println("o2 = " + o2);
sendRequest("get", "name");
Object o3 = handleResponse();
System.out.println("o3 = " + o3);
sendRequest("SADD", "runoobkey1", "王五", "赵六", "李四");
Object o4 = handleResponse();
System.out.println("o4 = " + o4);
sendRequest("SMEMBERS", "runoobkey1");
Object o5 = handleResponse();
System.out.println("o5 = " + o5);
} catch (Exception ex) {
try {
if (null != reader) {
reader.close();
}
if (null != writer) {
writer.close();
}
if (null != s) {
s.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static Object handleResponse() throws IOException {
Object o;
int prefix = reader.read();
switch (prefix) {
case '+':
o = reader.readLine();
break;
case '-':
throw new RuntimeException(reader.readLine());
case ':':
o = Long.parseLong(reader.readLine());
break;
case '$':
Integer len = Integer.parseInt(reader.readLine());
if (-1 == len) {
o = null;
} else if (0 == len) {
o = "";
} else {
// 简化 :直接读一行(正常应该都len 字节)
o = reader.readLine();
}
break;
case '*':
Integer arrayLng = Integer.parseInt(reader.readLine());
if (arrayLng <= 0) {
return null;
}
List<Object> list = new ArrayList<>(arrayLng);
o = list;
for (Integer i = 0; i < arrayLng; i++) {
list.add(handleResponse());
}
break;
default:
throw new RuntimeException("不合法的数据格式!");
}
return o;
}
// set name 张三
private static void sendRequest(String... args) {
// 数组元素的个数
writer.println("*" + args.length);
for (String arg : args) {
// 命令的字节数
writer.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
// 命令
writer.println(arg);
}
// 写出
writer.flush();
}
}
总结
Redis 使用 RESP 协议 进行客户端与服务端的通信,通过对数据进行不同的头部标识 来区分数据类型,通过长度来保证读取的完整性。