Skip to content

基于 TCP 的自定义应用层协议研究:以 Kafka、Redis、MySQL 为例

概览

结合 Kafka、Redis 和 MySQL,分析基础设施系统为什么会在 TCP 之上设计自定义应用层协议,以及这种设计在性能、语义表达和协议演进上的价值。

摘要

在高性能中间件、数据库、消息队列和缓存系统中,很多系统并没有直接采用 HTTP、REST 或 gRPC 作为内部通信协议,而是在 TCP 之上设计自己的应用层协议。严格来说,这不是“自定义 TCP 协议”,而是“基于 TCP 的自定义应用层协议”。TCP 负责可靠、有序、面向连接的字节流传输,而 Kafka、Redis、MySQL 等系统在 TCP 字节流之上定义自己的报文格式、请求响应模型、命令语义、版本协商、错误码、批量传输、认证流程和扩展机制。

本文认为:自定义 TCP 应用层协议不是为了炫技,而是为了让网络协议与系统的核心数据模型、性能目标和演进机制深度绑定。 对 Kafka、Redis、MySQL 这类基础设施软件而言,HTTP/gRPC 虽然成熟,但它们携带了通用 RPC 或 Web 语义;而消息队列、缓存、数据库需要的是更贴近自身领域模型的通信协议。因此,自定义协议的价值主要体现在:降低协议开销、提升解析效率、支持批量与流水线、表达领域语义、控制连接生命周期、实现精细版本兼容、便于多语言客户端长期演进。

关键词:TCP、自定义协议、Kafka Protocol、Redis RESP、MySQL Protocol、HTTP、gRPC、二进制协议、应用层协议


一、概念澄清:不是自定义 TCP,而是自定义 TCP 之上的应用层协议

工程中常说“自定义 TCP 协议”,这个说法并不严谨。TCP 是传输层协议,它只提供可靠字节流,不理解“请求”“响应”“命令”“SQL”“消息”“分区”“offset”这些业务概念。真正被自定义的是 TCP 之上的应用层协议。

也就是说,系统设计者通常不会修改 TCP 本身,而是设计如下内容:

text
TCP 连接

应用层报文边界

协议头

请求类型 / 命令类型

序列号 / correlation id

协议版本

认证信息

业务 payload

错误码 / 响应结构

例如 Kafka 官方协议文档明确说明,Kafka 使用的是 binary protocol over TCP,所有 API 都被定义为请求-响应消息对,消息本身带有长度边界。Kafka 客户端建立 socket 连接后,会连续写入请求并读取对应响应;Kafka 还强调长期复用 TCP 连接,以摊销 TCP 握手成本。(Apache Kafka)

所以,本文讨论的“自定义 TCP 协议”,本质是:

在 TCP 可靠字节流之上,设计面向特定系统语义的应用层协议。

这个概念非常关键。因为一旦把它误解成“重新发明 TCP”,方向就错了;真正有价值的是在 TCP 之上定义更贴合业务系统的数据交换格式和交互模型


二、为什么要自定义 TCP 应用层协议?

我的判断是:只有当通用协议的抽象成本开始伤害系统核心目标时,自定义协议才是合理的。 对大多数普通业务系统,HTTP/gRPC 足够好;但对 Kafka、Redis、MySQL 这类基础设施系统,自定义协议几乎是必然选择。

1. 为了获得更低的协议开销

HTTP 是为 Web 资源访问设计的协议,天然携带 method、path、header、status code、content-type、cookie、cache-control 等语义。gRPC 虽然比传统 REST 更适合服务间通信,但它本质上运行在 HTTP/2 之上。gRPC 官方协议说明中,请求头通过 HTTP/2 HEADERS 和 CONTINUATION frames 传递,并包含 :method:scheme:pathcontent-typegrpc-timeoutgrpc-encoding 等字段。(grpc.github.io)

这些机制非常强大,但对 Kafka、Redis、MySQL 来说,其中很多字段不是核心需求。

例如 Kafka 生产消息时,真正关心的是:

text
topic
partition
acks
timeout
record batch
compression
producer id
sequence number
transaction marker

Redis 执行命令时,真正关心的是:

text
command
key
arguments
reply type
pipeline order
push message

MySQL 查询时,真正关心的是:

text
handshake
capability flags
authentication
command type
SQL payload
result set
column metadata
row data
OK packet
ERR packet

这些领域语义用 HTTP 表达不是不可以,但会出现一层不必要的“语义转译”:

text
Kafka ProduceRequest → HTTP POST /produce
Redis GET key       → HTTP GET /redis/key
MySQL query         → HTTP POST /query

这在业务网关中可以接受,但对底层基础设施来说是多余的。自定义协议可以直接表达领域对象,减少无关字段、无关解析、无关抽象。


2. 为了获得稳定、明确、低成本的报文边界

TCP 是字节流协议,不保留消息边界。应用层协议必须自己解决:

text
一次 read() 读到半包怎么办?
一次 read() 读到多个包怎么办?
payload 多大?
怎么判断一条消息结束?

常见方案有三类:

text
1. 固定长度头 + payload length
2. 分隔符,例如 CRLF
3. TLV / VarInt / frame-based encoding

Kafka 使用的是长度分隔的二进制协议。Kafka 文档说明,所有消息都是 size-delimited,并由一组基础类型组成;其中 BYTES 类型也是先给出长度,再跟随原始字节。(Apache Kafka)

Redis RESP 也非常典型。RESP 使用首字节标识数据类型,bulk string 使用 $<length>\r\n<data>\r\n 格式。官方文档明确指出,RESP 使用长度前缀传输 bulk data,因此不需要像 JSON 那样扫描 payload 中的特殊字符,也不需要引号转义;bulk data 可以通过一次读取处理。(Redis)

MySQL 协议同样采用明确的 packet 格式。MySQL 官方文档说明,客户端和服务端之间的数据以 packet 交换,packet header 中包含 3 字节 payload length、1 字节 sequence id,payload 最大 16MB。(MySQL开发者专区)

这就是自定义协议的第一个核心优势:你可以把消息边界设计得非常适合自己的数据模型和 IO 模型。


3. 为了让协议直接服务于系统核心语义

HTTP/gRPC 的优势是通用,但通用也意味着它们不天然理解某个中间件的领域模型。

Kafka 的核心不是“调用一个远程方法”,而是:

text
主题 topic
分区 partition
副本 replica
leader broker
offset
record batch
consumer group
fetch position
事务
幂等生产
元数据发现

Kafka 协议直接把这些语义设计进请求和响应中。例如 Kafka 文档说明,客户端需要根据 topic partition 找到对应 leader broker;如果请求发错 broker,会收到 NotLeaderForPartition 错误;客户端通过 metadata request 获取当前集群、topic、partition、leader、broker host/port 等信息。(Apache Kafka)

这不是普通 RPC 语义,而是 Kafka 自身的分布式日志语义。

Redis 也是类似。Redis RESP 不是单纯传输 JSON,而是围绕 Redis 命令模型设计:客户端发送由 bulk strings 组成的数组,第一个 bulk string 通常是命令名,后续元素是参数;服务端根据命令返回 RESP 类型。(Redis)

MySQL 更明显。MySQL 协议不是“远程调用 executeSQL 方法”,而是围绕数据库连接生命周期设计:服务端握手、客户端响应、认证、能力协商、命令阶段、查询响应、结果集、错误包、OK 包。MySQL 官方 packet 文档展示了 packet header 和 sequence id;HandshakeV10 文档还描述了 SSL capability、SSLRequest、HandshakeResponse 等连接阶段行为。(MySQL开发者专区)

这说明一个关键事实:

自定义协议最大的价值不是“更快”,而是“协议本身就是系统模型的一部分”。


三、什么场景下应该自定义 TCP 应用层协议?

我认为,只有以下场景才值得自定义协议。否则,直接用 HTTP/gRPC 更稳妥。

1. 高吞吐、低延迟、长连接密集型系统

典型代表:

text
消息队列:Kafka、Pulsar、RocketMQ
缓存系统:Redis、Memcached
数据库:MySQL、PostgreSQL
RPC 框架:Dubbo、Thrift、自研 RPC
服务网格内部数据面
实时推送系统
网关与边缘代理
游戏服务器
金融行情系统

这些系统通常具有几个特点:

text
请求非常频繁
连接长期存在
payload 结构稳定
协议解析成本敏感
客户端数量多
服务端需要极致控制内存与 IO
需要批量、流水线、异步响应

如果一个系统每天只有几万次调用,HTTP/gRPC 的协议开销不是主要问题;但如果一个 broker、cache node 或 database node 每秒处理几十万、上百万请求,那么每个请求多解析几个 header、多做几次对象分配、多走几层通用抽象,都会变成真实成本。


2. 协议需要深度绑定领域语义

只要你的协议里天然存在这些概念,就适合自定义:

text
分区
offset
cursor
sequence id
事务 marker
批量 records
流式 fetch
订阅推送
认证插件
能力协商
自定义错误码
压缩标记
幂等标识
重试语义

Kafka 就是典型。Kafka 协议不是普通 RPC,它需要表达 produce、fetch、metadata、offset、group、transaction、api version 等一整套消息队列语义。Kafka 文档还强调,其 API 鼓励把小消息批量化;produce 和 fetch API 都面向一组消息,而不是单条消息,并且允许一次 produce/fetch 跨多个 topic 和 partition。(Apache Kafka)

这个设计如果强行套 HTTP,也可以做,但会非常别扭:

http
POST /topics/{topic}/partitions/{partition}/records
POST /consumer-groups/{group}/offsets
POST /metadata
POST /fetch

表面上符合 REST,实际已经不是资源建模,而是在 HTTP 里模拟 Kafka 协议。

我的判断是:一旦你开始在 HTTP path 和 header 里模拟另一个协议,就说明你真正需要的是自定义协议。


3. 需要长期兼容、多语言客户端和灰度升级

基础设施软件的协议一旦发布,就很难随意破坏兼容性。Kafka 在协议兼容性上做得非常典型:Kafka 文档说明,Kafka 使用 API key 和 API version 标识后续消息 schema;新客户端可以连接旧服务端,旧客户端也可以连接新服务端;客户端应使用双方都支持的最高 API version。(Apache Kafka)

这类版本协商非常适合放在自定义协议层,而不是依赖 HTTP 之外的约定。

自定义协议可以内建:

text
magic number
protocol version
api version
feature flags
capability negotiation
extension fields
tagged fields
deprecated marker
client name/client version
server feature list

MySQL 也类似,它通过 capability flags 表示客户端和服务端支持或希望启用的功能,例如 SSL 能力、认证方式、连接属性等。MySQL HandshakeV10 文档说明,如果客户端支持 SSL 并设置了相关 capability,会发送 SSLRequest,使服务端建立 SSL 层并等待后续 packet。(MySQL开发者专区)

这种协议演进能力,对数据库和中间件非常重要。


4. 需要控制连接生命周期和 IO 模型

HTTP/1.1、HTTP/2、gRPC 都有自己的连接、流、多路复用、header、trailers、flow control 模型。对普通业务服务来说,这是好事;但对底层中间件来说,通用模型可能反而限制实现。

Kafka 文档建议维持持久连接,以摊销 TCP handshake 成本;同时,Kafka 客户端通常不需要对单个 broker 建立多个连接池连接。Kafka 服务端还保证在单个 TCP 连接上,请求按发送顺序处理,响应也按顺序返回;客户端可使用非阻塞 IO 做 request pipelining,提高吞吐。(Apache Kafka)

Redis 也有 pipeline 机制。Redis 官方文档说明,Redis requests 可以被 pipelined,客户端可以一次发送多个命令,然后稍后等待响应。(Redis)

这些能力都与连接模型高度相关。自定义协议可以让系统明确规定:

text
一个连接上是否允许多个 in-flight 请求
响应是否必须按序返回
是否支持 pipeline
是否支持 server push
是否支持订阅模式
是否允许半双工 / 全双工
如何处理连接认证状态
如何处理协议切换
如何限流、踢连接、关闭连接

这类细节如果建立在 HTTP/gRPC 之上,很多时候要么受限于框架,要么要绕过框架。


四、自定义一个 TCP 应用层协议有什么好处?

1. 性能可控:更少字节、更少解析、更少对象分配

自定义协议可以做到:

text
固定长度字段
长度前缀
二进制整数
紧凑枚举
批量 payload
零拷贝友好
直接映射内存结构
减少字符串解析
减少 header map 构造

Kafka 的 primitive types 使用明确的二进制编码,例如 INT16、INT32、INT64、UUID、BYTES 等字段都有确定编码方式。(Apache Kafka)

Redis RESP 虽然兼顾可读性,但官方文档仍然强调它可以达到接近二进制协议的性能,因为 bulk string 采用长度前缀,不需要扫描 payload 的特殊字符,也不需要 quoting/escaping。(Redis)

这就是为什么 Redis 没有选择 JSON over HTTP。JSON 可读性强,但会引入:

text
字段名重复传输
字符串转义
数字解析
结构扫描
额外对象分配
无法天然表达二进制 blob

而 Redis 的命令天然就是:

text
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n

这比:

http
POST /redis
Content-Type: application/json

{"command":"GET","key":"key"}

更贴近 Redis 自己的模型,也更容易解析。


2. 协议可以天然支持批量传输

Kafka 的设计尤其说明问题。Kafka 不是把每条消息都当成一次 RPC,而是鼓励批量。Kafka 文档说明,produce 和 fetch API 都处理消息序列,并且允许一次请求跨多个 topic 和 partition。(Apache Kafka)

这对 Kafka 的性能是决定性的。因为消息队列的核心成本包括:

text
系统调用
网络包
broker 请求调度
磁盘 append
page cache
副本复制
consumer fetch
压缩 / 解压

如果每条消息都走一次 HTTP/gRPC 调用,即使单次调用很快,整体吞吐也会被请求粒度拖垮。

Kafka 的协议设计可以直接表达:

text
ProduceRequest
  topic A
    partition 0
      record batch
    partition 1
      record batch
  topic B
    partition 3
      record batch

这不是普通 RPC 的“调用方法”,而是消息系统特有的批量数据交换模型。


3. 可以精确表达错误码和状态机

HTTP 状态码只有一套通用语义:

text
200 OK
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
500 Internal Server Error
503 Service Unavailable

但 Kafka 需要表达:

text
NotLeaderForPartition
UnknownTopicOrPartition
OffsetOutOfRange
CoordinatorNotAvailable
RebalanceInProgress
UnsupportedVersion
TopicAuthorizationFailed

MySQL 需要表达:

text
OK packet
ERR packet
EOF packet
ResultSet packet
AuthSwitchRequest
AuthMoreData
LocalInFileRequest

Redis 需要表达:

text
simple string
bulk string
integer
array
map
push
simple error
bulk error
null

Redis RESP 直接把错误作为一种协议数据类型,simple error 以 - 开头;客户端收到 error reply 时应将其视为异常。(Redis)

这类设计比“HTTP 200 + JSON body 里放 errorCode”更加底层、直接、稳定。


4. 可以更好地做协议演进

自定义协议可以把演进机制作为第一等公民:

text
version
feature flag
capability flag
extension field
reserved field
tagged field
optional field
deprecated field

Kafka 的 API version 机制非常典型。Kafka 文档说明,API key 和 API version 两个 16-bit 数字一起唯一标识后续消息 schema;服务端会按请求中的 version 返回客户端期望的协议格式。(Apache Kafka)

MySQL 则更偏 capability flags 风格。客户端和服务端通过能力位协商功能,例如是否支持 SSL、认证插件、连接属性等。(MySQL开发者专区)

这比在 HTTP header 里塞一堆私有字段更加系统化:

http
X-Protocol-Version: 3
X-Feature-A: true
X-Client-Capabilities: ...

不是不能做,而是既然已经大规模自定义 header,说明你已经在 HTTP 之上发明了另一个协议。


5. 可以减少对通用框架的依赖

对于基础设施系统,协议是生态入口。Kafka、Redis、MySQL 都需要大量多语言客户端。如果协议依赖某个具体 RPC 框架,就会带来生态锁定。

例如如果 Kafka 内部协议一开始基于 gRPC,那么所有客户端都要依赖 gRPC runtime、HTTP/2 stack、Protobuf 生态。这对 Java、Go、C++ 还好,但对某些语言、嵌入式环境、代理系统、抓包分析、协议网关、兼容实现都会增加门槛。

Kafka 自定义协议后,任何语言只要能操作 TCP socket 并按协议编码/解码,就可以实现客户端。Redis RESP 更是如此,它被设计为简单、快速解析、可读,官方文档也说明 RESP 是 Redis 客户端应该实现的协议。(Redis)

这就是基础设施协议和业务 RPC 的不同:

text
业务 RPC:优先开发效率
基础设施协议:优先生态稳定性、跨语言实现、长期兼容

五、为什么 Kafka 不直接使用 HTTP/gRPC?

这是本文最核心的问题。我的结论很明确:

Kafka 不适合直接使用 HTTP/gRPC 作为核心 broker 协议,因为 Kafka 的协议本质是面向分区日志、批量消息、offset、元数据发现、broker 路由、版本协商和高吞吐 IO 的领域协议,而不是普通的服务方法调用协议。

1. Kafka 的核心单位不是 RPC 方法,而是分区日志操作

gRPC 的抽象是:

text
Service.Method(Request) -> Response

Kafka 的抽象是:

text
Produce(topic, partition, record batch)
Fetch(topic, partition, offset, max bytes)
Metadata(topic)
OffsetCommit(group, topic, partition, offset)
FindCoordinator(group/transactional id)

虽然这些也可以包装成 RPC 方法,但 Kafka 的性能关键在于:

text
批量 records
跨 partition produce/fetch
压缩 record batch
长连接复用
非阻塞 IO
协议级错误码
metadata 驱动 broker 路由
api version 兼容

这些不是 gRPC 的默认优势,而是 Kafka 自己的协议优势。


2. Kafka 需要极强的批处理语义

Kafka 官方文档直接说明,其 API 鼓励 batching;produce 和 fetch 都不是面向单条消息,而是面向消息序列;甚至一次 produce/fetch 可以覆盖多个 topic 和 partition。(Apache Kafka)

如果用 HTTP/gRPC,当然也能传一个 batch:

proto
rpc Produce(ProduceRequest) returns (ProduceResponse);

但问题是:此时 gRPC 只是一个外壳,真正的 Kafka 协议语义仍然要自己定义在 Protobuf message 里。于是会出现两层协议:

text
HTTP/2 frame
  gRPC message
    Kafka ProduceRequest
      topic/partition/record batch

而 Kafka 现在是:

text
TCP
  Kafka frame
    Kafka ProduceRequest
      topic/partition/record batch

少一层抽象,就少一层解析、约束和依赖。


3. Kafka 需要面向 broker/partition 的路由能力

Kafka 不是所有请求都可以发给任意 broker。客户端需要知道某个 topic partition 当前 leader 是哪个 broker,然后把 produce/fetch 请求发给正确 broker。Kafka 文档说明,客户端通过 metadata request 获取 broker、topic、partition、leader 等信息;如果 metadata 过期,会通过 socket error 或响应错误码触发刷新。(Apache Kafka)

这套模型和 HTTP 的负载均衡思想不同。

HTTP/gRPC 常见模型是:

text
client → load balancer → any healthy backend

Kafka 模型是:

text
client → metadata → partition leader broker

也就是说,Kafka 客户端本身就是协议级路由参与者。HTTP/gRPC 的通用负载均衡模型并不能天然表达这种分区 leader 路由。


4. Kafka 需要协议级版本兼容

Kafka 的升级场景非常复杂:

text
老 client 连接新 broker
新 client 连接老 broker
broker 滚动升级
client SDK 分批升级
跨版本集群
新字段灰度引入

Kafka 通过 API version 协商解决这些问题。文档明确说明,新旧客户端和服务端双向兼容;客户端应选择双方支持的最高 API version。(Apache Kafka)

gRPC/Protobuf 也有兼容机制,但 Kafka 的兼容粒度是 Kafka API 本身,而不只是字段兼容。它需要知道某个 broker 支持哪些 Kafka API、哪些版本、哪些字段、哪些错误码。这套机制内建在 Kafka 协议里,比套在 HTTP/gRPC 上更直接。


六、Redis 为什么使用 RESP,而不是 HTTP/gRPC?

Redis 是另一个非常典型的例子。Redis 的协议目标不是“企业级 RPC”,而是:

text
简单
快速
可读
容易实现客户端
支持 pipeline
支持不同返回类型

Redis 官方文档明确说 RESP 是 Redis 客户端与 Redis Server 通信的 wire protocol,并且它在设计上折中考虑了简单实现、快速解析和人类可读性。(Redis)

1. Redis 命令模型非常简单,不需要 HTTP

Redis 命令天然就是:

text
COMMAND key arg1 arg2 ...

RESP 表达这个模型非常自然:

text
*2\r\n$3\r\nGET\r\n$3\r\nfoo\r\n

如果用 HTTP,则可能变成:

http
POST /redis
Content-Type: application/json

{
  "command": "GET",
  "args": ["foo"]
}

这对 Redis 来说是明显过度设计。

Redis 是内存数据库,很多命令的执行时间可能是微秒级。如果协议解析和对象构造比命令执行本身还重,那就是本末倒置。


2. RESP 兼顾可读性和高性能

RESP 不是纯二进制协议,但它非常聪明。它用首字节区分类型:

text
+ simple string
- error
: integer
$ bulk string
* array
% map
> push

RESP bulk string 使用长度前缀,可以承载任意二进制数据。官方文档指出,这种方式不需要扫描 payload 中的特殊字符,也不需要转义;因此实现可以接近二进制协议性能,同时又比很多二进制协议更容易在高级语言中实现。(Redis)

这就是优秀协议设计的典型特征:

text
不是盲目二进制化
而是在性能、可读性、实现复杂度之间取平衡

3. Redis pipeline 需要轻量协议

Redis 支持 pipeline:客户端可以一次发送多个命令,然后稍后读取响应。官方文档明确说明 Redis requests 可以 pipelined。(Redis)

pipeline 对 Redis 非常关键,因为 Redis 单命令通常很快,网络 RTT 反而可能成为瓶颈。RESP 的简单请求响应结构非常适合 pipeline:

text
C: GET a
C: GET b
C: INCR c

S: value-of-a
S: value-of-b
S: integer-result

HTTP/1.1 pipeline 早期存在复杂问题;HTTP/2 虽然支持多路复用,但引入了更复杂的 frame、stream、flow control。Redis 的需求没有那么复杂,它需要的是简单、顺序、低成本、容易实现

所以 Redis 使用 RESP 是非常正确的选择。


七、MySQL 为什么使用自己的协议,而不是 HTTP/gRPC?

数据库协议和普通 RPC 的差异更大。MySQL 不是简单地执行:

text
execute(sql) -> result

它实际涉及:

text
连接握手
协议版本
服务端 greeting
认证挑战
认证插件
SSL 切换
capability flags
默认数据库
字符集
prepared statement
query command
result set metadata
row data
OK/ERR packet
事务状态
session state

MySQL 官方文档说明,客户端与服务端之间的数据以 packet 交换,packet header 包含 payload length 和 sequence id。(MySQL开发者专区)

MySQL HandshakeV10 文档还说明,当客户端支持 SSL 且 capability flags 中开启相关能力时,会发送 SSLRequest,使服务端建立 SSL 层并等待后续客户端包。(MySQL开发者专区)

这说明 MySQL 协议本身就是数据库连接状态机。

1. 数据库连接是有状态的

HTTP 天然偏无状态请求响应。虽然可以通过 cookie、session、connection pool 维护状态,但这不是 HTTP 的底层设计目标。

MySQL 连接则天然有状态:

text
当前用户
当前 database
当前 charset
当前 transaction
当前 prepared statement
当前 session variables
当前 connection attributes
当前 authentication state

如果用 HTTP/gRPC 表达这些状态,就需要额外维护 session id、token、上下文映射,反而复杂。

MySQL 自定义协议可以直接把连接状态绑定在 TCP connection 上:

text
TCP connection = database session

这是非常自然的设计。


2. 数据库结果集需要专门协议表达

SQL 查询结果不是普通 JSON。它包含:

text
column count
column metadata
column name
schema
table
type
flags
decimals
row data
EOF/OK
warning count
server status

如果全部用 JSON 表达,会有巨大冗余:

json
[
  {"id": 1, "name": "a", "age": 18},
  {"id": 2, "name": "b", "age": 20}
]

每一行都重复字段名,类型信息也不紧凑。而数据库协议通常会先发送列元数据,再发送每行数据,这样更适合大量结果集传输。

因此,数据库协议使用自定义 packet 是合理的。


3. 数据库协议需要能力协商和认证扩展

MySQL 的 capability flags 是协议演进的重要机制。客户端和服务端可以通过能力位协商是否支持某些功能,例如 SSL、认证插件、连接属性等。(MySQL开发者专区)

这类机制如果强行放到 HTTP/gRPC 中,会变成一堆自定义 header 或 metadata,但底层状态机仍然要自己实现。那不如直接定义数据库协议。


八、自定义协议与 HTTP/gRPC 的关系:不是谁替代谁,而是边界不同

这里必须给出一个明确判断:

业务服务之间的普通调用,优先使用 HTTP/gRPC;基础设施内部高频协议,才考虑自定义 TCP 应用层协议。

适合 HTTP/REST 的场景

text
开放 API
管理后台
低频业务接口
浏览器访问
调试友好优先
网关统一治理
第三方集成
CRUD 资源模型

适合 gRPC 的场景

text
微服务内部调用
强 schema
多语言 SDK
双向流
服务治理
云原生生态
需要 HTTP/2 能力
团队不想维护底层协议

gRPC 官方说明其协议承载在 HTTP/2 framing 上,并定义了 request headers、length-prefixed message、trailers 等结构。(grpc.github.io) 这意味着 gRPC 本身已经是一个成熟的高性能 RPC 方案,普通业务系统没必要重新造协议。

适合自定义 TCP 协议的场景

text
数据库协议
缓存协议
消息队列协议
高性能 RPC 框架
实时推送协议
游戏协议
金融行情协议
服务网格数据面协议
边缘代理内部协议

判断标准不是“我会不会写 Netty”,而是:

text
协议是否是系统核心竞争力?
通用协议是否引入明显额外成本?
是否需要表达复杂领域语义?
是否需要极致控制连接和 IO?
是否需要跨语言长期兼容?
是否需要大规模客户端生态?
是否能承担协议维护成本?

如果答案大部分是“是”,才值得自定义。


九、自定义协议的代价

自定义协议不是免费午餐。它至少带来以下成本:

text
协议设计成本
编解码器实现成本
多语言 SDK 成本
兼容性测试成本
抓包调试成本
安全审计成本
文档维护成本
协议 fuzz testing 成本
灰度升级成本
生态适配成本

HTTP/gRPC 的优势在于:

text
生态成熟
工具丰富
可观测性好
网关支持好
代理支持好
安全模型成熟
调试方便
团队学习成本低

所以我不建议普通业务系统随便自定义协议。很多团队所谓“自定义 TCP 协议”,最后会写成:

text
length + JSON

然后又自己补:

text
request id
timeout
retry
auth
tracing
compression
version
error code
schema
IDL
SDK
load balancing
health check

这其实是在低质量复刻 gRPC。这样的自定义协议没有意义。

真正值得自定义的是 Kafka、Redis、MySQL 这种情况:协议与系统内核强绑定,并且长期维护协议本身就是产品能力的一部分。


十、一个优秀自定义 TCP 协议应该怎么设计?

如果你要设计自己的协议,我建议至少考虑以下结构。

1. 基础帧格式

text
+----------------+----------------+----------------+----------------+
| magic          | version        | header length  | body length    |
+----------------+----------------+----------------+----------------+
| request id / correlation id                                      |
+------------------------------------------------------------------+
| command / api key                                                |
+------------------------------------------------------------------+
| flags                                                            |
+------------------------------------------------------------------+
| header extensions                                                |
+------------------------------------------------------------------+
| body                                                             |
+------------------------------------------------------------------+

2. 必备字段

text
magic number        // identify protocol
protocol version    // protocol evolution
request id          // match request and response
command/api key     // operation type
flags               // compression, encryption, tracing, etc.
body length         // solve TCP stream boundary
status/error code   // response status

3. 推荐能力

text
version negotiation
capability negotiation
heartbeat
authentication
authorization
compression
batching
pipeline
backpressure
server push
graceful close
structured error code
tracing context
client name/version
extension fields

4. 必须避免的错误

text
没有 length,靠 read() 次数判断一条消息
没有版本号
没有 request id
错误码只用字符串
协议文档缺失
服务端和客户端实现互相猜
没有 fuzz test
没有兼容性测试
没有抓包工具
没有最大包大小限制
没有慢客户端保护
没有认证状态机

尤其是第一条非常致命。TCP 是字节流,不能假设一次 read 就是一条完整消息。


十一、结论

自定义 TCP 应用层协议的本质,是让协议从“通用通信壳”变成“系统核心模型的一部分”。

Kafka 选择自定义二进制协议,是因为它需要服务于分区日志、broker 路由、metadata、batch、fetch、offset、API version 和高吞吐长连接。Redis 选择 RESP,是因为它需要简单、快速、可读、低成本解析,并天然支持命令模型和 pipeline。MySQL 选择自己的协议,是因为数据库连接是有状态的,需要握手、认证、能力协商、结果集元数据、行数据和会话状态。

HTTP/gRPC 是优秀的通用协议,但它们并不总是适合基础设施系统的内部核心协议。我的最终判断是:

普通业务系统不要轻易自定义协议;高性能基础设施系统应该认真考虑自定义协议。 当协议只是通信工具时,用 HTTP/gRPC;当协议本身就是系统模型、性能路径和生态边界时,应该自定义基于 TCP 的应用层协议。

从 Kafka、Redis、MySQL 的经验看,自定义协议的真正收益不是单纯“更快”,而是:

text
更贴近领域模型
更低协议开销
更强连接控制
更好批量能力
更清晰状态机
更稳定版本演进
更适合多语言客户端生态
更容易成为基础设施产品的长期边界

这也是为什么优秀的中间件和数据库系统,最终往往都会拥有自己的 wire protocol。

GitHub Discussions

参与讨论

评论会同步到 stellhub/stell-web 仓库的 GitHub Discussions。

Powered by VitePress and GitHub Discussions.