第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章 二进制协议命令 — 各操作对应的二进制帧格式详解。