TCP 协议示意
关于七层网络通信的基本原理,特别推荐这篇图文并茂的长文《TCP/IP笔记 - 综述》
TCP 通信基本特征
特征
1. 消息(结构化数据)被编码成字节流写入 TCP 通道。
2. TCP 通道不能保证字节流一定到达目的地,但能保证到达的字节流是 正确、有序 的。对于发送端而言,可以不停的写入数据,当网络出问题 ACK 超时会报错,但由于缓存的存在,发送端其实不知道有多少数据到达接收端。对于接收端而言,一直等待接收数据,一旦收到数据是能确定这些数据是连续、有序的,中间不可能有数据缺失,但接收端无法知道何时能收到下一个数据包。
3. TCP 通道像一个无形的管道,这个管道的流量由全链路复杂的网络环境决定,TCP 协议会自动调节(即拥塞控制)。
4. TCP 是全双工协议,读写互不干扰。注意,如图所示,读写是两个完全不同的通道,它们完全可能走不同的物理链路。
疑问
1. 对于接收端来说,虽然能接收到正确的有序的字节流,如何界定收到的字节流构成一个完整的消息体?这就是所谓 “”粘包” 问题。
2. 对于接收端来说,一直在等待读取数据,如何判断发送端是空闲还是失联?这就是 超时问题。
基于 TCP 构建数据通信协议,首先要解决的就是上面两个问题。另外,不管是客户端还是服务端,它们都同时是发送端和接收端。从应用层面来说,我们还需要构建 请求(Request)/响应(Response) 机制,比如浏览器调用后端 API 服务需要知道结果,或者消息服务器往客户端推送消息需要知道消息是否被客户端处理。当然也有一种消息从发送端发出后是不需要知道结果的,这种消息通常称为 通知(Notification)。
基于 TCP 协议构建应用层的通信协议
两个模式和三个问题
TCP 协议本质是流模式,基于它可以构建各种应用层通信协议,但其基本模式只有两种:
1. Streaming 流模式,如 HTTP/1 协议,redis 协议。
2. Multiplexing 多路复用模式,如 MongoDB 协议、常见的 RPC 协议。
随着技术的发展,也出现了这两种基本模式的混合体:
1. Streaming + Multiplexing,如基于 HTTP/1 实现的 JSON-RPC 协议。
2. Multiplexing + Streaming,如 HTTP/2,基于 HTTP/2 的 gRPC 等。
根据 TCP 协议的特征,我们应用层协议一般要解决三个问题:
1. 粘包问题,从字节流中分解出一个个独立的消息体;
2. 请求/响应机制;
3. 消息指令或类型定义,解决超时问题和实际应用的通信需求。
Streaming
最原始的 Streaming 模式就是一应一答模式。
相信不少人基于 TCP 开发网络通信时干过这种事:把一个请求数据变成字节写入 TCP,再等待对方的应答数据,收到应答后开始下一个请求。HTTP/1.0 就是这个模式的最典型。
用上面的管道示意图来理解,每次往管道放入一个数据包,然后等对方回复一个数据包,从而实现应用层需要的 请求(Request)/响应(Response) 机制。
这种模式下通信效率显然特别低,为了提升效率得开多个 TCP 通道,然而打开 TCP 通道不但有三次握手开销,还给服务器带来一定资源开销压力,特别是 Apache 那种传统的 web 服务。
既然是管道,其实是可以像流体一样不断的写入数据包,只要定义 请求(Request)/响应(Response) 的逻辑关系,这就是 Pipelining 机制,HTTP/1.1 和 redis 协议属于这种模式。
比如 HTTP/1.1 协议,允许客户端依次写入多个请求而无需等待应答,服务端则应该按照客户端的请求顺序依次进行响应,从而确保 请求(Request)/响应(Response) 一一对应。
HTTP/1
HTTP/1 是基于文本的协议:
1. 通过 CRLF (也就是 \r\n) 标志解决粘包问题,如果内容中有 \r\n,必须进行转义,第一行命令行、Headers 头部和实体主体各有不同的转义处理。
2. 请求/响应机制就是一应一答模式或者 Pipelining。
3. 第一行定义了丰富的消息类型,超时机制则是在浏览器或者服务端逻辑实现,协议层没有定义。
Redis 的 RESP 协议
RESP 也是基于文本的协议:
1. 定义了五种消息类型,分别由 +、-、:、$、* 字符开头,CRLF 结尾,其中 Bulk Strings 类型允许包含 CRLF。
2. 请求/响应机制是复杂的 Pipelining,允许客户端不断的写入请求,服务端会按照顺序响应请求。但是,根据请求命令的不同,对应响应体会有 零到无数 个。
3. 协议层定义了五种消息类型,其中的 Arrays 是结构化消息类型。对比 HTTP 协议,RESP 协议不用依赖于更上一层的 JSON、XML 协议等,就能构造出复杂的消息体。超时问题依然由客户端或服务端的逻辑实现。
HTTP/1 协议和 RESP 协议可以算是我们当前使用最广泛的协议,有很多服务都是基于 RESP 协议。然而,即便应用最广,也有 Pipelining 机制,基于 Streaming 的协议依然有一个痛点:头部阻塞,也就是如果某一个请求需要消耗很长处理时间才能响应,后续响应都得排队等候,即被阻塞。
Multiplexing
这是一种解决头部阻塞问题的更高效的模式,它不在依赖于 请求(Request)/响应(Response) 的顺序处理,允许请求并发发出,请求处理完成就立即响应,其核心就是 Request ID。
MongoDB 协议
MongoDB 协议 是基于二进制的协议,协议定义的内容很丰富:
1. 一个完整消息由 Header 和 Body 组成。Header 有 16 位,定义如下,Body 的长度则在 Header 中的 messageLength 定义,编码格式则是 bson。
2. Header 中的 requestID 是请求 ID,responseTo 是响应 ID,所以其请求/响应机制是Multiplexing。
3. Header 中的 opCode 定义了 11 中消息类型:
struct MsgHeader {
int32 messageLength; // total message size, including this
int32 requestID; // identifier for this message
int32 responseTo; // requestID from the original request
int32 opCode; // request type - see table below
}
Opcode 表:
| Opcode Name | Value | Comment |
|----------|-------|--------------|
| OP_REPLY | 1 | Reply to a client request. responseTo is set. |
| OP_MSG | 1000 | Generic msg command followed by a string. |
| OP_UPDATE | 2001 | Update document. |
| OP_INSERT | 2002 | Insert new document. |
| RESERVED | 2003 | Formerly used for OP_GET_BY_OID. |
| OP_QUERY | 2004 | Query a collection. |
| OP_GET_MORE | 2005 | Get more data from a query. See Cursors. |
| OP_DELETE | 2006 | Delete documents. |
| OP_KILL_CURSORS | 2007 | Notify database that the client has finished with the cursor. |
| OP_COMMAND | 2010 | Cluster internal protocol representing a command request. |
| OP_COMMANDREPLY | 2011 | Cluster internal protocol representing a reply to an OP_COMMAND. |
注意,只有 OP_QUERY 和 OP_GET_MORE 两种类型有 requestID,其它类型都没有!
所以,在 MongoDB 2.6 之前,写入、更新、删除操作等是没有响应结果的!那么如何确定写入是否成功呢?通过 getLastError 命令,这个命令是基于 OP_QUERY 的。每一个写入操作追加一个 getLastError 请求,查询上一次命令是否报错(很笨的设计有没有?相当于回退到一应一答的 Streaming 模式了)。
MongoDB 2.6 之后使用了 maxWireVersion 3 协议,扩展了数据库的 commands,可以进行各种各样的操作,可以在客户端使用 db.$command.help() 查看所有命令。而 commands 的本质就是 OP_QUERY 类型的查询,所以第三代协议相当于只使用 OP_QUERY 、OP_GET_MORE和 OP_REPLY 类型的消息,淘汰了其它类型。
TiKV 协议
TiKV 协议 是基于二进制的协议:
1. 一个完整消息由 Header 和 Body 组成。Header 有 16 位,定义如下,Body 的长度则在 Header 中的 payload_len 定义,编码格式由 protobuf 定义。
2. Header 中的 msg_id 是请求 ID,所以其请求/响应机制是 Multiplexing
3. 没有定义消息类型,消息类型由 protobuf 精确定义
struct MsgHeader {
uint16 MSG_MAGIC; // const MSG_MAGIC: u16 = 0xdaf4;
uint16 MSG_VERSION_V1; // const MSG_VERSION_V1: u16 = 1;
uint32 payload_len; // Body length
uint64 msg_id; // request ID
}
Streaming + Multiplexing
这种模式是由 JSON-RPC 2.0 specifications 出现引发的,很多人基于 HTTP 来实现 JSON-RPC 服务。
JSON-RPC 2.0
JSON-RPC 2.0 定义两类四种类型的消息,分别是:
1. Request object:
定义了 jsonrpc, method, params, id 四种属性,当 id 存在时,则为标准的 Request,如
{"jsonrpc":"2.0","method":"subtract","params": [42,23],"id":1}
Request 需要对方进行响应;不存在时则为 Notification,如
{"jsonrpc":"2.0","method":"update","params": [1,2,3,4,5]}
Notification 不需要对方响应。
2. Response object:
包括 jsonrpc, result, error, id 四种属性,id 必须存在,result, error 只能有一个存在,当 result 存在时,则为 Success Response,如
{"jsonrpc":"2.0","result":19,"id":1}
当 error 存在时,则为 Error Response,如
{"jsonrpc":"2.0","error":{"code":-32601,"message":"Method not found"},"id":"5"}
另外也有 RESP 协议配合 JSON-RPC 2.0 实现的 RPC 框架 toa-net,主要是利用 RESP 协议解决粘包问题,JSON-RPC 2.0 协议解决 Multiplexing 模式的 请求(Request)/响应(Response)。
Multiplexing + Streaming
HTTP/2
HTTP/2 协议在一个 TCP 通道建立了 N 个 Stream 流通道,每个 Stream 有唯一的 ID,从而实现 Multiplexing 模式,Stream 内则与原来的 HTTP/1 一样,是 Streaming 模式。