会话层管「这段对话从哪开始、到哪结束」,表示层管「字节怎么解释」。 这两件事在 TCP/IP 里没单独成层,但活一点没少——全压在应用身上。
1.6.1 看的是全景,这一节只看最上面三层。 这一节的实测是「编码转换」,因为它最能体现「表示层」的存在。
一、会话层:这段对话怎么算开始、怎么算结束
会话层要解决的是「上下文」:
| 要解决的 | HTTP 里的做法 | 没有会怎样 |
|---|---|---|
| 从哪开始 | 建立 TCP 连接 | 每次都要重新认证 |
| 谁的身份 | Cookie、Token | 每个请求都是陌生人 |
| 到哪结束 | Connection: close |
靠超时猜 |
第二行是「会话」最直观的例子:HTTP 本身是无状态的,
登录这件事完全靠 Cookie 或 Token 把身份一次次带上来。
所以「会话」不是一条连接,是一串请求之间的约定。
第三行容易被忽略:HTTP/1.1 默认保持连接,不写 Connection: close 服务端就不主动关。
这时候「对话结束」靠什么判断?
靠 Content-Length 或分块编码里的结束块——本质上还是在应用层自己造了一套「说完了」的信号。
二、表示层:字节怎么解释
同一句话,两种编码,字节数就不一样:
| 编码 | 字节数 | 前 8 字节 |
|---|---|
| UTF-8 | 20 | e5 9b be e8 a7 a3 54 43 |
| GBK | 16 | cd bc bd e2 54 43 50 2f |
UTF-8 每多一个字多 3 字节,GBK 多 2 字节:所以中文内容用 GBK 编码能省三分之一左右。 这也是早期国内网站偏爱 GBK 的原因——当年带宽贵,省字节就是省钱。 但代价是「能不能表示」:GBK 只覆盖常用汉字,遇到生僻字只能变成问号,而且不可逆。 这种「转过去就转不回来」的损失,是历史数据迁移里最常见的事故。 所以现在的原则是:内部一律 UTF-8,只在必须兼容旧系统时才转。 表示层还管两件事:压缩与加密。 它们和编码的共同点是「改变字节形态、不改变语义」—— 这正是「表示」这个词的含义:换一种表示方式。
三、应用层:业务语义在这
三层并成一层之后,应用层要自己扛四件事:
| 要扛的 | 典型字段 | 出错的表现 |
|---|---|---|
| 编码 | Content-Type: charset |
乱码 |
| 压缩 | Content-Encoding |
解出来是乱码 |
| 会话 | Cookie、Authorization |
反复登录 |
| 业务语义 | URL、方法、状态码 | 逻辑错 |
第一行那个「乱码」是最典型的表示层故障:网络全通、字节一个不差,就是看不懂。 这说明底下几层全对,错在「怎么解释这些字节」。 有分层概念,你才能把「通不通」与「懂不懂」分开判断,这一步分开,能省掉一半的排障时间。 第三行解释了为什么现代系统更爱用 Token: Cookie 是浏览器机制,跨端(App、小程序、服务端调用)不好用; Token 放在头部里,任何客户端都能带。 这仍然是「会话层」的活,只是换了个载体。
四、动手 Demo:会话层以上的处理,实质是「编码转换」
把一句话按 UTF-8 编码,再转成 GBK,再转回来。 看字节怎么变、内容丢不丢。
src = '图解TCP/IP 第5版'
u8 = src.encode('utf-8')
gb = src.encode('gbk')
back = gb.decode('gbk')
print('UTF-8 %d 字节 %s' % (len(u8), u8[:8].hex(' ')))
print('GBK %d 字节 %s' % (len(gb), gb[:8].hex(' ')))
print('转回来:%s' % back)
try:
src.encode('gbk')
except UnicodeEncodeError as e:
print('这句能编:ok')
print('生僻字试一下:', end='')
try:
'𠮷'.encode('gbk')
except UnicodeEncodeError:
print('GBK 编不了,会丢字')
实跑输出:
--------------------------------------------------------------------
会话层以上的处理,实质是「编码转换」
--------------------------------------------------------------------
**OSI 把「表示层」单列出来,专门管编码转换。**
**TCP/IP 里这活交给应用,但活本身还在。**
同一句话:UTF-8 编码 20 字节、GBK 编码 16 字节。
下面真做一次转换,看字节怎么变:
UTF-8 20 字节 → 解码 → 再编成 GBK 16 字节
再转回来:图解TCP/IP 第5版
编码 字节数 前 8 字节
---------------------------------------------
UTF-8 20 e5 9b be e8 a7 a3 54 43
GBK 16 cd bc bd e2 54 43 50 2f
**注意「转回来还是原文」这句话**:**说明这次转换是无损的**——**因为两种编码都能表示这些字符。**
**会丢字的情况是这样**:**把带生僻字的文本转成 GBK,遇到 GBK 里没有的字就只能变成问号。**
**这种「不可逆转换」是历史数据迁移里最常见的事故来源。**
**所以现在的原则是:内部一律 UTF-8,只在必须兼容旧系统时才转。**
UTF-8 是 20 字节、GBK 是 16 字节:同一句话,GBK 少 4 个字节。
注意前 8 个字节完全不同——
UTF-8 里汉字是 e5 9b be 这种三字节序列,GBK 是 cd bc 这种两字节。
「转回来还是原文」说明这次转换是无损的:因为这句话里的字符两种编码都能表示。
会丢字的情况是最后两行:生僻字在 GBK 里没有对应,只能抛异常或变成问号。
hex(' ') 是 Python 3.8 之后的写法:把字节直接打成空格分隔的十六进制,比手写循环省事。
30 秒实验
- 把
src换成一串纯英文,看两种编码的字节数还差多少——想想为什么英文没有差别。 - 把
src换成一段 emoji,看 GBK 会怎样。 - 查一下
Content-Type里charset的作用,想想如果没有这个字段,浏览器怎么猜编码。
常见误区
- 以为「会话」就是「连接」。 连接是 TCP 的,会话是请求之间的上下文, 一条连接上可以有很多次会话,一次会话也能跨多条连接。
- 以为编码转换总是无损的。 目标编码表示不了的字符会丢, 而且不可逆——这是历史数据迁移的经典事故。
- 以为分层被并掉就等于「不需要」。 活一点没少,只是没人替你干, 所以每个应用都要重做一遍编码与会话。
自测三问(点开对答案)
1. 会话层在 HTTP 里由什么承担? —— **Cookie、Token 承担「身份」,连接与 `Connection` 头承担「从哪开始到哪结束」。** HTTP 本身无状态,会话全靠这些字段一次次带上来。 2. 同一句中文,UTF-8 与 GBK 的字节数为什么不同? —— **UTF-8 一个汉字 3 字节,GBK 是 2 字节**,所以中文文本 GBK 更省。 代价是 GBK 覆盖不全,生僻字会丢。 3. 「乱码」说明哪几层是好的? —— **底下四层全好**:通、且字节一个不差。错在「怎么解释字节」,也就是表示层(应用的编码声明)。下一节放大下半段:传输层往下怎么把字节送出去,硬边界在哪。
- ☐ (本节完,下一节 1.6.3 传输层以下的处理)