【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分派器和资源处理这两个部分组成:
- Reactor分派器负责监听和分发事件,事件类型包含连接、读写、异常
- 资源处理负责业务处理,通常流程是 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;
}
相关文章:
【Linux后端服务器开发】Reactor模式实现网络计算器
目录 一、Reactor模式概述 二、日志模块:Log.hpp 三、TCP连接模块:Sock.hpp 四、非阻塞通信模块:Util.hpp 五、多路复用I/O模块:Epoller.hpp 六、协议定制模块:Protocol.hpp 七、服务器模块:Server.…...
【WebRTC---源码篇】(二:一)PeerConnection详解
Track的添加 上图是整体流程图 RTCErrorOr<rtc::scoped_refptr<RtpSenderInterface>> PeerConnection::AddTrack(rtc::scoped_refptr<MediaStreamTrackInterface> track,const std::vector<std::string>& stream_ids) {RTC_DCHECK_RUN_ON(signal…...
使用tinyxml解析和修改XML文件
首先要清楚XML文件包含哪些元素: 他是由元素、文本或者两者混合物组成。元素可以拥有属性,元素是指从开始标签到结束标签的部分。 <?xml version"1.0" encoding"UTF-8" ?> <books><book id"1001">&…...
[Docker实现测试部署CI/CD----相关服务器的安装配置(1)]
目录 0、CI/CD系统最终架构图规划IP地址 1、git配置Git下载pycharm配置gitidea配置git 2、GitLab安装与配置主机要求拉取镜像定义 compose.yml启动gitlab浏览器访问并修改密码查看登录密码修改密码 3、SonarQube 安装与配置拉取镜像修改虚拟内存的大小启动SonarQube登录 SonarQ…...
【自动化运维】编写LNMP分布式剧本
目录 一 playbook编写LNMP1.1环境设置1.2编写Nginx剧本1.3、编写Mysql剧本1.4准备PHP剧本 一 playbook编写LNMP 1.1环境设置 ip服务192.168.243.100ansible192.168.243.102nginx192.168.243.103PHP192.168.243.104mysql 1.2编写Nginx剧本 1.编写Nginx源 mkdir -p /etc/ans…...
用Rust实现23种设计模式之单例
话不多说,上代码! 1. 使用Arc Mutex 在这个例子中,我们使用了 Arc (原子引用计数)和 Mutex (互斥锁)来实现线程安全的单例。通过 get_instance 方法,我们可以获取到单例实例&…...
小米平板6将推14英寸版!与MIX Fold 3同步推出
今天,知名数码博主数码闲聊站爆料消息,称小米平板6将推出一款Max版本,预计与小米MIX Fold 3同步推出。 据介绍,小米平板6 Max将是小米首款14英寸大屏的旗舰平板,平板搭载骁龙8处理器,在性能释放、影音表现、…...
webpack 的一点知识
多个入口共享多个模块 在使用webpack搭建多页面应用时候需要多个入口,这个时候需要考虑到模块共享问题了 可以使用entry.dependOn 来处理 entry: {home: {import: "./pages/home/index.js",// 其中vendors里边使用到模块,不会打入home对应的…...
Python 双目摄像机控制(windows + linux)
一、Windows 下载 Download libusb-win32-devel-filter-1.2.6.0.exe (libusb-win32) 安装,在弹出框中选择摄像机usb设备 pip install pyusb pip install libusb 代码如下,注意如果报错要以管理员权限运行: import cv2 import usb.corecam…...
mybatisplus实现自动填充 时间
mybatisplus实现自动填充功能——自动填充时间 数据库表中的字段 创建时间 (createTime)更新时间 (updateTime) 每次 增删改查的时候,需要通过对Entity的字段(createTime,updateTime)进行set设置,但是,每…...
P5732 【深基5.习7】杨辉三角
题目描述 给出 n ( n ≤ 20 ) n(n\le20) n(n≤20),输出杨辉三角的前 n n n 行。 如果你不知道什么是杨辉三角,可以观察样例找找规律。 输入格式 输出格式 样例 #1 样例输入 #1 6样例输出 #1 1 1 1 1 2 1 1 3 3 1 1 4 6 4 1 1 5 10 10 5 11.题目…...
ubuntu调整路由顺序
Ubuntu系统跳转路由顺序 1、安装ifmetric sudo apt install ifmetric2、查看路由 route -n3、把Iface下面的eth1调到第一位 sudo ifmetric eth1 0命令中eth1是网卡的名称,更改网卡eth1的跃点数(metric值)为0(数值越小…...
集成学习算法是什么?如何理解集成学习?
什么是集成学习? 集成学习通过建立几个模型来解决单一预测问题。它的工作原理是生成多个分类器/模型,各自独立地学习和作出预测。这些预测最后结合成组合预测,因此优于任何一个单分类的做出预测。 机器学习的两个核心任务 任务一࿱…...
npm配置最新淘宝镜像
目录 1、设置淘宝镜像2、查看源 1、设置淘宝镜像 默认的官方镜像:https://registry.npmjs.org,切换国内淘宝镜像,访问下载更快。 npm config set registry https://registry.npmmirror.com/其他,如pnpm: pnpm confi…...
网络基础1
文章目录 网络基础11. 计算机网络背景1.1 网路发展1.2 认识 "协议" 2. 网络协议初识2.1 协议分层2.2 OSI七层模型2.3 TCP/IP五层(或四层)模型协议栈与OS的关系 3. 网络传输基本流程3.1 同一个局域网两台主机通信3.2 同一个路由器的两个子网通信 4. 网络中的地址管理4…...
2023年电赛---运动目标控制与自动追踪系统(E题)发挥题思路
前言 (1)因为博客编辑字数超过1W字会导致MD编辑器非常卡顿。所以我将发挥题和基础题的思路拆开了。 (2)更新日记: <1>2023年8月4日,9点20分。分离发挥题思路和基础题思路,增加了博主Huiye…...
61 # http 数据处理
node 中的核心模块 http 可以快速的创建一个 web 服务 const http require("http"); const url require("url");// req > request 客户端的所有信息 // res > respone 可以给客户端写入数据 const server http.createServer();server.on("r…...
模版下载和Excel文件导入
模版下载 模版下载 模版下载 /*** 生成模版** param* return AppResponse*/public AppResponse ExcelFile() throws IOException {// 创建一个新的Excel工作簿Workbook workbook new XSSFWorkbook();// 创建一个工作表Sheet sheet workbook.createSheet("页面拨测模板&…...
Datax 数据同步-使用总结(一)
1,实时同步? datax 通常做离线数据同步使用。 目前能想到的方案 利用 linux 的定时任务时间戳的方式做增量同步。 2,同步速度快不快? 单表同步速度还是挺快的 但是如果遇到复杂的 sql 查询,其同步效率,…...
代码随想录算法训练营第九天| 232.用栈实现队列,225.用队列实现栈
232. 用栈实现队列 232.用栈实现队列 这里用两个栈来模拟队列以此实现队列的先进先出, 注意点 1.dump()方法将stackin的元素移入到stackout的操作,是要将stackin的全部元素放到stackout中,所以用while循环判断 stackIn是否为空 2.新建栈…...
通过Wrangler CLI在worker中创建数据库和表
官方使用文档:Getting started Cloudflare D1 docs 创建数据库 在命令行中执行完成之后,会在本地和远程创建数据库: npx wranglerlatest d1 create prod-d1-tutorial 在cf中就可以看到数据库: 现在,您的Cloudfla…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...
Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
【HTML-16】深入理解HTML中的块元素与行内元素
HTML元素根据其显示特性可以分为两大类:块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...
