【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.mzph.cn/news/22631.shtml

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

相关文章

MySQL安装详细教程!!!

安装之前&#xff0c;先卸载你之前安装过的数据库程序&#xff0c;否则会造成端口号占用的情况。 1.首先下载MySQL:MySQL :: Download MySQL Community Server(下载路径) 2.下载版本不一样&#xff0c;安装方法略有不同&#xff1b;&#xff08;版本5的安装基本一致&#xff0c…

六、目录树生成工具_zDirTree

1、zDirTree工具简介 zDirTree可以根据文件资源生成目录树&#xff0c;就是用文本的形式把文件层级结构表示出来&#xff0c;可以方便理解文件结构。 2、zDirTree工具下载 (1)我没有找到这工具的官方下载地址。 (2)我是微信公众号"干货食堂"中下载。 3、软件使用…

健身计划:用思维导图记录你的健身目标、锻炼项目、时间安排等

现在&#xff0c;大家越来越在乎自己的身体健康&#xff0c;健身也成了大家工作之外非常重要的一件事。一个好的健身计划的制定可以让我们的健身计划事半功倍。 思维导图作为一种高效的可视化思维工具&#xff0c;在健身计划制定的过程中&#xff0c;可以让我们的各项任务与时间…

VS2017中Qt工程报错:无法解析的外部符号 __imp_CommandLineToArgvW,该符号在函数 WinMain 中被引用

工程报错:无法解析的外部符号 __imp_CommandLineToArgvW&#xff0c;该符号在函数 WinMain 中被引用 解决方法&#xff1a; 在输入的附加依赖项中增加 shell32.lib

动态代理类之万能模板

ProxyInvocationHandler package com.heerlin.demo03;import com.heerlin.demo02.Rent;import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;//用这个类&#xff0c;自动生成代理类 public class ProxyInvocationH…

分页Demo

目录 一、分页对象封装 分页数据对象 分页查询实体类 实体类用到的utils ServiceException StringUtils SqlUtil BaseMapperPlus,> BeanCopyUtils 二、示例 controller service dao 一、分页对象封装 分页数据对象 import cn.hutool.http.HttpStatus; import com.…

VBA技术资料1-146

VBA技术资料本周更新较多&#xff1a;单值查找并提示结果&#xff1b;多值查找并提示结果&#xff1b;复制整个数据范围到PowerPoint&#xff1b;更改PowerPoint文本框字体大小&#xff1b;调整PowerPoint图像为整幻灯片&#xff1b;在PowerPoint中添加末尾幻灯片&#xff1b;在…

安防监控视频融合EasyCVR平台接入RTSP流后设备显示离线是什么原因?

安防监控视频EasyCVR视频汇聚融合平台基于云边端智能协同架构&#xff0c;具有强大的数据接入、处理及分发能力&#xff0c;平台支持海量视频汇聚管理、全网分发、按需调阅、鉴权播放、智能分析等视频能力与服务。平台开放度高、兼容性强、可支持灵活拓展与第三方集成&#xff…

学生信息管理系统springboot学校学籍专业数据java jsp源代码mysql

本项目为前几天收费帮学妹做的一个项目&#xff0c;Java EE JSP项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 学生信息管理系统springboot 系统3权限&#xff1a;超…

【雕爷学编程】MicroPython动手做(33)——物联网之天气预报3

天气&#xff08;自然现象&#xff09; 是指某一个地区距离地表较近的大气层在短时间内的具体状态。而天气现象则是指发生在大气中的各种自然现象&#xff0c;即某瞬时内大气中各种气象要素&#xff08;如气温、气压、湿度、风、云、雾、雨、闪、雪、霜、雷、雹、霾等&#xff…

【基础类】—DOM事件系统性学习

一、基本概念&#xff1a;DOM事件的级别 // DOM0 element.onclickfunction(){} // DOM2, 新增了冒泡和捕获 element.addEventListener(click,function(){}, false) // DOM3, 新增更多事件类型 鼠标、键盘等 element.addEventListener(keyup,function(){}, false)二、DOM事件模…

解决在mybatis中使用class属性绑定映射文件出现的异常问题~

如下所示&#xff0c;当我在XML文件中通过class属性配置其mapper文件时&#xff0c;出现下述错误 <mappers><mapper class"mappers.userMapper"/> </mappers>错误描述&#xff1a; 解决方法如下所示&#xff1a;在pom.xml文件中添加下述代码 <…

网络安全进阶学习第十一课——MySQL手工注入(2)

文章目录 一、UA注入1、原理2、靶场演示&#xff1a;1&#xff09;一旦页面出现如下现状&#xff0c;就可以使用UA注入2&#xff09;BP抓包3&#xff09;修改User-Agent 二、referer注入1、原理2、靶场演示&#xff1a;1&#xff09;使用BP抓包2&#xff09;修改Referer 三、DN…

SQL基础复习与进阶

SQL进阶 文章目录 SQL进阶关键字复习ALLANYEXISTS 内置函数ROUND&#xff08;四舍五入&#xff09;TRUNCATE&#xff08;截断函数&#xff09;SEILING&#xff08;向上取整&#xff09;FLOOR&#xff08;向下取整&#xff09;ABS&#xff08;获取绝对值&#xff09;RAND&#x…

RPC原理与Go RPC详解

文章目录 RPC原理与Go RPC什么是RPC本地调用RPC调用HTTP调用RESTful API net/rpc基础RPC示例基于TCP协议的RPC使用JSON协议的RPCPython调用RPC RPC原理 RPC原理与Go RPC 什么是RPC RPC&#xff08;Remote Procedure Call&#xff09;&#xff0c;即远程过程调用。它允许像调用…

React Native元素旋转一定的角度

mMeArrowIcon: {fontSize: 30, color: #999, transform: [{rotate: 180deg}]},<Icon name"arrow" style{styles.mMeArrowIcon}></Icon>参考链接&#xff1a; https://reactnative.cn/docs/transforms https://chat.xutongbao.top/

AcWing 24:机器人的运动范围 ← BFS、DFS

【题目来源】https://www.acwing.com/problem/content/description/22/【题目描述】 地上有一个 m 行和 n 列的方格&#xff0c;横纵坐标范围分别是 0∼m−1 和 0∼n−1。 一个机器人从坐标 (0,0) 的格子开始移动&#xff0c;每一次只能向左&#xff0c;右&#xff0c;上&#…

【雕爷学编程】Arduino动手做(182)---DRV8833双路电机驱动模块2

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

EventBus 开源库学习(一)

一、概念 EventBus是一款在 Android 开发中使用的发布-订阅事件总线框架&#xff0c;基于观察者模式&#xff0c;将事件的接收者和发送者解耦&#xff0c;简化了组件之间的通信&#xff0c;使用简单、效率高、体积小。 一句话&#xff1a;用于Android组件间通信的。 二、原理…

做好“关键基础设施提供商”角色,亚马逊云科技加快生成式AI落地

一场关于生产力的革命已在酝酿之中。全球管理咨询公司麦肯锡在最近的报告《生成式人工智能的经济潜力&#xff1a;下一波生产力浪潮》中指出&#xff0c;生成式AI每年可能为全球经济增加2.6万亿到4.4万亿美元的价值。在几天前的亚马逊云科技纽约峰会中&#xff0c;「生成式AI」…