应用层自定义协议与序列化

1、协议和序列化

之前写UDP和TCP的时候,本质上来说也是一种协议,比如使用UDP实现一个英汉词典的功能,我们就规定了客户端只能发送英文单词,然后服务端会根据英文单词查找中文意思返回。再比如TCP实现的远程命令执行,我们就规定了客户端必须输入Linux的指令。

在网络通信中,我们都是通过字符串来发送接受数据的,那么如果现在我们想传递一些结构化的数据呢?
比如今天我在微信群里聊天,那么我发出一条消息,我需要将头像、昵称、消息发给服务器,然后再有服务器转发给群里的其他人。那么是一条一条的发送给服务器还是整合在一起发送给服务器呢?当然是把这三条整合在一起发送给服务器最好。头像其实是字符串,因为你的头像上传到服务器了,所以就是一个路径,昵称和消息也是字符串,所以我们可以把这三个数据打包成一个大字符串发送给服务器。然后服务器再转发给其他用户,其他用户将信息提取出来。而这些信息肯定也是要在一个结构体里面保存起来的,而客户端和服务器都认识这个结构体,这就是双方约定好的协议。

那么如果是一个结构体对象数据如何传递给对方呢?
在这里插入图片描述
那么我们能不能直接将上图定义的data数据直接以二进制方式发送给服务器呢?可以是可以,不过可能会有问题。比如我们服务端是C/C++写的,但是客户端并不一定也是用C/C++写的,还有客户端的机器可能是32/64位的,所以会有问题。

因此我们还有下面这种更好的方式:
在这里插入图片描述
如图,聊天信息比如:消息、时间、昵称,这些信息是在一个结构体里面的,我们将他们提取出来转换成一个大字符串,把信息由多变一,方便网络发送,这个过程就叫做序列化。服务端在接受的时候,获取这个大字符串,然后将每个信息提取出来,把信息由一变多,方便上层处理,这个过程就叫做反序列化。


TCP全双工+面向字节流:
之前在TCP服务端HandlerRequest那里read和write是不完善的。
在这里插入图片描述
1、为什么TCP支持全双工?
实际上我们创建套接字sockfd后,操作系统会给我们创建两个缓冲区:发送缓冲区和接收缓冲区。而我们调用系统调用write向网络发送数据本质上是将我们的数据拷贝到发送缓冲区中,然后再由操作系统将发送缓冲区的数据发送给对方的。对方read如果没有数据会阻塞住,当数据发送到对方的接收缓冲区后,read再把数据从接收缓冲区拷贝到上层。因此write和read向网络发送或接收数据,本质上都是在拷贝数据。而当主机A将数据写入发送缓冲区然后发送给主机B的接收缓冲区的时候,主机B也可以将数据写入自己的发送缓冲区,然后发送到主机A的接收缓冲区中,因此TCP是全双工的。所以当我们将数据拷贝到发送缓冲区后,就由操作系统自主决定什么时候发送了,所以TCP叫做传输控制协议,这里的控制就是由操作系统自主来控制的。

2、TCP是面向字节流的?
由于TCP发送数据是由操作系统自己控制的,如果今天主机B迟迟没有将接收缓冲区的数据读走,上层可能还在进行数据的处理来不及读走,现在主机B的接收缓冲区可能快满了,只剩下10个字节的空间了。现在主机A要发送20字节的数据,先将这20字节数据拷贝到发送缓冲区中,然后主机A识别主机B只剩下10字节的空间了,所以就只发送过去10字节的数据。那么很不巧,这时候主机B上层将缓冲区的数据全部读走了,但是最后的数据就无法被正确解析了,因为最后只有10个字节的数据,数据是不完整的。因此TCP需要应用层自己保证报文的完整性。
TCP面向字节流,也就是说数据发送是按字节来发送的,发送方可能发送很多次,接收方可能一次就将它全部读完。就比如自来水公司给你们家通了自来水,你想用多少就用多少,你可以用瓶子接,也可以用盆接,还可以用桶接。TCP将应用层传递的数据视为无结构的字节流,不维护消息之间的边界。
而UDP是面向数据报的,所以UDP是要保证数据完整的发送给对方的。

所以,操作系统内部可能会存在大量的报文来不及处理,这么多报文操作系统要不要管理起来呢?要,如何管理?——先描述,再组织。
在这里插入图片描述
操作系统中描述报文的结构体就是struct sk_buff。那么网络通信需要通过文件描述符来实现,所以必定要创建struct file对象,如果是普通文件对象,struct file中的private_data就为空,如果是网络通信,那么该指针指向struct socket对象,而struct socket对象里面有个struct sock* sk,它又指向一个struct sock对象,sock里面有两个队列,一个接收队列,一个写队列,它们都是struct sk_buff_head类型的。而这两个队列结构里面又有struct sk_buff*的指针next和prev,它们指向了一个一个的报文sk_buff。


今天我们要实现一个网络版本的计算器,那么就需要制定协议。
网络功能我们就使用TCP来实现。
约定方案一:
1、客户端发送一个形如:1+2的字符串。
2、这个字符串中有两个操作数,都是整数。
3、两个整数之间有一个运算符,可以是加减乘除中的任意一个。
4、数字和运算符之间没有空格。
那么服务端接收到客户端发送过来的数据就按约定好的进行解析读取,计算后返回给客户端结果。这本质上就是在制定协议。

约定方案二:
1、定义结构体来表示我们需要交互的信息。
2、发送数据时将这个结构体按照一个规则转换成字符串,接收到数据时再按相同的规则将字符串转回结构体。这个过程就叫做序列化和反序列化。

首先我们需要给网络计算器制定协议:协议就是双方约定好的结构化的数据。
在这里插入图片描述
那么我们定义两个结构体Request和Response。Request里面有两个整形变量和一个字符,将来就通过x oper y来计算结果返回给客户端。Response里面有result和code,result表示计算结果,而由于除0是有问题的,所以我们通过code来表示结果是否可信,比如0表示成功,1表示除0结果不可信。那么客户端服务器都约定好,将来客户端发送数据给服务器,那么服务器也能根据数据提取出Request对象,再将计算结果写入Response对象发送给客户端,客户端接收后也能获取Response对象然后获得计算结果。这就是在制定协议。


其次我们要选择序列化的方案:
1、自己做:我们可以自己来做序列化和反序列化的方案,比如我们约定将来客户端发送过来的数据都是"xopery",但是由于TCP是面向字节流的,因此用户层需要判断报文的完整性。我们在该数据前面加上数据长度和\r\n,在该数据后面再加上\r\n。也就是下面这个样子:
在这里插入图片描述
发送过来的数据为11+22,长度为5,所以前面加上5\r\n,后面加上\r\n。这样将来如果发送过来的是多个报文,我只要找到\r\n,然后前面就是有效载荷的长度,我们将前面长度提取出来,根据长度获取后面的数据即可。

2、使用工具:有xml && json && protobuf。
我们可以使用工具,我们选择jsoncpp。
在这里插入图片描述
如图,它会将数据转换成上图这种格式,左边这种是有\n的,右边是直接使用,间隔的。序列化后转换成这种格式的字符串后发送给对方,将来对方拿着这个数据再反序列化就可以获取数据。


2、Jsoncpp

Jsoncpp是一个用于处理 JSON 数据的 C++ 库。它提供了将JSON数据序列化为字符串以及从字符串反序列化为C++数据结构的功能。 Jsoncpp是开源的,广泛用于各种需要处理JSON数据的C++项目中。

特性:
1.简单易用:Jsoncpp 提供了直观的 API,使得处理 JSON 数据变得简单。
2.高性能:Jsoncpp 的性能经过优化,能够高效地处理大量 JSON 数据。
3.全面支持:支持 JSON 标准中的所有数据类型,包括对象、数组、字符串、数字、布尔值和 null。
4.错误处理:在解析 JSON 数据时, Jsoncpp 提供了详细的错误信息和位置,方便开发者调试。

首先需要安装jsoncpp:

ubuntu: sudo apt-get install libjsoncpp-dev
Centos: sudo yum install jsoncpp-devel

在这里插入图片描述
安装后/usr/include目录下就有个jsoncpp文件,里面的json就包含了需要引入的头文件,将来在使用的时候需要引入如:#include <jsoncpp/json/reader.h>,因为系统默认只能找到/usr/include。


序列化:
1、使用Json::Value的toStyledString方法:
优点:将Json::Value对象直接转换为格式化的JSON字符串。

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
   
   
    Json::Value root;
    root["name"] = "joe";
    root["sex"] = "男";
    std::string s = root.toStyledString();

    std::cout << s << std::endl;
    return 0;
}

在这里插入图片描述
编译后报错,需要指明链接文件:-ljsoncpp
在这里插入图片描述


2、使用Json::StreamWriter:
优点:提供了更多的定制选项,如缩进、换行符等。

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
   
   
    Json::Value root;
    root["name"] = "joe";
    root["sex"] = "男";
    Json::StreamWriterBuilder wbuilder; // StreamWriter的工厂
    std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
    std::stringstream ss;
    writer->write(root, &ss);
    std::cout << ss.str() << std::endl;
   return 0;
}

在这里插入图片描述


3、使用Json::FastWriter:
优点:比StyledWriter更快,因为它不添加额外的空格和换行符。

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
   
   
    Json::Value root;
    root["name"] = "joe";
    root["sex"] = "男";

    Json::FastWriter writer1;
    Json::StyledWriter writer2;
    std::string s1 = writer1.write(root);
    std::string s2 = writer2.write(root);

    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
    return 0;
}

在这里插入图片描述


反序列化:使用 Json::Reader
优点:提供详细的错误信息和位置,方便调试。

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

int main()
{
   
   
    Json::Value root1;
    root1["name"] = "joe";
    root1["sex"] = "男";
    Json::StreamWriterBuilder wbuilder;
    std::unique_ptr<Json::StreamWriter> writer(wbuilder.newStreamWriter());
    std::stringstream ss;
    writer->write(root1, &ss);
    std::cout << ss.str() << std::endl;

    std::string json_string = ss.str();
    Json::Reader reader;
    Json::Value root2;
    bool parsingSuccessful = reader.parse(json_string, root2);
    if (!parsingSuccessful)
    {
   
   
        std::cout << "Failed to parse JSON: " << reader.getFormattedErrorMessages() << std::endl;
        return 1;
    }
    std::string name = root2["name"].asString();
    std::string sex = root2["sex"].asString();
    std::cout << "name: " << name << std::endl;
    std::cout << "sex: " << sex << std::endl;
    return 0;
}

在这里插入图片描述


3、使用Jsoncpp实现网络版计算器

直接将之前写的TCP远程命令执行拿过来,去掉CommandExec.hpp,还需要添加Calculator.hpp和Protocol.hpp。
在这里插入图片描述

下面我们先实现Protocol.hpp,如下:

#pragma once

#include <iostream>
#include <string>
#include <sstream>
#include <memory>
#include <jsoncpp/json/json.h>

bool Encode(std::string& message)
{
   
   

}

bool Decode(std::string& package, std::string* content)
{
   
   
    
}

class Request
{
   
   
public:
    Request()
    :_x(0)
    ,_y(0)
    ,_oper(0)
    {
   
   }

    Request(int x, int y, char oper)
    :_x(x)
    ,_y(y)
    ,_oper(oper)
    {
   
   }

    bool Serialize(std::string& out_string)
    {
   
   

    }

    bool Deserialize(std::string& in_string)
    {
   
   

    }

    void Print()
    {
   
   
        std::cout << _x << std::endl;
        std::cout << _y << std::endl;
        std::cout << _oper << std::endl;
    }

    int X() const {
   
    return _x; }
    int Y() const {
   
    return _y; }
    char Oper() const {
   
    return
<think>嗯,用户现在想了解如何设计实现自定义应用层协议,之前他们问的是Java游戏模组开发,现在转向了网络协议设计,这可能是因为他们在开发模组时需要处理网络通信的问题。需要结合他们提供的三个引用内容,这些引用提到了自定义协议的必要性、序列化、TCP实现以及XML格式的例子。首先,我得明确用户的需求。他们可能是在开发游戏模组时需要让客户端服务器通信,比如同步玩家位置、物品信息等,所以需要自定义协议。引用里提到了网络版计算器的例子,使用序列化反序列化,还有JSONXML的结构,这些可以作为参考。 接下来,我得整理设计自定义协议的步骤。根据引用内容,关键点包括协议结构设计(报文格式、字段定义)、序列化方法选择、传输层协议选择(如TCP)、错误处理等。需要分步骤讲解,比如确定需求、设计报文、选择序列化方式、实现编解码、测试等。 然后要考虑如何将引用中的例子融入进去。比如网络计算器使用JSON的例子,可以说明序列化的具体实现。还有XML格式的请求,展示不同格式的应用。可能需要比较不同序列化方式的优缺点,帮助用户选择合适的方法。用户可能遇到的问题包括如何处理粘包、数据校验、协议升级等,这些需要涵盖在实现注意事项里。另外,性能优化方面,比如压缩数据,也要提到。最后,生成相关问题,引导用户深入思考协议设计中的具体问题,比如如何选择序列化格式、处理粘包、设计校验机制等。需要确保问题用户的实际开发场景相关,帮助他们解决可能遇到的挑战。</think>### 自定义应用层协议设计实现指南 基于TCP实现自定义协议需要明确定义**报文结构**、**序列化方式****通信流程**。以下是关键步骤示例: --- #### 一、协议设计核心要素 1. **报文格式** - **定界符**:使用特殊字符(如`<requset>`/`</requset>`)或长度前缀标识报文边界[^3] - **字段定义**:结构化数据(如用户ID、坐标位置),支持嵌套或键值对形式[^3] - **示例XML格式**: ```xml <request> <userId>1234</userId> <position>100 80</position> </request> ``` 2. **序列化反序列化** - **二进制协议**:直接编码整数/浮点数(性能高但可读性差) - **文本协议**:JSON/XML(易调试,但传输效率较低)[^1] - **示例JSON序列化**: ```json { "userId": 1234, "position": [100, 80] } ``` 3. **传输层选择** - **TCP协议**:保证可靠传输,需处理粘包问题(通过长度字段或分隔符)[^2] - **UDP协议**:低延迟但需自行处理丢包重传 --- #### 二、实现步骤(以网络计算器为例) 1. **定义协议规范** - 请求格式:`{"op": "+", "a": 5, "b": 3}` - 响应格式:`{"result": 8}` 2. **实现序列化工具** ```java // 使用Jackson库实现JSON序列化 ObjectMapper mapper = new ObjectMapper(); String request = mapper.writeValueAsString(new CalculatorRequest("+", 5, 3)); byte[] data = request.getBytes(StandardCharsets.UTF_8); ``` 3. **封装Socket通信** ```python # TCP服务端接收逻辑(处理粘包) def recv_all(sock, length): data = b'' while len(data) < length: packet = sock.recv(length - len(data)) if not packet: return None data += packet return data ``` 4. **协议解析处理** ```java // 反序列化并执行计算 CalculatorRequest req = mapper.readValue(inputStream, CalculatorRequest.class); double result = calculate(req.op, req.a, req.b); String response = mapper.writeValueAsString(new CalculatorResponse(result)); ``` --- #### 三、实现注意事项 1. **粘包处理** - 方案1:固定长度头部(如4字节表示后续数据长度) - 方案2:使用分隔符(如`\r\n\r\n`) 2. **数据校验** - 添加校验字段(如CRC32或MD5哈希) - 示例:`{"data": ..., "checksum": "a1b2c3"}` 3. **协议升级** - 版本号字段:`{"version": 2, ...}` - 向后兼容性设计 --- #### 四、性能优化技巧 1. **二进制协议优化** - 使用Protobuf/FlatBuffers替代JSON - 压缩算法(如gzip) 2. **TCP参数调优** - 设置合理的`SO_SNDBUF``SO_RCVBUF` - 启用Nagle算法(需权衡延迟吞吐量) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值