速成课 · No. 05
两个程序只有在就规则达成一致时才能协作——发送什么、以什么形状发送、谁在什么时候说话。协议就是这份约定,而挑对协议,就是在选择这场对话怎么进行。
只讲精髓 · 每个协议一幅画面 · 用例子代替 RFC
协议是两个程序用来对话的、约定好的一套规则——一条消息长什么样,以及谁可以在什么时候说什么。把规则定对,千差万别的系统就能彼此听懂。
协议是一门语言加一套礼仪
两位没有共同母语的外交官仍能谈判——因为他们事先就一门语言、以及发言的顺序达成了一致。任意一边一破,剩下的就只是噪声。
对两个程序而言,协议同时定下两样东西:消息的格式(好让两边都能读懂它),以及编排(谁先开口,回复何时到来,怎么结束)。HTTP、gRPC、WebSocket——每一个不过是关于这两件事的一份不同约定。同样的活儿,不同的举止。
协议是分层的
一封信:邮政系统负责搬运信封,根本不在乎里头是什么;纸上的语言则是你和读信人之间的事。两份独立的约定,叠在一起。
底下坐着一个传输协议(TCP 或 UDP),它只负责在机器之间搬运字节。上面坐着一个应用协议(HTTP、gRPC、MQTT),它赋予那些字节以含义。你通常工作在最上层——但当事情变慢、变得不稳定时,底层就会渗上来,所以知道它在那儿是值得的。
每个协议都回答三个问题
一场对话需要定下三件事:谁开口、用什么语言,以及是轮流说还是两边同时说。
谁来发起——是客户端来问,还是服务端能推送?数据取什么形状——JSON 文本、紧凑的二进制,还是一道连续的流?时机——一次性的,还是一条任意一边都能说话的长连接?协议之间几乎每一处差别,都是对这三个问题的不同回答。
没有最好的协议,只有合不合适
你不会用对讲机发婚礼请柬,也不会靠寄信去协调一场救援。
一个协议是为某种形状的对话调校的:稀少而可靠,还是持续而快速;一对一,还是一对多;给人读,还是尽可能地小。本事不在于懂最新的协议——而在于把协议对上两边实际需要怎么对话。
协议是一份约定。按对话的形状去挑它,而不是按什么时髦去挑。
在你构建的几乎所有东西底下,坐着几个你很少直接碰、却始终依赖的协议。懂它们——它们决定了上面到底能做成什么。
TCP:一条保证送达的可靠线路
一通电话,对方每说完一句都回一声「收到」——慢一些,但什么都不会丢、不会乱。
TCP 建立一条连接,并保证每一个字节都按顺序抵达,把丢掉的重传一遍。它是网页、邮件和大多数 API 底下的主力。你在建立连接和延迟上付一点点代价,换来数据完整且正确的承诺——通常是一笔很划算的买卖。
UDP:发出去就不管了,快但会丢
在一间嘈杂的房间里喊话——快,但要是漏掉一个词,你也不会停下来重说,只管接着往下讲。
UDP 发送数据包,不保证抵达、也不保证顺序。这听上去很糟,直到你要的是速度而非完美:视频通话、实时游戏、DNS 查询——在这些场景里,掉一帧也好过你等着重传时卡顿。(下文的 HTTP/3 就是建在它之上的。)
HTTP:网页的请求-响应语言
一个商店柜台:你要一样东西,得到一个答复,而店员在你转身离开的那一刻就把你忘了。
HTTP 跑在 TCP 之上,用动词(GET、POST、PUT、DELETE)和状态码(200、404、500)定义了我们熟悉的请求/响应。它是无状态的——每个请求各自独立——而这恰恰是让网页能扩展到数十亿次调用的原因。你会碰到的几乎每一个 API,说的都是它。
HTTPS 与 TLS:同一样东西,但封了口
同一封信,如今装进了一个防拆封的信封,外面还有寄件人经过核验的签名。
TLS 把 HTTP(以及别的协议)裹进加密和身份之中,于是中间任何人都无法读取或伪造这些流量。它就是 HTTPS 里的那个「S」——而且不再是可选的:浏览器、应用商店和 API 都默认它在。只要数据离开你的机器,它就该待在 TLS 里头。
HTTP/1.1 → 2 → 3:同一条管道,越修越宽
一条单车道公路变成了一条多车道高速——然后又重修了一遍,好让一辆抛锚的车不再堵住每一条车道。
HTTP/1.1 每条连接一次只处理一个请求;HTTP/2 在一条连接上多路复用许多请求;HTTP/3 转到 QUIC(基于 UDP)之上,以消灭 head-of-line blocking、用大约一个来回就打开连接,并在手机从 Wi-Fi 切到蜂窝网络时不掉线。还是你早已会写的那些请求——底下换了一条更快的路。
你构建在栈的顶层,但底层决定了什么是可能的。而加密不是一层你日后再拴上去的东西。
大多数应用对服务端、服务对服务的对话,都是「问一个问题,得一个答复」。三个协议占了主导,各以不同的方式拿简单去换威力。
REST:在朴素的 HTTP 动词之上的资源
一间井井有条的图书馆,每本书都有固定的地址,而你在每处都用同样的四个动作:看它、加一本、改它、移走它。
REST 把一切都建模成 URL 上的资源(/users/42),用 HTTP 动词去操作它们,通常返回 JSON。它简单、可缓存、能在浏览器里调试,而且到处都被理解——**公开 API 的默认选择。**它的弱点:你常常拿到的数据多于所需,或者得发好几次调用才能填满一屏。
GraphQL:精确地要你想要的那些字段
一处自助餐,在那里你不是从固定的套餐里选,而是把一张精确的清单递给厨房,然后拿回正好那些——不多,不少。
GraphQL 给你一个端点,在那里客户端写一个查询,精确说出它需要的那些字段,一个来回搞定——终结了 REST 那种取多了和取少了的毛病。当许多不同的客户端(网页、移动端、合作方)需要一张复杂数据图上不同的切片时,它无比好用。代价是:缓存更难,而那份灵活也把实打实的复杂度推到了你的服务端上。
gRPC:像调本地函数一样调用远程函数
同一栋楼里两个房间之间的一台对讲机——简短、快速、私密,因为两边早已共享同一套暗号。
gRPC 在 HTTP/2 之上用 Protocol Buffers(紧凑的二进制)让一个服务直接调用另一个服务的方法,带着一份有类型的契约,流式也是内建的。它快而严——**内部服务对服务流量的理想之选。**取舍是:它对浏览器并不天然友好,而那个二进制负载也不是人能读的,所以你得靠工具来调试,而不是靠眼睛。
还有那位长者:SOAP
一份一式三份的正式合同,盖了章、做了公证——笨重,但毫不含糊。
在 REST 之前,SOAP 把调用裹进严格的 XML 信封里,配上正式的契约。它啰嗦又老派,但它那份严谨至今仍跑在银行、支付和企业集成里。你多半不会去新建一个——但你很可能不得不去跟一个对话。
用 REST 好让所有人都懂。用 GraphQL 好让客户端来挑。用 gRPC 好让你自己的服务之间跑得快。
朴素的请求/响应有个缺口:服务端没法先开口。当客户端需要在事情发生的当下就听到消息时,你就伸手去拿这里头的一个。
polling:不停地问「有新的吗?」
长途旅行中的一个孩子,每两分钟问一次「到了吗?」。简单——而且多半是白费口舌。
最粗糙的选项:客户端按一个定时器反复发请求。long-polling 改进了它,把每个请求挂着,直到真的有话要说,然后客户端立刻再问一次。它哪儿都能用,也不需要什么特殊协议——但比起一道真正的流,它既浪费又迟钝。一个不错的兜底,一个糟糕的默认。
Server-Sent Events:一道来自服务端的单向流
一场电台广播——电台持续不断地向你播送;你只管听,不在同一个频道上回话。
SSE 保持一条 HTTP 连接打开,让服务端把一道更新流推给客户端,带自动重连,几乎没有开销。它对单向的推送堪称完美:实时比分、通知、一块滴答跳动的仪表盘——而它正是 LLM 把 token 流式送进一个聊天界面的方式。如果客户端不需要在同一条线上回话,SSE 就是最简单的实时工具。
WebSocket:一条双向、两头都开着的线路
一条没挂上、一直敞着的电话线——任意一边一有话就能立刻说出口,无需重拨。
WebSocket 把一条 HTTP 连接升级成一个持久的、全双工的通道,在那里客户端和服务端可以自由地互发消息,延迟最低,每条消息的开销也极小。当两边都在不停对话时,它是对的工具:**聊天、多人游戏、协同编辑、实时交易。**它比 SSE 更费心运维,所以在你真正需要双向时再用它。
webhook:别打给我们,我们会打给你
在一家店里留下你的号码,好让他们在你的订单到货时给你来电——而不是每小时折回去查一遍。
webhook 把方向翻了过来:**你注册一个 URL,而当某个事件发生时,另一个服务向它发一个 HTTP 请求。**例子:Stripe 在一笔支付成功的那一刻向你的端点 POST;GitHub 在每一次 push 时给你的服务器发个提醒。这是各个服务彼此通知、而无需任何人守在一个 polling 循环里的标准办法。
如果服务端必须先开口,就别再 polling 了。单向流用 SSE,双向流用 WebSocket。
有时候 A 根本就不该直接调用 B。在它俩之间放一个 broker,发送方就能发了便不管,而接收方则按自己的节奏干活。
这个想法:一个替你存着消息的中间人
发件人和收件人之间的一个邮局——你把信投进去就走;他们准备好了再来取。两边不必同时在场。
发送方不去直接调用,而是把一条消息交给一个 broker,由它递送给一个或多个接收方。这把它们在时间上解了耦(接收方可以慢、可以短暂宕机),也在认知上解了耦(发送方不必知道谁在听)。它是那些有韧性的、忽高忽低的、异步系统的脊梁。
消息队列(AMQP / RabbitMQ):一个任务,一个 worker
一家小餐馆里的订单高峰——单子堆在挂轨上,哪个厨师空下来就抓走下一张。
一个队列把任务存着,直到一个 worker 取走一个、处理它、并确认它。像 **AMQP(RabbitMQ)**这样的协议还加上了智能路由、重试和投递保证。例子:把「发送欢迎邮件」或「生成发票」丢进一个队列,好让 web 请求立刻返回,而由一个 worker 去料理那个慢的部分。
Kafka:一份很多人都能回放的持久日志
一座报纸档案馆——每一期都按顺序留存,任意多个读者都能从任何地方开始,各自按自己的速度往后读。
Kafka 与其说是一个队列,不如说是一份持久的、有序的事件日志,许多消费者各自独立地读它,而它在被读过之后仍然留着。它是为巨大吞吐量而生的。例子:一道「下单了」的流同时喂给分析、搜索索引和邮件——每个消费者各自记着自己的位置。队列:消息一被处理就没了。Kafka:历史留着。
MQTT:为不可靠网络打造的微型 pub/sub
一支送货车队的对讲机频道——短促的几声,低功耗,就算有一辆短暂地开进隧道也没事。
MQTT 是一个为 IoT 打造的、超轻量的发布/订阅协议:消息极小,电量和带宽都用得极省,还能容忍不稳的链路。例子:成千上万个传感器、智能家居设备或车辆通过蜂窝网络上报遥测数据。当那个「客户端」是一片弱网上的廉价芯片时,MQTT 能塞进 HTTP 塞不进的地方。
别让调用方为慢活儿干等。把它交给一个 broker,然后继续往前走。
协议是信封;里头的负载仍然需要一个两边都认同的形状。那个选择,是可读性与速度之间一笔安静的取舍。
JSON:可读、通用、默认之选
一张谁都能一眼看懂的手写便条——稍微长了点,但不需要解码器。
JSON 是文本,人能读懂,而且到处都受支持——网页和 REST API 的默认 body。你能在浏览器里一眼扫过它,靠读来调试。代价是体积和解析速度:它啰嗦,也不是最快的——通常没问题,偶尔在大规模下成为瓶颈。
Protocol Buffers 及其同类:小、快、二进制
一个条形码——你读不懂,但能被瞬间扫描,而且打包得很紧。你需要那份 schema 才能解码它。
像 Protocol Buffers(gRPC 用的)、还有 MessagePack 和 Avro 这样的二进制格式,依据一份共享的 schema 把数据序列化成一个紧凑、快速的负载。赢在体积和速度;代价是你没法用眼睛读它,而且两边都得共享那份 schema。对于高流量、内部、性能攸关的流量来说,这代价值得。
XML:啰嗦、正式、仍然健在
一份法律文书——满是标签和结构,精确,而且谁都不会拿它来随手写张便条。
XML 早于 JSON,带着丰富的 schema 和校验,代价是笨重。你会在 SOAP 服务、文档格式和配置里遇到它。新东西很少会选它,但它在不少你不得不去集成的老系统里,仍然是承重的。
文本是为了被读懂,二进制是为了快。大多数 API 默认用可读的,是对的。
你不是给一个系统挑一个协议——你是给每一场对话挑对的那一个。真实的应用同时跑着好几个。
把协议对上那场对话
一个工具箱里有锤子、螺丝刀、扳手。你不会去争哪个最好——你看面前的活儿是什么。
按对话的形状来定:一个许多客户端都来用的公开 API 是 REST;两个需要速度的内部服务是 gRPC;一个挑确切字段的客户端是 GraphQL;服务端流式送出更新是 SSE;两边都在线是 WebSocket;慢的后台活儿是一个队列;一个服务告诉另一个某个事件发生了,是一个 webhook。问题从来不是「哪个协议」,而是「这条线上,用哪一个」。
从简单起步;只在疼了的时候才去够那些异类
你不会为了把一袋面粉运过院子,就铺一条铁路。
大多数产品靠 HTTPS 之上的 REST + JSON,外加一个跑后台作业的队列,就能走得很远很远。gRPC、GraphQL、Kafka 和 WebSocket 是对一些具体痛点的回答——服务对服务的延迟、饥饿的客户端、巨大的事件量、实时互动。在你感到那个确切的痛时才采用它们,而不是因为它们听上去现代。对得上那场对话的、最简单的协议,通常赢。
- 谁先开口——是客户端,还是服务端? - 消息多频繁、多大? - 一对一,还是随时间一对多? - 它需要可读吗,还是要尽可能地小和快? - 客户端是谁——一个浏览器、一个移动应用、我自己的另一个服务,还是一片 小小的设备? - 在我去够更花哨的东西之前,朴素的 REST + JSON 够不够?
- 你给一个浏览器不得不调用的公开 API 挑了 gRPC。 - 一个刷新按钮就能搞定的地方用了 WebSocket。 - 用 Kafka 去搬一天几百条消息。 - 给一个每次都取同样三个字段的客户端搞了 GraphQL。 - 给那些你不断需要靠手去读、去调试的数据用了一个二进制格式。
- 客户端和服务端达成一致,中间没有黏合代码在做翻译。 - 这个协议对上了那股流量——流式给流,请求/响应给问题。 - 你能用这个协议预期的工具调试它——一个浏览器、日志,或 proto 工具。 - 加密默认开着,处处如此。
- 每一场对话都用对得上它的、最简单的协议,不多。
现代应用不是建在一个协议之上的。它们建在「每场对话用对的那个协议」之上——而所有这些协议外面都裹着 TLS。