应用层【协议再识/序列化与反序列化】

了解网络通信中为什么必须引入序列化/反序列化TCP字节流带来的挑战,以及协议定制的

本质。

目录

一、为什么read()读取TCP数据存在"Bug"?

[1.1 问题的根源:TCP是面向字节流的协议](#1.1 问题的根源:TCP是面向字节流的协议)

[1.2 字节流带来的粘包/拆包问题](#1.2 字节流带来的粘包/拆包问题)

二、重新认识"协议":从字符串到结构化数据

[2.1 协议的本质是什么?](#2.1 协议的本质是什么?)

[2.2 两种约定方案对比](#2.2 两种约定方案对比)

方案一:纯文本协议(如"1+1")

[方案二:结构化协议 + 序列化(推荐)](#方案二:结构化协议 + 序列化(推荐))

三、为什么不建议直接发送结构体?

[3.1 看似可行的方案](#3.1 看似可行的方案)

[3.2 不建议的原因分析](#3.2 不建议的原因分析)

[原因1:内存对齐问题(Memory Alignment)](#原因1:内存对齐问题(Memory Alignment))

原因2:跨语言兼容性

原因3:大小端问题(Endianness)

[3.3 那为什么OS内核可以传递结构体?](#3.3 那为什么OS内核可以传递结构体?)

四、序列化与反序列化详解

[4.1 核心概念](#4.1 核心概念)

[4.2 实际报文格式设计](#4.2 实际报文格式设计)

五、重新理解read、write与网络通信本质

[5.1 read/write的本质是"拷贝"](#5.1 read/write的本质是"拷贝")

[5.2 TCP为什么是全双工的?](#5.2 TCP为什么是全双工的?)

[5.3 TCP自主决定:什么时候发?发多少?出错了怎么办](#5.3 TCP自主决定:什么时候发?发多少?出错了怎么办)


一、为什么read()读取TCP数据存在"Bug"?

1.1 问题的根源:TCP是面向字节流的协议

在学习应用层协议之前,我们常用这样的代码读取数据:

复制代码
char buffer[1024];
ssize_t n = read(sockfd, buffer, sizeof(buffer)-1);  // 读取字符串

这个代码存在严重隐患! 因为TCP是面向字节流的协议,它只保证:

  • 数据按发送顺序到达

  • 数据不会丢失(在连接正常的情况下)

  • 但不保证:一次read就能读完一个完整的"报文"

1.2 字节流带来的粘包/拆包问题

假设客户端连续发送两条消息:

  • 消息A:"你好啊 2024-05-09 10:30:00 张三"

  • 消息B:"吃了吗 2024-05-09 10:30:01 李四"

由于TCP的字节流特性,接收方的缓冲区可能出现以下情况:

场景 接收到的数据
粘包 "你好啊 2024-05-09 10:30:00 张三吃了吗 2024-05-09 10:30:01 李四"
拆包(半包) 第一次read:"你好啊 2024-05-09 10";第二次read:"30:00 张三"
完整包 "你好啊 2024-05-09 10:30:00 张三"

关键问题:接收方如何知道一个完整报文的边界在哪里?

结论 :在TCP中,要读到完整的报文,必须由接收方的应用层自己确定报文边界这是TCP协议设计的基本原则------传输层只负责可靠传输,报文语义由应用层定义。


二、重新认识"协议":从字符串到结构化数据

2.1 协议的本质是什么?

协议是一种"约定" 。在Socket编程中,read/write收发的是字节流(字符串),但我们要传输的往往是结构化的数据

复制代码
// 一条聊天消息的构成
struct Message {
    std::string path;       // 头像 -> 图片的路径
    std::string nick_name;  // 昵称 -> string
    std::string msg;        // 消息内容 -> 字符串
};

协议定制的本质 :就是在定制双方都能认识的、符合通信和业务需要的结构化数据 。所谓的结构化数据,其实就是struct或者class

2.2 两种约定方案对比

方案一:纯文本协议(如"1+1")

  • 客户端发送形如"1+1"的字符串

  • 双方约定:两个操作数都是整数,中间用+连接,无空格

  • 缺点:扩展性差,解析复杂,容易出错

方案二:结构化协议 + 序列化(推荐)

  • 定义结构体/类来表示交互信息

  • 发送时 :将结构体按照规则转换成字符串 → 序列化

  • 接收时 :将字符串按照相同规则转回结构体 → 反序列化

  • 优点:扩展性好,类型安全,便于维护

从今天开始:如果我们要进行网络协议式的通信,在应用层强烈建议使用序列化和反序列化方案。至于直接传递结构体的方案,除非场景特殊,否则不建议。


三、为什么不建议直接发送结构体?

3.1 看似可行的方案

既然双方都用C/C++,为什么不直接这样?

复制代码
struct Request {
    int x;
    int y;
    char oper;  // + - * / %
};

struct Request req = {10, 20, '+'};
write(sockfd, &req, sizeof(req));  // 直接发送二进制结构体

疑问:可以直接发送二进制对象吗?因为CS双方都能认识这个结构体啊!!

答案:可以,但是不建议!!

3.2 不建议的原因分析

原因1:内存对齐问题(Memory Alignment)

复制代码
struct Request {
    int x;      // 4字节
    int y;      // 4字节  
    char oper;  // 1字节
    // 编译器可能在这里填充3字节,使总大小为12字节而非9字节!
};
  • 不同编译器、不同平台(x86 vs ARM)、不同编译选项的对齐策略不同

  • 发送方用GCC编译,接收方用Clang编译,结构体内存布局可能不同

  • 结果 :接收方解析出的xyoper值完全错乱

原因2:跨语言兼容性

发送方 接收方 能否直接传递结构体?
C C++ 可能可以(但内存对齐需完全一致)
C++ Python ❌ 不可行
C++ Java ❌ 不可行
Java Go ❌ 不可行
  • C/C++的结构体内存布局与Java的对象内存布局完全不同

  • Python没有真正的"结构体"概念

  • 网络通信需要跨平台、跨语言!

原因3:大小端问题(Endianness)

复制代码
int x = 0x12345678;
// 大端存储:12 34 56 78
// 小端存储:78 56 34 12
  • x86架构是小端,网络协议规定大端(字节序转换)

  • 直接发送结构体不做转换,在不同架构机器间通信会出错

3.3 那为什么OS内核可以传递结构体?

关键区别

  • OS内部协议栈全部用C语言编写,运行在同一台机器上,编译器相同,内存对齐一致

  • 网络通信:跨越不同机器、不同OS、不同编译器、可能不同语言

OS内部传递结构体对象可以,是因为环境完全可控;网络通信环境不可控,必须采用通用的文本/字节序列化方案。


四、序列化与反序列化详解

4.1 核心概念

序列化(Serialization):把信息由多变一,方便网络发送

  • 将结构化的struct/class转换为字节流/字符串

  • 例如:{"datax":10,"datay":20,"oper":"+"}

反序列化(Deserialization):把信息一变多,方便上层处理

  • 将字节流/字符串还原为结构化的struct/class

  • 上层业务代码直接操作对象属性,无需关心字符串解析

4.2 实际报文格式设计

为了处理TCP字节流的边界问题,我们设计如下报文格式:


五、重新理解read、write与网络通信本质

5.1 read/write的本质是"拷贝"

核心认知

  1. write/read就是收发消息 ------但write和read不是直接把数据发送到网络中!

  2. 主机间通信的本质:把发送方的发送缓冲区内部的数据,拷贝到对端的接收缓冲区!

  3. 计算机世界:通信即拷贝!

5.2 TCP为什么是全双工的?

原因 :在任何一台主机上,TCP连接既有发送缓冲区,又有接收缓冲区

  • 主机A可以同时:向自己的发送缓冲区写数据(发消息)+ 从自己的接收缓冲区读数据(收消息)

  • 主机B同理

  • 因此,内核中可以在发消息的同时,也可以收消息

这就是为什么一个TCP sockfd读写都是它的原因! 两个缓冲区独立工作,互不影响,形成了全双工通信。

5.3 TCP自主决定:什么时候发?发多少?出错了怎么办?

复制代码
write(sockfd, buffer, strlen(buffer));  // 数据拷贝到发送缓冲区
// 注意:此时数据可能还没真正发送到网络!
  • 什么时候发:由TCP的Nagle算法、拥塞控制等决定

  • 发多少:由滑动窗口、MSS(最大报文段长度)决定

  • 出错了怎么办:TCP重传机制、超时处理

这就是为什么TCP叫做"传输控制协议"(Transmission Control Protocol)------它控制了一切传输细节,应用层只需调用write/read即可。

相关推荐
A小辣椒21 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式