【计网】自界说序列化反序列化(三) —— 实现网络版盘算器【下】

[复制链接]
发表于 2025-12-17 14:37:39 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
🌎实现网络版盘算器【下】

  本序次列化与反序列化所用到的代码,Tcp服务自界说序列化反序列化实现网络版盘算器。


文章目次:
实实现网络版盘算器【下】
    客户端实现
    基于守卫历程的改写

🚀客户端实现

  在这之前,我们已经将服务器端的代码部分做好了准备,如今万事俱备只欠客户端发起毗连,而客户端在这里不准备那么多的封装了,与之前写的客户端雷同,我们想要客户端以 ./cal_client ip port 的情势来创建客户端:
  1. #include <iostream>
  2. #include <string>
  3. #include <memory>
  4. #include <ctime>
  5. #include "Socket.hpp"
  6. #include "Log.hpp"
  7. #include "Protocol.hpp"
  8. void Usage(std::string proc)
  9. {
  10.     std::cout << "Usage:\n\t" << proc << " serverip serverport\n"
  11.               << std::endl;
  12. }
  13. using namespace socket_ns;
  14. using namespace protocol_ns;
  15. // ./tcp_client serverip serverport
  16. int main(int argc, char *argv[])
  17. {
  18.     if (argc != 3)
  19.     {
  20.         Usage(argv[0]);
  21.         exit(1);
  22.     }
  23.     std::string serverip = argv[1];// 服务器端ip
  24.     uint16_t serverport = std::stoi(argv[2]);// 服务器端port
  25.    
  26.         return 0;
  27. }
复制代码
  而Udp客户端构建对象就不须要这么贫苦了,由于我们早在最开始已经将Socket类举行了封装,如许我们就不须要在调用原生接口在客户端裸露式调用:
  1. InetAddr serveraddr(serverip, serverport);// 通过ip和port构建InetAddr对象
  2. std::unique_ptr<Socket> cli = std::make_unique<TcpSocket>();// 子类对象构造父类指针方便多态式调用
  3. bool res = cli->BuildClientSocket(serveraddr);// 建立客户端Socket 链接
复制代码
  此时,客户端的Socket服务就已经构建完毕,网络通讯已经做好准备,只要服务器通,客户端随时都可以发起毗连。接着就是处理处罚客户端的业务逻辑。
  我们要知道客户端是要给服务器端发送哀求而且获取相应的一个过程,获取乐成之后将相应举行反序列化拿到终极的结果。为了方便测试,我们这里让客户端采取固定的提问方式不绝对客户端发送哀求获取相应而且分析,我们将构建哀求以及吸收相应封装为一个 Factory类。
  此中,客户端哀求让 x 为 1-10的随机数,y为 0-4的随机数,让他们举行模运算,并将盘算构造为Request类,返回值为Request的指针:
  1. class Factory
  2. {
  3. public:
  4.     Factory()
  5.     {
  6.         srand(time(nullptr) ^ getpid());
  7.         opers = "+/*/%^&|";
  8.     }
  9.    
  10.     std::shared_ptr<Request> BuildRequest()
  11.     {
  12.         int x = rand() % 10 + 1;
  13.         usleep(x * 10);
  14.         int y = rand() % 5; // [0,1,2,3,4]
  15.         usleep(y * x * 5);
  16.         char oper = opers[rand() % opers.size()];
  17.         std::shared_ptr<Request> req = std::make_shared<Request>(x, y, oper);
  18.         return req;
  19.     }
  20.    
  21.     std::shared_ptr<Response> BuildResponse()
  22.     {
  23.         return std::make_shared<Response>();
  24.     }
  25.    
  26.     ~Factory()
  27.     {
  28.     }
  29. private:
  30.     std::string opers;// 操作数: +/-/*/\/% ...
  31. };
复制代码
  为了方便测试,我们这里只启动一个客户端,这个客户端不绝的给服务器发送数据,以是我们须要将待发送哀求以及返回的相应放在while循环内不绝发送获取分析。
  我们想要积贮一批数据,然后在一次性发送,如许就能测试服务器的功能是否有题目,是否能处理处罚多批数据,以是在这里我们一次性构建五个哀求让后在发送给服务器端,同样构建哀求时须要对数据举行序列化和添加长度报头:
  1. Factory factory;
  2. std::string inbuffer;
  3. while (res)
  4. {
  5.     sleep(1);
  6.     // 构建请求
  7.     std::string str;
  8.     for (int i = 0; i < 5; ++i)// 一次性构建5个请求
  9.     {
  10.         auto req = factory.BuildRequest();
  11.         // 对请求进行序列化
  12.         std::string send_str;
  13.         req->Serialize(&send_str);
  14.         std::cout << "Serialize: \n"
  15.                   << send_str << std::endl;
  16.         // 添加长度报头
  17.         send_str = Encode(send_str);
  18.         std::cout << "Encode: \n"
  19.                   << send_str << std::endl;
  20.         str += send_str;
  21.     }
  22.     // "len"\r\n"{}"\r\n
  23.     cli->Send(str);// 发送请求
  24. }
复制代码
  我们将讨情发送之后,客户端就静静期待服务器端返回的相应,固然,与服务器端吸收消息雷同,客户端吸收的每条应答肯定就是完备的应答吗?不肯定,以是我们将读取到的数据举行Decode(),如许我们对全部的应答举行分析,假如是一条完备的应答Decode接口就会返回一个response对象,对象里就是分析过后一条完备的应答内容:
  1. while (res)
  2. {
  3.     sleep(1);
  4.     // 构建请求
  5.     std::string str;
  6.     for (int i = 0; i < 5; ++i)
  7.     {
  8.         auto req = factory.BuildRequest();
  9.         // 对请求进行序列化
  10.         std::string send_str;
  11.         req->Serialize(&send_str);
  12.         std::cout << "Serialize: \n"
  13.                   << send_str << std::endl;
  14.         // 添加长度报头
  15.         send_str = Encode(send_str);
  16.         std::cout << "Encode: \n"
  17.                   << send_str << std::endl;
  18.         str += send_str;
  19.     }
  20.     // "len"\r\n"{}"\r\n
  21.     cli->Send(str);
  22.     // 读取应答
  23.     int n = cli->Recv(&inbuffer);
  24.     if (n <= 0)
  25.         break;
  26.     std::string package = Decode(inbuffer);
  27.     if (package.empty())
  28.         continue;
  29. }
复制代码
  那么今后,我们获取的应答就肯定是一条完备的应答,但是这个应答此时照旧序列化状态,我们须要将其举行反序列化处理处罚,末了输出相应结果即可:
  1. while (res)
  2. {
  3.     sleep(1);
  4.     // 构建请求
  5.     std::string str;
  6.     for (int i = 0; i < 5; ++i)
  7.     {
  8.         auto req = factory.BuildRequest();
  9.         // 对请求进行序列化
  10.         std::string send_str;
  11.         req->Serialize(&send_str);
  12.         std::cout << "Serialize: \n"
  13.                   << send_str << std::endl;
  14.         // 添加长度报头
  15.         send_str = Encode(send_str);
  16.         std::cout << "Encode: \n"
  17.                   << send_str << std::endl;
  18.         str += send_str;
  19.     }
  20.     // "len"\r\n"{}"\r\n
  21.     cli->Send(str);
  22.     // 读取应答
  23.     int n = cli->Recv(&inbuffer);
  24.     if (n <= 0)
  25.         break;
  26.     std::string package = Decode(inbuffer);
  27.     if (package.empty())
  28.         continue;
  29.     // 读到的package一定是一个完整的应答
  30.     auto resp = factory.BuildResponse();
  31.     // 反序列化
  32.     resp->Deserialize(package);
  33.     // 拿到了结构化的应答
  34.     std::cout << resp->_result << "[" << resp->_code << "]" << std::endl;
  35. }
复制代码
  为了更好地表现服务器端对报文的处理处罚是否正确,我们在TcpServerMain内的Service服务举行渺小调解,前面,我们让客户端不绝地对服务器端发出哀求,那么服务器端的Service也要不绝地行止置处罚哀求,并发送到客户端:
  1. void ServiceHelper(socket_sptr sockptr, InetAddr client)
  2. {
  3.     int sockfd = sockptr->SockFd();
  4.     LOG(DEBUG, "get a new link, info %s:%d, fd : %d", client.IP().c_str(), client.Port(), sockfd);
  5.     std::string clientaddr = "[" + client.IP() + ":" + std::to_string(client.Port()) + "]# ";
  6.     std::string inbuffer;
  7.     while (true)
  8.     {
  9.         sleep(5);
  10.         Request req;
  11.         // 1. 读取数据
  12.         int n = sockptr->Recv(&inbuffer);
  13.         if (n < 0)
  14.         {
  15.             LOG(DEBUG, "client %s quit", clientaddr.c_str());
  16.             break;
  17.         }
  18.         
  19.         // 2. 分析数据
  20.         std::string package;
  21.         while (true)
  22.         {
  23.             sleep(1);
  24.             std::cout << "inbuffer" << inbuffer << std::endl;
  25.             package = Decode(inbuffer);
  26.             if (package.empty())
  27.                 break;
  28.             std::cout << "-----------------------begin----------------------" << std::endl;
  29.             std::cout << "resq string:\n"
  30.                       << package << std::endl;
  31.             // 3.反序列化
  32.             req.Deserialize(package);
  33.             // 4. 业务处理
  34.             Response resp = _cb(req);
  35.             // 5. 对应答进行序列化
  36.             std::string send_str;
  37.             resp.Serialize(&send_str);
  38.             std::cout << send_str << std::endl;
  39.             // 6. 添加长度报头
  40.             send_str = Encode(send_str);
  41.             // 7. 发送到对端
  42.             sockptr->Send(send_str);
  43.         }
  44.     }
  45. }
复制代码
  那么我们全部准备都已经做好了,接下来就是通讯时间:

  如许网路版本盘算器我们就实现完成了。在这个项目当中,我们发现,我们把从Tcp内读取的报文,大概读到半个,大概读到一个半,大概其他特别不完备报文环境,这种环境我们称为 Tcp粘包题目。而我们使用Encode() 和 Decode() 接口就是为了办理tcp粘包题目的。

🚀基于守卫历程的改写

  我们知道,我们在毗连长途服务器的时间,实际上就是打开一个终端文件,假如有多个毗连就会打开多个终端文件,我们从一台装备向另一台装备举行重定向的时间就是云云:

  而且我们可以将消息发送到别的一个终端文件当中,使用如下下令举行重定向:
  1. echo "message" >> /dev/pts/n #这里n指的是任何一个存在的终端文件
复制代码
  这里假如你是使用XShell来测试上面的下令,你很大概不会乐成,由于版本升级的缘故起因,但是我们能知道这个征象就行。总而言之,当我们毗连Linux服务器的时间,会给我们打开一个终端文件,再启动bash下令行表明器。

  起首是创建终端文件,其次bash被启动,而bash又作为全部历程的父历程,bash则会打开创建的终端文件。而一样平常终端文件与启动的bash会被打包称为一个 会话(具体在以后守卫历程章节中看到),而每个会话都会有本身的 会话id(sid),而一样平常 会话的id是终端中的第一个历程的pid也就是bash:

  但是,只要在当前终端下启动的任何服务(历程),都属于当前的会话!比如:

  以是会话就像是bash历程中的管理容器,假如一个会话被烧毁了,那么会话里的全部历程也都会停止,

  但是本日,我想要一种不受会话影响的历程,也就是不受用户登录退出的影响,独立于会话之外的历程,比如我们的网络版盘算器服务器端,我们不想让其受用户注册烧毁的影响,以是我们可以编写代码将其变为 守卫历程。
  实际上这么做的意义就是创建一个新的会话,在Linux中给我们提供了 setsid() 接口:

  setsid()会创建出一个新的会话,不外有一个要求:调用历程不能是历程组的组长。那么什么是历程组呢?很简单,每个历程组都有一个唯一的标识符,通常是历程组的组长(Leader)的历程ID,而组长就是他们之中第一个启动的历程:

  以是,我们在步伐中直接创建子历程,而且退出父历程,那么,那么当前历程就可以调用setid()接口了,这个历程也就独立出会话之外,成为一个全新的会话,我们称之为 守卫历程(精灵历程)!使用雷同一下代码:
  1. if(fork() > 0) exit(0);
  2. setsid();
复制代码
  固然,假如你嫌贫苦,大可不必写长点的代码,由于Linux早就给我们想好了,给我们提供了一个 Daemon() 接口:



  • nochdir 参数:是否更改当前历程的工作目次。假如更改,守卫历程的目次就会切换为根目次,假如不更改,则在启动时的路径下。
  • nocliose参数:是否须要举行输入输出的处理处罚。
  Linux每个终端下都会存在一个null文件:/dev/null,假如去读取这个文件,文件内是没有任何内容的,假如对该文件举行写,同样也不会生存任何信息,而是立即抛弃。我们知道,当我们创建了守卫历程,也就意味着脱离了原来的会话,以是也就没有原来的终端文件了,而假如我们要使网络盘算器变为守卫历程,而网络盘算器中存在大量的IO使用,为了克制由于没有对应的终端文件举行IO而堕落,我们可以将 0,1,2三个文件形貌符全部重定向到 /dev/null 当中。
  1. #include <iostream>
  2. #include <unistd.h>
  3. int main()
  4. {
  5.     std::cout << "Pid is: " << getpid() << std::endl;
  6.     sleep(1);
  7.     daemon(0, 0);
  8.     while(true)
  9.     {
  10.         std::cout << "hello test" << std::endl;
  11.         sleep(1);
  12.     }
  13.    
  14.     return 0;
  15. }
复制代码
  以上是一个简单的测试样例,daemon内部会主动的fork而且退出父历程:

  颠末测试我们可以看到,hello.exe 的TTY,也就是终端文件变成了 “?”, 也就表现已经不属于当前的会话了,而SID同样与当前历程的SID差别,而且SID为守卫历程的pid。假如我们检察守卫历程的工作目次:

  可以看到,守卫历程当前工作目次实际上就是在根目次,假如我们同时检察该守卫历程的文件fd就会发现:

  由此可见,daemon接口的两个参数实际上是bool值范例的,第一个参数表现是否更改工作目次,第二个参数表现是否更改重定向,假如我们把daemon参数设置为daemon(0, 0):


  将daemon参数设置为(1, 1)就会导致我们输出的内容照旧在上一个会话下,而且Ctrl C 也无法停止历程(可使用 kill -9 process_pid 杀死历程),当我们查询历程工作目次时,也能发现其在当前的工作目次下,而fd也指向了第一个终端文件。
  以是一样平常环境下,我们直接调用 daemon(0, 0)即可,但是我们网络版盘算器不但仅有许多的IO,还写了许多很紧张的日志日志信息啊,这么设置守卫历程,我们就无法在终端上看到日志日志信息了,不消担心,由于早在编写日志日志之初,我们就已经给日志设置为两个选择,1. 将信息打印到表现器上。2. 将日志信息打印到终端文件上。我们可以将其打印到日志文件当中:

  随后启动服务器,将其变为一个守卫历程,然后启动一个客户端毗连服务器端:


  我们之前界说的文件路径就是在当前目次下,而我们创建了守卫历程,而且将工作目次改为了根目次,以是我们的log.txt文件只能出如今根目次了。

  以上就是网络版盘算器实现的全过程了,假如这三篇文章对您有所资助的话,还望点赞支持~~
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表