RESP 协议的工作原理

1. RESP 的设计哲学

RESP 是 Redis 自己设计的文本协议,有几个关键设计目标:

  • 简单 :人能直接看懂,用 \r\n 作为分隔符。
  • 解析快:每种类型第一个字节就决定了类型,解析时无需回溯。
  • 二进制安全 :可以传输任意字节(包括 \r\n 本身),通过长度前缀保证。
  • 支持多路复用(Redis 6.0+ 才支持):但早期设计只支持单请求-单回复,所以解析器也是简单的顺序解析。

2. 五种核心数据类型的编码规则

类型 第一个字节 示例 说明
简单字符串 + +OK\r\n 状态信息,不能包含 \r\n
错误 - -ERR unknown command\r\n 错误信息,格式类似简单字符串
整数 : :1000\r\n 有符号整数,如 INCR 返回值
批量字符串 $ $6\r\nfoobar\r\n 二进制安全:$ 后跟字节长度,再跟数据和 \r\n
数组 * *2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n 嵌套结构,可包含任何类型

特殊值

  • 空批量字符串:$-1\r\n → 对应 Python 的 None
  • 空数组:*-1\r\n → 也代表 None

3. 解析器的核心原理

解析器的本质是一个 有限状态机(FSM),它逐字节读取数据,根据当前状态决定下一步动作。

3.1 状态定义(简化版)

  • 状态 0:等待第一个字节,确定类型
  • 状态 1 :正在读取 \r\n 分隔的整数(长度或数组元素个数)
  • 状态 2:正在读取批量字符串的实际内容
  • 状态 3:遇到数组,准备递归解析数组内的元素

3.2 解析流程(以批量字符串为例)

假设输入字节流:b"$5\r\nhello\r\n"

  1. 读第一个字节 $ → 进入批量字符串模式。
  2. 继续读取直到遇到 \r\n,中间部分 "5" 解析为整数 length = 5
  3. 跳过 \r\n(2 字节)。
  4. 从当前位置读取 length 个字节("hello")。
  5. 然后跳过结尾的 \r\n(2 字节)。
  6. 将读到的 b"hello" 按需解码(例如 UTF-8)返回。

关键点 :解析器必须 严格按长度读取 ,不能依赖 \r\n 来查找结尾(因为数据本身可能包含 \r\n)。这正是二进制安全的实现方式。

3.3 数组解析(嵌套结构)

数组的解析是递归的:

复制代码
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
  1. 读到 *count = 2
  2. 递归解析第一个元素:$3\r\nfoo\r\n → 得到 "foo"
  3. 递归解析第二个元素:$3\r\nbar\r\n → 得到 "bar"
  4. 返回 ["foo", "bar"]

3.4 流式解析(TCP 分片问题)

网络数据可能分多个 TCP 包到达,例如:

第一个包:b"*2\r\n$3\r\nfo"

第二个包:b"o\r\n$3\r\nbar\r\n"

解析器不能假设一次性收到完整数据。所以解析器会维护一个 缓冲区 ,每次 feed 新数据时,尝试从缓冲区中提取一个完整的 RESP 回复。如果数据不足,就暂停并等待更多数据。

实现技巧:解析器在读取长度字段后,知道还需要多少字节,如果缓冲区不够,就记录当前状态并返回"需要更多数据"。


4. 手动实现一个最小但完整的 RESP 解析器(Python)

python 复制代码
class RespParser:
    def __init__(self):
        self.buffer = b''
    
    def feed(self, data: bytes):
        """接收新数据,返回所有已解析的完整回复(列表)"""
        self.buffer += data
        replies = []
        while True:
            reply, consumed = self._parse_one(self.buffer)
            if reply is None:   # 数据不完整
                break
            replies.append(reply)
            self.buffer = self.buffer[consumed:]
        return replies
    
    def _parse_one(self, data: bytes):
        """尝试解析一个完整的 RESP 数据类型,返回 (result, consumed_bytes) 或 (None, 0) 表示需要更多数据"""
        if not data:
            return None, 0
        
        # 根据第一个字节分发
        type_byte = data[0]
        if type_byte == ord('+'):
            # 简单字符串:找到 \r\n
            end = data.find(b'\r\n', 1)
            if end == -1:
                return None, 0
            result = data[1:end].decode()
            return result, end + 2
        elif type_byte == ord('-'):
            end = data.find(b'\r\n', 1)
            if end == -1:
                return None, 0
            result = Exception(data[1:end].decode())  # 当作异常抛出
            return result, end + 2
        elif type_byte == ord(':'):
            end = data.find(b'\r\n', 1)
            if end == -1:
                return None, 0
            result = int(data[1:end])
            return result, end + 2
        elif type_byte == ord('$'):
            # 批量字符串
            end = data.find(b'\r\n', 1)
            if end == -1:
                return None, 0
            length = int(data[1:end])
            if length == -1:
                return None, end + 2   # NULL 值
            payload_start = end + 2
            payload_end = payload_start + length
            if len(data) < payload_end + 2:
                return None, 0  # 数据不足
            # 检查结尾 \r\n
            if data[payload_end:payload_end+2] != b'\r\n':
                raise ValueError("Invalid RESP format")
            result = data[payload_start:payload_end]  # 返回 bytes
            return result, payload_end + 2
        elif type_byte == ord('*'):
            end = data.find(b'\r\n', 1)
            if end == -1:
                return None, 0
            count = int(data[1:end])
            if count == -1:
                return None, end + 2
            # 递归解析 count 个元素
            consumed = end + 2
            result = []
            for _ in range(count):
                if consumed >= len(data):
                    return None, 0
                elem, n = self._parse_one(data[consumed:])
                if elem is None and n == 0:  # 数据不完整
                    return None, 0
                result.append(elem)
                consumed += n
            return result, consumed
        else:
            raise ValueError(f"Unknown RESP type byte: {type_byte}")

说明

  • _parse_one 返回 (结果, 消费字节数),当数据不足时返回 (None, 0)
  • 批量字符串返回 bytes,调用者可按需解码。
  • 错误类型这里简单转为 Exception 对象。

5. 反向过程:编码(生成 RESP 命令)

客户端向 Redis 发送命令也需要 RESP 格式。例如 SET key value 编码为:

复制代码
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

实现一个简单的编码函数:

python 复制代码
def encode_resp(obj):
    """将 Python 对象编码为 RESP 字节串"""
    if obj is None:
        return b'$-1\r\n'
    if isinstance(obj, int):
        return f':{obj}\r\n'.encode()
    if isinstance(obj, str):
        data = obj.encode()
        return f'${len(data)}\r\n'.encode() + data + b'\r\n'
    if isinstance(obj, bytes):
        return f'${len(obj)}\r\n'.encode() + obj + b'\r\n'
    if isinstance(obj, (list, tuple)):
        parts = [f'*{len(obj)}\r\n'.encode()]
        for item in obj:
            parts.append(encode_resp(item))
        return b''.join(parts)
    if isinstance(obj, Exception):
        return f'-{obj}\r\n'.encode()
    raise TypeError(f"Unsupported type: {type(obj)}")

6. 性能优化原理

生产级解析器(如 hiredis)会做很多优化:

  • 零拷贝 :对于大的批量字符串,不复制数据,直接返回内存视图(memoryview)。
  • 预分配缓冲区:避免频繁的内存分配。
  • 使用查表法解析整数 :比 int() 快。
  • 避免递归:用迭代栈处理嵌套数组。

但这些都是在理解基本原理之上的工程优化。


7. 总结:核心原理一句话

RESP 解析器 = 前缀驱动的状态机 + 长度前缀保证二进制安全 + 递归处理嵌套数组

相关推荐
炽烈小老头2 小时前
【 每天学习一点算法 2026/04/06】常数时间插入、删除和获取随机元素
学习·算法
Keep Running *2 小时前
Docker_学习笔记
笔记·学习·docker
lingggggaaaa2 小时前
PHP模型开发篇&MVC层&RCE执行&文件对比法&1day分析&0day验证
开发语言·学习·安全·web安全·php·mvc
Amazing_Cacao11 小时前
深度观察 | 从“产区玄学”到“液态战场”:精品巧克力的终极试金石
学习
深蓝海拓12 小时前
S7-1500PLC学习笔记:MOVE_BLK、MOVE_BLK_VARIANT、BLKMOV的区别
笔记·学习·plc
darkhorsefly13 小时前
玩24算的益处
学习·游戏·24算
深蓝海拓14 小时前
S7-1500学习笔记:用户自定义数据类型(UDT)
笔记·学习·plc
罗罗攀14 小时前
PyTorch学习笔记|神经网络的损失函数
人工智能·pytorch·笔记·神经网络·学习
aP8PfmxS215 小时前
从零学习Kafka:数据存储
分布式·学习·kafka