hello~ 很高兴见到大家! 这次带来的是Linux网络中关于网络基础这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?个 人 主 页: 默|笙文章目录一、应用层自定义协议1.1 再谈协议1. 重新理解read、write、recv、send和tcp为什么支持全双工2. 关于应用层协议与粘包问题3. 序列化与反序列化1.2 自定义协议1. 介绍2. Jsoncpp 的介绍与使用1. 使用 Json::Value 对象提供的toStyledString()方法2. 使用 Json::StyledWriter 与 Json::FastWriter 对象提供的方法3. 使用 StreamWriter 序列化法基于 StreamWriterBuilder推荐4. Json 序列化的多种使用方法Json::Value 嵌套Json数组5. 使用 Json_Reader 对象提供的方法反序列化3. 封包与解包4. 应用层再理解代码自定义协议代码一、应用层自定义协议1.1 再谈协议1. 重新理解read、write、recv、send和tcp为什么支持全双工首先重新认识一下 read() 和 write() 函数。这两个函数本质上是数据拷贝函数是用户态与内核态之间的桥梁。我们的用户程序运行在应用层通过套接字 API 调用它们。当调用 write() 发送数据时它只是将用户空间的数据拷贝到内核中的发送缓冲区当调用 read() 接收数据时它只是从内核中的接收缓冲区将数据拷贝到用户空间。至于数据如何封装、何时发送、如何路由、如何重传全都由操作系统内核的协议栈完成用户程序无需也不应干预。这两个函数之所以能支持全双工通信是因为TCP 连接在内核中维护了两个独立的缓冲区一个发送缓冲区一个接收缓冲区。因此同一时刻既可以有数据被拷贝进发送缓冲区也可以有数据从接收缓冲区被读出读写互不干扰。那么这两个缓冲区在哪里操作系统在底层随时可能收到海量的并发报文这些报文需要被妥善管理。Linux 内核对此贯彻了“先描述再组织”的核心思想先为每个报文分配一个 sk_buff 结构体进行描述再将这些 sk_buff 通过双向链表链接成队列。我们所说的“发送缓冲区”和“接收缓冲区”其物理本质就是这两个由双向链表构成的管理队列。也正是这种数据结构让内核能够从容应对海量报文按需取用。为什么 read、write、recv、send 这些函数会有阻塞问题本质上是因为 I/O 操作需要满足特定条件才能进行当接收缓冲区为空时read 无数据可读只能等待当发送缓冲区已满时write 无空间可写也只能等待。此时内核会将调用线程挂起直到条件满足再唤醒。这属于典型的I/O 同步机制。TCP 具备读写缓冲区也是读写都是用同一个 socket 的原因。2. 关于应用层协议与粘包问题由此引出一个关键问题系统应该在什么时候发送数据一次发送多少出错了怎么办这些底层细节全部由 TCP 协议栈根据滑动窗口、拥塞控制、Nagle 算法等机制自行决定内核会根据实际情况对数据进行合并或拆分用户无法精确控制每一次收发在物理层面的形态。而 TCP 是面向字节流的协议它不保留应用层消息的边界。如果不人为约定消息边界接收方就可能一次读到多个消息粘包或者只读到半个消息半包。这就是所谓的“粘包/半包”问题。因此TCP 只保证了字节流的可靠传输但无法保证应用层消息的完整可解析。要解决这个问题必须由应用层自己来定义消息边界也就是需要约定一套应用层协议例如固定长度、分隔符、或者“长度头 数据体解决粘包问题一次读多少个字节规定”。实际数据什么时候发发多少出错了怎么办由TCP控制所以TCP叫做传输控制协议。3. 序列化与反序列化假如我们要实现一个网络版的计算器我们有两种方式来约定用户层协议第一种客户端发送一个形如 “12” 的字符串该字符串包含两个操作数均为整型两个数字之间有一个字符作为运算符且运算符仅支持 数字和运算符之间不能含有空格。第二种定义结构体来表⽰我们需要交互的信息;通过定义结构体来表示需要交互的信息发送数据时将该结构体按照特定规则转换为字节流接收数据时再按照相同的规则将字节流还原为结构体我们先来看第二种方式——约定结构体。这种方式在内核层非常常见因为内核代码统一使用 C 语言且通信双方运行在同一套 ABI 下结构体的内存布局天然一致但如果在应用层直接使用就会出问题不同平台、不同编程语言对结构体的内存对齐、字段顺序、数据类型大小的处理各不相同因此不具备良好的通用性和可移植性。但无论是第一种方式还是第二种方式其本质都是一样的都要保证一端在发送时构造的数据能够被另一端准确无误地接收并解析。所以我们引入序列化与反序列化网络通信统一处理这样就能够具备很好的扩展性。在发送端应用层数据在进入传输层之前会先被序列化为与平台无关的字节流接收端从传输层收到字节流后再通过反序列化还原为应用层能够识别的结构化数据。序列化会将要发送的内容转化成字符串或字节流。1.2 自定义协议1. 介绍自定义协议本质上是客户端与服务端约定使用同一种消息结构在 C 中这种结构通常表现为类或结构体。客户端与服务端之间的通信可概括为两类一类是客户端向服务端发送请求Request另一类是服务端向客户端返回响应Response。因此我们需要设计两种报文类型。这两种报文内部都应实现序列化和反序列化函数发送消息时进行序列化接收并解析消息时进行反序列化。序列化本质上就是将已有的数据类型转换为字符串更准确地说是转换为适合传输或存储的格式如字符串或字节流反序列化则是将字符串或字节流还原为原来的数据类型。序列化和反序列化函数当然可以自己实现但并不推荐因为已经有现成的库可用比如 JSON。在 C 中要使用 JSON需要先下载并引入 JSON 库也就是 JsonCpp。2. Jsoncpp 的介绍与使用接下来我们介绍 JsonCpp 库提供的三种序列化方法和一种反序列化方法。toStyledString()自带格式化序列化StyledWriter美化序列化FastWriter快速序列化StreamWriterBuilder流式序列化 / StreamWriter 序列化1. 使用 Json::Value 对象提供的toStyledString()方法Json::Value root;//先创建一个Json::Value类型的root对象root[name]mosheng;root[sex]woman;root[age]20;std::string sroot.toStyledString();std::coutsstd::endl;无论是当前展示的这种方法还是接下来要介绍的两种方法它们的第一步都是创建 Json::Value 对象并填充数据。这种填充采用键值对key-value形式便于在反序列化时定位目标字段。toStyledString() 方法最终序列化后的结果如图所示。这实际上是一个字符串。别看它有好几行JSON 之所以处理成这样主要是为了方便阅读因此在处理过程中加入了许多换行符等。2. 使用 Json::StyledWriter 与 Json::FastWriter 对象提供的方法Json::Value root;//先创建一个Json::Value类型的root对象root[name]mosheng;root[sex]woman;root[age]20;//version2://Json::StyledWriter writer;Json::FastWriter writer;std::string swriter.write(root);std::coutsstd::endl;其中使用 Json::StyledWriter 写出的结果与 Json::Value 自带的 toStyledString() 方法相同而 Json::FastWriter 则不会将字符串分行因为它没有添加换行符、缩进等格式化内容因此构建速度更快所以称为 Fast。Styled 则表示“带格式/美化输出”。3. 使用 StreamWriter 序列化法基于 StreamWriterBuilder推荐Json::Value root;//先创建一个Json::Value类型的root对象root[name]mosheng;root[sex]woman;root[age]20;Json::StreamWriterBuilder wbuilder;std::unique_ptrJson::StreamWriterswriter(wbuilder.newStreamWriter());std::stringstream ss;swriter-write(root,ss);std::coutss.str()std::endl;4. Json 序列化的多种使用方法Json::Value 嵌套Json::Value 不仅可以将普通类型转换为字符串还可以将自身类型如嵌套的对象或数组转换为字符串。例如Json::Value sub_root1;sub_root1[name]mosheng;sub_root1[sex]woman;sub_root1[age]20;Json::Value sub_root2;sub_root2[math]80;sub_root2[chinese]90;Json::Value root;root[who]sub_root1;root[score]sub_root2;Json::StyledWriter writer;//Json::FastWriter writer;std::string swriter.write(root);std::coutsstd::endl;Json数组Json::Value sub_root1;sub_root1[name]mosheng;sub_root1[sex]woman;sub_root1[age]20;Json::Value sub_root2;sub_root2[math]80;sub_root2[chinese]90;Json::Value one;one[who]sub_root1;one[score]sub_root2;//像数组一样使用Json::Value root;for(inti0;i3;i)root.append(one);Json::StyledWriter writer;//Json::FastWriter writer;std::string swriter.write(root);std::coutsstd::endl;5. 使用 Json_Reader 对象提供的方法反序列化std::string json_string{\name\:\mosheng\, \age\:20, \city\:\新疆\};Json::Reader reader;Json::Value root;boolparseokreader.parse(json_string,root);//将 json_string 解析后存入 Json::Value 对象 root 中std::string nameroot[name].asString();intageroot[age].asInt();std::string cityroot[city].asString();std::coutnamestd::endl;std::coutagestd::endl;std::coutcitystd::endl;在使用 key 获取到对应的 value 之后还需要显式指定将其转换为哪种数据类型。序列化时原始数据的类型在代码中是明确的编译器或 JsonCpp 可以据此自动完成转换反序列化时从 JSON 解析得到的只是 Json::Value因此需要我们显式指定目标类型例如调用 asString()、asInt() 等接口。3. 封包与解包仅仅对数据进行序列化还不够。序列化虽然能让数据具备良好的跨平台传输与存储能力但并不能解决 TCP 粘包问题。为了解决该问题我们需要对序列化后的数据进行封包主要做法是添加一个表示有效载荷长度的字段并规定该长度字段占用 4 字节。这样在解析报文时可以先读取长度字段再根据长度判断报文是否完整只有确认完整后才读取整个报文。如此便可避免只读取到部分数据即“半包”的问题。在期望的报文格式中第一个特殊分隔符用于分隔长度字段与有效载荷内容第二个分隔符则仅用于方便调试debug。4. 应用层再理解广义的应用层可以进一步细分为应用层、表示层和会话层。结合代码来看就很容易理解为什么要这样分层。其中应用层负责处理具体业务功能表示层负责对信息进行加工处理如格式转换、序列化、加密压缩等会话层负责建立、管理和维护通信会话处理网络交互与 I/O 相关问题。再补充一点读写文件本质上同样是字节流操作并不天然带有“一条记录”或“一个对象”的边界。那么反序列化时怎么知道该读多少字节、对象在哪里结束关键就在于文件的写入和读取要采用同一套序列化/反序列化规则。写入时不仅写入对象内容还要写入结构信息比如长度前缀、类型标记、字段数量、分隔符、文件头、记录数或索引读取时先解析这些元信息再按长度或格式读取后续字节从而准确还原对象。这样文件读写和网络通信一样本质上都是“序列化 → 字节流 → 反序列化”的过程。今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~让我们共同努力, 一起走下去!
阅读完成 · 觉得有帮助?