序列化与反序列化

2023-12-15 10:44:44

目录

TCP的特点

介绍

在线计算器

封装套接字

服务器的编写

calculator

protocol 协议

宏的定义及头文件

request

response

客户端的编写


TCP的特点

在TCP通信中,TCP的特点是有连接,可靠,面向字节流:

  1. 有连接在TCP套接字编写的时候,也可以体会到,我们在编写的时候需要进程设置监听套接字,还有 accept, 并且客户端也需要connect。

  2. 可靠目前我们是体会不到的,但是我们会在说TCP协议的时候会说到可靠性。

  3. 还有就是字节流,字节流这里先简单的说一下概念,就是发送的次数与接收的次数不一致。

在前面学习UDP的时候,UDP是面向数据报的,而数据报就是发一个报文,如果拿的话,就需要拿一个报文,而TCP面向字节流的话,就是一次性可以拿一部分,也可以全部拿走。

所以说UDP就是发送一次们就需要接收一次,而TCP是可以多次发送,但是只接收一次!

那么就是说如果我们使用TCP通信的话,那么就是需要有问题需要注意的:

我们怎么知道,我们收到了一个数据,是否是一个完整的报文?

因为我们可能会收到一半数据,也可能收到一个多的数据,所以我们并不能保证我们收到的是完整的报文!

所以下面我们的代码主要就是为了解决这个问题。

而今天需要做的工作就是序列化与反序列化。

介绍

我们就先介绍一下,我们今天打算如何写序列化和反序列化:

  1. 我们打算写一个网络版本的计算器,客户端发送数据,服务器处理好后返回答案即可。

  2. 所以我们需要一个客户端,还有服务器。

  3. 客户端只需要做的就是生产数据,然后发送给服务器。

  4. 服务器就需要接收数据,接收数据后,处理完成后,将处理结果返回。

  5. 而在客户端发送之前,由于我们直接发送我们的请求的话,也就是类似于结构体,但是由于主机和主机不同,所以结构体在不同的环境里面恐怕有区别,所以不建议直接发送结构体,所以我们在发送的时候,做好将数据给序列化为字符串,然后发送出去,就可以理解为序列化就是转成字符串。

  6. 所以服务器在收到数据后,是需要将字符串转换成结构体的也就是反序列化,将客户端的请求反序列化为请求的结构体,然后进行计算,计算后将答案封装成结构体,发送给客户端,但是服务器发送的时候也是同样的问题,需要序列化。

  7. 那么当客户端接收到服务器的响应时候,接收到的还是一个字符串,而客户端就需要将响应的字符串反序列化为结构体。

上面就是我们要写的大概逻辑,我们还回对服务器进行封装,同时我们对网络套接字也进行一定的封装。

在线计算器

封装套接字

套接字的封装,我相信不需要仔细的说明,因为在这之前,已经写了好几次套接字了:

#include "log.hpp"
?
class Sock
{
public:
 ? ?Sock(){};
?
 ? ?// socket 函数封装
 ? ?int Socket()
 ?  {
 ? ? ? ?// TCP套接字
 ? ? ? ?int listensock = socket(AF_INET, SOCK_STREAM, 0);
 ? ? ? ?if(listensock < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(FATAL, "创建套接字失败! errno: %d %s", errno, strerror(errno));
 ? ? ? ? ? ?exit(errno);
 ? ? ?  }
 ? ? ? ?return listensock;
 ?  }
?
 ? ?void Bind(const int listensock, const uint16_t port, const std::string& ip = "0.0.0.0")
 ?  {
 ? ? ? ?struct sockaddr_in local;
 ? ? ? ?local.sin_family = AF_INET;
 ? ? ? ?local.sin_port = htons(port);
 ? ? ? ?inet_aton(ip.c_str(), &local.sin_addr);
 ? ? ? ?
 ? ? ? ?int r = bind(listensock, (struct sockaddr*)&local, (socklen_t)sizeof(local));
 ? ? ? ?if(r < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(FATAL, "bind 失败! errno: %d %s", errno, strerror(errno));
 ? ? ? ? ? ?exit(errno);
 ? ? ?  }
 ?  }
?
 ? ?void Listen(int listensock)
 ?  {
 ? ? ? ?int r = listen(listensock, 0);
 ? ? ? ?if(r < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(FATAL, "设置监听套接字失败! errno: %d %s", errno, strerror(errno));
 ? ? ? ? ? ?exit(errno);
 ? ? ?  }
 ?  }
?
 ? ?int Accept(int listensock, struct sockaddr* out_sockaddr, socklen_t* out_len)
 ?  {
 ? ? ? ?int sockfd = accept(listensock, out_sockaddr, out_len);
 ? ? ? ?if(sockfd < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(ERROR, "accept 获取新套接字失败!");
 ? ? ? ? ? ?return -1;
 ? ? ?  }
 ? ? ? ?return sockfd;
 ?  }
?
 ? ?void Connect(int sock, const std::string& ip, const uint16_t port)
 ?  {
 ? ? ? ?struct sockaddr_in peer;
 ? ? ? ?peer.sin_family = AF_INET;
 ? ? ? ?peer.sin_port = htons(port);
 ? ? ? ?inet_aton(ip.c_str(), &peer.sin_addr);
?
 ? ? ? ?int r = connect(sock, (struct sockaddr*)&peer, (socklen_t)sizeof(peer));
 ? ? ? ?if(r < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(FATAL, "连接失败! errno: %d %s", errno, strerror(errno));
 ? ? ? ? ? ?exit(errno);
 ? ? ?  }
 ?  }
private:
 ? ?static Log log;
};
?
Log Sock::log;

这里的封装就不介绍了,下面我们将服务器也封装一下:

那么服务器如何封装呢?其实我们在前面也封装过好多次,但是这次我们还是在介绍一下。

成员变量

服务器需要什么?首先是IP和端口,还有监听套接字,还有就是需要我们想要执行的回调函数,以及我们前面封装的 sock对象,为了方便套接字编写!

成员函数

我们可以在服务器的构造函数里面将套接字等准备好,所以我们不需要在写一个init函数。

那么我们还需要一个启动的函数,就是让服务器一直在 accept 如果有连接到达,那么就创建一个新的线程,去执行回调函数。

以及还可以写一个send和recv 方便我们调用。

上面的服务器函数的轮廓就是这样,而我们在服务器里面只需要创建一个服务器的对象。

下面设置回调函数。

然后再启动服务器即可。

所以我们再看一下服务器怎么写?

服务器的编写

  1. 服务器里面,我们就调用服务器对象的构造函数

  2. 设置服务器里面的回调函数

  3. 启动服务器

int main(int argc, char *argv[])
{
 ? ?if (argc != 2)
 ?  {
 ? ? ? ?usage(argv[0]);
 ? ? ? ?exit(0);
 ?  }
 ? ?uint16_t port = atoi(argv[1]);
 ? ?std::unique_ptr<TcpServer> sev(new TcpServer(port));
 ? ?sev->setCallBack(calculator);
 ? ?sev->start();
 ? ?return 0;
}

而我们的 calculator 函数,就是我们想要执行的函数。

那么这个函数我们需要怎么写呢?

其中 calculator 函数里面就是需要从套接字里面读取数据,然后对读取到的数据进行解码,还要进行反序列化,反序列化后就可以计算了,计算出结果后将结果序列化,序列化后为字符串添加应用层报头,然后进行返回数据。

而这就是我们的应用岑协议,所以我们特需要一个协议的头文件,为了处理序列化以及反序列化,还有编码也解码。

下面我们再看一下服务器的头文件的编写:

typedef std::function<void(int)> threadCall_t;
?
class TcpServer
{
private:
 ? ?struct threadDate
 ?  {
 ? ?public:
 ? ? ? ?threadDate()
 ? ? ?  {
 ? ? ?  }
?
 ? ? ? ?threadDate(int sock, TcpServer *self)
 ? ? ? ? ?  : _sock(sock), _self(self)
 ? ? ?  {
 ? ? ?  }
?
 ? ?public:
 ? ? ? ?int _sock;
 ? ? ? ?TcpServer *_self;
 ?  };
?
public:
 ? ?// 构造函数里面,我们进行套接字的初始化,以及绑定等动作
 ? ?TcpServer(const uint16_t port, const std::string ip = "0.0.0.0")
 ? ? ?  : _port(port), _ip(ip)
 ?  {
 ? ? ? ?_listensock = _sock.Socket();
 ? ? ? ?log(INFO, "创建监听套接字成功~ listensock: %d", _listensock);
 ? ? ? ?_sock.Bind(_listensock, _port, _ip);
 ? ? ? ?log(INFO, "绑定成功~");
 ? ? ? ?_sock.Listen(_listensock);
 ? ? ? ?log(INFO, "设置监听套接字成功~");
 ?  }
?
 ? ?~TcpServer()
 ?  {
 ? ? ? ?if (_listensock > 0)
 ? ? ? ? ? ?close(_listensock);
 ?  }
    
 ? ?// 启动函数就是我们进行 accept 然后去执行回调函数
 ? ?void start()
 ?  {
 ? ? ? ?while (true)
 ? ? ?  {
 ? ? ? ? ? ?struct sockaddr_in peer;
 ? ? ? ? ? ?socklen_t len;
 ? ? ? ? ? ?int sock = _sock.Accept(_listensock, (struct sockaddr *)&peer, &len);
 ? ? ? ? ? ?log(INFO, "接收到一个新的连接 sock: %d", sock);
 ? ? ? ? ? ?// 下面创建线程去执行任务
 ? ? ? ? ? ?pthread_t tid;
 ? ? ? ? ? ?// 这是线程的数据,里面有一个新的套接字,还有一个就是服务器类型的指针
 ? ? ? ? ? ?// 传入这个指针是为了让 routine 函数可以找到回调函数,因为 routine 函数是 static 的
 ? ? ? ? ? ?threadDate *td = new threadDate(sock, this);
 ? ? ? ? ? ?pthread_create(&tid, nullptr, routine, (void *)td);
 ? ? ?  }
 ?  }
?
 ? ?void setCallBack(threadCall_t callBack)
 ?  {
 ? ? ? ?_callBack = callBack;
 ?  }
    
 ? ?// recv 函数就是对 recv 系统调用的封装,而将接收到的数据追加到 str 上
 ? ?static ssize_t Recv(int sock, std::string& str)
 ?  {
 ? ? ? ?char buffer[1024];
 ? ? ? ?// 0 表示阻塞读取
 ? ? ? ?ssize_t s = recv(sock, buffer, sizeof(buffer)-1, 0);
 ? ? ? ?if (s < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(DEBUG, "服务器读取数据失败!");
 ? ? ?  }
 ? ? ? ?else if(s == 0)
 ? ? ?  {
 ? ? ? ? ? ?log(INFO, "对端关闭连接!");
 ? ? ? ? ? ?return 0;
 ? ? ?  }
 ? ? ? ?buffer[s] = 0;
 ? ? ? ?log(DEBUG, "接收到一条数据: %s", buffer);
 ? ? ? ?str += buffer;
 ? ? ? ?return s;
 ?  }
?
 ? ?// Send 函数就是对 send 系统调用的封装,而这就是直接发送就可以
 ? ?static ssize_t Send(int sock, const std::string& resp)
 ?  {
 ? ? ? ?ssize_t s = send(sock, resp.c_str(), resp.size(), 0);
 ? ? ? ?if(s < 0)
 ? ? ?  {
 ? ? ? ? ? ?log(ERROR, "发送失败!");
 ? ? ? ? ? ?return -1;
 ? ? ?  }
 ? ? ? ?return s;
 ?  }
?
private:
 ? ?// 创建的线程就会执行该函数,而该函数就是对回调函数的封装调用
 ? ?static void *routine(void *args)
 ?  {
 ? ? ? ?pthread_detach(pthread_self());
 ? ? ? ?threadDate *td = static_cast<threadDate *>(args);
 ? ? ? ?TcpServer *self = td->_self;
 ? ? ? ?self->_callBack(td->_sock);
 ? ? ? ?close(td->_sock);
 ? ? ? ?// 因为线程的参数是 new 出来的所以是需要 delete 的
 ? ? ? ?delete td;
 ?  }
?
private:
 ? ?int _listensock;
 ? ?std::string _ip;
 ? ?uint16_t _port;
 ? ?threadCall_t _callBack;
 ? ?Sock _sock;
 ? ?static Log log;
};
?
Log TcpServer::log;

那么我们打算怎么写协议的头文件呢?

calculator

我们先把服务器里面的 calculator 函数写一下,将 calculator 里面对于协议的序列化反序列化的轮廓写出来:

我们打算如何写这个 calculator 函数呢?

  1. 首先是从套接字里面读取数据。

  2. 读取到数据后,我们进行 decode 也就是将应用层的协议解析出来。

  3. 解析出一个后,我们对这个数据进行反序列化,得到一个请求的结构化数据。

  4. 有了结构化数据就可以计算了,计算后,我们得到一个响应的结构化数据。

  5. 有了响应后,我们需要对响应进行反序列化,得到一个字符串。

  6. 有了这个字符串,我们继续添加应用层报头。

  7. 添加完成后,我们就发送数据给客户端。

response computer(request req)
{
 ? ?response resp;
 ? ?switch (req._op)
 ?  {
 ? ?case '+':
 ?  {
 ? ? ? ?resp._result = req._x + req._y;
 ?  }
 ? ?break;
 ? ?case '-':
 ?  {
 ? ? ? ?resp._result = req._x - req._y;
 ?  }
 ? ?break;
 ? ?case '*':
 ?  {
 ? ? ? ?resp._result = req._x * req._y;
 ?  }
 ? ?break;
 ? ?case '/':
 ?  {
 ? ? ? ?if (req._y == 0)
 ? ? ? ? ? ?resp._code = 1;
 ? ? ? ?else
 ? ? ? ? ? ?resp._result = req._x / req._y;
 ?  }
 ? ?break;
 ? ?case '%':
 ?  {
 ? ? ? ?if (req._y == 0)
 ? ? ? ? ? ?resp._code = 1;
 ? ? ? ?else
 ? ? ? ? ? ?resp._result = req._x % req._y;
 ?  }
 ? ?break;
 ? ?default:
 ?  {
 ? ? ? ?log(ERROR, "没有此种运算!");
 ?  }
 ? ?break;
 ?  }
 ? ?return resp;
}
?
void calculator(int sock)
{
 ? ?std::string buffer;
 ? ?while (true)
 ?  {
 ? ? ? ?// 1. 读取数据
 ? ? ? ?// 读取到的数据都会添加到 buffer 里面,buffer 里面可能不只有一个数据
 ? ? ? ?ssize_t s = TcpServer::Recv(sock, buffer);
 ? ? ? ?if (s < 0)
 ? ? ?  {
 ? ? ? ? ? ?continue;
 ? ? ?  }
 ? ? ? ?else if (s == 0)
 ? ? ?  {
 ? ? ? ? ? ?break;
 ? ? ?  }
 ? ? ? ?log(DEBUG, "服务器读取数据成功 buffer: %s", buffer.c_str());
 ? ? ? ?// 2. 读取到的数据是序列化的,现在需要反序列化
 ? ? ? ?std::string message;
 ? ? ? ?request req;
 ? ? ? ?// 我们循环的处理 buffer 里面的数据,知道 buffer 里面的数据被处理完了
 ? ? ? ?while (true)
 ? ? ?  {
 ? ? ? ? ? ?// 我们 decode 就是解码,每一次解析出来一个数据,如果没有数据,那么就返回空串
 ? ? ? ? ? ?message = req.decode(buffer);
 ? ? ? ? ? ?log(DEBUG, "decode 结果: %s", message.c_str());
 ? ? ? ? ? ?// 如果是空串,那么说明 buffer 里面现在没有一个完整的数据,所以需要继续读取,就 break 出去,继续读取
 ? ? ? ? ? ?if (message.empty())
 ? ? ? ? ? ? ? ?break;
 ? ? ? ? ? ?// 到了这里,说明是有一个完整的报文的,而这个数据就被保存到 message 里面,然后我们对这个数据进行但序列化,也就是反序列化成一个结构体
 ? ? ? ? ? ?if (!req.deserialize(message))
 ? ? ? ? ?  {
 ? ? ? ? ? ? ? ?// 反序列化失败了,重新读取数据
 ? ? ? ? ? ? ? ?std::cerr << "buffer: " << buffer << std::endl;
 ? ? ? ? ? ? ? ?break;
 ? ? ? ? ?  }
 ? ? ? ? ? ?std::cerr << "获取了一个任务: " << req._x << req._op << req._y << "=?" << std::endl;
 ? ? ? ? ? ?// 3. 计算然后构建响应
 ? ? ? ? ? ?// 反序列化成功后,我们就可以进行计算,然后返回一个响应的结构化数据
 ? ? ? ? ? ?response resp = computer(req);
 ? ? ? ? ? ?// 4. 方便响应的发送,将需要发送的数据序列化
 ? ? ? ? ? ?std::string respMessage = resp.serialize(resp);
 ? ? ? ? ? ?// 4.5 还需要再加上长度报头
 ? ? ? ? ? ?respMessage = resp.encode(respMessage);
 ? ? ? ? ? ?// 5. 发送数据
 ? ? ? ? ? ?s = TcpServer::Send(sock, respMessage);
 ? ? ?  }
 ?  }
}

上面就是 calculator 函数编写的逻辑,但是我们目前还没有协议的头文件,所以我们需要将该函数里面用到的协议的函数写好!

那么我们现在就开始看一下我们的协议的定制,以及应用层协议的添加等...

protocol 协议

我们先想清楚,我们打算如何序列化,反序列化,以及应用层报头里面有哪些字段?

我们的服务就是一个在线的计算器。

所以我们再序列化的时候,将每一个数字和字符使用空格隔开即可。

而我们的报头也是很简单,我们只需要添加正文的长度,但是由于都是字符,所以我们需要将报头以及有效载荷使用标记隔开,我们这里使用\r\n来充分隔符!

宏的定义及头文件
#include <iostream>
#include "log.hpp"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define SEP "\r\n"
#define SEP_LEN strlen(SEP)

上面就是我们下面使用的分隔符等,还有使用到的头文件。

下面我们看一下协议的编写:

对于客户端发过来的数据,我们称之为请求,对于服务器发送回去的数据,我们称之为响应,而请求和响应的结构体是不同的,这里我们简单的认为请求里面的成员变量就是下面这个样子:

struct request
{
    int _x;
    int _y;
    char _op;
};

我们这个就是简单的计算,_x 表示第一个数字, _y 表示第二个数据, _op 表示操作符,而操作符我们认为只有 “+,-,*,/,%” 这几种。

序列化就是将这个结构体里面的这三个变量序列化为下面这样:

_x(SPACE)_op(SPACE)_y // 括号只是为了区分,协议里面是没有括号的

序列化后还需要添加报头,我们认为我们的报头很简单就是有效载荷的长度,而上面序列化后的数据就是有效载荷的长度。

而为了添加报头后方便拆分,我们将长度后面加一个 SEP ,再加上上面的有效载荷,然后为了每一个报文的可读性,我们为每一个报文后面再加上 SEP

所以就是下面这个样子:

length(SEP)_x(SPACE)_op(SPACE)_y(SEP)

而这里我们认为 SPACE 就是空格,而 SEP 就是 \r\n 所以下面我们用 1+1 这个算式举例:

5\r\n1 + 1\r\n// 1+1 序列化后就是 1空格+空格1,而这个就是有效载荷,而这个的长度就是5

现在基本概念说清楚了,下面我们就看一下如何编写,实际上 request 和 response 的序列化和反序列化基本一样,只有一点差别。

所以下面我就只介绍一个:

request
class request
{
public:
    request(){};

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

private:
public:
    // x(SPACE)+(SPACE)y
    // length\r\nx(SPACE)+(SPACE)y\r\n
    // 序列化函数,就是将结构化的数据变成字符串,而每一个成员变量之间使用空格(SPACE)隔开
    // 序列化的函数很简单
    std::string serialize()
    {
        std::string ret;
        ret += std::to_string(_x);
        ret += SPACE;
        ret += _op;
        ret += SPACE;
        ret += std::to_string(_y);
        return ret;
    }
	// 序列化好后,得到的字符串就是有效载荷,所以我们还需要添加报头,报头就是有效载荷的长度
    std::string encode(std::string &s)
    {
        std::string ret = std::to_string(s.size());
        ret += SEP;
        ret += s;
        ret += SEP;
        return ret;
    }

    // 当服务器收到请求时候,是有报头的,所以我们需要去掉报头并分析,有效载荷的长度
    // 然后将有效载荷分离出来,这就是解码
    std::string decode(std::string &s)
    {
        // length\r\n1 + 2\r\n
		// 因为当我们收到数据后,我们知道需要一个这样的数据,这样才算一个完整的报文
        // 那么我们想要拿到长度报头,我们需要找到第一个 \r\n 所以我们直接 find
        int pos1 = s.find(SEP);
        if (pos1 == std::string::npos)
            return "";
        // 当找到后,我们需要对这个长度进行解析,我们需要计算当前缓冲区里是否有一个完整的报文
        int size = atoi(s.substr(0, pos1).c_str());
        int need = pos1 + SEP_LEN + size + SEP_LEN;
        if (s.size() < need)
            return "";
        // 这里表示是有完整的报文的,所以我们将有效载荷提取出来
        std::string ret = s.substr(pos1 + SEP_LEN, size);
        // 提取结束后,我们对缓冲区里面解析过的数据进行删除即可。
        s.erase(0, need);
        return ret;
    }

    // 对有效载荷进行分序列化
    bool deserialize(const std::string &str)
    {
        // 反序列化需要找到两个空格,其中第一个空格前面是数字,后面是字符
        log(DEBUG, "请求反序列化开始...");
        int pos = str.find(SPACE, 0);
        if (pos == std::string::npos)
        {
            // 有问题
            log(ERROR, "请求反序列化失败!");
            return false;
        }
        log(DEBUG, "找到了第一个位置 pos: %d", pos);
        // 第一个没问题的话,就找第二个空格,第二个空格前面是字符,后面是数字
        int pos1 = str.find(SPACE, pos + SPACE_LEN);
        if (pos == std::string::npos)
        {
            // 有问题
            log(ERROR, "请求反序列化失败!");
            return false;
        }
        log(DEBUG, "找到了第二个位置 pos: %d", pos1);
        // 发现都没有问题之后,就开始对请求的对象进行赋值
        _x = std::atoi(str.substr(0, pos).c_str());
        log(DEBUG, "x: %s", str.substr(0, pos).c_str());
        _y = std::atoi(str.substr(pos1 + SPACE_LEN).c_str());
        log(DEBUG, "y: %s", str.substr(pos1 + SPACE_LEN, str.size()).c_str());
        _op = str[pos1 - 1];
        log(DEBUG, "算式为: %d%c%d", _x, _op, _y);
        log(DEBUG, "请求反序列化结束...");
        return true;
    }

public:
    int _x;
    int _y;
    char _op;
    static Log log;
};

这就是协议的定制。

response

下面的响应的协议的定制也基本是一样的:

class response
{
public:
    response(){};

    response(int result, int code = 0)
        : _result(result), _code(code){};

private:
public:
    // length\r\nXXX YYY\r\n
    std::string encode(std::string &s)
    {
        std::string ret = std::to_string(s.size());
        ret += SEP;
        ret += s;
        ret += SEP;
        return ret;
    }

    std::string decode(std::string &s)
    {
        int pos1 = s.find(SEP);
        if (pos1 == std::string::npos)
            return "";
        int size = atoi(s.substr(0, pos1).c_str());
        int need = pos1 + SEP_LEN + size + SEP_LEN;
        if (s.size() < need)
            return "";
        std::string ret = s.substr(pos1 + SEP_LEN, size);
        s.erase(0, need);
        return ret;
    }

    // _result(SPACE)_code
    std::string serialize(const response &resp)
    {
        std::string ret;
        ret += std::to_string(resp._result);
        ret += SPACE;
        ret += std::to_string(resp._code);

        return ret;
    }

    bool deserialize(const std::string &str)
    {
        int pos = str.find(SPACE);
        if (pos == std::string::npos)
        {
            log(ERROR, "响应反序列化失败");
            return false;
        }
        _result = atoi(str.substr(0, pos).c_str());
        _code = atoi(str.substr(pos + SPACE_LEN).c_str());

        return true;
    }

public:
    int _result;
    int _code = 0;
    static Log log;
};

客户端的编写

客户端的编写是比服务器的要简单的,客户端这里我们就不详细说了。

  1. 客户端也需要套接字,所以需要创建套接字,还有连接服务器。

  2. 通过用户输入获取数据。

  3. 对获取的数据进行分析,将数据转化为 _x(SPACE)_op(SPACE)_y 这样的字符串

  4. 然后对上面的有效载荷进行添加报头,也就是 length,length后面还需要加分隔\r\n 然后加上有效载荷在加上\r\n

  5. 然后发送给服务器,等服务器计算完毕后,然后数据

  6. 然后客户端读取数据,将读取到的数据同样放到缓冲区中,因为缓冲区里面可能不止一个响应

  7. 然后对缓冲区的数据进行解码,获取到一个响应的有效载荷

  8. 然后对响应的有效载荷进行反序列化

  9. 然后将计算的结果打印出来

Sock sock;
Log log;

void usage(char *proc)
{
    cout << "\nuse: " << proc << " server_ip  server_port\n"
         << endl;
}

request getCal()
{
    request req;
    std::cerr << "please Enter _X> ";
    std::cin >> req._x;
    std::cerr << "please Enter _OP> ";
    std::cin >> req._op;
    std::cerr << "please Enter _Y> ";
    std::cin >> req._y;

    return req;
}

bool isOperartor(char c)
{
    if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%')
        return true;
    else
        return false;
}
// 1+1
std::string analyze(const std::string &equat)
{
    std::string ret;
    int i = 0;
    // 找第一个数字
    for (; i < equat.size(); ++i)
    {
        if (!(equat[i] >= '0' && equat[i] <= '9'))
        {
            if (equat[i] == ' ' || isOperartor(equat[i]))
            {
                ret += equat.substr(0, i + 1);
                break;
            }
            else
            {
                return "";
            }
        }
    }
    log(DEBUG, "i 的位置: %d", i);
    ret += SPACE;
    // 找字符,并且去掉空格
    for (; i < equat.size(); ++i)
    {
        if (isOperartor(equat[i]))
            ret += equat[i];
        else if ((equat[i] >= '0' && equat[i] <= '9'))
            break;
        else if (equat[i] == ' ')
            continue;
        else
            return "";
    }
    log(DEBUG, "i 的位置: %d", i);
    ret += SPACE;
    ret += equat.substr(i);
    return ret;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(0);
    }

    // 1. 创建套接字
    int sockfd = sock.Socket();
    cout << "创建套接字成功 sock: " << sockfd << endl;
    // 2. 连接
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    sock.Connect(sockfd, ip, port);
    cout << "客户端连接成功~" << endl;

    char buffer[1024];
    std::string client_buffer;
    while (true)
    {
        std::cerr << "please Enter> ";
        std::string equation;
        std::getline(std::cin, equation);
        // 0. 分析字符串
        std::string message = analyze(equation);
        if (message.empty())
            continue;
        log(DEBUG, "message: %s", message.c_str());
        // 1. 获取算式
        request req;
        // 1.5 将获取到的消息,反序列化
        req.deserialize(message);

        // std::cin >> req._x >> req._op >> req._y;
        // 2. 序列化
        std::string reqMessage = req.serialize();
        // 2.5 增加长度报头
        reqMessage = req.encode(reqMessage);
        log(DEBUG, "客户端请求序列化成功 req: %s", reqMessage.c_str());
        // 3. 发送
        ssize_t s = send(sockfd, reqMessage.c_str(), reqMessage.size(), 0);
        if (s < 0)
        {
            log(ERROR, "客户端发送数据失败!");
            continue;
        }
        log(DEBUG, "客户端数据发送成功~");
        // 4. 接收数据
        s = recv(sockfd, buffer, 1024, 0);
        if (s < 0)
        {
            log(ERROR, "客户端接收数据失败!");
            continue;
        }
        log(DEBUG, "客户端数据接收成功~");
        // 接收成功就反序列化
        buffer[s] = 0;
        client_buffer += buffer;
        std::string ret;
        response resp;
        while (true)
        {
            ret = resp.decode(client_buffer);
            if (ret.empty())
                break;
            if (!resp.deserialize(ret))
            {
                continue;
                break;
            }
            log(DEBUG, "客户端数据反序列化成功~");
            // 计算成功
            if (resp._code == 0)
                cout << req._x << req._op << req._y << "=" << resp._result << endl;
            else
            {
                cout << req._x << req._op << req._y << "=?" << endl;
                log(INFO, "计算出错了 _code: %d", resp._code);
            }
        }
    }
    return 0;
}

客户端这里就不详细介绍了,基本和服务器也是相同的。

同时也可以继续扩招,这个服务只能进行 x op y 的操作,也可以对这个进行扩展,还有就是在输入的时候也是可以扩展的,可以扩展为直接输入字符串,然后对字符串进行解析,解析为有效载荷,然后进行反序列化,在进行序列化,在添加报头,和前面基本一样。

文章来源:https://blog.csdn.net/m0_73455775/article/details/135010151
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。