HTTP 的两个角色不是「两台机器」,是两个角色:先发请求的那个是客户端, 回请求的那个是服务端。同一台机器可以两者都是。
第一章走完了整条链路。从这一节起拆报文本身,第一个问题是:这条连接的两端分别是谁、谁先说话。
一、角色由「谁先发起」决定
HTTP 是请求/响应模型:永远是客户端先发请求,服务端再回响应。
服务端不能主动推消息(要推得用 WebSocket,见 9.3 节)。
这条规矩决定了三件事:谁先建立 TCP 连接、谁先发第一个字节、
以及服务端能不能在没有请求的时候发东西——答案是不能。
Demo 里本机自连自:一个进程连自己起的服务端,127.0.0.1 既是客户端地址也是服务端地址,
只有端口不同。所以「客户端」「服务端」是角色名,不是身份名。
二、一请求一响应,一对一
一个请求只对应一个响应。 这一点在 HTTP/1.1 里尤其严格:同一个连接上,请求和响应不能交错——发了请求就得等响应回来, 除非用管线化(2.7.2),而它已经废弃了。 HTTP/2 打破了这个限制:同一条连接上可以有多个并行的流,每个流有自己的编号,响应可以乱序。 但「谁先发起」这条没变——仍然是客户端先开口。
三、服务端看见的只有请求
服务端拿不到「用户是谁」「上次问了什么」「浏览器开了几个标签」,它看到的只有这一段字节:请求行、 首部、正文。全部信息都在这段字节里。 这条限制看起来苛刻,其实是 HTTP 能活这么久的原因:服务端不需要记住任何东西, 所以任何一个实例都能处理任何一个请求,加机器就能扩容。代价是每个请求都要把上下文重说一遍。
四、动手 Demo:自己连自己:一条连接上的两个角色
在本机起一个服务端,再用裸 socket 连上去,把客户端这边用的端口和服务端记下的对端地址都打出来。
def resp(raw, st):
first, hd, body = H.parse_headers(raw)
seen[u'line'] = first
return (u'HTTP/1.1 200 OK', [(u'Content-Type', u'text/plain')],
u'我是服务端'.encode())
with H.server_multi(resp, keepalive=False) as port:
s = socket.create_connection((u'127.0.0.1', port), timeout=2.0)
local = s.getsockname() # 客户端这边用的端口
H.MULTI.state[u'peer'] = local
s.sendall(_req()) # GET /index.html HTTP/1.1
got = H.read_all(s)
实跑输出:
--------------------------------------------------------------------
谁是客户端、谁是服务端
--------------------------------------------------------------------
本机既是客户端也是服务端 127.0.0.1
客户端这边用的端口 56487(内核临时分配)
服务端看见的请求行 GET /index.html HTTP/1.1
服务端记下的对端 127.0.0.1:56487
客户端收到的 HTTP/1.1 200 OK,正文「我是服务端」
角色不是由机器决定的,是由**谁先发起**决定的。
同一台机器、甚至可以同一个进程,发请求时是客户端,回请求时是服务端。
连接一旦建立,**两个方向都能传数据**——HTTP 只规定谁先说话。
客户端这边用的端口 56428(内核临时分配)、服务端记下的对端 127.0.0.1:56428——两个地址完全一样,
只有端口不同。一条 TCP 连接由「源 IP + 源端口 + 目的 IP + 目的端口」四元组唯一确定。
服务端看到的请求行是 GET /index.html HTTP/1.1,它从这行里只知道「要什么」,不知道「谁要」。
30 秒实验
- 把
keepalive=False改成True,在同一条连接上连发三次,看服务端记下的对端端口会不会变。 - 同时开两个 socket 连上去,比较两个客户端端口——应该是两个不同的数。
- 想一想:如果服务端在没收到请求时主动发数据,客户端会怎么处理?
常见误区
- 「服务端是那台性能好的机器」——角色按行为分,不按机器分。
- 「客户端只能发、服务端只能收」——连接建立后两个方向都能传数据,HTTP 只规定谁先说话。
- 「服务端能主动推送」——原生 HTTP 不行,要推送得靠长轮询、SSE 或 WebSocket。
自测三问(点开对答案)
1. HTTP 里谁是客户端? —— 先发起请求的那一方。同一个程序在不同时刻可以是客户端也可以是服务端。 2. 一条 TCP 连接靠什么唯一确定? —— 源 IP、源端口、目的 IP、目的端口,四元组。 3. 为什么 HTTP/1.1 里一个连接一次只能有一个在途请求? —— 协议用「请求—响应」严格配对,没有编号机制区分响应属于哪个请求。角色分了。下一节看它们交换的那两段字节长什么样。
- ☐ (本节完,下一节 2.2 通过请求和响应的交换达成通信)
相关笔记
- TCP/IP的分层管理
- 36 课 · 图解TCP/IP(第5版)(四元组与连接标识)
- 📘 图解HTTP · 索引