强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Memcached 传输协议精讲 / 第06章 二进制协议基础

第06章 二进制协议基础

二进制协议是 Memcached 的高效通信方式,适用于对性能要求极高的场景。


6.1 二进制协议概述

设计动机

二进制协议的出现是为了解决文本协议的几个限制:

限制 文本协议 二进制协议
解析开销 需要字符串分割、类型转换 固定偏移量直接读取
错误处理 只能通过文本消息描述 提供标准化状态码
元数据传递 有限(只有 flags) 支持扩展字段
二进制数据 需要长度字段辅助 原生支持

版本历史

时间 事件
2009 Facebook 工程师提出二进制协议草案
2009 Memcached 1.3.x 开始支持
2011 协议规范基本稳定
2018 Meta 协议作为补充出现

当前状态

注意: 二进制协议在 2018 年后已不再积极开发,Memcached 社区将重心转向了 Meta 协议。但二进制协议仍然完全支持,且在某些客户端库中是默认选择。


6.2 协议帧结构

请求帧格式

Byte/     0       |       1       |       2       |       3
     /------------+---------------+---------------+---------------\
    |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
    +---------------+---------------+---------------+---------------+
 0  | Magic         | Opcode        | Key length                    |
    +---------------+---------------+---------------+---------------+
 4  | Extras length | Data type     | Reserved / Status             |
    +---------------+---------------+---------------+---------------+
 8  | Total body length                                             |
    +---------------+---------------+---------------+---------------+
12  | Opaque                                                        |
    +---------------+---------------+---------------+---------------+
16  | CAS (8 bytes)                                                 |
    |                                                               |
    +---------------+---------------+---------------+---------------+
24  | Extras (variable length)                                      |
    +---------------+---------------+---------------+---------------+
    | Key (variable length)                                         |
    +---------------+---------------+---------------+---------------+
    | Value (variable length)                                       |
    +---------------+---------------+---------------+---------------+

响应帧格式

Byte/     0       |       1       |       2       |       3
     /------------+---------------+---------------+---------------\
    |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
    +---------------+---------------+---------------+---------------+
 0  | Magic         | Opcode        | Key length                    |
    +---------------+---------------+---------------+---------------+
 4  | Extras length | Data type     | Status                        |
    +---------------+---------------+---------------+---------------+
 8  | Total body length                                             |
    +---------------+---------------+---------------+---------------+
12  | Opaque                                                        |
    +---------------+---------------+---------------+---------------+
16  | CAS (8 bytes)                                                 |
    |                                                               |
    +---------------+---------------+---------------+---------------+
24  | Extras (variable length)                                      |
    +---------------+---------------+---------------+---------------+
    | Key (variable length)                                         |
    +---------------+---------------+---------------+---------------+
    | Value (variable length)                                       |
    +---------------+---------------+---------------+---------------+

6.3 头部字段详解

Magic(魔数)

方向 说明
0x80 请求 标识这是一个请求帧
0x81 响应 标识这是一个响应帧
MAGIC_REQUEST  = 0x80
MAGIC_RESPONSE = 0x81

Opcode(操作码)

操作码指定请求的操作类型,详见下表:

Opcode 名称 说明
0x00 Get 获取
0x01 Set 设置
0x02 Add 添加
0x03 Replace 替换
0x04 Delete 删除
0x05 Increment 递增
0x06 Decrement 递减
0x07 Quit 退出
0x08 Flush 清空
0x09 GetQ 静默获取
0x0A No-op 空操作
0x0B Version 版本
0x0C GetK 获取 + 返回 Key
0x0D GetKQ 静默获取 + 返回 Key
0x0E Append 追加
0x0F Prepend 前插
0x10 Stat 统计
0x11 SetQ 静默设置
0x12 AddQ 静默添加
0x13 ReplaceQ 静默替换
0x14 DeleteQ 静默删除
0x15 IncrementQ 静默递增
0x16 DecrementQ 静默递减
0x17 QuitQ 静默退出
0x18 FlushQ 静默清空
0x19 AppendQ 静默追加
0x1A PrependQ 静默前插
0x20 SASL List SASL 机制列表
0x21 SASL Auth SASL 认证
0x22 SASL Step SASL 认证步骤

Key Length(键长度)

  • 16 位无符号整数(Big Endian)
  • 指定 key 字段的字节长度
  • 可以为 0(某些命令不需要 key)

Extras Length(附加数据长度)

  • 8 位无符号整数
  • 指定 Extras 字段的字节长度
  • Extras 位于 Body 的最前面

Data Type(数据类型)

  • 8 位无符号整数
  • 目前只定义了 0x00(Raw bytes)

Status(状态码)

仅在响应帧中有效,详见状态码表。

Total Body Length(总 Body 长度)

  • 32 位无符号整数(Big Endian)
  • 等于 extras_length + key_length + value_length

Opaque(不透明标识)

  • 32 位整数
  • 客户端设置,服务端原样返回
  • 用于将请求与响应关联(异步场景)

CAS

  • 64 位无符号整数(Big Endian)
  • CAS 唯一标识符
  • GetQ/GetKQ 命令中可省略

6.4 状态码

状态码表

状态码 名称 说明
0x0000 Success 操作成功
0x0001 Key Not Found Key 不存在
0x0002 Key Exists Key 已存在(CAS 冲突)
0x0003 Value Too Large Value 过大
0x0004 Invalid Arguments 无效参数
0x0005 Item Not Stored Item 未存储
0x0006 Non-numeric Value 非数值(incr/decr)
0x0007 Not My VBucket 不属于当前 vBucket
0x0008 Auth Error 认证错误
0x0009 Auth Continue 认证继续
0x0020 Auth Challenge 认证挑战
0x0081 Unknown Command 未知命令
0x0082 Out of Memory 内存不足
0x0083 Not Supported 不支持
0x0084 Internal Error 内部错误
0x0085 Busy 忙碌
0x0086 Temporary Failure 临时失败

状态码分类

def classify_status(status: int) -> str:
    if status == 0x0000:
        return "SUCCESS"
    elif status <= 0x0009:
        return "CLIENT_ERROR"  # 业务错误
    elif status <= 0x0080:
        return "RESERVED"
    else:
        return "SERVER_ERROR"  # 服务端错误

6.5 二进制帧编解码

Python 实现

#!/usr/bin/env python3
"""binary_protocol.py — Memcached 二进制协议编解码器"""

import struct
from dataclasses import dataclass
from typing import Optional

# 常量
MAGIC_REQUEST  = 0x80
MAGIC_RESPONSE = 0x81

HEADER_FORMAT = "!BBHBBHIIQ"  # 24 字节头部
HEADER_SIZE = 24

# 操作码
OP_GET      = 0x00
OP_SET      = 0x01
OP_ADD      = 0x02
OP_REPLACE  = 0x03
OP_DELETE   = 0x04
OP_INCR     = 0x05
OP_DECR     = 0x06
OP_QUIT     = 0x07
OP_FLUSH    = 0x08
OP_VERSION  = 0x0B
OP_APPEND   = 0x0E
OP_PREPEND  = 0x0F
OP_STAT     = 0x10

# 状态码
STATUS_SUCCESS      = 0x0000
STATUS_KEY_NOT_FOUND = 0x0001
STATUS_KEY_EXISTS   = 0x0002
STATUS_VALUE_TOO_LARGE = 0x0003
STATUS_INVALID_ARGS = 0x0004
STATUS_NOT_STORED   = 0x0005
STATUS_NON_NUMERIC  = 0x0006
STATUS_UNKNOWN_CMD  = 0x0081
STATUS_OUT_OF_MEM   = 0x0082
STATUS_INTERNAL_ERR = 0x0084

@dataclass
class BinaryRequest:
    opcode: int
    key: bytes = b""
    value: bytes = b""
    extras: bytes = b""
    cas: int = 0
    opaque: int = 0

    def encode(self) -> bytes:
        key_length = len(self.key)
        extras_length = len(self.extras)
        body_length = extras_length + key_length + len(self.value)

        header = struct.pack(
            HEADER_FORMAT,
            MAGIC_REQUEST,   # magic
            self.opcode,     # opcode
            key_length,      # key length
            extras_length,   # extras length
            0x00,            # data type
            0x0000,          # reserved
            body_length,     # total body length
            self.opaque,     # opaque
            self.cas         # cas
        )

        return header + self.extras + self.key + self.value

@dataclass
class BinaryResponse:
    opcode: int
    status: int
    key: bytes
    value: bytes
    extras: bytes
    cas: int
    opaque: int

    @classmethod
    def decode(cls, data: bytes) -> 'BinaryResponse':
        if len(data) < HEADER_SIZE:
            raise ValueError("数据不足")

        (magic, opcode, key_length, extras_length,
         data_type, status, body_length,
         opaque, cas) = struct.unpack(HEADER_FORMAT, data[:HEADER_SIZE])

        if magic != MAGIC_RESPONSE:
            raise ValueError(f"无效的响应魔数: 0x{magic:02x}")

        body = data[HEADER_SIZE:HEADER_SIZE + body_length]
        extras = body[:extras_length]
        key = body[extras_length:extras_length + key_length]
        value = body[extras_length + key_length:]

        return cls(
            opcode=opcode,
            status=status,
            key=key,
            value=value,
            extras=extras,
            cas=cas,
            opaque=opaque
        )

    @property
    def success(self) -> bool:
        return self.status == STATUS_SUCCESS


# 编码示例
req = BinaryRequest(
    opcode=OP_SET,
    key=b"user:1001",
    value=b'{"name":"Bob"}',
    extras=struct.pack(">II", 0, 3600)  # flags + exptime
)
encoded = req.encode()
print(f"请求帧大小: {len(encoded)} 字节")
print(f"Hex: {encoded.hex()}")

6.6 请求-响应匹配

使用 Opaque 关联

def send_request_with_opaque(sock, request: BinaryRequest, opaque: int):
    request.opaque = opaque
    sock.sendall(request.encode())

def recv_response(sock) -> BinaryResponse:
    header = b""
    while len(header) < HEADER_SIZE:
        chunk = sock.recv(HEADER_SIZE - len(header))
        if not chunk:
            raise ConnectionError("连接关闭")
        header += chunk

    (_, _, key_length, extras_length, _, _, body_length, _, _) = \
        struct.unpack(HEADER_FORMAT, header)

    body = b""
    while len(body) < body_length:
        chunk = sock.recv(body_length - len(body))
        if not chunk:
            raise ConnectionError("连接关闭")
        body += chunk

    return BinaryResponse.decode(header + body)

6.7 文本协议 vs 二进制协议对比

维度 文本协议 二进制协议
请求格式 set key 0 0 5\r\nhello\r\n 24B 头 + extras + key + value
响应格式 STORED\r\n 24B 头 + extras + key + value
解析复杂度 字符串分割 + 类型转换 固定偏移量 struct 解析
可调试性 telnet 直接调试 需要专门工具
扩展性 有限 灵活(extras 字段)
生产使用 最广泛 较少

6.8 业务场景

场景一:高性能客户端库

二进制协议适合构建高性能客户端库:

import struct
import socket
from concurrent.futures import ThreadPoolExecutor

class BinaryMemcachedClient:
    def __init__(self, host='127.0.0.1', port=11211):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port))
        self._opaque_counter = 0

    def _next_opaque(self) -> int:
        self._opaque_counter += 1
        return self._opaque_counter

    def set(self, key: str, value: bytes, flags: int = 0,
            exptime: int = 0, cas: int = 0) -> bool:
        extras = struct.pack(">II", flags, exptime)
        req = BinaryRequest(
            opcode=OP_SET,
            key=key.encode(),
            value=value,
            extras=extras,
            cas=cas,
            opaque=self._next_opaque()
        )
        self.sock.sendall(req.encode())
        resp = recv_response(self.sock)
        return resp.success

    def get(self, key: str) -> tuple[bytes, int, int] | None:
        req = BinaryRequest(
            opcode=OP_GET,
            key=key.encode(),
            opaque=self._next_opaque()
        )
        self.sock.sendall(req.encode())
        resp = recv_response(self.sock)

        if resp.status == STATUS_KEY_NOT_FOUND:
            return None

        flags = struct.unpack(">I", resp.extras[:4])[0] if resp.extras else 0
        return resp.value, flags, resp.cas

    def delete(self, key: str) -> bool:
        req = BinaryRequest(
            opcode=OP_DELETE,
            key=key.encode(),
            opaque=self._next_opaque()
        )
        self.sock.sendall(req.encode())
        resp = recv_response(self.sock)
        return resp.status == STATUS_SUCCESS

    def version(self) -> str:
        req = BinaryRequest(opcode=OP_VERSION, opaque=self._next_opaque())
        self.sock.sendall(req.encode())
        resp = recv_response(self.sock)
        return resp.value.decode()

    def close(self):
        req = BinaryRequest(opcode=OP_QUIT, opaque=self._next_opaque())
        self.sock.sendall(req.encode())
        self.sock.close()


# 使用
client = BinaryMemcachedClient()
print(f"Version: {client.version()}")
client.set("test", b"hello", flags=0, exptime=300)
result = client.get("test")
if result:
    print(f"Value: {result[0].decode()}")
client.close()

6.9 注意事项

编号 注意事项 说明
1 字节序 所有多字节字段使用 Big Endian(网络字节序)
2 头部固定 24 字节 不可变长
3 Body 长度验证 extras_length + key_length + value_length == body_length
4 魔数校验 响应必须以 0x81 开头
5 CAS 为 0 请求中 CAS=0 表示不做 CAS 检查

6.10 扩展阅读


上一章: 第05章 检索命令深入 下一章: 第07章 二进制协议命令 — 各操作对应的二进制帧格式详解。