📚 第二章 TCP/IP基础知识
📄 一句话概括

表示层与会话层被并进应用层,代价是每个应用都得自己管编码与会话。 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 秒实验

  1. 把 Set-Cookie 那一行删掉,看第二次请求还能不能「认出」你。
  2. 把 charset 改成 gbk,看浏览器会怎么解释中文。
  3. 想一下:为什么「Cookie 自动带」会成为安全问题,而「Token 手动带」不会?

常见误区

  1. 以为 HTTP 是有状态的。 它本身无状态,会话是靠 Cookie 每次带一遍伪造出来的。
  2. 以为「会话」就是「连接」。 连接是 TCP 的,会话是请求之间的上下文, 一条连接上可以有多次会话。
  3. 以为并层只有好处。 它的代价是「同一个问题被解决几十遍」,压缩、编码、会话都是如此。
自测三问(点开对答案) 1. 应用层要自己扛哪四件事? —— **编码、压缩、会话、业务语义。** 前两件本来是表示层的,第三件本来是会话层的。 2. 「表示层」在 HTTP 里落在哪个字段? —— **`Content-Type` 里的 `charset`**(管编码)与 `Content-Encoding`(管压缩)。 3. 为什么「Cookie 自动带」会成为安全问题? —— **因为浏览器会自动附带,跨站请求也会带上**(CSRF)。 而 Token 放在自定义头里不会自动带,所以能防 CSRF。

下一节进入 2.5:一次完整的通信示例——从发送到接收,把四层走一遍。

相关笔记