【Linux网络编程】自定义协议+序列化+反序列化

本文深入探讨了协议、序列化和反序列化的概念。介绍了业务协议是如何将结构化对象序列化后在网络传输,再反序列化供上层使用。通过编写网络版计数器,展示了Cal TCP服务端和客户端的实现,解决了TCP读取完整报文的问题。还提及了Json等现成的序列化反序列化方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自定义协议+序列化+反序列化

在这里插入图片描述

喜欢的点赞,收藏,关注一下把!在这里插入图片描述

1.再谈 “协议”

协议是一种 “约定”。在前面我们说过父亲和儿子约定打电话的例子,不过这是感性的认识,今天我们理性的认识一下协议。 socket api的接口, 在读写数据时,都是按 “字符串”(其实TCP是字节流,这里是为了理解) 的方式来发送接收的。如果我们要传输一些 “结构化的数据” 怎么办呢?

结构化的数据就比如说我们在使用QQ群聊时除了消息本身、还能看见头像、时间、昵称。这些东西都要发给对方。这些东西都是一个个字符串,难道是把消息、头像、时间、昵称都单独发给对方吗?那分开发的时候,未来群里有成百上千名人大家都发,全都分开发,接收方还要确定每一部分是谁的进行匹配,那这样太恶心了。

实际上这些信息可不是一个个独立个体的而是一个整体。为了理解暂时当作多个字符串。把多个字符串形成一个报文或者说打包成一个字符串(方便理解,其实是一个字节流)然后在网络中发送。多变一方便未来在网络里整体发送。而把多变一的过程,我们称之为序列化

这里用多个字符串形容也不太准确,下面给具体解释。

在这里插入图片描述

经过序列化的过程变成一个整体后发到网络里,经过网络传输发送给对方,发是整体当作一个字符串发的。接收方收的也是整体收的,所以收到一个报文或者说字符串。但是收到的字符串有什么东西我怎么知道,qq作为上层要的是谁发的、什么时候、发的什么具体的信息,所以接收方收到这个整体字符串后,必须把它转成多个字符串,这种一变多的过程,我们称之为反序列化

在这里插入图片描述

业务结构数据在发送网络中的时候,先序列化在发送,收到的一定是序列字节流,要先进行反序列化,然后才能使用。

刚才说过这里用多个字符串不太对只是为了理解,实际上未来多个字符串实际是一个结构体。是以结构体(结构化的数据)作为体现的,然后把这个结构体转成一个字符串,同理对方收到字符串然后转成对应的结构化的数据。

在这里插入图片描述

为什么要把字符串转成结构化数据呢?未来这个结构化的数据一定是一个对象,然后使用它的时候,直接对象.url 、对象.time 拿到。

而这里的结构体如message就是传说中的业务协议
因为它规定了我们聊天时网络通信的数据。
在这里插入图片描述

未来我们在应用层定协议就是这种结构体类型,目的就是把结构化的对象转换成序列化结构发送到网络里,然后再把序列化结构转成对应的结构体对象,然后上层直接使用对象进行操作! 这是业务协议,底层协议有自己的特点。

这样光说还是不太理解,下面找一个应用场景加深理解刚才的知识。所以我们写一个网络版计数器。里面体现出业务协议,序列化,反序列化,在写TCP时要注意TCP时面向字节流的,接收方如何保证拿到的是一个完整的报文呢?而不是半个、多个?这里我们都通过下面写代码的时候解决。而UDP是面向数据报的接收方收到的一定是一个完整的报文,因此不考虑刚才的问题。

2.Cal TCP服务端

自定义协议,但协议是一对的。因此有一个请求,一个响应。

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

    Request(int x, int y, char op) : _x(x), _y(y), _op(op)
    {
   
    }

public:
	//这里就是我们的约定,未来形成 “x op y” ,就一定要求x在前面,y在后面,op在中间这约定好的
    int _x;//第一个数字
    int _y;//第二个数字
    char _op;//操作符
};


class Response
{
   
public:
    Response() : _exitcode(0), _result(0)
    {
   
    }

    Response(int exitcode, int result) : _exitcode(exitcode), _result(result)
    {
   
    }

public:
	//约定
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

以前我们写过服务器的代码,有些东西就直接用了,这里服务器是多进程版本。

我们这里主要进行业务逻辑方面的设计。
如果有新链接来了我们就进行处理,因此给一个handlerEntry函数,这里没写在类里主要是为了解耦。并且也把业务逻辑进行解耦给一个回调函数,handlerEntry函数你做你的序列化反序列化等一系列工作,和我没关系。我只做我的工作就行了。

//业务逻辑处理
typedef function<void(const Request &, Response &)> func_t;

void handlerEntry(int sock, func_t callback)
{
   
	//1.读取
    // 1.1 你怎么保证你读到的消息是 【一个】完整的请求

	//2. 对请求Request,反序列化
	//2.1 得到一个结构化的请求对象
	//Request req=...;
	
	// 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
    // 3.1 得到一个结构化的响应
    //Response resp=...;
    //callback(req,resp);// req的处理结果,全部放入到了resp
    
	// 4.对响应Response,进行序列化
    // 4.1 得到了一个"字符串"

	// 5. 然后我们在发送响应

}

class CalServer
{
   
public:
    //。。。

    void start(func_t func)
    {
   
        // 子进程退出自动被OS回收
        signal(SIGCHLD, SIG_IGN);
        for (;;)
        {
   
            // 4.获取新链接
            struct sockaddr_in peer;
            socklen_t len = (sizeof(peer));
            int sock = accept(_listensock, (struct sockaddr *)&peer, &len); // 成功返回一个文件描述符
            if (sock < 0)
            {
   
                logMessage(ERROR, "accpet error");
                continue;
            }
            logMessage(NORMAL, "accpet a new link success,get new sock: %d", sock);

            // 5.通信   这里就是一个sock,未来通信我们就用这个sock,tcp面向字节流的,后序全部都是文件操作!

            // version2 多进程信号版
            int fd = fork();
            if (fd == 0)
            {
   
                close(_listensock);
                handlerEntry(sock, func);
                close(sock);
                exit(0);
            }
            close(sock);
        }
    }

	//。。。

private:
    uint16_t _port;
    int _listensock;
};
#include "CalServer.hpp"
#include <memory>

void Usage(string proc)
{
   
    cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}

// req: 里面一定是我们的处理好的一个完整的请求对象
// resp: 根据req,进行业务处理,填充resp,不用管理任何读取和写入,序列化和反序列化等任何细节
void Cal(const Request &req, Response &resp)
{
   

}

// ./tcpserver port
int main(int argc, char *argv[])
{
   
    if (argc != 2)
    {
   
        Usage(argv[0]);
        exit(USAGG_ERR);
    }
    uint16_t serverport = atoi(argv[1]);

    unique_ptr<CalServer> tsv(new CalServer(serverport));
    tsv->initServer();

    tsv->start(Cal);

    return 0;
}

整体就是这样的逻辑,我们现在把软件分成三层。第一层获取链接进行处理,第二层handlerEntery进行序列化反序列化等一系列工作,第三层进行业务处理callback。

现在逻辑清晰了,我们一个个补充代码

为什么说保证你读到的消息是 【一个】完整的请求?因为TCP是面向字节流的,我们保证不了,所以要明确 报文和报文的边界。

TCP有自己内核级别的发送缓冲区和接收缓冲区,而应用层也有自己的缓冲区,我们自己写的代码调用read,write发送读取使用的buffer就是对应缓冲区。其实我们调用的所有的发送函数,根本就不是把数据发送到网络中!
发送函数,本质是拷贝函数!!!

write只是把数据从应用层缓冲区拷贝到TCP发送缓冲区,由TCP协议决定什么时候把数据发送到网络,发多少,出错了怎么办。所以TCP协议叫做传输控制协议!!

最终数据经过网络发送被服务端放到自己的接收缓冲区里,然后我们在应用层调用read,实际在等接收缓冲区里有没有数据,有数据就把数据拷贝应用层的缓冲区。没有数据就是说接收缓冲区是空的,read就会被阻塞。

在这里插入图片描述

所以网络发送的本质:

C->S: tcp发送的本质,其实就是将数据从c的发送缓冲区,拷贝到s的接收缓冲区。
S->C: tcp发送的本质,其实就是将数据从s的发送缓冲区,拷贝到c的接收缓冲区。

c->s发,并不影响s->c发,因为用的是不同的成对的缓冲区,所以tcp是全双工的!

这里主要想说的是,tcp在进行发送数据的时候,发收方一直发数据但是对方正在做其他事情来不及读数据,所以导致接收方的接收缓冲区里面存在很多的报文,因为是TCP面向字节流的所以这些报文是挨在一起,最终读的时候怎么保证读到的是一个完整的报文交给上层处理,而不是半个,多个。就是因为我们有接收缓冲区的存在,因此首先我们要解决读取的问题。

在这里插入图片描述
明确 报文和报文的边界:

  1. 定长
  2. 特殊符号
  3. 自描述方式

我们给每个报文前面带一个有效载荷长度的字段,未来我先读到这个长度,根据这个长度在读取若干字节,这样就能读取到一个报文,一个能读到,n个也能读到。有效载荷里面是请求或者响应序列化的结果。

在这里插入图片描述

//有效载荷->报文
string Enlenth(const string &text)
{
   }

//将读到的一个完整报文分离出有效载荷
bool Delenth(const string &packge, string *text)
{
   }

未来读取到一个完整的报文就看这两个函数的具体实现了。

还有不管是请求和响应未来都需要做序列化和反序列化,因此在这两个类中都要包含这两个函数。

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

    Request(int x, int y, char op) : _x(x), _y(y), _op(op)
    {
   
    }
	
	//序列化
    bool serialize(string *out)
    {
   
    }
	//反序列化
    bool deserialize(const string &in)
    {
   
    }

public:
    int _x;
    int _y;
    char _op;
};

关于这个序列化我们可以自己写,也可以用现成的,不过我们是初学先自己写感受一下,等都写完我们在介绍现成的。

序列化就是怎么把这个结构化的数据形成一个规定好格式的字符串。

#define SEP " "
#define SEP_LEN strlen(SEP)
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP)


bool serialize(string *out)
{
   
    // 结构化 -> "x op y"  //规定字符串必须是是 “第一个参数 操作数 第二个参数”
    *out = "";
    string x_string = to_string(_x);
    string y_string = to_string(_y);

    *out += x_string;
    *out += SEP;
    *out += _op;
    *out += SEP;
    *out += y_string;

    return true;
}

反序列化就是把这个字符串变成规定好的结构化的数据

bool deserialize(const string &in)
{
   
    // "x op y" -> 结构化
    auto left = in.find(SEP);
    auto right = in.rfind(SEP);
    if (left == string::npos || right == string::npos)
        return false;
    if (left == right)
        return false;
    if (right - (left + SEP_LEN) != 1)//防止op是其他不合规的操作符如++
        return false;

    string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - start
    string y_string = in.substr(right + SEP_LEN);

    if (x_string.empty())
        return false;
    if (y_string.empty())
        return false;
        
    _x = stoi(x_string);
    _y = stoi(y_string);
    _op = in[left + SEP_LEN];

    return true;
}

读取一个完整的请求,后面在填写,先补充其他逻辑

void handlerEntery(int sock, func_t callback)
{
   
    string inbuffer;
    while (true)
    {
   
        
        // 1. 读取
        // 1.1 你怎么保证你读到的消息是 【一个】完整的请求
		string req_str;//代表报文分离之后读取到的字符串(有效载荷)


        // 2. 对请求Request,反序列化
        // 2.1 得到一个结构化的请求对象
        Request req;
        if (!req.deserialize(req_str))
            return;

        // 3. 计算机处理,req.x, req.op, req.y --- 业务逻辑
        // 3.1 得到一个结构化的响应
        Response resp;
        
评论 73
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值