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

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

第06章 二进制协议基础

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


6.1 二进制协议概述

设计动机

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

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

版本历史

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

当前状态

注意: 二进制协议在 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名称说明
0x00Get获取
0x01Set设置
0x02Add添加
0x03Replace替换
0x04Delete删除
0x05Increment递增
0x06Decrement递减
0x07Quit退出
0x08Flush清空
0x09GetQ静默获取
0x0ANo-op空操作
0x0BVersion版本
0x0CGetK获取 + 返回 Key
0x0DGetKQ静默获取 + 返回 Key
0x0EAppend追加
0x0FPrepend前插
0x10Stat统计
0x11SetQ静默设置
0x12AddQ静默添加
0x13ReplaceQ静默替换
0x14DeleteQ静默删除
0x15IncrementQ静默递增
0x16DecrementQ静默递减
0x17QuitQ静默退出
0x18FlushQ静默清空
0x19AppendQ静默追加
0x1APrependQ静默前插
0x20SASL ListSASL 机制列表
0x21SASL AuthSASL 认证
0x22SASL StepSASL 认证步骤

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 状态码

状态码表

状态码名称说明
0x0000Success操作成功
0x0001Key Not FoundKey 不存在
0x0002Key ExistsKey 已存在(CAS 冲突)
0x0003Value Too LargeValue 过大
0x0004Invalid Arguments无效参数
0x0005Item Not StoredItem 未存储
0x0006Non-numeric Value非数值(incr/decr)
0x0007Not My VBucket不属于当前 vBucket
0x0008Auth Error认证错误
0x0009Auth Continue认证继续
0x0020Auth Challenge认证挑战
0x0081Unknown Command未知命令
0x0082Out of Memory内存不足
0x0083Not Supported不支持
0x0084Internal Error内部错误
0x0085Busy忙碌
0x0086Temporary 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\n24B 头 + extras + key + value
响应格式STORED\r\n24B 头 + 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 字节不可变长
3Body 长度验证extras_length + key_length + value_length == body_length
4魔数校验响应必须以 0x81 开头
5CAS 为 0请求中 CAS=0 表示不做 CAS 检查

6.10 扩展阅读


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