协议 · Protocol
两台机器能"连上"只是第一步——能"听懂对方说什么"才是关键。协议就是两台机器"说话之前先约好的规矩":先说什么、再说什么、用什么格式、出错怎么办。
你跟一位外国朋友打电话——上来第一句你肯定说"Hello, do you speak Chinese?" 而不是直接开始谈生意。
对方答"Yes, I do."——好,双方达成一致:用中文。然后才进入正题:
"我这批货 100 件,每件 80 元……"
这"先打招呼、确认语言、再谈正事"的过程,就是一次"握手"(Handshake)。
协议做的事,就是把这些"寒暄、约定、确认、确认收到的细节"全部标准化,让两台机器也能这么有条不紊地聊天。
为什么"能连上"不等于"能沟通"
把两台电脑用网线插在一起,电信号确实能过去——但那只是"一串高低电平"。接收方怎么知道这串电平从哪里开始、到哪里结束、哪几位是"收件人"、哪几位才是正文?没有事先的约定,收到的一切都只是噪声。
所以网络世界里有一句话:物理连通解决"到得了",协议解决"听得懂"。前者是硬件的事,后者是约定的事——而后者才是这一节的主角。
换成大白话就是:网线通了,只等于你俩坐进了同一间会议室;能不能谈成事,还得看你们说不说同一种语言。打个比方,你去菜市场买菜,摊主张口一句方言,你一个字没听懂——你们中间可没有任何"信号障碍",声音清清楚楚地传到了耳朵里,问题纯粹在于没有一套双方都认的说话规矩。协议就是那套规矩。
如果你把一段中文用 UTF-8 编码存成文件,再用 GBK 打开,屏幕上会出现"锟斤拷"这种乱码。数据一个字节都没丢,链路完全正常,唯一的问题是写的人和读的人用了不同的约定。
这就是"没有共同协议"最直观的样子——网络里的协议不匹配,后果和这个乱码一模一样。
协议的三要素
不管是几十年前的电报规程,还是今天的 HTTP/3,任何一个网络协议都可以被拆成同样的三件事:格式怎么写、字段啥意思、谁先谁后。记住这三个词,以后你翻任何一份协议文档都不会迷路:
三个词听着学术,翻译成人话就是寄快递填单子那三件事:语法——单子上哪一格填收件人、哪一格填电话,格式写死了,填错格快递就派不出去;语义——"易碎品"这三个字盖上去意味着什么,全网快递员都得是同一个理解;时序——必须先揽收、再运输、最后派送,你不能让快递员先按门铃再去仓库取货。任何协议都逃不出这三件事。
语法 · Syntax
"格式怎么写"——比如 HTTP 请求第一行必须是"GET /index.html HTTP/1.1",顺序、空格、换行都写死了。
类比:信封必须"省-市-区-街道-门牌号"按顺序写。
语义 · Semantics
"每个字段是啥意思"——比如 HTTP 状态码 200 表示成功,404 表示没找到,500 表示服务器挂了。
类比:信封左上角贴"加急"贴纸——谁都知道意味着啥。
时序 · Timing
"谁先说话、什么时候说、出错怎么办"——比如 TCP 三次握手的顺序:你问"在吗" → 我答"在" → 你说"那我开始了"。
类比:打电话先响铃,对方接起才能说话,不能说挂就挂。
光看定义还有点飘,我们拿一段真实的 HTTP 请求报文把三要素一次全指出来。下面这几行就是你每次打开网页时,浏览器真正发出去的纯文本:
GET /index.html HTTP/1.1 <- 语法:动词 空格 路径 空格 版本,一格都不能错
Host: example.com <- 语法:字段名 冒号 空格 值
User-Agent: Mozilla/5.0
Accept: text/html
Accept-Encoding: gzip, br
Connection: keep-alive
<- 语法:一个空行,表示"头部到此结束"
(这里之后才是正文,GET 请求通常没有正文)
语法体现在哪?——"必须是三段、用空格隔开"、"字段名和值之间必须是冒号加空格"、"头部结束必须是一个空行(准确说是 \r\n\r\n)"。这些规则死板到近乎苛刻:你少写一个空格,服务器就直接回 400 Bad Request。语义体现在哪?——GET 的含义是"我只要读,别改数据",Host 的含义是"我要访问的是这台服务器上的哪个网站"(这一行让一个 IP 能托管几千个网站),Accept-Encoding: gzip 的含义是"你可以压缩了再发给我,我解得开"。时序体现在哪?——必须客户端先发请求、服务器后回响应,且在 HTTP/1.1 里一个连接上的请求必须按顺序应答,不能乱序。
服务器的回答同样严格:
HTTP/1.1 200 OK <- 语义:200 = 成功
Content-Type: text/html; charset=utf-8
Content-Length: 3241 <- 语义:正文正好 3241 字节,读够就停
Content-Encoding: gzip
Cache-Control: max-age=3600 <- 语义:这份内容你可以自己存 1 小时
<!DOCTYPE html><html>…(3241 字节的压缩正文)
注意那个 Content-Length——它是"语义"的绝佳例子。接收方靠这个数字知道什么时候该停止读取。如果这个数字写错了,会发生两种灾难:写大了,接收方一直傻等"还没收完"直到超时;写小了,多出来的字节被当成"下一个请求的开头"来解析,整个连接彻底错乱。说白了 Content-Length 就是快递面单上写的"内含 3 件"——收件人靠这个数字知道拆到第几件可以签收。写成"5 件",他会在门口一直等剩下两件等到快递站关门;写成"1 件",多出来的两件就被他当成隔壁邻居的包裹给拆了。协议里的每个字段都不是装饰,它们都是接收方赖以生存的判断依据。
三要素还有一个不对称的地方值得注意:语法错误容易发现,语义错误极难发现。语法写错,对方立刻报错;但如果你把"用户余额"字段的单位理解成"元"而对方发的是"分",两边的解析都成功、没有任何报错,只是所有金额都错了 100 倍。真实世界里的接口事故,绝大多数是语义误解,不是语法错误。这也是为什么好的协议文档花在解释字段含义上的篇幅,永远远超解释格式。
"语法错误容易发现、语义错误极难发现"这条,打个比方就特别扎心:语法错误好比你把快递单的收件电话填成了 10 位数——快递小哥当场就退单给你;语义错误好比你在"重量"那一栏填了个"3",你以为单位是斤,快递公司按公斤算,钱多收了一倍还没人报错。单子填得漂漂亮亮,流程一路绿灯,钱就是不对。真实的线上事故,八成是后者。
为什么一定要分层:软件工程的一次伟大胜利
在讲"有哪几层"之前,先回答一个更根本的问题:为什么不能写一个协议把所有事一次办完?假设真有这么一个"万能协议",它得同时负责:光纤里的激光怎么闪、网线上怎么避免两台机器同时说话、全球几十万个网络之间怎么选路、丢了包怎么重传、拥塞了怎么减速、HTML 用什么编码、密码怎么加密……
想象一下这个万能协议长什么样:一家餐厅里只招一种员工,这个员工既要种菜、又要杀鱼、还要炒菜、端盘子、收钱、擦桌子、修水管、跟税务局打交道。一个人干得完吗?勉强能。但你想给菜单加一道新菜,就得把这个全能员工从种菜开始重新培训一遍——这就是不分层的真实代价。
后果会是灾难性的,而且是四重灾难:
- 改一处动全身WiFi 从 4 升到 6,是最底下两层的事。如果协议不分层,升级 WiFi 就得同时改浏览器、改邮件客户端、改所有 App。而现实中你换个路由器,一行代码都不用改。
- 组合数爆炸假设有 8 种物理介质、5 种传输方式、100 种应用。不分层要写 8×5×100 = 4000 个协议;分层只需要 8+5+100 = 113 个。分层把乘法变成了加法,这是它最大的数学价值。
- 无法分工网卡厂商懂电磁波不懂 HTML,浏览器团队懂 HTML 不懂激光调制。分层给了每一群人一份可以独立完成、边界清晰的工作说明书。
- 无法演进1983 年定型的 IP 协议至今还在跑,而它上面的应用层已经从 telnet 换到了 HTTP/3。不分层,任何一层的过时都意味着推倒重来。
分层的核心机制其实只有一句话:每一层只跟"上一层"和"下一层"打交道,且只暴露服务、不暴露实现。TCP 只对上层承诺"你给我的字节,我保证按顺序、不重不漏地送到";它绝口不提这些字节是走光纤还是走 5G——它根本不知道,也不需要知道。这种"故意的无知"在软件工程里叫封装,是所有大型系统能被建造出来的唯一办法。
"故意的无知"这个说法很妙,打个比方就是餐厅前厅和后厨的分工:服务员只承诺"您点的菜我保证一道不漏、按顺序端上来",他绝不会告诉你这菜是煤气灶炒的还是电磁炉炒的——他自己也不知道,而且他不需要知道。哪天厨房把煤气灶全换成电磁炉,服务员一句话都不用改。TCP 对上层的承诺,跟这位服务员一字不差。
当然,分层也有代价,值得诚实地说出来。第一是开销:每层都要加自己的头部,一个 1500 字节的以太网帧里,14(以太网)+ 20(IP)+ 20(TCP)= 54 字节是纯头部开销,约 3.6%;如果传的是几十字节的小包(比如游戏操作、语音帧),头部可能比数据还长。第二是性能损失:数据在层间传递要多次内存拷贝,这也是为什么高性能场景会出现"跨层优化"这种打破分层原则的做法。第三是重复劳动:TCP 有校验和,以太网也有 CRC,IP 头也有校验和,同一件事做了三遍。
那个 3.6% 的头部开销换算一下就有感觉了:54 字节头部配 1446 字节数据,相当于寄一箱 30 斤的东西,纸箱和填充泡沫占了 1 斤多——完全划得来。但要是你只寄一支口红(比如游戏里一个 20 字节的操作指令),纸箱还是那么大,泡沫还是那么多,包装比货重两倍还多。这就是为什么实时游戏和语音特别在意小包开销,而下载大文件的人压根不在乎。
协议栈:五层协议在一次点击里同时工作
你可能听过"OSI 7 层"、"TCP/IP 4 层"——这其实就是把"一次完整对话"拆成多个不同层次的协议,每层只管一件事:
"分层"这件事,最贴切的类比是寄国际快递时的层层套箱:你把礼物装进小盒(应用层的内容),小盒放进带缓冲的中盒并贴上"易碎、需签收"(传输层),中盒装进大纸箱写上"寄往德国柏林"(网络层),大纸箱扔上开往机场的货车(链路层),最后飞机把它运过大洋(物理层)。每一层只认自己那层的箱子和自己那层的标签,压根不关心里面还套了几层。
| 层次 | 负责 | 类比 | 代表协议 |
|---|---|---|---|
| 应用层 | 这次对话的"业务"是什么 | 写信还是寄包裹 | HTTP · SMTP · DNS |
| 传输层 | 这次对话"靠不靠谱" | 挂号信 vs 平信 | TCP · UDP |
| 网络层 | 这封信"送到哪个城市" | 分拣中心 | IP · ICMP |
| 链路层 | "送到下一个路口" | 邮递员最后一公里 | Ethernet · WiFi |
| 物理层 | "怎么把信号发出去" | 用光缆还是无线电 | 光纤 · 5G · 双绞线 |
你写一封信给朋友——你决定内容(应用层);你决定要不要挂号(传输层);你写上收信城市(网络层);邮递员负责送到对方楼下(链路层);最后邮车 走哪条路(物理层)。
每层都有专门的"工人"和专门的"规则",互不干扰——这就是分层的智慧。
这一整摞协议叠在一起,就叫协议栈(Protocol Stack)。"栈"这个字用得很准:数据发出去时从上往下一层层加壳,收进来时从下往上一层层剥壳,严格的后进先出。你打开一个网页时,这一摞协议是这样同时开工的:
【发送方 · 你的电脑】 【接收方 · 服务器】
应用层 HTTP: "GET /index.html" HTTP: 解析请求,返回页面
↓ 加 TCP 头(20B) ↑ 剥掉 TCP 头
传输层 TCP: 端口 54321 → 443 TCP: 检查序号、校验和、排序
↓ 加 IP 头(20B) ↑ 剥掉 IP 头
网络层 IP: 192.168.1.20 → 93.184.216.34 IP: 确认目标是我
↓ 加以太网头(14B) ↑ 剥掉以太网头
链路层 Ethernet: 我的MAC → 网关MAC Ethernet: CRC 校验通过
↓ ↑
物理层 ━━━━━━ 电磁波 / 光脉冲 / 电压 ━━━━━━━━━━━━━━━━━━
一句"GET /index.html"(24 字节)出门时已变成 78 字节的以太网帧
这张图里藏着一个精妙的对称性:每一层只跟对面的同一层"对话"。你的 TCP 层是在跟服务器的 TCP 层商量窗口大小,中间经过的十几个路由器完全插不上话——它们只看得懂网络层。这种"同层对话、跨层封装"的结构叫对等层通信(peer-to-peer communication),是分层模型最核心的设计。用邮政来类比:你和朋友在"信件内容层"对话,两地邮局在"分拣层"对话,两个邮递员在"投递层"对话——三组人各说各话,互不干扰,却共同完成了一次通信。
这里的关键点值得单独说透:你的 TCP 只跟对方的 TCP 说话,中间的路由器一句也插不上嘴。好比你寄一封信,信里写"记得带伞"——这句话是你和收信人之间的对话,中间经过的邮局、分拣中心、邮递员,一个都没资格参与讨论今天下不下雨,他们只看信封。同层对话、跨层封装说白了就是这个意思。
顺手记住每一层数据单元的正式名字,读文档时会省很多力气:应用层叫报文(Message),传输层 TCP 叫段(Segment)、UDP 叫数据报(Datagram),网络层叫分组/包(Packet),链路层叫帧(Frame),物理层就是比特流(Bit)。日常聊天大家一律叫"包",但看规范时这些词是严格区分的。
这几个名字乱得像绕口令,但记法很简单:想象同一批货在不同环节的叫法——在你手上叫"礼物",装了盒叫"包装件",进了快递系统叫"包裹",装上车叫"一车货"。东西压根没变,只是每个环节的人按自己的习惯换了个称呼。报文、段、包、帧,就是同一批数据在五个环节里的四个昵称。
还有一件事必须点明:OSI 七层是理论模型,TCP/IP 四层是现实实现。OSI 由国际标准化组织设计得极其工整(会话层、表示层各司其职),但它太复杂、落地太晚,商业上彻底败给了先跑起来的 TCP/IP。今天的现实是:我们用 OSI 的术语("这是二层问题"、"七层负载均衡"),跑 TCP/IP 的代码。这个"名义标准输给事实标准"的故事,在技术史上一遍遍重演。
这段关系换成大白话:OSI 好比学校发的那本《课程标准》,写得极其周正;TCP/IP 好比老师实际用的那本教案,只有四章,但真能把课讲完。今天的现实是——大家嘴上引用《课程标准》的章节号("这是二层问题"),手里翻的却是那本教案。名义上的标准和实际跑着的东西,从来不是一回事。
你每天用到的 8 个协议
| 协议 | 它管什么 | 你什么时候用到它 |
|---|---|---|
| HTTP / HTTPS | 浏览网页 | 每天打开任何网页 |
| DNS | 域名翻译成 IP | 你输入 baidu.com 的瞬间 |
| TCP | 可靠传输 | 聊天消息、邮件、文件下载 |
| UDP | 快速传输(不保证到达) | 视频会议、直播、游戏 |
| IP | 把数据包送到目标城市 | 每一次网络通信 |
| ICMP | 网络诊断 | 你 ping 一台服务器时 |
| DHCP | 自动分配 IP | 你每次连 WiFi |
| SMTP | 发邮件 | 你点"发送"按钮时 |
这八个用一句生活话就能串起来:你输网址那一刻,DNS 帮你查通讯录问到号码;TCP 好比寄挂号信、非要对方签收;UDP 好比往楼下喊一嗓子、听没听见不管;IP 负责把包裹送到正确的城市;ICMP 是你打过去问"喂喂听得见吗"的那声试探;DHCP 是你进小区时物业给你分的门牌号;HTTP 是你跟服务员说的那句"来一份宫保鸡丁";SMTP 是你把信投进邮筒的那个动作。八个协议,八个日常动作,一个不多。
请求-响应模型:绝大多数协议的骨架
把上面那 8 个协议摊开看,你会发现它们的交互形状高度雷同:一方问,一方答。这个"一问一答"的结构叫请求-响应模型(Request-Response),它是协议世界里最主流的骨架,也是理解一切网络行为的默认框架。
请求-响应说白了就是餐厅点菜:你(客户端)不开口,厨房(服务器)绝不会主动给你端菜;你说一句"来份炒饭",厨房就出一份炒饭。一问一答,一次一对,你不问它就一直等着。这也解释了为什么厨房再空闲也不会主动给你送吃的——不是它不想,是这套规矩里压根没有"厨房先说话"这一条。
但"一问一答"其实有好几种变体,各解决不同的问题。这是初学者最容易混淆的一块,一张表就能理清:
| 交互模式 | 形状 | 谁先说话 | 典型协议 / 场景 |
|---|---|---|---|
| 请求-响应 | 问 → 答,一次一对 | 客户端 | HTTP、DNS 查询、数据库查询 |
| 轮询 Polling | 反复问"有新消息吗" | 客户端 | 老式网页每 5 秒刷一次,浪费带宽 |
| 长轮询 Long-Poll | 问了以后服务器先不答,有消息才答 | 客户端 | 早期网页聊天室的标准做法 |
| 服务器推送 | 服务器主动说话 | 服务器 | SSE、WebSocket、微信新消息通知 |
| 发布-订阅 | 订阅一个"主题",有内容就自动来 | 双向 | MQTT(物联网)、Kafka |
| 流式 Streaming | 建立一次连接,持续单向灌数据 | 服务器 | 视频直播、gRPC Streaming |
这里有一个反直觉但极其重要的事实:HTTP 协议本身不允许服务器主动说话。它的设计是纯粹的"客户端问、服务器答",服务器没有任何办法在没被问的时候插一句嘴。那微信、钉钉、网页版邮箱是怎么"主动"弹出新消息的?答案是它们全都在耍花招——要么让客户端偷偷不停地问(轮询),要么让客户端问了以后服务器故意不回、憋着直到有消息才回(长轮询),要么干脆升级到 WebSocket 这个允许双向说话的协议。
那三种"花招"用点菜来理解,一秒就懂:轮询是你每隔十秒钟就喊一次服务员"我的菜好了吗"(问一百次,九十九次白问,服务员烦你也烦);长轮询是你喊一次"好了叫我",服务员就站在你桌边不走,菜好了才开口(省事,但占着一个服务员);WebSocket是你和厨房之间拉了根对讲机,谁想说话随时按一下——这才是真正的双向。
WebSocket 的升级过程本身就是一个协议协商的漂亮案例。它不另开端口,而是借 HTTP 的壳"变身":
# 客户端先发一个看起来很正常的 HTTP 请求,但夹带了升级请求
GET /chat HTTP/1.1
Host: chat.example.com
Upgrade: websocket <- "我想换个协议聊"
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
# 服务器同意了
HTTP/1.1 101 Switching Protocols <- 101 这个状态码专门用于"换协议"
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# 从这一行之后,这个 TCP 连接上跑的就不再是 HTTP 了,
# 而是 WebSocket 帧格式,双方都能随时主动发消息。
为什么要这么绕,而不直接开一个新端口跑 WebSocket?因为现实世界的防火墙只信 80 和 443。公司网络、酒店 WiFi、运营商网关,大量只放行这两个端口。借 HTTP 的壳完成升级,等于是"穿着 HTTP 的制服过安检,进去以后再换衣服"。说白了,这就跟坐地铁一样:安检口只认那几种能过机的包,你想带的东西再正当,也得先装进一个能过安检的袋子里。101 Switching Protocols 这个状态码干的活儿,就是"过完安检以后当场把袋子换掉"。协议设计从来不是纯技术选择,它得跟已经存在的几十亿台设备妥协。
有状态 vs 无状态:协议记不记得你
这是协议设计里最重要的一条分水岭,也是面试和实际架构里出现频率最高的概念。定义极简:服务器处理这一次请求时,需不需要依赖"上一次请求留下的记忆"?需要的叫有状态,不需要的叫无状态。
打个比方,这就是小区门口那家便利店的老板和火车站窗口的售票员的区别:便利店老板记得你上次赊了五块钱、爱喝哪个牌子的水(有状态);火车站售票员每次都当你是生面孔,你必须把出发地、到达地、日期、身份证从头说一遍(无状态)。前者省你的话,后者省他的记忆——而且换一个窗口办也完全一样。
| 有状态 · Stateful | 无状态 · Stateless | |
|---|---|---|
| 服务器记忆 | 为每个客户端维护一份上下文 | 处理完就忘,每次请求都是初次见面 |
| 典型协议 | FTP、SMTP、TCP、SSH、数据库连接 | HTTP、DNS、UDP、大多数 REST API |
| 好处 | 交互简洁——之前说过的不用重复 | 可任意扩容:请求打到哪台服务器都一样 |
| 坏处 | 服务器内存吃紧、挂了状态就丢、难做负载均衡 | 每次请求都要携带全部信息,报文更大 |
| 崩溃后果 | 整个会话失效,得重新登录/重连 | 几乎无感——换台机器接着处理 |
用两个真实协议对比一下就很清楚。FTP 是有状态的:你 cd /docs 进了一个目录,之后 get a.txt 服务器知道你要的是 /docs/a.txt——它记住了你"当前在哪"。HTTP 是无状态的:你请求 /docs/,再请求 /docs/a.txt,服务器对这两次请求毫无关联认知,第二次请求必须自己把完整路径写全。
那问题来了:既然 HTTP 无状态,为什么你登录淘宝一次,之后浏览几十个页面都不用重新登录?因为工程师在无状态的协议上,用了一个巧妙的补丁:把"记忆"存在客户端,每次请求自己带过来。
Cookie 干的活儿,说白了就是医院给你的那张就诊卡:医院不可能记住每天几千个病人分别是谁,所以它给你一张卡,每次挂号你自己刷一下——卡一刷,医院立刻"想起"你是谁、上次开了什么药。记忆压根不在医院脑子里,在你手里那张卡上。这就是无状态协议装出"记得你"的全部秘密。
# 第一次:你登录成功,服务器发一张"通行证"给你
HTTP/1.1 200 OK
Set-Cookie: session=a8f3e91c...; Max-Age=604800; HttpOnly; Secure
# 之后每一次请求,浏览器自动把通行证附上
GET /cart HTTP/1.1
Cookie: session=a8f3e91c... <- 服务器一看就知道"哦是小明"
# 服务器端逻辑:拿这个 session ID 去 Redis 里查是谁
# 协议依然无状态(每次请求都自带全部信息),
# 但用户体验上有了"记忆"。
无状态的价值在云时代被放大到了极致。正因为 HTTP 无状态,淘宝才能在双十一临时加开一万台服务器——你的第一个请求可能打到杭州的机器,第二个打到南京的,第三个又回杭州,全都能正确处理,因为每台机器都不需要"认识"你,只需要能读懂你带来的那张通行证。如果 HTTP 是有状态的,你的所有请求就必须固定打到同一台机器(这叫"会话粘滞"),那台机器一挂你就得重新登录,扩容也会变成噩梦。
反过来说,有状态并非落后。SSH、数据库连接、视频通话都必须有状态——它们需要维护加密密钥、事务上下文、编解码器状态,这些东西每次重建的代价远大于维护它们的代价。判断标准很简单:如果"上下文"很小或很容易由客户端携带,就设计成无状态;如果上下文很重、重建很贵,就得有状态。
这个判断标准换成大白话:如果你能把"上次说到哪儿"写在一张小纸条上随身带着,就无状态;如果那是一整柜子的档案、搬都搬不动,就只能有状态。比如银行柜台给你办一笔长业务,中间要核身份、验密码、打印凭条,这些东西不可能每一步都让你重新排一次队——所以它得有状态。而查一下余额这种事,刷卡就行,谁给你办都一样。
协议为什么"必须双方都遵守"
这是新手最容易困惑的一点——协议不是"厂家规定",它是双方都必须遵守的"约定"。如果两台机器用了不同的协议,它们就互相听不懂——就像你打电话说的是中文,对方只懂西班牙语,根本聊不下去。
再补一个更狠的例子:协议不对,比压根连不上更麻烦。连不上你至少知道"断了,去修";协议不对是电话通了、双方都在说话、都觉得自己说得很清楚,结果聊了十分钟发现完全是两回事。这好比两个人在会议室里各说一门方言,都以为对方在点头同意,散会才发现签的合同不是一份东西。
保安对讲机上有"1-16 频道"。你说 "1 频道",对方也必须调到 1 频道——频道就是"频率协议"。
换成 WiFi 也是同样:2.4G / 5G / 6G 是不同"频道",WiFi 4 / 5 / 6 / 7 是不同"协议版本"——路由器和手机必须支持同一版本才能聊。
你买过"WiFi 6 路由器但手机只支持 WiFi 5"的情况吗?它能工作,但只会按双方都支持的最老协议跑——这就是协议协商。
明文协议 vs 加密协议:那把小锁到底锁了什么
互联网早期的协议全是明文的——因为发明它们的是一群互相信任的科研人员,压根没想过会有人偷看。后果是:只要能碰到数据流经的任何一台设备,你就能完整读到所有内容,包括密码。
明文协议说白了就是用明信片寄信:不用信封,字全写在外面,从邮局到分拣中心到邮递员,谁顺手一瞥都能读完。当年这么设计不是疏忽,是因为那时候整个网络上只有几百个互相认识的研究员——好比一个只有二十户人的小村子,家家都不锁门,因为压根没有外人会来。今天的互联网是个几十亿人的超级都市,还不锁门就是找事了。
这不是理论风险,而是可以现场演示的事实。用 telnet 手动敲一个 HTTP 请求,你会发现整个过程就是纯文本聊天:
$ telnet example.com 80
Connected to example.com.
GET / HTTP/1.1 <- 你手打的,服务器完全接受
Host: example.com
<- 敲一个空行
HTTP/1.1 200 OK <- 服务器回你了
Content-Type: text/html
...
# 换成 443 端口(HTTPS)再试,你会看到一堆乱码
$ telnet example.com 443
GET / HTTP/1.1
<- 服务器直接掐断连接:你没做 TLS 握手,我不认识你
下面这张表列出常见协议的"明文版"和"加密版"对应关系。规律非常清楚:几乎每个老协议都在后来长出了一个带 S 的加密孪生兄弟。
| 用途 | 明文协议(端口) | 加密协议(端口) | 明文时代的风险 |
|---|---|---|---|
| 网页浏览 | HTTP(80) | HTTPS(443) | 密码、Cookie、浏览内容全暴露 |
| 远程登录 | Telnet(23) | SSH(22) | root 密码直接被抓走 |
| 文件传输 | FTP(21) | SFTP / FTPS(22 / 990) | 账号与文件内容全裸奔 |
| 收邮件 | POP3(110)/ IMAP(143) | POP3S(995)/ IMAPS(993) | 邮箱密码与全部邮件 |
| 发邮件 | SMTP(25) | SMTPS(465 / 587) | 邮件内容可被篡改 |
| 域名解析 | DNS(53) | DoH(443)/ DoT(853) | 你访问过哪些网站全被记录 |
要理解那把小锁到底提供了什么,得把它拆成三件独立的事——这三件事经常被笼统地叫做"加密",其实是三种不同的保障:
- 机密性 · Confidentiality别人抓到你的包也读不懂内容。靠对称加密(AES)实现。注意它保护不了"元数据"——你访问了哪个域名、传了多少字节、什么时间访问,中间人依然一清二楚。
- 完整性 · Integrity内容在路上被改过一个字节,接收方立刻发现。靠消息认证码(HMAC/AEAD)实现。这防的是"运营商往你的网页里插广告"这类劫持。
- 身份认证 · Authentication确认你连的真是银行,不是长得一样的钓鱼站。靠证书和 CA 体系实现。这一条往往比加密本身更重要——加密得再好,如果对面是骗子,你只是把密码安全地送给了骗子。
这三件事用寄挂号信来分,一秒就清楚:机密性=信装在不透光的信封里,路上没人读得到内容;完整性=信封上有火漆封印,被人拆开过你一眼就能看出;身份认证=收信人得出示身份证签收,证明他真是收件人不是隔壁冒名的。三件事缺一件,这封信都算不上安全——尤其是第三件:信封再厚、封印再牢,送错了人就全白搭。
常见误解值得澄清两个。第一:HTTPS 不等于"这个网站安全可信"。任何人花几分钟就能给自己的钓鱼站申请一张免费证书,浏览器照样显示小锁。小锁只保证"你连的确实是地址栏里那个域名,且传输没被偷看",它对网站本身是好人还是坏人一无所知。第二:HTTPS 隐藏不了你访问的域名。TLS 握手时客户端要在 SNI 字段里明文写出目标域名(不然一个 IP 上的几千个网站不知道该拿哪张证书给你),所以运营商依然能记录你访问过哪些站,只是看不到具体页面内容。ECH(Encrypted Client Hello)就是为了补上这个洞而生的新标准。
那个"藏不住域名"的漏洞,通俗地说就是:你的信封是不透光的,但信封外面必须写着收件人的地址,不然邮局根本不知道往哪送。所以运营商永远知道你"给谁写了信、写了多厚的一封、几点写的",只是不知道信里写了什么。很多人以为开了 HTTPS 就完全隐身了,这是最普遍的误会之一。
协议的版本演进与兼容性
网络用了 50 年,怎么没崩?因为新协议几乎都向后兼容——你 WiFi 6 路由器也接受 WiFi 5 手机;HTTP/2 浏览器也能跟 HTTP/1.1 服务器对话。这就是为什么 1980 年的老标准到今天还能用。好的协议不是取代旧的,而是兼容旧的。
向后兼容打个比方,就像小区换了新的门禁系统,但老住户手里那张旧卡照样能刷——新系统悄悄多认了一种旧格式。要是物业哪天宣布"旧卡全部作废,全体重新办卡",那一天小区门口能堵到晚上。互联网跑了五十年没崩,靠的就是从来没敢干这件事。
以 HTTP 这条演进线为例,你能看到一个协议如何在三十年里被逼着做了三次大手术,每次都在解决上一代留下的病根:
| 版本 | 年份 | 关键改进 | 它解决的痛点 |
|---|---|---|---|
| HTTP/0.9 | 1991 | 只有一个 GET,返回纯 HTML,连头部都没有 | 能用就行 |
| HTTP/1.0 | 1996 | 加入头部、状态码、Content-Type | 能传图片和非 HTML 内容了 |
| HTTP/1.1 | 1997 | 长连接(keep-alive)、Host 头、分块传输 | 不用每张图都重开一次 TCP;一个 IP 能托管多个网站 |
| HTTP/2 | 2015 | 二进制分帧、多路复用、头部压缩、服务器推送 | 解决"队头阻塞":一个连接里几十个请求可以并行 |
| HTTP/3 | 2022 | 底层从 TCP 换成 QUIC(基于 UDP) | 解决 TCP 层的队头阻塞;移动网络切换 WiFi/5G 不断连 |
这条线里最激烈的一步是 HTTP/3。它把底层的 TCP 换成了 UDP——这在十年前是不可想象的,因为 TCP 的可靠传输被认为是天经地义的基础。为什么要换?因为 TCP 有个无法绕开的毛病:它按字节流严格保序。一个网页并行下载 50 个资源,如果其中一个包丢了,TCP 会把后面所有已经到达的数据都扣在缓冲区里等那个丢的包补齐——哪怕后面的数据属于完全无关的另一张图片。这就是"队头阻塞"。HTTP/2 在应用层解决了它,但 TCP 层的阻塞依然存在,只能连 TCP 一起换掉。这告诉你一件事:分层的边界不是神圣的,当某一层成为瓶颈时,人们真的会把它整个换掉。
"队头阻塞"这个词听着玄,其实就是超市排队结账:你后面排了五十个人,前面那位大爷在翻遍全身找一张优惠券——货都在筐里、钱都在手上,全体五十人干等着。TCP 严格保序就是这个规矩:不管后面的包多完整,前面丢了一个,全都得在缓冲区里排着。QUIC 干的活儿相当于超市开了自助结账机——大爷继续找券,别人扫码就走。
兼容性在工程上是怎么实现的?三种手段,都很朴素:
- 版本号协商客户端说"我最高支持 1.1",服务器说"那就用 1.1"。双方取交集里最高的那个。这就是你 WiFi 6 路由器带 WiFi 5 手机时跑 WiFi 5 速率的原因。
- 可选字段与"忽略未知"协议规定"看不懂的头部字段,请直接忽略,不要报错"。这条铁律让新版本可以随意加字段而不打死老客户端。HTTP 头部的可扩展性全靠它。
- 另开端口 / 平行部署新旧协议同时提供服务,客户端自己选。HTTP/3 就是通过响应头里的
Alt-Svc: h3=":443"告诉浏览器"下次你可以试试用 h3 连我"。
反面教材也很有教育意义:IPv6 恰恰是不向后兼容的——IPv4 的主机无法直接和 IPv6 的主机通信,因为地址长度和头部格式完全变了。结果是什么?IPv6 在 1998 年就定稿了,二十多年后全球普及率才刚过一半,中间催生了 NAT64、6to4、双栈这一堆过渡技术。这是整个网络史上关于"不兼容的代价有多大"最昂贵的一课。
那三种兼容手段,用生活话讲就是:版本号协商=你说"我会中文和英文",对方说"我只会中文",那就说中文(取双方都会的里头最好的那个);忽略未知字段=表格上多出一栏你看不懂的,就空着别管,千万别把整张表退回去;平行部署=新旧两个窗口同时开着,谁愿意去哪个去哪个。IPv6 的悲剧在于它三条都没做到——相当于换门禁的时候连门框一起换了,旧卡插都插不进去。
RFC:协议是怎么被"定下来"的
你可能好奇:既然协议是全世界共同的约定,那到底谁有权定它?答案会让你意外——没有任何政府或公司说得上话,靠的是一群工程师写文档、公开吵架、然后看谁的实现真的跑得起来。
这些文档叫 RFC(Request for Comments,"请求评论")。名字本身就是这个体系的精神写照:它不叫"标准",不叫"规定",而叫"请大家来提意见"。第一份 RFC 1 写于 1969 年 4 月,作者是加州大学洛杉矶分校的一名研究生 Steve Crocker——他当时怕自己的语气显得太权威,才特意起了这么客气的名字。这个客气的名字延续了五十多年,如今编号已经过了 9600 号。
RFC 这套体系,打个比方就像小区里定"垃圾几点扔"这件事:没有哪个部门下红头文件,是几个热心住户在群里发了个提议,然后全楼吵了两个月,最后试着按这个法子扔了一个月发现真行得通,于是就成了规矩。听起来松散,但正因为它是吵出来又试出来的,落地时几乎没人反对。
| RFC 编号 | 内容 | 为什么值得知道 |
|---|---|---|
| RFC 791 | IPv4 | 1981 年定稿,今天全球一半流量还在用它 |
| RFC 793 | TCP | 1981 年,三次握手、拥塞控制的源头(2022 年被 RFC 9293 整合替代) |
| RFC 1035 | DNS | 1987 年,整个域名系统的规范 |
| RFC 2616 → 7230~7235 → 9110 | HTTP/1.1 | 被拆分、重写过多次,说明"标准也在不断返修" |
| RFC 9000 | QUIC | 2021 年,HTTP/3 的底座 |
| RFC 2324 | 超文本咖啡壶控制协议 | 愚人节玩笑文档,却贡献了真实存在的状态码 418 I'm a teapot |
一份新协议要走完的路大致是:有人写出草案(Internet-Draft)→ 在 IETF 的邮件列表里被公开批斗几个月甚至几年 → 至少要有两个独立的、能互通的实现(这条是关键)→ 被 IESG 批准 → 分配一个 RFC 编号,成为"提议标准"→ 若被广泛采用,升为"互联网标准"。整个过程遵循 IETF 的一句名言:"We reject kings, presidents and voting. We believe in rough consensus and running code."(我们不要国王、总统和投票,我们信奉大致的共识和跑得起来的代码。)
"至少两个能互通的实现"这条要求,是这个体系最聪明的设计。它意味着一份标准如果写得含糊不清、有二义性,那两组人各自照着实现出来的东西就一定连不上——问题会在成为标准之前就暴露。这道门槛把无数纸上漂亮但实际无法落地的设计挡在了外面。相比之下,OSI 七层模型走的是"先由委员会设计完美标准,再要求厂商实现"的路,结果就是设计得极其工整却迟迟落不了地,最终败给了"先跑起来再补文档"的 TCP/IP。
"必须有两个能互通的实现"这条门槛,说白了就是考试要交两份独立答卷,两份得对得上:如果题目本身出得含糊,两个人各自理解、各自作答,答案一定不一致——问题在成为标准之前就暴露了。这一条把无数"写在纸上很美、真做起来两边接不上"的设计挡在了门外,是整个互联网标准体系里最聪明的一道设计。
最后一个实用建议:RFC 是公开且免费的,地址是 rfc-editor.org 或 datatracker.ietf.org。当你对某个协议行为有疑问、网上的答案又互相矛盾时,直接去翻 RFC 原文,那是唯一的最终答案。它们读起来比想象中友好得多——因为写的时候本来就是给同行看的,不是给律师看的。
读懂一次握手
以"TCP 三次握手"为例,让你感受协议有多严谨:
① 客户端:SYN
"我想和你建立连接,我的初始序号是 X。"——只发,不等回话。
② 服务器:SYN + ACK
"我听到了你的 SYN,确认号 X+1;同时我也 SYN,我的初始序号是 Y。"——一次说两件事。
③ 客户端:ACK
"我听到了你的 SYN,确认号 Y+1。"——三次握手完成,双方都知道对方"听到了自己",连接建立。
为什么要三次?两次行不行?答:不行——两次时,服务器不知道客户端有没有收到自己的 SYN。三次之后,双方都明确知道对方收到了自己。协议的设计就是这么严谨。
更深一层的原因跟"旧包还魂"有关。假设只要两次握手,那么一个在网络里迷路了三十秒的旧 SYN 包,终于绕到服务器时,服务器会以为这是一个新连接请求,立刻分配内存、建立连接、等着数据——但客户端早就走了,这个连接会一直空占资源。三次握手的第三个 ACK 相当于让客户端最后确认一句"这个连接我确实还要",把这种幽灵连接挡在门外。协议里那些看起来"多余"的步骤,几乎每一个都是被某个真实事故逼出来的。
三次握手换成大白话就是打电话开头那三句:你"喂?"(SYN)→ 对方"喂,听得到,你能听到我吗?"(SYN+ACK)→ 你"能听到,那我说了"(ACK)。为什么非要第三句?因为到第二句为止,只有你确认了"对方听得见我",对方还不知道"你听不听得见他"。他要是这时候就开始说正事,可能全白说了。三句话之后,两个人都确认了"我说的话对方能听见"——这才叫连接建立。
握手不只有三次这一种。把常见协议的"开场仪式"排在一起,你能明显看到安全和速度之间的拉锯:
| 协议 | 建立连接需要几个往返(RTT) | 做了什么 |
|---|---|---|
| UDP | 0 | 不握手,直接开发。所以它快,也所以它不可靠 |
| TCP | 1(三次握手实际只占 1.5 个 RTT) | 确认双向可达、交换初始序号 |
| TCP + TLS 1.2 | 1 + 2 = 3 | 再加两轮:交换证书、协商密钥套件、生成会话密钥 |
| TCP + TLS 1.3 | 1 + 1 = 2 | TLS 1.3 把握手精简掉一个往返,这是它最大的卖点 |
| QUIC(HTTP/3) | 1,重连时 0 | 把传输层握手和加密握手合并成一次,还能复用上次的密钥 |
这张表解释了一个你能实际感受到的现象:跨国网站第一次打开特别慢。假设北京到美国单程 90ms,一个 RTT 就是 180ms。用 TCP + TLS 1.2,光是"打招呼"就要 3 个 RTT = 540 毫秒,这时候一个字节的网页内容都还没开始传。换成 QUIC,同样的距离只需 1 个 RTT = 180ms,直接省掉 0.36 秒。这就是为什么全球性的大公司拼命推 HTTP/3——在跨洋链路上,减少握手往返次数比增加带宽有效得多。
那 540 毫秒是个什么感觉?差不多是你眨五次眼的时间,也是人耳能明确察觉到"卡了一下"的门槛。更关键的是这半秒里一个字都还没开始传——纯粹花在"喂喂喂你好你好"上。这好比你去银行办事,取号、核身份、验密码折腾了十分钟,真正办的那笔业务只用了三十秒。QUIC 干的活儿就是把前面那十分钟压到三分钟,业务本身一点没变快,但你体感上快了一大截。
协议 = 网络里的"共同语言"。它规定了格式、含义、时序,让两台互不相识的机器能听懂对方。
三要素记牢:语法(格式怎么写,错一个空格就 400)、语义(每个字段啥意思,写错了不报错但全盘皆错)、时序(谁先说话、出错怎么办)。
分层是协议世界最重要的组织方式,它把"8×5×100 个组合"变成了"8+5+100 个协议"——把乘法变成加法。每层只跟上下相邻的层打交道,且只对同层的"对端"说话,中间的路由器插不上嘴。代价是每层都要加头部(约 3.6% 开销)和多次内存拷贝。
交互形状上,绝大多数协议是请求-响应;HTTP 天生不许服务器主动说话,所以才有轮询、长轮询、WebSocket 这些花招。状态上,HTTP 的无状态是它能在云上无限扩容的根本原因,Cookie 只是把"记忆"甩给了客户端。
协议还在不断演进,规律是能兼容就兼容——HTTP 从 0.9 走到 3,连底层 TCP 都换成了 QUIC;而 IPv6 因为不兼容,二十多年才普及一半。所有这些约定,都由公开免费的 RFC 文档定义,靠的是"大致的共识和跑得起来的代码"。
你不需要背下所有协议——只要记住一件事:遇到"两台机器连不上"的问题,第一步就是问:它们的协议对得上吗?版本对得上吗?谁该先说话?