【Linux后端服务器开发】Reactor模式实现网络计算器

目录

一、Reactor模式概述

二、日志模块:Log.hpp

三、TCP连接模块:Sock.hpp

四、非阻塞通信模块:Util.hpp

五、多路复用I/O模块:Epoller.hpp

六、协议定制模块:Protocol.hpp

七、服务器模块:Server.hpp    server.cc

八、客户端模块:Client.hpp    client.cc


前情提示:在学习Reactor模式之前,需要熟悉socket套接字及TCP网络通信,需要熟悉 select / poll / epoll 三种多路转接IO,需要理解Linux文件系统的文件描述符与基础IO,需要理解服务器server与客户端client的设计。

【Linux后端服务器开发】基础IO与文件系统_命运on-9的博客-CSDN博客

【Linux后端服务器开发】TCP通信设计_命运on-9的博客-CSDN博客

【Linux后端服务器开发】协议定制(序列化与反序列化)_命运on-9的博客-CSDN博客

【Linux后端服务器开发】select多路转接IO服务器_命运on-9的博客-CSDN博客

【Linux后端服务器开发】poll/epoll多路转接IO服务器_命运on-9的博客-CSDN博客

一、Reactor模式概述

Reactor是什么?reactor的英文翻译是【反应堆】,reactor设计模式是一种事件处理模式。

如何让一个server服务器连接多个client客户端并处理业务?我们可以采用多进程 / 多线程的方法,但是无论是进程还是线程,对系统资源的消耗都是较大的,于是我们可以采用 select / poll / epoll 的多路复用IO方法,而在三种不同的多路复用方法中,性能最优的是epoll。

epoll的LT模式和ET模式该如何选择?LT和ET是不同的事件通知策略,LT水平触发是阻塞式通知,ET边缘触发是非阻塞式通知,ET模式会倒逼程序员一次性将就绪的数据读写完毕,这样也就使得一般情况下ET模式的效率更高,所以在Reactor模式的设计中,采用ET模式。

Reactor模式也叫Dispatcher(分派器)模式,它的工作原理是什么呢? I/O多路复用监听事件,当有事件就绪时,根据事件类型分配给某个进程/线程。

Reactor模式主要由Reactor分派器和资源处理这两个部分组成:

  1. Reactor分派器负责监听和分发事件,事件类型包含连接、读写、异常
  2. 资源处理负责业务处理,通常流程是 read -> 业务逻辑 -> send

Reactor模式是灵活多变的,根据不同的业务场景有不同的设计,可以是【单Reactor】也可以是【多Reactor】,可以是【单进程/线程】 也可以是【多进程/线程】,不过此文中的Reactor网络计算器设计采用的是【单Reactor 单进程/线程】模式。

【单Reactor 单进程/线程】模式设计

初始化TcpServer,创建listensock,并将listensock添加进epoller模型中进行监听,listensock会生成第一个Connection对象(注册_Accepter接口处理读取连接任务),之后的TcpClient的连接请求都是由这个绑定了_Accepter接口的listensock进行监听。

epoller模型Wait()等待TcpClient的请求,如果是连接请求,则TcpServer会派发任务给listensock对象,让其调用Sock对象的Accept接口创建新的套接字sock进行IO,sock会创建一个新的Connection对象(注册_Reader、_Sender、_Excepter接口处理读/写/异常任务)。

Connection进行业务处理的时候,根据TCP通信协议的通信流程是 read -> 业务逻辑 -> send,若是在通信中遇到异常,则会调用_Excepter接口关闭连接。

在TCP通信设计中,我们需要设计通信数据的序列化和反序列化,这便是在Connection对象读取到数据之后的业务处理逻辑,通过序列化和反序列化将数据发送给TcpClient,TcpClient收到服务器发送数据后再通过序列化和反序列化拿到想要的数据。

在服务器设计的时候,日志功能是可以省略的,但是加上日志功能的服务器功能更完整并且方便调试和服务器维护。

二、日志模块:Log.hpp

日志模块里面将日志分为(DEBUG、NORMAL、WARNING、ERROR、FATAL)五个记录等级,并且定义了不同的错误类型,日志记录需要记录进程的IP和端口号以及记录时间。

由于Linux系统的gdb调试是很复杂的,通过在代码中添加DEBUG的打印日志更方便调试。

#pragma once#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <cstdarg>
#include <unistd.h>#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define NUM 1024enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,EPOLL_CREATE_ERR
};const char* To_Level_Str(int level)
{switch (level){case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return nullptr;}
}std::string To_Time_Str(long int t)
{// 将时间戳转化为tm结构体struct tm* cur;cur = gmtime(&t);cur->tm_hour = (cur->tm_hour + 8) % 24; // 东八区char tmp[NUM];std::string my_format = "%Y-%m-%d %H:%M:%S";strftime(tmp, sizeof(tmp), my_format.c_str(), cur);std::string cur_time = tmp;return cur_time;
}void Log_Message(int level, const char *format, ...)
{char logprefix[NUM];std::string cur_time = To_Time_Str((long int)time(nullptr));snprintf(logprefix, sizeof(logprefix), "[%s][%s][pid: %d]",To_Level_Str(level), cur_time.c_str(), getpid());char logcontent[NUM];va_list arg;va_start(arg, format);vsnprintf(logcontent, sizeof(logcontent), format, arg);std::cout << logprefix << logcontent << std::endl;
}

三、TCP连接模块:Sock.hpp

Sock对象里面将TCP网络连接的底层接口进行了封装,更方便其他模块对于TCP连接的调用。

Sock对象不再对Accept()的连接失败做处理,将处理权交给了TcpServer。

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"const static int g_defaultfd = -1;
const static int g_backlog = 32;class Sock
{
public:Sock(): _listensock(g_defaultfd){}Sock(int listensock): _listensock(listensock){}int Fd(){return _listensock;}void Socket(){// 1. 创建socket文件套接字对象_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){Log_Message(FATAL, "create socket error");exit(SOCKET_ERR);}Log_Message(NORMAL, "create socket success: %d", _listensock);int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}void Bind(int port){// 2. bind绑定自己的网络信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0){Log_Message(FATAL, "bind socket error");exit(BIND_ERR);}Log_Message(NORMAL, "bind socket success");}void Listen(){// 3. 设置socket 为监听状态if (listen(_listensock, g_backlog) < 0) // 第二个参数backlog后面在填这个坑{Log_Message(FATAL, "listen socket error");exit(LISTEN_ERR);}Log_Message(NORMAL, "listen socket success");}int Accept(std::string *clientip, uint16_t *clientport, int* err){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(_listensock, (struct sockaddr *)&peer, &len);*err = errno;if (sock >= 0){*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}void Close(){if (_listensock != g_defaultfd)close(_listensock);}~Sock(){this->Close();}private:int _listensock;
};

四、非阻塞通信模块:Util.hpp

Util.hpp设置静态成员函数Set_Noblock(),将ET通知策略的套接字sock设置为非阻塞模式。

本次网络计算器的设计是ET模式,故所有的连接在新建连接时都需要设置为非阻塞模式。

#pragma once#include <iostream>
#include <fcntl.h>
#include <unistd.h>class Util
{
public:static bool Set_Nonblock(int fd){int fl = fcntl(fd, F_GETFL);if (fl < 0)return false;fcntl(fd, F_SETFL, fl | O_NONBLOCK);return true;}
};

五、多路复用I/O模块:Epoller.hpp

epoll模型是一个操作系统层面的模型,我们将控制epoll的系统接口封装在Epoller对象中方便TcpServer的调用。

无论是TcpClient的连接请求还是业务请求,从epoll的角度来看,都是一个对文件描述符的读事件,epoll只需要做事件通知即可。

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/epoll.h>
#include <unistd.h>#include "Log.hpp"const static int g_default_epfd = -1;
const static int g_size = 64;class Epoller
{
public:Epoller(): _epfd(g_default_epfd){}void Create(){_epfd = epoll_create(g_size);if (_epfd < 0){Log_Message(FATAL, "epoll create error: %s", strerror(errno));exit(EPOLL_CREATE_ERR);}}// user -> kernelbool Add_Event(int sock, uint32_t events){struct epoll_event ev;ev.events = events;ev.data.fd = sock;int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);return n == 0;}// kernel -> userint Wait(struct epoll_event revs[], int num, int timeout){return epoll_wait(_epfd, revs, num, timeout);}bool Control(int sock, uint32_t event, int action){int n = 0;if (action == EPOLL_CTL_MOD){struct epoll_event ev;ev.events = event;ev.data.fd = sock;n = epoll_ctl(_epfd, action, sock, &ev);}else if (action == EPOLL_CTL_DEL){n = epoll_ctl(_epfd, action, sock, nullptr);}else{n = -1;}return n == 0;}void Close(){if (_epfd != g_default_epfd)close(_epfd);}~Epoller(){this->Close();}private:int _epfd;
};

六、协议定制模块:Protocol.hpp

TCP通信的序列化与反序列化就是网络服务器的业务逻辑,因为TCP通信是字节流传输,无法传输结构化数据,所有我们需要自定义协议做序列化与反序列化处理,进行字符串数据与结构化数据的转换。

序列化与反序列化可以完全编写函数做字符串数据处理,也可以通过调用Json库做字符串数据处理,调用Json库需要加上 -ljsoncpp 动态链接库。

这里的序列化与反序列化,包含了客户端Client和服务器Serer双端的业务逻辑。

#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>#include "Log.hpp"using namespace std;#define SEP " "
#define LINE_SEP "\r\n"enum
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERR
};// "x op y" -> "content_len"\r\n"x op y"\r\n
string En_Length(const string& text)
{string send_str = to_string(text.size());send_str += LINE_SEP;send_str += text;send_str += LINE_SEP;return send_str;
}// "content_len"\r\n"x op y"\r\n
bool De_Length(const string& package, string* text)
{auto pos = package.find(LINE_SEP);if (pos == string::npos)return false;string text_len_str = package.substr(0, pos);int text_len = stoi(text_len_str);*text = package.substr(pos + strlen(LINE_SEP), text_len);return true;
}// 通信协议不止一种,需要将协议进行编号,以供os分辨
// "content_len"\r\n"协议编号"\r\n"x op y"\r\nclass Request
{
public:Request(int x = 0, int y = 0, char op = 0): _x(x), _y(y), _op(op){}// 序列化bool Serialize(string* out){Json::Value root;root["first"] = _x;root["second"] = _y;root["oper"] = _op;Json::FastWriter write;*out = write.write(root);return true;}// 反序列化bool Deserialiaze(const string& in){Json::Value root;Json::Reader reader;reader.parse(in, root);_x = root["first"].asInt();_y = root["second"].asInt();_op = root["oper"].asInt();return true;}public:int _x, _y;char _op;
};class Response
{
public:Response(int exitcode = 0, int res = 0): _exitcode(exitcode), _res(res){}bool Serialize(string* out){Json::Value root;root["exitcode"] = _exitcode;root["result"] = _res;Json::FastWriter writer;*out = writer.write(root);return true;}bool Deserialize(const string& in){Json::Value root;Json::Reader reader;reader.parse(in, root);_exitcode = root["exitcode"].asInt();_res = root["result"].asInt();return true;}public:int _exitcode;int _res;
};// 读取数据包
// "content_len"\r\n"x op y"\r\n
bool Parse_One_Package(string& inbuf, string* text)
{*text = "";// 分析处理auto pos = inbuf.find(LINE_SEP);if (pos == string::npos)return false;string text_len_string = inbuf.substr(0, pos);int text_len = stoi(text_len_string);int total_len = text_len_string.size() + 2 * strlen(LINE_SEP) + text_len;if (inbuf.size() < total_len)return false;// 至少有一个完整的报文*text = inbuf.substr(0, total_len);inbuf.erase(0, total_len);return true;
}bool Recv_Package(int sock, string& inbuf, string* text)
{char buf[1024];while (true){ssize_t n = recv(sock, buf, sizeof(buf) - 1, 0);if (n > 0){buf[n] = 0;inbuf += buf;auto pos = inbuf.find(LINE_SEP);if (pos == string::npos)continue;string text_len_str = inbuf.substr(0, pos);int text_len = stoi(text_len_str);int total_len = text_len_str.size() + 2 * strlen(LINE_SEP) + text_len;cout << "\n收到响应报文:\n" << inbuf;if (inbuf.size() < total_len){cout << "输入不符合协议规定" << endl;continue;}*text = inbuf.substr(0, total_len);inbuf.erase(0, total_len);break;}else{return false;}}return true;
}

七、服务器模块:Server.hpp    server.cc

Server初始化创建listensock、创建epoll模型、创建事件就绪队列(struct epoll_event* _recv),listensock套接字会创建一个注册了_Accepter接口的Connection对象,负责新建Client的连接。

所有的Connection对象通过哈希表管理,极大的提高了效率。每一个Connection对象都有读写缓冲区(_inbuffer / _outbuffer),可以绑定回调的_Recver、_Sender、_Excepter函数,以对事件做读、写、异常处理。

服务器的本质就是一个死循环,循环的等待epoll模型的事件通知然后再Dispatch分派任务做读写处理,若遇到异常问题,将其转化为读写问题再去分派任务。

我们将网络计算服务器的计算任务放入了server.cc中进行函数定义,为了解耦我们也可以再单独创建一个Task()对象,但是此处由于计算任务比较简单,我们就直接在server.cc源文件中定义了。

服务器中的所有监听的Connection对象,我们将其读监听设置为一直开启,将其写监听设置为按需开启,当写任务完成后即关闭写监听。

Server.hpp

#pragma once#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>
#include <cassert>
#include <unistd.h>#include "Sock.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "Util.hpp"
#include "Protocol.hpp"using namespace std;class Connection;
class TcpServer;static const uint16_t g_defaultport = 8080;
static const int g_num = 64;using func_t = function<void(Connection*)>;class Connection
{
public:Connection(int sock, TcpServer* tcp): _sock(sock), _tcp(tcp){}void Register(func_t recver, func_t sender, func_t excepter){_recver = recver;_sender = sender;_excepter = excepter;}void Close(){close(_sock);}public:int _sock;string _inbuffer;   // 输入缓冲区string _outbuffer;  // 输出缓冲区func_t _recver;   // 读func_t _sender;   // 写func_t _excepter; // 异常TcpServer *_tcp; // 可以省略// uint64_t last_time;     // 记录最近访问时间,可用于主动关闭某时间段内未访问的连接
};class TcpServer
{
public:TcpServer(func_t service, uint16_t port = g_defaultport): _service(service), _port(port), _revs(nullptr){}void InitServer(){// 1. 创建socket_sock.Socket();_sock.Bind(_port);_sock.Listen();// 2. 创建epoller_epoller.Create();// 3. 将目前唯一的一个sock,添加到Epoller中Add_Connection(_sock.Fd(), EPOLLIN | EPOLLET,bind(&TcpServer::Accepter, this, placeholders::_1), nullptr, nullptr);_revs = new struct epoll_event[g_num];_num = g_num;}void Enable_Read_Write(Connection* conn, bool readable, bool writeable){uint32_t event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;_epoller.Control(conn->_sock, event, EPOLL_CTL_MOD);}// 事件派发void Dispatch(){int timeout = -1;while (1){Loop(timeout);// Log_Message(DEBUG, "timeout ...");// 遍历_conn_map,计算每一个节点的最近访问时间做节点控制}}~TcpServer(){_sock.Close();_epoller.Close();if (nullptr == _revs)delete[] _revs;}private:void Add_Connection(int sock, uint32_t events, func_t recver, func_t sender, func_t excepter){// 1. 首先为该sock创建Connection并初始化,并添加到_conn_mapif (events & EPOLLET)Util::Set_Nonblock(sock);Connection *conn = new Connection(sock, this);// 2. 给对应的sock设置对应的回调方法conn->Register(recver, sender, excepter);// 3. 其次将sock与它要关心的时间"写透式"注册到epoll中,让epoll帮我们关心bool f = _epoller.Add_Event(sock, events);assert(f);// 4. 将kv添加到_conn_map中_conn_map.insert(pair<int, Connection*>(sock, conn));Log_Message(DEBUG, "Add_Connection: add new sock: %d in epoll and unordered_map", sock);}void Recver(Connection *conn){char buffer[1024];while (1){ssize_t s = recv(conn->_sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;conn->_inbuffer += buffer;      // 将读到的数据入队列Log_Message(DEBUG, "\n收到client[%d]请求报文:\n%s", conn->_sock, conn->_inbuffer.c_str());_service(conn);}else if (s == 0){// 异常回调if (conn->_excepter){conn->_excepter(conn);return;}}else{if (errno == EAGAIN || errno == EWOULDBLOCK){break;}else if (errno == EINTR){continue;}else{if (conn->_excepter){conn->_excepter(conn);return;}}}}}void Sender(Connection *conn){while (1){ssize_t s = send(conn->_sock, conn->_outbuffer.c_str(), conn->_outbuffer.size(), 0);if (s >= 0){if (conn->_outbuffer.empty())break;elseconn->_outbuffer.erase(0, s);}else{if (errno == EAGAIN || errno == EWOULDBLOCK){break;}else if (errno == EINTR){continue;}else{if (conn->_excepter){conn->_excepter(conn);return;}}}}// 如果没有发送完毕,需要对对应的sock开启写事件的关心,发完了,关闭对写事件的关心if (!conn->_outbuffer.empty())conn->_tcp->Enable_Read_Write(conn, true, true);elseconn->_tcp->Enable_Read_Write(conn, true, false);}void Excepter(Connection *conn){_epoller.Control(conn->_sock, 0, EPOLL_CTL_DEL);conn->Close();_conn_map.erase(conn->_sock);Log_Message(DEBUG, "关闭 %d 文件描述符的所有资源", conn->_sock);delete conn;}void Accepter(Connection *conn){while (1){string clientip;uint16_t clientport;int err = 0;int sock = _sock.Accept(&clientip, &clientport, &err);if (sock > 0){Add_Connection(sock, EPOLLIN | EPOLLET, bind(&TcpServer::Recver, this, placeholders::_1),bind(&TcpServer::Sender, this, placeholders::_1), bind(&TcpServer::Excepter, this, placeholders::_1));Log_Message(DEBUG, "get a new link, info: [%s : %d]", clientip.c_str(), clientport);}else{if (err == EAGAIN || err == EWOULDBLOCK)break;else if (err == EINTR)continue;elsebreak;}}}bool Is_Connection_Exists(int sock){auto iter = _conn_map.find(sock);return iter != _conn_map.end();}void Loop(int timeout){int n = _epoller.Wait(_revs, _num, timeout); // 获取已经就绪的事件for (int i = 0; i < n; ++i){int sock = _revs[i].data.fd;uint32_t events = _revs[i].events;// 将所有异常问题,转化成读写问题if (events & EPOLLERR)events |= (EPOLLIN | EPOLLOUT);if (events & EPOLLHUP)events |= (EPOLLIN | EPOLLOUT);// 读写事件就绪if ((events & EPOLLIN) && Is_Connection_Exists(sock) && _conn_map[sock]->_recver)_conn_map[sock]->_recver(_conn_map[sock]);if ((events & EPOLLOUT) && Is_Connection_Exists(sock) && _conn_map[sock]->_sender)_conn_map[sock]->_sender(_conn_map[sock]);}}private:uint16_t _port;Sock _sock;Epoller _epoller;unordered_map<int, Connection*> _conn_map;struct epoll_event* _revs;int _num;func_t _service;
};

server.cc

#include "Server.hpp"
#include <memory>using namespace std;// 计算任务
bool Cal(const Request& req, Response& resp)
{resp._exitcode = OK;resp._res = 0;if (req._op == '/' && req._y == 0){resp._exitcode = DIV_ZERO;return false;}if (req._op == '%' && req._y == 0){resp._exitcode = MOD_ZERO;return false;}switch (req._op){case '+':resp._res = req._x + req._y;break;case '-':resp._res = req._x - req._y;break;case '*':resp._res = req._x * req._y;break;case '/':resp._res = req._x / req._y;break;case '%':resp._res = req._x % req._y;break;default:resp._exitcode = OP_ERR;break;}return true;
}void Calculate(Connection* conn)
{string one_package;while (Parse_One_Package(conn->_inbuffer, &one_package)){string req_str;if (!De_Length(one_package, &req_str))return;// 对请求体Request反序列化,得到一个结构化的请求对象Request req;if (!req.Deserialiaze(req_str))return;Response resp;Cal(req, resp);string resp_str;resp.Serialize(&resp_str);conn->_outbuffer += En_Length(resp_str);cout << "构建完整的响应报文: \n" << conn->_outbuffer << endl;}// 直接发if (conn->_sender)conn->_sender(conn);
}static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";exit(1);
}string Transaction(const string &request)
{return request;
}// ./select_server 8080
int main(int argc, char *argv[])
{// if(argc != 2)//     Usage();// unique_ptr<SelectServer> svr(new SelectServer(atoi(argv[1])));// std::cout << "test: " << sizeof(fd_set) * 8 << std::endl;unique_ptr<TcpServer> svr(new TcpServer(Calculate));svr->InitServer();svr->Dispatch();return 0;
}

八、客户端模块:Client.hpp    client.cc

Client.hpp

#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Protocol.hpp"using namespace std;class Client
{
public:Client(const std::string& server_ip, const uint16_t& server_port): _sock(-1), _server_ip(server_ip), _server_port(server_port){}void Init(){_sock = socket(AF_INET, SOCK_STREAM, 0);if (_sock < 0){std::cerr << "socket error" << std::endl;exit(1);}}void Run(){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(_server_port);server.sin_addr.s_addr = inet_addr(_server_ip.c_str());if (connect(_sock, (struct sockaddr*)&server, sizeof(server)) < 0){std::cerr << "connect error" << std::endl;exit(1);}else{string line;string inbuf;while (true){cout << "mycal>>> ";getline(cin, line);Request req = Parse_Line(line);     // 输入字符串,生成Request对象string content;req.Serialize(&content);                // Request对象序列化string send_str = En_Length(content);   // 序列化字符串编码 -> "content_len"\r\n"x op y"\r\nsend(_sock, send_str.c_str(), send_str.size(), 0);// 将服务器的返回结果序列化与反序列化string package, text;if (!Recv_Package(_sock, inbuf, &package))continue;if (!De_Length(package, &text))continue;Response resp;resp.Deserialize(text);cout << "计算结果: " << endl;cout << "exitcode: " << resp._exitcode << ", ";cout << "result: " << resp._res << endl << endl;}}}// 将输入转化为Request结构Request Parse_Line(const string& line){int status = 0;     // 0:操作符之前    1:遇到操作符    2:操作符之后int cnt = line.size();string left, right;char op;int i = 0;while (i < cnt){switch (status){case 0:if (!isdigit(line[i])){if (line[i] == ' '){i++;break;}op = line[i];status = 1;}else{left.push_back(line[i++]);}break;case 1:i++;if (line[i] == ' ')break;status = 2;break;case 2:right.push_back(line[i++]);break;}}return Request(stoi(left), stoi(right), op);}~Client(){if (_sock >= 0)close(_sock);}private:int _sock;string _server_ip;uint16_t _server_port;
};

client.cc

#include "Client.hpp"
#include <memory>using namespace std;static void Usage(string proc)
{cout << "\nUsage:\n\t" << proc << " local_port\n\n";exit(1);
}int main(int argc, char* argv[])
{if (argc != 3)Usage(argv[0]);string server_ip = argv[1];uint16_t server_port = atoi(argv[2]);unique_ptr<Client> tcli(new Client(server_ip, server_port));tcli->Init();tcli->Run();return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/77142.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于SpringBoot+Vue的地方美食分享网站设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

Docker实战-操作Docker容器实战(一)

导语   在之前的分享中&#xff0c;我们介绍了关于如何去操作Docker镜像&#xff0c;下面我们来看看如何去操作容器。 简单来讲&#xff0c;容器是镜像运行的一个实例&#xff0c;与镜像不同的是镜像只能作为一个静态文件进行读取&#xff0c;而容器是可以在运行时进行写入操…

2023再谈前端状态管理

目录 什么是状态管理&#xff1f; 状态 常见模式 要解决的问题 心智模型 React Context Context 的问题 优点 缺点 React 外部状态管理库 概览 Class 时代 Redux 单向数据流 三大原则 如何处理异步 如何处理数据间联动 优点 缺点 Dva icestore Mobx 设…

危大工程智慧工地源码,微服务+Java+Spring Cloud +UniApp +MySql 物联网、人工智能、视频AI分析

一套智慧工地管理平台源码&#xff0c;PC端移动APP端可视货数据管理端源码 智慧工地可视化系统利用物联网、人工智能、云计算、大数据、移动互联网等新一代信息技术&#xff0c;通过工地中台、三维建模服务、视频AI分析服务等技术支撑&#xff0c;实现智慧工地高精度动态仿真&a…

windows上给oracle打补丁注意事项

打补丁的过程 1、升级opatch工具&#xff0c;检查剩余空间用于存放ORACLE_HOME的备份&#xff0c;设置oracle_home环境变量,通过readme中的先决条件来检查现有补丁是否和本次补丁冲突 2、opatch apply 升级数据库软件&#xff0c;这个必须数据库文件不要被进程调用 在windows上…

美团前端研发框架Rome实践和演进趋势

本文整理自美团技术沙龙第76期《大前端研发协同效能提升与实践》&#xff0c;为大家介绍了美团到店前端研发框架Rome实践和演进趋势。 具体来讲&#xff0c;本文首先介绍了Rome整体的工程生态、演变路径、规模化升级以及工程框架外的开发辅助工具&#xff1b;第二部分&#xff…

GoogLeNet卷积神经网络-笔记

GoogLeNet卷积神经网络-笔记 GoogLeNet是2014年ImageNet比赛的冠军&#xff0c; 它的主要特点是网络不仅有深度&#xff0c; 还在横向上具有“宽度”。 由于图像信息在空间尺寸上的巨大差异&#xff0c; 如何选择合适的卷积核来提取特征就显得比较困难了。 空间分布范围更广的…

matlab智能算法程序包89套最新高清录制!matlab专题系列!

关于我为什么要做代码分享这件事&#xff1f; 助力科研旅程&#xff01; 面对茫茫多的文献&#xff0c;想复现却不知从何做起&#xff0c;我们通过打包成品代码&#xff0c;将过程完善&#xff0c;让您可以拿到一手的复现过程以及资料&#xff0c;从而在此基础上&#xff0c;照…

OpenCV基础

目录 图像基本操作数据读取——图像数据读取——视频截取部分图像数据 ROI——Region of Interest颜色通道提取边界填充数值计算图像融合 图像处理灰度图HSV图像阈值图像平滑(滤波)形态学-腐蚀操作形态学-膨胀操作开运算与闭运算梯度运算礼帽与黑帽 图像梯度——Sobel算子看看L…

Vue 2.x 项目升级到 Vue 3详细指南【总结版】

文章目录 0.前言1.升级教程1.1. 升级 Vue CLI&#xff1a;1.2. 安装 Vue 3&#xff1a;1.3. 更新 Vue 组件&#xff1a;1.4. 迁移全局 API&#xff1a;1.5. 迁移路由和状态管理器&#xff1a;1.6. 迁移 TypeScript&#xff1a;1.7. 迁移测试代码&#xff1a; 2.迁移总结2.0. 这…

深入学习 Redis - 谈谈你对 Redis 的 RDB、AOF、混合持久化的了解吧?

目录 一、Redis 是怎么存储数据的&#xff1f; 二、Redis 具体是按照什么样的策略来实现持久化的&#xff1f; 2.1、RDB&#xff08;Redis Database&#xff09; 2.1.1、触发机制 2.1.2、bgsave 命令处理流程 2.1.3、RDB 文件的处理 2.1.4、演示效果 1&#xff09;手动执…

STL容器适配器 -- stack和queue(使用+实现)(C++)

stack和queue stackstack的介绍stack的使用stack的实现 queuequeue的介绍queue的使用queue的实现 deque简单介绍deque&#xff08;双端队列&#xff09;双开口连续打引号的原因 deque底层结构deque的迭代器封装结构&#xff08;复杂&#xff09;deque的优缺点 栈和队列数据结构…

Java版Spring Cloud+Spring Boot+Mybatis+uniapp知识付费平台讲解+免费搭建 qt

&#xfeff;Java版知识付费源码 Spring CloudSpring BootMybatisuniapp前后端分离实现知识付费平台 提供职业教育、企业培训、知识付费系统搭建服务。系统功能包含&#xff1a;录播课、直播课、题库、营销、公司组织架构、员工入职培训等。 提供私有化部署&#xff0c;免费售…

《面试1v1》Kafka的ack机制

&#x1f345; 作者简介&#xff1a;王哥&#xff0c;CSDN2022博客总榜Top100&#x1f3c6;、博客专家&#x1f4aa; &#x1f345; 技术交流&#xff1a;定期更新Java硬核干货&#xff0c;不定期送书活动 &#x1f345; 王哥多年工作总结&#xff1a;Java学习路线总结&#xf…

P25透明屏:探究在商业广告领域的应用表现

P25透明屏是一种新型的显示屏技术&#xff0c;具有高透明度和高分辨率的特点。 它可以将图像或视频直接投影到透明的表面上&#xff0c;使得观众可以透过屏幕看到背后的景物&#xff0c;同时也能够看到屏幕上的内容。 P25透明屏广泛应用于商业展示、户外广告、产品展示等领域…

年薪930万,谷歌薪资大揭秘

硅谷大厂中&#xff0c;谷歌员工称得上是科技行业中收入最高的一些人。 据统计&#xff0c;谷歌工程师在2022年总薪酬中位数为279,802美元&#xff08;约200万人民币&#xff09;&#xff0c;但这仅是基本工资。 如果计入股权和奖金&#xff0c;他们的收入甚至更高。 近来&am…

PHP-Mysql好运图书管理系统--【白嫖项目】

强撸项目系列总目录在000集 PHP要怎么学–【思维导图知识范围】 文章目录 本系列校训本项目使用技术 首页必要的项目知识ThinkPHP的MVCThinkTemplateThinkPHP 6和ThinkPHP 5 phpStudy 设置导数据库前台展示页面后台的管理界面数据库表结构项目目录如图&#xff1a;代码部分&a…

Mybatis引出的一系列问题-Spring事务的探究

1 spring事务的传播特性 package com.zs.service;Service public class UserService {Autowiredprivate UserDao userDA0;Transactionalpublic void transfer(String fromName, String toName, Integer money) {userDA0.out(fromName, money);int a 1 / 0;userDA0.in(toName,…

[CKA]考试之一个 Pod 封装多个容器

由于最新的CKA考试改版&#xff0c;不允许存储书签&#xff0c;本博客致力怎么一步步从官网把答案找到&#xff0c;如何修改把题做对&#xff0c;下面开始我们的 CKA之旅 题目为&#xff1a; Task 创建一个Pod&#xff0c;名字为kucc1&#xff0c;这个Pod包含4容器&#xff…

思科模拟器配置静态路由(下一跳使用IP)

Router0配置代码&#xff1a;##端口配置 Router(config)#int fastEthernet 0/0 Router(config-if)#ip address 192.168.10.254 255.255.255.0 Router(config-if)#no shutdown Router(config-if)#int fastEthernet 0/1 Router(config-if)#ip address 192.168.20.1 255.255.255.2…