
序列化与自定义协议把结构体拆成字节流再把字节流拼回来我的github(https://github.com/xcx55/ubuntu-linux-project)感谢各位大佬参观我的githubTCP 编程跑通之后8-31紧接着 9 月 1 号到 3 号解决一个所有 TCP 程序员都绕不开的问题网络上传的是字节流可上层想传的是结构化数据中间的鸿沟怎么填这篇从 read/write 的本质讲起一路讲到 sk_buff、粘包问题和一套完整的自定义协议。一、先纠正一个直觉read/write 是拷贝函数很多初学者以为write是把数据发出去、read是把数据收回来。笔记里第一句话就把这个直觉掰正了read 和 write 是拷贝函数。就算是文件系统read/write 也是把数据写到内核缓冲区后面由线程再刷入。write/read 的任务是把数据交给操作系统结束。所以网络 IO 的真实图景是那张手绘图的核心用户进程 内核TCP 对端内核TCP 对端用户进程 | write() 拷贝 → 发送缓冲区 ────网络────→ 接收缓冲区 ← 拷贝 read() |写满发送缓冲区write 阻塞接收缓冲区没数据read 阻塞——细节下一节解释。二、为什么 TCP/UDP 是全双工因为有两个缓冲区协议各自拥有两个缓冲区发送一个、接收一个两个方向互不干扰地读写——这就是全双工的物理基础。而每一个缓冲区都是一个生产者消费者模型只不过生产者/消费者从两个线程变成了用户和内核read 阻塞的本质缓冲区没数据OS 让这个线程 cond 等待数据来了中断唤醒 read 线程write 阻塞的本质缓冲区写满cond 等待对端读走腾出空间唤醒 write 线程。read/write 拷贝完数据后再去 cond 等待下一轮。文件系统也是这套思路——管道的阻塞、文件的读写和网络的阻塞是同一个模型抢锁 同步。到这里信号、线程、网络三章在cond 等待队列这个点上彻底合流。三、内核怎么管理报文sk_buff缓冲区里面长什么样答案是队列 管理结构左边是描述一个报文的数据结构右边是真实的报文数据——两者分开结构里有个data指针指向报文数据网络层加包时 data 指针再上一层加包再——报文的层层封装就是管理结构里指针的移动每个报文都有自己的管理结构叫sk_buff成员里有 next/prev 双链指针、len/data_len、cb[48] 控制块等发送缓冲区和接收缓冲区就是sk_buff 管理队列sk_buff_head queue一个节点描述一个报文读取报文就是从这个队列里拿管理结构。又一个先描述、再组织缓冲区本身不存数据它存的是 sk_buff 链表。这句笔记的感叹值得保留“看来文件的缓冲区也是类似的办法至少也是有管理结构的”——Linux 的每个角落都在重复同一个设计模式。四、粘包问题TCP 会少量多次TCP 不像 UDP 一次一个完整报文它可能一点一点地发送数据对方接收缓冲区字节不够时也会分批收。于是就有了经典的粘包问题发送端分了三次发接收端一次 read 以为收到了全部——但其实只到了半截。数据不会丢TCP 保证可靠性丢了也有补救但边界没了。UDP 为什么没这个问题因为它面向数据报每个报文有天然边界TCP 面向字节流边界是用户层的责任。五、应用层不推荐直接传结构体要传结构化的数据struct 或 class第一反应是二进制结构体直接发。笔记里明确否了这条路应用层不推荐传递结构体——可能有一些语言不支持二进制结构体。那推荐什么序列化发送时多变 1把结构体转成统一的形态字符串方便传输接收时1 变多反序列化还原成结构体方便上层处理只要序列化和反序列化的方式一样即可两端语言不一样无所谓。这就是协议是一种约定的落地所谓协议定制本质就是定制双方都认识的、符合通信和业务需要的结构化数据。六、自定义协议len\r\n value \r\n序列化解决了结构体变字符串但没解决粘包——TCP 还是可能把一个 JSON 拆成几片发过来。所以要再套一层报文定制真实 send 的序列 JSON value 的长度 \r\n JSON value 本身 \r\n解析规则报文最前面是一个长度报头4 字节 int假设前 4 字节表示 20说明接下来要 read 满 20 个字节才算一个完整报文连 4 个字节都没读到 → 读取失败继续攒第一个\r\n用来切出长度和载荷的边界第二个\r\n是因为 TCP 分片发送作为报文结束的兜底分隔。网络流上读取顺序是递归推导的服务刚启动时第一个读到的必然是某个 JSON 报文的长度字节因为初始状态定了缺失的部分会被继续等待下一个报文一定又从 len 开始——所以逻辑只需要处理读够 len → 读载荷 → 再读 len这一个循环。用到的字符串操作也很朴素find返回\r\n的下标substr(起始下标, 个数)截取——协议解析没有魔法就是字符串切割。七、JSON不要手写序列化笔记里的原话“不推荐自己做序列化和反序列化反序列化使用 JSON”Json::Value是万能对象obj[key] value生成 KV 结构什么类型都能装序列化 把 Value 转成 JSON 看得懂的字符串反序列化 传入 JSON 字符串、取出字段JSON 字符串才是 TCP/UDP 真正传输的内容——面向字节流就是字符串的 IO还有个细节网络传输的 JSON 字符串没有\0长度由报头负责不靠 C 字符串的结束符。一个应用层的额外收益序列化和反序列化可以提高应用层的性能结构体对齐、字节序、语言差异的坑一次全部绕开。八、完整链路一次网络通信的全景把所有层串起来就是笔记里那张流程图的文字版客户端 服务端 结构体数据 {message, time, nickname} → 序列化成 JSON 字符串 → 封包 len\r\n value \r\n → write/send 拷贝进发送缓冲区 ──────── TCP 字节流 ────────→ 接收缓冲区 → read 拷贝出来 → 解包拆 len 报头 → 反序列化得到数据 → 业务计算 → 结果再序列化 → 再封包 → send ← ────────────────────────────── → 解包 → 反序列化 → 客户端拿到结果核心思想就两个加包 JSON 字符串。再怎么封装、继承、多态都是这两个动作的组织形式。九、工程结构模板方法与分层代码层面笔记还留了两个设计点模板方法模式父类有一个虚函数内部调用了另外三个虚函数子类只覆盖那三个外层流程不动——“调用我子类覆盖后的三个函数”。配合虚表机制编译器链接器生成完整虚表、运行时call *基址eax*4实时计算函数地址UDP 和 TCP 重合的功能用继承合并差异部分留给子类线程池里不要用 unique_ptr拷贝语义的坑子进程拷贝栈是写时拷贝虚拟地址不用变——前面章节的知识在这里顺手复用。最后一句总结很到位这个代码分了很多层——应用层序列化、协议层封包解包、传输层缓冲区、内核 sk_buff 队列每一层只做一件事靠约定衔接。网络编程写到这一步才算真正摸到协议这两个字的分量。