协议不只规定「对的怎么写」,还规定「错了怎么答」。 没有明确的拒绝,对端只能干等或乱猜——这才是协议真正要解决的问题。
1.3.1 讲了协议是什么,这一节讲「没有协议会怎样」。 1.3.3 会用人对话打比方,把这套规则讲得更具体。
一、没有协议的三个后果
假设两台机器都没有协议,只有一根线连着:
| 后果 | 具体表现 | 协议怎么解决 |
|---|---|---|
| 不知道从哪开始 | 收到一串字节,分不清头尾 | 起始行 + 固定格式 |
| 不知道谁该说话 | 两边同时发,撞车 | 请求/应答模型 |
| 不知道错在哪 | 收到乱码,无法定位 | 错误码 + 校验 |
第二行那个「撞车」是最早的真实问题:共享介质上的设备会同时发,信号叠加成噪声。 所以以太网设计了一整套「先听后发、冲突重发」的规则(CSMA/CD)。 第三行那个「不知道错在哪」是排障成本的最大来源: 有了错误码,问题能定位到「哪一层、哪一类」;没有错误码,只能靠猜。
二、错误码的设计
HTTP 的状态码分五类,第一位就表明了性质:
| 范围 | 含义 | 典型 |
|---|---|---|
| 1xx | 还在处理 | 100 Continue |
| 2xx | 成功 | 200 OK |
| 3xx | 要你换个地方 | 301 永久重定向 |
| 4xx | 你这边有问题 | 400、401、404 |
| 5xx | 我这边有问题 | 500、502、503 |
第四行与第五行的分界最重要:4xx 是「客户端错了」,5xx 是「服务端错了」。 这个分界决定了「谁来修」。 所以排障时看到 4xx 先查自己发的请求,看到 5xx 先查服务端日志——方向一开始就不能错。 这也说明协议设计里的「分类」不是整理癖,它是给排障者的一条最短路径。
三、协议也是一种防御
明确的拒绝能挡住的东西,比想象的多:
| 情形 | 有协议时 | 没协议时 |
|---|---|---|
| 格式错误 | 立即回 400 | 可能一直等 |
| 超长请求 | 回 414 或断开 | 缓冲区溢出 |
| 未知方法 | 回 501 | 行为不确定 |
第二行那个「缓冲区溢出」是历史上一大类漏洞的来源:不检查长度,直接往固定大小的缓冲区里拷。 所以「按协议拒绝」不只是礼貌,是安全边界——协议里那些「必须拒绝」的条款,本质是防御规则。 这也是为什么写服务端第一件事是「参数校验」:校验就是执行协议里的拒绝条款。
四、动手 Demo:不按协议来,对端会怎么处理
起一个最小的 HTTP 服务,发四条请求:一条标准、三条故意写错。 看错误的那三条各自拿到什么回应——关键是「有没有明确拒绝」。
import socket, threading, http.server, socketserver
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Length', '2')
self.end_headers()
self.wfile.write(b'ok')
def log_message(self, *a):
pass
httpd = socketserver.TCPServer(('127.0.0.1', 0), H)
port = httpd.server_address[1]
threading.Thread(target=httpd.serve_forever, daemon=True).start()
cases = [('标准', b'GET / HTTP/1.0\r\n\r\n'),
('缺方法', b'/ HTTP/1.0\r\n\r\n'),
('版本错', b'GET / HTTP/9.9\r\n\r\n'),
('方法乱写', b'FOO / HTTP/1.0\r\n\r\n')]
for tag, raw in cases:
c = socket.create_connection(('127.0.0.1', port), timeout=2)
c.sendall(raw)
buf = b''
while True:
d = c.recv(512)
if not d:
break
buf += d
c.close()
first = [x for x in buf.decode('latin-1').split('\r\n')
if x.startswith('HTTP/')]
print('%-10s %s' % (tag, first[0] if first else '(无状态行)'))
httpd.shutdown()
实跑输出:
--------------------------------------------------------------------
不按协议来,对端会怎么处理
--------------------------------------------------------------------
**协议的「必要性」最好用反例证明**:故意发一条格式不对的请求,看对端是崩掉、还是明确拒绝。
服务端已起在 54170,只认标准 HTTP 请求。
情形 请求字节 首行应答
-----------------------------------------------------------
标准请求 18 HTTP/1.0 200 OK
缺方法 14 <!DOCTYPE HTML>
<html lang="en">
版本写错 18 <!DOCTYPE HTML>
<html lang="en">
方法乱写 18 HTTP/1.0 501 Unsupported method
**四条里只有第一条拿到 200,其余全被明确拒绝。**
**这就是协议存在的意义**:**它不只规定「对的怎么写」,还规定「错了怎么答」。**
**没有 400 这一类明确拒绝,对端只能干等或乱猜。**
四条里只有第一条拿到 200,其余三条都被明确拒绝。
答案行不是从响应体的第一行取的——Python 的错误页会先输出 HTML,状态行夹在中间,
所以要遍历找 HTTP/ 开头的那一行。
这个细节本身就是个教训:解析响应要按协议找状态行,不能假设「第一行就是」。
很多简易客户端就栽在这里。
30 秒实验
- 再加一条超长 URL 的请求(比如路径里放 10000 个 a),看返回 414 还是直接断开。
- 发一条不带
\r\n\r\n的请求再关掉连接, 看服务端会不会一直等——这就是「没有明确结束标记」的后果。 - 用
curl -v发同样的四条请求,看它的输出和你手工发有什么不同。
常见误区
- 以为协议只是「格式说明」。 它还包括「异常处理」, 而且异常部分往往更重要——它决定了系统出错时的行为。
- 以为拒绝请求是「不友好」。 明确拒绝是安全边界,含糊处理才是隐患。
- 以为 4xx 和 5xx 只是数字不同。 它们划的是责任边界:4xx 你改,5xx 我改。 方向错了会浪费大量时间。
自测三问(点开对答案)
1. 没有协议会带来哪三个后果? —— **不知道从哪开始(分不清头尾)、不知道谁该说话(撞车)、不知道错在哪(无法定位)。** 协议分别用起始行、请求应答模型、错误码解决。 2. HTTP 状态码 4xx 与 5xx 的分界意味着什么? —— **责任归属**:4xx 是客户端错了,5xx 是服务端错了。**排障时先看这个分界,方向就不会错。** 3. 为什么说「按协议拒绝」是一种防御? —— **因为不检查长度、不校验格式,就会出缓冲区溢出这类漏洞。**协议里「必须拒绝」的条款, 本质是防御规则。下一节换个更容易懂的说法:把协议比成人跟人的对话。
- ☐ (本节完,下一节 1.3.3 协议如同人与人的对话)