国标规定信令用 UTF-8,一个汉字占三字节;
而 SIP 的 Content-Length 数的是字节,不是字符。
这两个事实一撞,就是国标对接里最常见、也最难查的低级故障。
上一节讲数据交换,这一节把它收到最小:一个字段的长度算错会怎样。 这是纯工程细节,标准里只有一句话,但现场踩坑率极高。
一、字节和字符不是一个数
先分清两个概念:字符数是「有几个字」,字节数是「占了多少存储」。
纯英文里两者相等,一有中文就分家了。
UTF-8 里一个汉字占三字节,GB2312 里占两字节,而 Python 的 len() 数的是字符。
所以 len(name) 和 len(name.encode()) 差了三倍。
这不是 Python 的问题,是「文本」和「字节」本来就是两层:
协议报文走的是字节,人看的是字符,中间这次编码转换就是坑的入口。
记住一条就够用:凡是「长度」「偏移」「截断」这些词,一律按字节算;
凡是「几个字」「多长」这种话,一律按字符算。
二、Content-Length 的两种算法
SIP 报文的结构是「头+空行+正文」,Content-Length 告诉收方「正文有多少字节」。
按字符算会发生什么:
收方读到指定字节数就认为报文结束,但它读到的是前 9 个字节,而不是 27 个,
于是 XML 停在半路,闭合标签没了。
最坑的地方是报错的样子:
解析器报的是「XML 格式错误」或「意外的文件结束」,而不是「长度不对」。
报错位置和病因隔着两层,这就是这类故障难查的原因。
排查时有个快办法:抓包看 Content-Length 和正文实际字节数对不对得上。
正文里只要有一个中文,这两个数就一定不等,一眼就能看出来。
三、编码声明必须与实际一致
XML 头里有一句 encoding="UTF-8",这是给收方看的「我按什么编码写的」。
声明和实际不一致,收方就会按错的规则解,结果是乱码。
中文环境下这件事特别容易错:很多国产设备的默认编码是 GB2312,而国标要求 UTF-8。
设备把 GB2312 的字节贴上一个写着 UTF-8 的声明,平台解出来就是一堆问号。
对接时的正确做法:先抓一个包看 Content-Type 后面的 charset,
再看 XML 声明,两个都得是 UTF-8 才行。
光看文档不算数,设备实现经常和文档不一致。
还有一类更隐蔽的:报文里混了「全角空格」或「零宽字符」,
字符数看着对、字节数也对,但解析器就是认不出来。
这两个东西肉眼看不见,只能靠工具扫。
四、动手 Demo:一个汉字三字节,Length 却按字符数算
拿一个真实的中文设备名,量它在三种编码下各占多少字节。 再算「按字符数写 Length」会少多少字节,以及编码声明和实际不一致会怎样。
name = "宜昌市梅园中学北门"
for enc in ("utf-8", "gb2312", "utf-16"):
b = name.encode(enc)
print(enc, len(b), len(name), "%.2f" % (len(b) / len(name)))
b = name.encode("utf-8")
print("正确 Content-Length:", len(b))
print("错误 Content-Length:", len(name))
print("差", len(b) - len(name), "字节")
xml = '<?xml version="1.0" encoding="GB2312"?>...'
print(len(xml.encode("gb2312")), len(xml.encode("utf-8")))
实跑输出:
--------------------------------------------------------------------
一个汉字三字节,Length 却按字符数算
--------------------------------------------------------------------
**国标信令的字符集是 UTF-8**,**但 SIP 报文里有个 `Content-Length` 字段,它数的是字节。**
**这两个事实一撞,就是国标对接最常见的低级故障。**
设备名:宜昌市梅园中学北门
编码 字节数 字符数 倍数
-------------------------------------------------
**utf-8** **27** **9** **3.00**
**gb2312** **18** **9** **2.00**
**utf-16** **20** **9** **2.22**
**第一行和第二行的 3 倍关系就是问题的根源**:
**中文在 UTF-8 里一个占三字节,在 GB2312 里占两字节。**
**而 `len()` 在 Python 里数的是字符。**
**写错的后果是这样**:
正确写法:Content-Length: 27(字节数)
错误写法:Content-Length: 9(字符数)
差 18 字节 → 收方读完就截,XML 尾部闭合标签丢失 → 解析失败
**差这 18 字节,报错信息会是「XML 格式错误」**,
**而不是「长度不对」**——**因为收方确实按你说的长度读完了,只是读到的是半截。**
**这就是这类故障难查的原因:报错位置和病因隔着两层。**
**还有一个隐形的坑是编码声明**:
声明 GB2312 并用 GB2312 编:87 字节,正常
声明 GB2312 却用 UTF-8 编:96 字节,收方按 GB2312 解 → 乱码
**声明和实际编码必须一致**,
**这在中文环境下尤其容易错**——**因为很多国产设备的默认编码是 GB2312,而国标要求 UTF-8。**
**对接时先抓一个包看 `Content-Type` 后面的 `charset`,比猜快得多。**
本机默认编码:utf-8
文件系统编码:utf-8
**最后一行提醒一件事**:
**脚本跑得通不代表设备认得**——**本机默认 UTF-8,设备可能是 GB2312,两边都「能运行」,一碰就乱。**
第一行和第二行就是全部问题的根源:UTF-8 下一个字三字节,GB2312 下两字节。 九个字,一个是二十七字节,一个是十八字节。 第三行值得看一眼: UTF-16 是二十字节、倍数是 2.22——因为它每个字符固定两字节,再加两个字节的字节序标记。 所以「一个汉字几字节」这个问题没有统一答案,取决于编码。 正确写 27、错误写 9,差 18 字节:收方读完 9 个字节就收工,XML 停在半路。 它报的错会是「格式错误」,而不是「长度不对」。 最后两行是编码声明的坑:同一个 XML,GB2312 编出来 87 字节,UTF-8 编出来 96 字节。 声明写的是 GB2312,收方就按 GB2312 解, 哪怕实际是 UTF-8 编的,前面几个 ASCII 字符还能对上,一遇到中文就全乱。 注意本机默认编码是 UTF-8: 所以脚本在本机跑得通,不代表设备认得——两边都「能运行」,一碰就乱,这是最难查的一类问题。
30 秒实验
- 算一下:字符串「中文」的字符数和 UTF-8 字节数各是多少?(2 和 6)
- 想一下:为什么
Content-Length算错时报的是 XML 错而不是长度错? - 想一下:为什么「本机能跑」不能证明「设备能认」?
常见误区
- 以为「一个汉字两字节」。 那是 GB2312,UTF-8 下是三字节。
- 以为「
len()数的是字节」。 Python 3 里它数的是字符,要.encode()才是字节。 - 以为「声明写了 UTF-8 就是 UTF-8」。 声明和实际编码可以不一致,得看实际字节。
自测三问(点开对答案)
1. UTF-8 下一个汉字占几字节?GB2312 下呢? —— **UTF-8 是三字节,GB2312 是两字节**。 2. `Content-Length` 数的是字符还是字节? —— **字节**。SIP 是字节协议,正文里所有长度都按字节算。 3. 为什么编码声明和实际不一致会导致乱码? —— **收方按声明的编码去解字节**,**规则不对,多字节字符的边界就全错了。**下一节讲控制要求:200 毫秒的跟手感觉,到底花在哪几段上。
- ☐ (本节完,下一节 10.06 控制要求)