Redis原理篇-RESP通信协议

介绍了Redis使用的RESP通信协议,并用Java模拟了一个简单的Redis客户端

本文基于黑马2022的Redis课程原理篇编写,课程地址:黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目

RESP协议

概述

Redis是一个客户端-服务器(C/S)架构的软件。

  • 客户端(Client):命令行工具 redis-cli、Java客户端(如Jedis、Lettuce)、图形化桌面工具(如RDM)等。
  • 服务端(Server):安装并启动的Redis进程。

客户端与服务端的交互通常分为两步(不包括Pipeline和Pub/Sub):

  1. 客户端向服务端发送一条命令。
  2. 服务端解析并执行命令,返回响应结果给客户端。

为了确保双方能够互相理解发送的数据格式,客户端发送命令的格式和服务端响应结果的格式必须遵循一个共同的规范,这个规范就是通信协议

Redis采用的通信协议是 RESP(REdis Serialization Protocol,Redis序列化协议)

  • 协议版本演进:
    • Redis 1.2 版本中引入了RESP协议。
    • Redis 2.0 版本中,RESP协议被确立为与Redis服务端通信的标准,称为 RESP2
    • Redis 6.0 版本中,协议从RESP2升级到了 RESP3。RESP3增加了更多数据类型,并支持6.0版本的新特性(如客户端缓存)。

尽管RESP3协议功能更强,但由于兼容性考虑,Redis 6.0及以后的版本在默认情况下仍然使用 RESP2 协议。因此,本教程重点介绍 RESP2 协议。

数据类型

RESP2协议规定了5种不同的数据类型。在传输时,第一个字节 用于标识数据类型,后续是数据内容,以 \r\n(CRLF)结尾。

单行字符串(Simple Strings)

  • 首字节+
  • 格式+{字符串内容}\r\n
  • 说明:用于传输普通的、非二进制安全的单行字符串。内容中不允许出现 \r\n 字符。
  • 用途:常用于服务端返回简单的成功状态,如 +OK\r\n
  • 解析方式:读取直到遇到 \r\n 为止,之前的所有内容即为字符串内容。

错误(Errors)

  • 首字节-
  • 格式-{错误信息}\r\n
  • 说明:用于表示服务端执行过程中出现的异常。格式与单行字符串类似,但语义是错误。
  • 用途:服务端返回错误信息给客户端,例如 -ERR unknown command\r\n
  • 特点:客户端发送的命令中不会出现此类型,它仅由服务端发送。

整数(Integers)

  • 首字节:
  • 格式:{数字字符串}\r\n
  • 说明:用于传输整数值,内容为数字格式的字符串。
  • 用途:用于返回计数结果、递增/递减操作后的值等,例如 :10\r\n
  • 示例:执行 INCR mycounter 后,若结果为1,响应为 :1\r\n

多行字符串(Bulk Strings)

  • 首字节$
  • 格式
    • 普通情况:$字节数\r\n{字符串内容}\r\n
    • 空字符串:$0\r\n\r\n
    • 不存在(Null):$-1\r\n
  • 说明:用于表示二进制安全的字符串。它记录了字符串的字节长度,因此可以包含 \r\n 等任意二进制数据。最大支持512MB。
  • 结构解析
    1. $ 标识这是一个多行字符串。
    2. 接下来的数字是字符串的字节大小(不含 \r\n)。
    3. 紧跟 \r\n 结束长度部分的声明。
    4. 然后是实际的字符串数据,精确读取指定长度的字节。
    5. 最后以 \r\n 结束。
  • 示例$5\r\nhello\r\n 表示字符串 hello
  • 特殊值
    • 长度为0:表示空字符串。
    • 长度为-1:表示不存在(Null),常用于表示键不存在或获取不到数据。

image-20260609204349491

数组(Arrays)

  • 首字节*
  • 格式*{元素数量}\r\n{元素1}{元素2}...
  • 说明:用于表示一个有序的元素集合。元素可以是上述四种类型中的任意一种,甚至可以是数组本身(嵌套)。
  • 结构解析
    1. * 标识这是一个数组。
    2. 接下来的数字是数组中元素的个数
    3. 紧跟 \r\n 结束个数声明。
    4. 后续跟随指定个数的元素,每个元素都按其自身类型的RESP协议格式进行编码。
  • 主要用途
    • 客户端发送命令:命令在RESP协议中被编码为数组。数组的每个元素对应命令的一个参数(命令名是第一个参数)。
      • 示例:发送 SET name 虎哥 命令,其RESP编码为:*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$6\r\n虎哥\r\n,(元素个数为3,分别是 SETname虎哥,它们都是多行字符串)
    • 服务端返回多结果:例如执行 LRANGE mylist 0 -1 返回列表多个元素时,响应就是一个数组。

image-20260609210305150

模拟Redis客户端

概述

下面通过Java Socket编程手动简单模拟一个Redis客户端,深入理解RESP(REdis Serialization Protocol) 通信协议。将不使用任何第三方客户端库,仅基于JDK,实现与Redis服务器的连接、命令发送与响应解析。

实现步骤

核心流程大致如下:

  1. 建立连接:通过Socket连接到Redis服务器的IP和端口。
  2. 获取IO流:从Socket中获取输入流和输出流,用于数据收发。
  3. 发送请求:按照RESP协议格式,通过输出流向Redis发送命令。
  4. 解析响应:按照RESP协议格式,从输入流中读取并解析Redis返回的响应数据。
  5. 释放连接:在finally块中关闭流和Socket连接,释放资源。

创建项目与建立连接

先创建一个普通的Java项目(非Maven),仅依赖JDK。然后使用java.net.Socket类建立TCP连接。

1
2
3
4
// 示例:连接到本地Redis服务器
String host = 192.168.150.101; // 应替换为你的Redis服务器IP
int port = 6379;
Socket socket = new Socket(host, port);

获取IO流

输出流:推荐使用PrintWriter包装,因为它可以方便地按行(println)输出,自动添加换行符。

输入流:推荐使用BufferedReader包装,以便按行(readLine)读取。

理论修正与简化:从协议安全角度看,读取操作应使用字节流(InputStream,因为响应中的多行字符串(Bulk String)是二进制安全的,可能包含\r\n。但为简化教程代码,此处暂时使用字符流(BufferedReader),并假设响应字符串中不包含换行符。一个更严谨的实现应使用字节流读取指定长度的数据。

1
2
3
4
5
6
7
8
9
// 输出流 (用于发送请求)
PrintWriter writer = new PrintWriter(
    new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8)
);

// 输入流 (用于读取响应,已简化)
BufferedReader reader = new BufferedReader(
    new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)
);

发送请求 (实现 sendRequest 方法)

RESP协议中,客户端请求是一个数组,格式为:*<元素数量>\r\n$<字节长度1>\r\n<内容1>\r\n...

$后面的长度指的是内容字符串转换为字节后的长度,而非字符长度。

编写一个接受可变参数 String… args的方法,以发送任意命令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 发送请求的通用方法
private static void sendRequest(String args) throws IOException {
    // 1. 输出数组长度: *3\r\n
    writer.println(* + args.length);
    // 2. 循环输出每个参数: $3\r\nSET\r\n$4\r\nname\r\n$6\r\n虎哥\r\n
    for (String arg : args) {
        writer.println($ + arg.getBytes(StandardCharsets.UTF_8).length); // 字节长度
        writer.println(arg);
    }
    writer.flush(); // 刷新缓冲区,确保数据发送
}

解析响应 (实现 handleResponse 方法)

RESP响应由首字节标识数据类型,共5种。

读取第一个字符(reader.read()),通过switch判断类型并分别处理。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static Object handleResponse() throws IOException {
    // 1. 读取首字节,判断数据类型
    int prefix = reader.read();
    switch (prefix) {
        case +: // 单行字符串 (Simple String),例如 “+OK\r\n”
            return reader.readLine(); // 直接读取一行
        case -: // 错误 (Error),例如 “-ERR unknown command\r\n”
            throw new RuntimeException(reader.readLine()); // 读取一行作为异常信息
        case :: // 整数 (Integer),例如 “:1000\r\n”
            return Long.parseLong(reader.readLine()); // 读取一行并转为Long
        case $: // 多行字符串 (Bulk String),例如 “$5\r\nhello\r\n”
            // 先读取字符串的字节长度
            int len = Integer.parseInt(reader.readLine());
            if (len == -1) return null;    // 表示不存在的键 (Null Bulk String)
            if (len == 0) return ““;       // 表示空字符串
            // 简化处理:假设字符串不含\r\n,直接读取一行。严谨做法应读取len个字节
            return reader.readLine();
        case *: // 数组 (Array),例如 “*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n”
            return readBulkString(); // 调用专门方法处理数组
        default:
            throw new RuntimeException(错误的数据格式!”);
    }
}

处理数组响应 (readBulkString 方法)

数组可能嵌套其他数据类型(包括数组)。

实现逻辑:

  1. 读取数组长度 len
  2. 创建一个List集合,循环len次。
  3. 递归调用 handleResponse() 解析数组中的每一个元素。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private static List<Object> readBulkString() throws IOException {
    int len = Integer.parseInt(reader.readLine());
    if (len <= 0) return null;
    List<Object> list = new ArrayList<>(len);
    for (int i = 0; i < len; i++) {
        // 递归调用,解析数组中的每一个元素
        list.add(handleResponse());
    }
    return list;
}

资源释放

finally块中,按顺序关闭readerwritersocket,并处理可能的IOException

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
finally {
    // 释放连接
    try {
        if (reader != null) reader.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        if (writer != null) writer.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        if (socket != null) socket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

测试

直接发送SET命令可能会报错 NOAUTH Authentication required,因为Redis服务器设置了密码。

需要在发送实际命令前,先发送AUTH命令进行授权。

1
2
3
4
5
6
// 在main方法中,连接成功后:
sendRequest(AUTH, 123321); // 先发送授权命令,填自己Redis的密码
handleResponse();              // 解析授权响应(OK)
// 然后再发送业务命令
sendRequest(SET, name, 虎哥);
System.out.println(handleResponse()); // 输出: OK

完整代码

  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
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public class Main {

    static Socket s;
    static PrintWriter writer;
    static BufferedReader reader;

    public static void main(String[] args) {
        try {
            // 1.建立连接
            String host = "192.168.150.101";
            int port = 6379;
            s = new Socket(host, port);
            // 2.获取输出流、输入流
            writer = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
            reader = new BufferedReader(new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));

            // 3.发出请求
            // 3.1.获取授权 auth 123321
            sendRequest("auth", "123321");
            Object obj = handleResponse();
            System.out.println("obj = " + obj);

            // 3.2.set name 虎哥
            sendRequest("set", "name", "虎哥");
            // 4.解析响应
            obj = handleResponse();
            System.out.println("obj = " + obj);

            // 3.2.set name 虎哥
            sendRequest("get", "name");
            // 4.解析响应
            obj = handleResponse();
            System.out.println("obj = " + obj);

            // 3.2.set name 虎哥
            sendRequest("mget", "name", "num", "msg");
            // 4.解析响应
            obj = handleResponse();
            System.out.println("obj = " + obj);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.释放连接
            try {
                if (reader != null) reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (writer != null) writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (s != null) s.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static Object handleResponse() throws IOException {
        // 读取首字节
        int prefix = reader.read();
        // 判断数据类型标示
        switch (prefix) {
            case '+': // 单行字符串,直接读一行
                return reader.readLine();
            case '-': // 异常,也读一行
                throw new RuntimeException(reader.readLine());
            case ':': // 数字
                return Long.parseLong(reader.readLine());
            case '$': // 多行字符串
                // 先读长度
                int len = Integer.parseInt(reader.readLine());
                if (len == -1) {
                    return null;
                }
                if (len == 0) {
                    return "";
                }
                // 再读数据,读len个字节。我们假设没有特殊字符,所以读一行(简化)
                return reader.readLine();
            case '*':
                return readBulkString();
            default:
                throw new RuntimeException("错误的数据格式!");
        }
    }

    private static Object readBulkString() throws IOException {
        // 获取数组大小
        int len = Integer.parseInt(reader.readLine());
        if (len <= 0) {
            return null;
        }
        // 定义集合,接收多个元素
        List<Object> list = new ArrayList<>(len);
        // 遍历,依次读取每个元素
        for (int i = 0; i < len; i++) {
            list.add(handleResponse());
        }
        return list;
    }

    // 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();
    }
}
本站于2025年3月26日建立
使用 Hugo 构建
主题 StackJimmy 设计