Redis通信协议

RESP协议

Redis是一个CS架构的软件,通信一般分两步(不包括pipeline和PubSub):

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

因此客户端发送命令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。

而在Redis中采用的是RESP(Redis Serialization Protocol)协议:

  • Redis1.2版本引入了RESP协议
  • Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2
  • Redis 6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型并且支持6.0的新特性--客户端缓存

但目前,默认使用的依然是RESP2协议,也是我们要学习的协议版本(以下简称RESP)。

在RESP中,通过首字节的字符来区分不同数据类型,常用的数据类型包括5种:

  1. 单行字符串 :首字节是 '+',后面跟上单行字符串,以CRLF( "\r\n" )结尾。例如返回"OK": "+OK\r\n"
  2. 错误 (Errors):首字节是 '-',与单行字符串格式一样,只是字符串是异常信息,例如:"-Error message\r\n"
  3. 数值 :首字节是 ':',后面跟上数字格式的字符串,以CRLF结尾。例如:":10\r\n"
  4. 多行字符串 :首字节是 '$',表示二进制安全的字符串,最大支持512MB:
    • 如果大小为0,则代表空字符串:"$0\r\n\r\n"
    • 如果大小为-1,则代表不存在:"$-1\r\n"
  1. 数组 :首字节是 '*',后面跟上数组元素个数,再跟上元素,元素数据类型不限:

模拟Redis客户端

java 复制代码
public class Main {

    static Socket socket;
    static PrintWriter pw; // writer字符流------好处:可以直接按行输出
    static BufferedReader br; // reader字符流------好处:可以按行读

    public static void main(String[] args) {
        try {
            // 1.建立连接
            String host = "xx.xx.xx.xx";
            int port = 6379;
            socket = new Socket(host, port);


            // 2.获取输出流、输入流
            pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
            br = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

            // 3.发出请求
            sendRequest("auth", "123321");
            // 4.解析响应
            Object obj = handleResponse();
            System.out.println("obj = " + obj);

            // 发出请求
            sendRequest("set", " name", "cammy");
            // 解析响应
            obj = handleResponse();
            System.out.println("obj = " + obj);

            // 发出请求
            sendRequest("get", " name");
            // 解析响应
            obj = handleResponse();
            System.out.println("obj = " + obj);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.释放连接
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (pw != null) {
                pw.close();
            }
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    /**
     * 发送请求
     */
    private static void sendRequest(String... args) {
        pw.println("*" + args.length);
        for (String arg : args) {
            pw.println("$" + arg.getBytes(StandardCharsets.UTF_8).length);
            pw.println(arg);
        }
        // auth 123321
        // pw.println("*2");
        // pw.println("$4");
        // pw.println("auth");
        // pw.println("$6");
        // pw.println("123321");

        // set name cammy
        // pw.println("*3");
        // pw.println("$3");
        // pw.println("set");
        // pw.println("$4");
        // pw.println("name");
        // pw.println("$6");
        // pw.println("cammy");
        pw.flush();
    }

    /**
     * 处理响应
     * @return
     */
    private static Object handleResponse() throws IOException {
        // 读取首字节,判断数据类型标识
        // 特殊字符它的一个字符就是一个字节
        int prefix = br.read();
        // 判断数据类型标识
        switch (prefix) {
            case '+':
                // 单行字符串直接读一行
                return br.readLine();
            case '-':
                // 异常
                throw new RuntimeException(br.readLine());
            case ':':
                // 数字
                return Long.parseLong(br.readLine());
            case '$':
                // 读取多行字符串
                // 先读长度,再度数据
                int len = Integer.parseInt(br.readLine());
                if (len == -1) {
                    return null;
                }
                if (len == 0) {
                    return "";
                }
                // 读取数据,读len个字节。(todo:假设没有特殊字符,所以读一行)
                return br.readLine();
            case '*':
                // 数组
                return readBulkString();
            default:
                throw new RuntimeException("不能识别的数据类型");
        }

    }

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

}
相关推荐
AskHarries14 分钟前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术2 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎3 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode3 小时前
Redis 在生产项目的使用
前端·后端
用户559822481223 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode3 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战3 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha3 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn3 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425913 小时前
ShardingJDBC
后端