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

了解网络通信中为什么必须引入序列化/反序列化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即可。

相关推荐
天草二十六_简村人1 小时前
对接AI大模型之nginx代理配置SSE接口
运维·网络·nginx·http·阿里云·ai·云计算
小趴菜克鲁里1 小时前
Cocos Creator 进阶:打造灵活可控的进度条动画组件(循环与分段)
运维·nginx
北风toto1 小时前
log4j中文日志乱码问号-Linux启动jar包,输出中文日志变成问号?
linux·log4j·jar
实心儿儿1 小时前
Linux —— 库的制作和原理(3)
linux·运维·服务器
techdashen1 小时前
Unweight:Cloudflare 如何在不损失精度的情况下把大模型压缩 22%
网络·人工智能
yyuuuzz1 小时前
独立站部署的几个常见技术问题
运维·服务器·网络·云计算·aws
十子木1 小时前
linux 安装claude code
linux
hzxpaipai2 小时前
网站建设哪家好?从性能、后台和运维看派迪科技的建站思路
运维·科技
a珍爱上了a强2 小时前
配置uboot启动参数,linux启动过程打印每个模块初始化的耗时时间
linux·运维·服务器