表示层与会话层被并进应用层,代价是每个应用都得自己管编码与会话。
charset 是表示层的落地,Cookie 是会话层的落地——都是应用层字段。
2.4.5 讲的是传输层,这一节讲最上面那一层。 这一节会把「并掉的三层」各自在 HTTP 里找出来,看它们落在哪个字段。
一、并掉三层之后
应用层要自己扛四件事:
| 要扛的 | 对应 OSI 的哪层 | HTTP 里的字段 |
|---|---|---|
| 编码 | 表示层 | Content-Type: charset |
| 压缩 | 表示层 | Content-Encoding |
| 会话 | 会话层 | Cookie、Authorization |
| 业务语义 | 应用层 | URL、方法、状态码 |
前两行都是「表示层」的活:编码解决「字节怎么解释」,压缩解决「怎么少传点」。 在 OSI 里这是两层统一管的,在 TCP/IP 里变成两个 HTTP 头。 所以「看 HTTP 头就能知道表示层在干什么」。 第三行是「会话层」的活:HTTP 本身是无状态的, 「登录状态」完全是靠每个请求都带一遍身份来伪造出来的。 所以「会话」不是一条连接,是一串请求之间的约定。
二、Cookie 与会话
一次「登录」在协议上发生的事:
| 步骤 | 发生什么 | 用的是哪个头 |
|---|---|---|---|
| 1 | 客户端提交凭据 | Authorization 或表单 |
| 2 | 服务端下发凭据 | Set-Cookie |
| 3 | 客户端保存 | 浏览器本地 |
| 4 | 后续请求自动带上 | Cookie |
第二行那个「下发」是会话的起点:服务端在响应头里放一个 Set-Cookie,
浏览器存下来,之后每次请求自动带上。
所以「登录」这件事,本质是「拿到一个字符串并在之后每次重复它」。
第四行那个「自动带上」是浏览器替你做的事:你在代码里看不到这一步,
所以「Cookie 是怎么带上去的」常常让人困惑。
而 App 或服务端调用就没有这个自动机制——
所以现代系统更爱用 Token,放在 Authorization 头里手动带。
这里有个安全上的要点:Cookie 是「浏览器自动带」的,所以它能被「跨站请求」利用(CSRF)。
Token 放在自定义头里则不会自动带,这就是「为什么 Token 能防 CSRF」的技术原因。
三、代价与收益
并掉三层,换来了什么:
| 方面 | 合并前(OSI 设想) | 合并后(现实) |
|---|---|---|
| 统一性 | 统一处理编码 | 每个应用各做一遍 |
| 灵活性 | 受协议栈约束 | 想怎么做都行 |
| 重复劳动 | 无 | 每个应用都要重写 |
第二行是收益:编码、压缩、会话这几件事,各家需求差别极大,硬统一不如各自为政。
比如二进制协议(gRPC)根本不需要 charset,如果协议栈强制管编码,它反而成了负担。
第三行是代价:同一个问题被解决了几十遍,而且每家做法都不同。
典型的例子是「压缩」:
HTTP 有 gzip/br,WebSocket 有 permessage-deflate,数据库协议又有自己的一套。
这就是「并层」的长期成本——它把「一次做对」变成了「无数次各做一遍」。
四、动手 Demo:应用层自己管编码与会话
起一个 HTTP 服务,第一次不带 Cookie、第二次带上。 看服务端怎么靠一个字段认出「同一个人」。
import http.server, socket, socketserver, threading
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
ck = self.headers.get('Cookie') or '(无)'
body = ('path=%s cookie=%s' % (self.path, ck)).encode()
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.send_header('Set-Cookie', 'sid=abc123; Path=/')
self.end_headers()
self.wfile.write(body)
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()
def get(path, cookie=None):
c = socket.create_connection(('127.0.0.1', port), timeout=3)
req = 'GET %s HTTP/1.1\r\nHost: x\r\n' % path
if cookie:
req += 'Cookie: %s\r\n' % cookie
req += 'Connection: close\r\n\r\n'
c.sendall(req.encode())
buf = b''
while True:
d = c.recv(4096)
if not d:
break
buf += d
c.close()
return buf
get('/first')
buf = get('/second', 'sid=abc123')
print(buf.decode().split('\r\n\r\n')[-1])
httpd.shutdown()
实跑输出:
--------------------------------------------------------------------
应用层自己管编码与会话
--------------------------------------------------------------------
**TCP/IP 把表示层与会话层并进了应用层,代价是应用得自己干这两件事。**下面起一个 HTTP 服务,
看它怎么用头部字段补上。
**第一次请求没带 Cookie,服务端回了 `Set-Cookie: sid=abc123`**;
**第二次带上它,服务端就认得是同一个人了。**
**这就是「会话层」在 TCP/IP 里的落地方式**:**没有独立的会话层协议,只有应用层的 Cookie 字段。**
**HTTP 本身是无状态的**,**「登录状态」完全是靠每个请求都带一遍身份来伪造出来的。**
**`charset=utf-8` 那个字段就是「表示层」的落地方式**:**它告诉对端「这些字节按 UTF-8 解释」。**
**少了它,中文就变乱码**——**这就是 1.6.2 说的「活还在,只是没人替你干」。**
第一次请求没带 Cookie,服务端回了 Set-Cookie: sid=abc123:
第二次带上它,服务端就认得是同一个人了。
这就是「会话层」在 TCP/IP 里的落地方式:没有独立的会话层协议,只有应用层的 Cookie 字段。
charset=utf-8 那个字段就是「表示层」的落地方式:它告诉对端「这些字节按 UTF-8 解释」。
少了它,中文就变乱码——这就是 1.6.2 说的「活还在,只是没人替你干」。
30 秒实验
- 把
Set-Cookie那一行删掉,看第二次请求还能不能「认出」你。 - 把
charset改成gbk,看浏览器会怎么解释中文。 - 想一下:为什么「Cookie 自动带」会成为安全问题,而「Token 手动带」不会?
常见误区
- 以为 HTTP 是有状态的。 它本身无状态,会话是靠 Cookie 每次带一遍伪造出来的。
- 以为「会话」就是「连接」。 连接是 TCP 的,会话是请求之间的上下文, 一条连接上可以有多次会话。
- 以为并层只有好处。 它的代价是「同一个问题被解决几十遍」,压缩、编码、会话都是如此。
自测三问(点开对答案)
1. 应用层要自己扛哪四件事? —— **编码、压缩、会话、业务语义。** 前两件本来是表示层的,第三件本来是会话层的。 2. 「表示层」在 HTTP 里落在哪个字段? —— **`Content-Type` 里的 `charset`**(管编码)与 `Content-Encoding`(管压缩)。 3. 为什么「Cookie 自动带」会成为安全问题? —— **因为浏览器会自动附带,跨站请求也会带上**(CSRF)。 而 Token 放在自定义头里不会自动带,所以能防 CSRF。下一节进入 2.5:一次完整的通信示例——从发送到接收,把四层走一遍。
- ☐ (本节完,下一节 2.5.1 数据包首部)