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

news2024/11/18 17:48:26

目录

一、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 1024

enum
{
    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 -> kernel
    bool 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 -> user
    int 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\n
 
class 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_map
        if (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;
                else
                    conn->_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);
        else
            conn->_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;
                else
                    break;
            }
        }
    }

    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\n

                send(_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.coloradmin.cn/o/836920.html

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

相关文章

java实现钉钉群机器人@机器人获取信息后,机器人回复(机器人接收消息)

1.需求 鉴于需要使用钉钉群机器人回复&#xff0c;人们提出的问题&#xff0c;需要识别提出的问题中的关键词&#xff0c;后端进行处理实现对应的业务逻辑 2.实现方式 用户群机器人&#xff0c;附带提出的问题&#xff0c;后端接收消息后识别消息内容&#xff0c;读取到关键…

G-channel 实现低光图像增强

G-channel 之前研究低光图像增强时&#xff0c;看到一篇博客&#xff0c;里面介绍了一种方法&#xff0c;没有说明出处&#xff0c;也没有说明方法的名字&#xff0c;这里暂时叫做 G-channel 算法。 博客地址&#xff1a;低照度图像增强&#xff08;附步骤及源码&#xff09;…

VS2019/2022 开发CAD ObjectArx 2016

开发版本配置图 ObjectARX开发VC版本对照表R14~AutoCAD2024 下载地址 Visual Studio 2012/2013下载地址 CAD 版本Wizard和SDK安装包 ObjectARX官方下载地址(不定期更新) 序言 写这篇文章是想记录一下我的配置历程&#xff0c;因为我的电脑C盘空间不足以安装太多的CAD版本和…

详解AMQP协议

目录 1.概述 1.1.简介 1.2.抽象模型 2.spring中的amqp 2.1.spring amqp 2.2.spring boot amqp 1.概述 1.1.简介 AMQP&#xff0c;Advanced Message Queuing Protocol&#xff0c;高级消息队列协议。 百度百科上的介绍&#xff1a; 一个提供统一消息服务的应用层标准高…

爬虫---练习源码

选取的是网上对一些球员的评价&#xff0c;来评选谁更加伟大一点 import csv import requests import re import timedef main(page):url fhttps://tieba.baidu.com/p/7882177660?pn{page}headers {User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/53…

Python:判断一个数是否为质数或者输出100以内的质数

质数 质数又称素数。一个大于1的自然数&#xff0c;除了1和它自身外&#xff0c;不能被其他自然数整除的数叫做质数。 如&#xff1a;2&#xff0c;3&#xff0c;5&#xff0c;7。。。 判断一个数是否为质数 range(2, n)&#xff1a;范围在2~n&#xff08;不包括n&#xff09;之…

Mock.js的基本使用方法

官网网址&#xff1a;Mock.js (mockjs.com) 当前端工程师需要独立于后端并行开发时&#xff0c;后端接口还没有完成&#xff0c;那么前端怎么获取数据&#xff1f; 这时可以考虑前端搭建web server自己模拟假数据&#xff0c;这里我们选第三方库mockjs用来生成随机数据&#xf…

Java阶段五Day16

Java阶段五Day16 文章目录 Java阶段五Day16问题解析启动servlet冲突问题nacos注册中心用户信息验证失败前端效果不对前端请求到后台服务的流转过程 远程dubbo调用业务需求dubbo配置xml配置domain层代码 补充远程调用 师傅详情接口抽象开发WorkderServerControllerWorkerServerS…

一百四十二、Linux——查看Linux服务器架构的版本类型

一、目的 查看已经安装好的Linux服务器架构的版本类型&#xff0c;看服务器版本是32位还是64位 而且可以区分出是kettle的文件x86或x86_64&#xff0c;x86是32位&#xff0c;而x86_64是64位 注意&#xff1a; 32位的查询结果为i386、i686 64位的查询结果为x86_64 二、Linu…

idea配置docker部署

安装docker插件 setting -> plugins 配置docker远程连接 参考&#xff1a;docker配置远程连接端口 https://blog.csdn.net/jinying_51eqhappy/article/details/132103423?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%221…

探索CSS计数器:优雅管理网页元素的计数与序号

113. 探索CSS计数器&#xff1a;优雅管理网页元素的计数与序号 在前端开发中&#xff0c;我们经常需要对网页元素进行计数与序号&#xff0c;如有序列表、表格行号、步骤指示等。为了优雅地管理这些计数与序号&#xff0c;CSS提供了一种强大的功能&#xff1a;CSS计数器&#…

PHP实现首字母头像

<?php $name"哈哈"; $logoletter_avatar($name);echo <img src".$logo." style" border-radius: 50%;">;function letter_avatar($text) {$total unpack(L, hash(adler32, $text, true))[1];$hue $total % 360;list($r, $g, $b) hs…

【2023】Git版本控制-远程仓库详解

目录 创建远程仓库向远程仓库推送数据文件从第二台主机本地拉取远程仓库数据第一台主机同步远程仓库数据tag标签git忽略文件 Git远程仓库是Git版本控制系统的一个概念&#xff0c;它是一个存储Git代码的远程服务器。 你可以将本地Git仓库上传到远程仓库&#xff0c;以便与其他…

WSL1升级为WSL2

首先需要启用组件 使用管理员打开Powershell并运行 Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform启用后会要求重启计算机 从https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi获取WSL2 Linux内核更新包&#xff0c;…

实力肯定!Coremail入选中国信通院“铸基计划”2023全景图

近日&#xff0c;由中国信息通信研究院&#xff08;以下简称“中国信通院”&#xff09;主办的“2023数字生态发展大会”暨中国信通院“铸基计划”年中会议在京召开。 会上发布了《高质量数字化转型产品及服务全景图&#xff08;2023&#xff09;》&#xff0c;Coremail凭借着优…

【设计模式——学习笔记】23种设计模式——观察者模式Observer(原理讲解+应用场景介绍+案例介绍+Java代码实现)

文章目录 案例引入原始方案实现实现问题分析 介绍基础介绍登场角色 案例实现案例一类图实现分析 案例二类图实现 观察者模式在JDK源码的应用总结文章说明 案例引入 有一个天气预报项目&#xff0c;需求如下&#xff1a; 气象站可以将每天测量到的温度、湿度、气压等等以公告的…

修改状态栏The application could not be installed: INSTALL_FAILED_ABORTEDList

打开theme修改状态栏为可见。 <resources xmlns:tools"http://schemas.android.com/tools"><!-- Base application theme. --><style name"Base.Theme.MyApplication" parent"Theme.AppCompat.DayNight"><!-- Customize yo…

从入门到精通:Postman调试微信支付接口的绝佳方法

前期准备 在使用 Postman 调试微信支付接口之前&#xff0c;你需要做好以下准备&#xff1a; 安装 Postman 客户端应用&#xff0c;或使用网页版&#xff1b;成为 微信支付商户&#xff1b;已申请 商户API私钥。 当你已经具备这三个条件&#xff0c;就可以进入微信支付接口调…

【C语言学习】整数范围、整数越界、无符号数

1.整数范围 对于一个字节&#xff08;8位&#xff09;&#xff0c;可以表达的范围是00000000 ~ 11111111 其中00000000 ——> 0 11111111 ~ 10000000 ——> -1 ~ -128&#xff08;从大到小&#xff09; 00000001 ~ 01111111 ——> 1~127&#xff08;从小到大&#xff…

Yolov8新版本解读(二):优化点如何加入新版本,通过加入轻量级网络Ghostnetv2对比说明

本文目的: 最近yolov8进行了一次较大的更新,对一些优化点加在哪个位置上有些变动,因此本文主要通过具体案列进行对比和说明,以便在新版本上能够轻松上手。 老版本 ultralytics/nn 新版本更新为: modules文件夹下内容如下: 解读: 将modules.py拆分为 1.__init__.…