多路转接(下)——Reator模式

目录

一、实现一个Reator模式的服务器

1.一些准备工作

(1)下面的头文件可直接使用

(2)一些头文件需要进行一些封装才能更适合当前代码的使用。

(3)下面的文件就需要我们自己实现了

2.初步编写服务器代码

(1)构建服务器类

(2)对initserver进行初步编写

3.新建Connection类

(1)构建该类的原因

(2)管理相关类变量

(3)重新编写initserver

4.任务分发

(1)TcpServer添加成员变量

(2)实现dispatcher和loop函数

(3)实现server.cc并进行试运行

5.实现各个执行函数

6.增加序列化与反序列化代码

7.运行

二、总代码

1.服务器

2.客户端

三、Reator模式


一、实现一个Reator模式的服务器

1.一些准备工作

在我们编写服务器前需要定义一些预备的头文件用于封装各种操作。

(1)下面的头文件可直接使用

err.hpp

#pragma once

#include<iostream>

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR,
    EPOLL_CREATE_ERROR
};

log.hpp

注意一下,下面使用logmessage打印的日志等级为DEBUG,我们都可以注释掉。

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>

//一个文件用于保存正常运行的日志,一个保存错误日志
#define LOG_FILE "./log.txt"
#define  ERROR_FILE "./error.txt"

//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

//将运行等级转换为字符串
const char* to_string(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;
    }
}

//将固定格式的日志输出到屏幕和文件中
//第一个参数是等级,第二个参数是需要输出的字符串
void logmessage(int level, const char* format, ...)
{
    //输出到屏幕
    char logprefix[1024];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串
    
    char logcontent[1024];
    va_list arg;//可变参数列表
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    std::cout << logprefix << logcontent << std::endl;
    //输出到文件中
    //打开两个文件
    FILE* log = fopen(LOG_FILE, "a");
    FILE* err = fopen(ERROR_FILE, "a");
    if(log != nullptr && err != nullptr)
    {
        FILE* cur = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING)
            cur = log;
        if(level == ERROR || level == FATAL)
            cur = err;
        if(cur)
            fprintf(cur, "%s%s\n", logprefix, logcontent);
        fclose(log);
        fclose(err);
    }
}

(2)一些头文件需要进行一些封装才能更适合当前代码的使用。

sock.hpp

//对原Sock进行修改,使监听套接字只维护在该类中,并增加一些配套成员函数
#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"
#include"err.hpp"

class Sock
{
private:
    static const int backlog = 32;//全链接队列长度为32
    static const int default_sock = -1;//初始化的默认套接字
public:
    Sock()
        :_listensock(default_sock)
    {}

    ~Sock()
    {
        if(_listensock != default_sock)
            close(_listensock);
    }

    void Socket()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
        if(_listensock < 0)//创建套接字失败打印错误原因
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到

        //打开端口复用保证程序退出后可以立即正常启动
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    void Bind(int port)
    {
        struct sockaddr_in local;//储存本地网络信息
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
        
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
        {
            logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
            exit(BIND_ERROR);//退出
        }
        logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到
    }

    void Listen()
    {
        //listen设置socket为监听模式
        if(listen(_listensock, backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        logmessage(NORMAL, "listen socket success");
    }

    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;//储存本地网络信息
        socklen_t len = sizeof(peer);
        int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
        
        if(sock < 0)
        {
            logmessage(ERROR, "accept fail");//接收新文件描述符失败
        }
        else
        {
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }

    //返回监听套接字的文件描述符
    int FD()
    {
        return _listensock;
    }

    //关闭监听套接字
    int Close()
    {
        if(_listensock != default_sock)
            close(_listensock);
    }
private:
    int _listensock;
};

util.hpp

//将原来写过的将套接字设为非阻塞的代码封装在Util类中
#pragma once
#include<iostream>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>

class Util
{
public:
    //将文件描述符设为非阻塞
    static bool SetNonBlock(int fd)
    {
        int fl = fcntl(fd, F_GETFL);//获取文件描述符的标志,该标志是一个位图结构
        if(fl < 0)//获取失败
        {
            std::cerr << "fctnl:" << strerror(errno) << std::endl;//打印错误码
            return false;
        }
        else
        {
            fcntl(fd, F_SETFL, fl | O_NONBLOCK);//将该文件描述符设为非阻塞
            return true;
        }
    }
};

(3)下面的文件就需要我们自己实现了

epoller.hpp

这个文件定义了Epoller类,用于封装一些我们需要的epoll操作。(我们只实现了前四个函数,剩下的会在以后逐渐补齐)

//epoll的处理逻辑全部保存在该文件中
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/epoll.h>
#include"err.hpp"
#include"log.hpp"

const static int default_epfd = -1;
const static int size = 128;

class Epoller
{
public:
    Epoller()
        :_epfd(default_epfd)
    {}

    ~Epoller()
    {
        if(_epfd != default_epfd)
            close(_epfd);
    }

    //创建Epoll模型
    void Create()
    {
        _epfd = epoll_create(size);
        if(_epfd < 0)
        {
            //创建失败
            logmessage(FATAL, "epoll_create error, error code: %d, error string: %s", errno, strerror(errno));
            exit(EPOLL_CREATE_ERROR);
        }
    }

    //向Epoll模型中增加需要关心的事件
    bool AddEvent(int sock, uint32_t events)
    {
        //构建一个epoll_event结构体
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;

        //向epoll模型中插入该节点
        int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
        //返回值为0表示插入成功,否则失败
        if(ret == 0)
            return true;
        else
            return false;
    }

    //查看已经就绪的文件描述符
    int Wait(struct epoll_event revs[], int num, int timeout)
    {
        int ret = epoll_wait(_epfd, revs, num, timeout);
        return ret;
    }

    //对文件描述符做一定操作,我们只封装修改和删除结构体的代码
    int Control(int sock, uint32_t events, int handle)
    {
        int ret = 0;
        //用户要修改数据
        if(handle == EPOLL_CTL_MOD)
        {
            //构建新的结构体覆盖旧的
            struct epoll_event ev;
            ev.events = events;
            ev.data.fd = sock;
            ret = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
        }
        //用户要删除数据
        else if(handle == EPOLL_CTL_DEL)
        {
            ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
        }
        //出错了
        else
            ret = -1;

        return ret;
    }

    void Close()
    {
        if(_epfd != default_epfd)
            close(_epfd);
    }
private:
    int _epfd;//epoll模型的文件描述符
};

2.初步编写服务器代码

(1)构建服务器类

对于整个服务器而言,我们将服务器的主要运行代码定义在tcpserver.hpp中。server.cc做为源文件,err.hpp、util.hpp、log.hpp、epoller.hpp、sock.hpp为各种封装组件。

首先,需要定义一个TcpServer类,新增initserver、start等函数。成员变量包括管理套接字操作的Sock变量,管理epoll操作的Epoller变量,还有一个端口号。

#pragma once
#include<iostream>
#include<poll.h>
#include<string>
#include<functional>
#include"log.hpp"
#include"Epoller.hpp"
#include"err.hpp"
#include"sock.hpp"
#include"util.hpp"

namespace tcpserver
{
    static const int default_port = 8080;//默认端口号为8080
    static const int num = 128;//设置元素数为2048,当然poll可监视的文件描述符无上限,也可以设计令其动态增长
    static const int default_value = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素

    class TcpServer
    {
    public:
        //构造函数
        TcpServer(int port = default_port)
            :_port(port)
        {}

        //析构函数
        ~TcpServer()
        {}

        void initserver()
        {}

        void start()
        {
            while(1)
            {}
        }
    private:
        uint16_t _port;//进程的端口号
        Sock _sock;//维护套接字的类
        Epoller _epoller;//封装各种epoll接口的类
    };
}

(2)对initserver进行初步编写

initserver的执行流程如下:

创建listen套接字、创建epoll模型、将套接字文件描述符设为非阻塞、将listen套接字加入模型。

void initserver()
{
    //创建监听套接字
    _sock.Socket();
    _sock.Bind(_port);
    _sock.Listen();

    //创建epoll模型
    _epoller.Create();

    //加入按照ET模式工作的epoll模型的套接字必须是非阻塞的,所以还需要将listen套接字设为非阻塞
    Util::SetNonBlock(_sock.FD());
    //将listen套接字加入epoll模型,且模型按ET模式工作
    _epoller.AddEvent(_sock.FD(), EPOLLIN | EPOLLET);
}

3.新建Connection类

(1)构建该类的原因

我们在之前的普通epoll服务器中使用了下面的代码:

这个代码实际上是有问题的,由于TCP面向字节流的特点,所以程序员需要自己解决读取完整报文的问题。也就是,我们进行一次读取并不能保证读到一个完整报文。而且该缓冲区是在栈上的,每次读完都会被清空。

所以,我们需要对每个套接字都各维护一个读缓冲区、一个写缓冲区,这样才能保证数据能够将报文保留下来,以便后续操作。

最终,我们决定再定义一个Connection类用于维护所有的套接字。其中既包括缓冲区,还包括可以处理读事件、写时间、异常事件的成员函数。这样,我们就可以允许我们自己在TcpServer类中定义函数传递给相应的套接字,实现不同的处理方式。

其实严格来讲listen套接字并不需要缓冲区,但是为了统一接口,我们也将其作为一个普通链接看待。

(2)管理相关类变量

服务器中往往会有很多链接,每个链接都有这样一个Connection变量,而这些变量都需要维护,所以我们在tcpserver的成员变量内维护一个哈希表保存这些变量。

由于我们加入了Connection的新对象,所以initserver还需要增加管理这些变量的内容。

最后我们决定再定义一个能够处理所有新增的套接字的函数AddConnection,将这些代码统一封装在里面。

定义一个AddConnection将套接字数据加入数据结构中进行管理,定义四个函数用于执行不同时间的处理。

(3)重新编写initserver

重新编写初始化函数,而且后期还会增加成员变量,依旧需要修改。

4.任务分发

(1)TcpServer添加成员变量

TcpServer中需要增加一个_revs结构体数组和一个_num变量。

前面我们也定义了num。

initserver中也要增加初始化代码。

析构函数也可以编写了。

(2)实现dispatcher和loop函数

我们将原来的start函数重命名为dispatcher。

在循环内使用loop函数封装代码。

(3)实现server.cc并进行试运行

server.cc

执行代码,和之前基本没有变化。但随着我们实现的推进,还需要对其进行不断地微调修改。有了源文件,我们就能试运行该程序了。

#include"tcpserver.hpp"
#include"err.hpp"
#include<memory>

using namespace std;
using namespace tcpserver;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    unique_ptr<TcpServer> p(new TcpServer());
    p->initserver();
    p->dispatcher();
    return 0;
}

5.实现各个执行函数

实现各个处理函数,省略号表示上下还有代码。

class TcpServer
    {
    private:
        
        ......
        
        //Connection变量是否存在
        bool Connection_exist(int sock)
        {
            auto it = _connections.find(sock);
            if(it == _connections.end())
                return false;
            else
                return true;
        }

        

        //出现错误的处理函数
        void Excepter(Connection* con)
        {
            logmessage(DEBUG, "Excepter in");
            //出错了我们直接断开连接
            //将数据从epoll模型中删除
            _epoller.Control(con->_sock, 0, EPOLL_CTL_DEL);
            //将该链接的Connection变量删除
            _connections.erase(con->_sock);
            //将该链接的文件描述符关闭
            con->Close();
            //删该除节点
            delete con;
            logmessage(DEBUG, "Excepter out");
        }

        //接收数据的处理函数
        void Recver(Connection* con)
        {
            logmessage(DEBUG, "Recver in");
            char buffer[1024];//定义缓冲区
            while(1)//由于ET模式只有一次提醒机会,所以我们需要不断读取,直到无数据可读为止
            {
                ssize_t n = recv(con->_sock, buffer, sizeof(buffer)-1, 0);
                if(n > 0)//读到了数据
                {
                    buffer[n] = 0;//在末尾加上/0
                    con->_inbuffer += buffer;//追加到该链接的缓冲区
                    logmessage(NORMAL, "client# %s", buffer);
                    _func(con);//处理该链接的数据
                }
                else if (n == 0)//无数据可读
                {
                    if(con->_excepter)
                    {
                        con->_excepter(con);
                        break;
                    }
                }
                else//出错
                {
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;//没有数据就绪了,直接跳出循环
                    else if(errno == EINTR)
                        continue;//正在进行信号处理,继续读取等待处理完成即可
                    else//出错了,让Excepter处理错误,然后跳出循环
                    {
                        if(con->_excepter)
                        {
                            con->_excepter(con);
                            break;
                        }
                    }
                }
            }
            logmessage(DEBUG, "Recver out");
        }

        //发送数据的处理函数
        void Sender(Connection* con)
        {
            logmessage(DEBUG, "Sender in");
            while(1)//将数据循环全部发送
            {
                int n = send(con->_sock, con->_outbuffer.c_str()
                    , con->_outbuffer.size(), 0);//先发送数据
                if(n > 0)//此次发送数据成功
                {
                    con->_outbuffer.erase(0, n);//删除缓冲区中已发送的数据
                    if(con->_outbuffer.empty())
                    {
                        //此次发送就已经将所有数据发完了
                        break;
                    }
                    //还有数据,继续循环发送
                }
                else//此次发送数据失败
                {
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;//没有数据就绪了,直接跳出循环
                    else if(errno == EINTR)
                        continue;//正在进行信号处理,继续读取等待处理完成即可
                    else//出错了,让Excepter处理错误,然后跳出循环
                    {
                        if(con->_excepter)
                        {
                            con->_excepter(con);
                            break;
                        }
                    }
                }
            }

            //如果数据没发完,需要让epoll对该描述符的写事件进行关心。
            //如果发完了,要关闭对写事件的关心
            if (!con->_outbuffer.empty())
                con->_pts->EnableReadWrite(con, true, true);
            else
                con->_pts->EnableReadWrite(con, true, false);
            logmessage(DEBUG, "Sender out");
        }

        //接收链接的处理函数,只用于listen套接字的读事件处理
        void Accepter(Connection* con)
        {
            logmessage(DEBUG, "Accepter in");
            //循环接收多个链接,直到没有链接接收时退出
            while(1)
            {
                std::string clientip;
                uint16_t clientport;
                int err = 0;
                //epoll负责等,accept直接接收,err负责将本次调用的错误码从函数中带出
                int sock = _sock.Accept(&clientip, &clientport, &err);
                if(sock > 0)//接收成功
                {
                    //将链接管理起来
                    AddConnection(sock, EPOLLIN | EPOLLET, 
                        std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                        std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                        std::bind(&TcpServer::Excepter, this, std::placeholders::_1));
                    //打印成功信息
                    logmessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
                }
                else//接收失败
                {
                    if(err == EAGAIN || err == EWOULDBLOCK)
                        break;//没有数据就绪了,直接跳出循环
                    else if(err == EINTR)
                        continue;//正在进行信号处理,继续读取等待处理完成即可
                    else//出错了,跳出执行
                        break;
                }
            }
            logmessage(DEBUG, "Accepter out");
        }
        
        .......
};

6.增加序列化与反序列化代码

我们直接使用之前写的网络计算器代码即可。

protocol.hpp

#pragma once
//#define MYSELF
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
    //按"content_len"\r\n"content"\r\n拼接字符串
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;
    return send_string;
}

//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
    //查找左侧的\r\n
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;//没找到表明数据有问题
    //截取正文长度的字符串并转为整形
    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    //截取正文放入储存正文的string里
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

class Request
{
public:
    Request()
        :_a(0)
        ,_b(0)
        ,_op(0)
    {}

    Request(int a, int b, char op)
        :_a(a)
        ,_b(b)
        ,_op(op)
    {}
    
    //Request序列化
    //Request结构体转化为字符串"a op b"
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        out->clear();//清空
        
        //将变量转为字符串
        std::string a_string = std::to_string(_a);
        std::string b_string = std::to_string(_b);

        *out = a_string;
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += b_string;
#else
        Json::Value root;
        root["first"] = _a;
        root["second"] = _b;
        root["oper"] = _op;

        Json::FastWriter writer;
        // Json::StyledWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    //Request反序列化
    //字符串"a op b"转化为Request结构体
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        auto left = in.find(SEP);//查找左侧的SEP
        auto right = in.rfind(SEP);//查找右侧的SEP
        if (left == std::string::npos || right == std::string::npos)
            return false;//找不到,数据有问题
        if (left == right)
            return false;//只有一个SEP,数据有问题
        if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
            return false;//指向的不是一个位置,数据有问题

        //按左闭右开的方式构造两个数字
        std::string a_string = in.substr(0, left);
        std::string b_string = in.substr(right + SEP_LEN);

        //读取到的数字不能为空
        if (a_string.empty() || b_string.empty())
            return false;

        //填入数据
        _a = std::stoi(a_string);
        _b = std::stoi(b_string);
        _op = in[left + SEP_LEN];
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        _a = root["first"].asInt();
        _b = root["second"].asInt();
        _op = root["oper"].asInt();
#endif
        return true;
    }

public:
    int _a;
    int _b;
    char _op;
};

class Response
{
public:
    Response()
        :_exitcode(0)
        ,_result(0)
    {}
    
    Response(int exitcode, int result)
        :_exitcode(exitcode)
        ,_result(result)
    {}

    //Response序列化
    //Response结构体转化为字符串"exitcode result"
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        out->clear();//清空

        //将变量转为字符串
        std::string ec_string = std::to_string(_exitcode);
        std::string res_string = std::to_string(_result);

        //拼接字符串
        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = _exitcode;
        root["result"] = _result;

        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    //Response反序列化
    //字符串"exitcode result"转化为Response结构体
    bool deserialize(const std::string &in)
    {
    #ifdef MYSELF
        auto mid = in.find(SEP);//查找中间的SEP
        if (mid == std::string::npos)
            return false;//找不到,出错

        //按左闭右开的方式构造两个数字
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        //读取到的退出码和计算结果不能为空
        if (ec_string.empty() || res_string.empty())
            return false;

        //填入数据
        _exitcode = std::stoi(ec_string);
        _result = std::stoi(res_string);
    #else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);
        
        _exitcode = root["exitcode"].asInt();
        _result = root["result"].asInt();
    #endif
        return true;
    }

public:
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
    char buffer[1024];//接收网络数据的字符串
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
        if (n > 0)//读到了数据
        {
            buffer[n] = 0;
            inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
            auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
            if (pos == std::string::npos)
                //如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
                continue;//接着到最开始读数据直到读全
            
            //执行至此时,报文"content_len"\r\n这部分一定读全了
            //可以将"content_len"的部分取出来并转化为整形
            std::string text_len_string = inbuffer.substr(0, pos);
            int text_len = std::stoi(text_len_string);

            //我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
            //一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
            //所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
            if (inbuffer.size() < total_len)
            {
                std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
                continue;//报文没读全继续到上面读取
            }

            //此时缓冲区中就至少有一个完整的报文
            *text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
            inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
            break;//准备处理报文,跳出循环
        }
        else//没读到数据
            return false;
    }
    return true;
}

bool divonepackage(std::string &inbuffer, std::string *text)
{
    *text = "";
    // 分析处理
    auto pos = inbuffer.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    std::string text_len_string = inbuffer.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;

    if (inbuffer.size() < total_len)
        return false;

    // 至少有一个完整的报文
    *text = inbuffer.substr(0, total_len);
    inbuffer.erase(0, total_len);
    return true;
}

server.cc

#include"tcpserver.hpp"
#include"protocol.hpp"
#include<memory>

using namespace std;
using namespace tcpserver;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}

//测试函数
// void send_back(Connection* con)
// {
//     con->_outbuffer += con->_inbuffer;
//     con->_inbuffer.erase(con->_inbuffer.begin(), con->_inbuffer.end());
//     if (con->_sender)
//         con->_sender(con);
// }

//计算器函数
const string ops = "+-*/%";
bool calculate(const Request &req, Response &resp)
{
    // req已经有结构化完成的数据啦,你可以直接使用
    resp._exitcode = OK;
    resp._result = 0;
    switch (req._op)
    {
    case '+':
        resp._result = req._a + req._b;
        break;
    case '-':
        resp._result = req._a - req._b;
        break;
    case '*':
        resp._result = req._a * req._b;
        break;
    case '/':
    {
        if (req._b == 0)
            resp._exitcode = DIV_ZERO;
        else
            resp._result = req._a / req._b;
    }
    break;
    case '%':
    {
        if (req._b == 0)
            resp._exitcode = MOD_ZERO;
        else
            resp._result = req._a % req._b;
    }
    break;
    default:
        resp._exitcode = OP_ERROR;
        break;
    }
    return true;
}

//处理数据的函数
void handler(Connection* con)
{
    printf("handler in\n");
    string one_package;
    while(divonepackage(con->_inbuffer, &one_package))//读取完整报文
    {
        string text;
        //去掉报头
        if(!delength(one_package, &text))
            return;
            
        //正文反序列化构造request
        Request req;
        if(!req.deserialize(text))
            return;

        //处理request得到response
        Response resp;
        calculate(req, resp);
        //respone序列化得到正文
        string result;
        resp.serialize(&result);
        //增加报头
        con->_outbuffer += enlength(result);
        //打印结果
        cout << result << endl;
        //发送
        if (con->_sender)
            con->_sender(con);
    }
    printf("handler out\n");
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    unique_ptr<TcpServer> p(new TcpServer(handler));
    p->initserver();
    p->dispatcher();
    return 0;
}

7.运行

我们使用之前实现的网络计算器客户端作为客户端,连接服务器并实现计算处理。

二、总代码

1.服务器

epoller.hpp

//epoll的处理逻辑全部保存在该文件中
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/epoll.h>
#include"err.hpp"
#include"log.hpp"

const static int default_epfd = -1;
const static int size = 128;

class Epoller
{
public:
    Epoller()
        :_epfd(default_epfd)
    {}

    ~Epoller()
    {
        if(_epfd != default_epfd)
            close(_epfd);
    }

    //创建Epoll模型
    void Create()
    {
        _epfd = epoll_create(size);
        if(_epfd < 0)
        {
            //创建失败
            logmessage(FATAL, "epoll_create error, error code: %d, error string: %s", errno, strerror(errno));
            exit(EPOLL_CREATE_ERROR);
        }
    }

    //向Epoll模型中增加需要关心的事件
    bool AddEvent(int sock, uint32_t events)
    {
        //构建一个epoll_event结构体
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = sock;

        //向epoll模型中插入该节点
        int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock, &ev);
        //返回值为0表示插入成功,否则失败
        if(ret == 0)
            return true;
        else
            return false;
    }

    //查看已经就绪的文件描述符
    int Wait(struct epoll_event revs[], int num, int timeout)
    {
        int ret = epoll_wait(_epfd, revs, num, timeout);
        return ret;
    }

    //对文件描述符做一定操作,我们只封装修改和删除结构体的代码
    int Control(int sock, uint32_t events, int handle)
    {
        int ret = 0;
        //用户要修改数据
        if(handle == EPOLL_CTL_MOD)
        {
            //构建新的结构体覆盖旧的
            struct epoll_event ev;
            ev.events = events;
            ev.data.fd = sock;
            ret = epoll_ctl(_epfd, EPOLL_CTL_MOD, sock, &ev);
        }
        //用户要删除数据
        else if(handle == EPOLL_CTL_DEL)
        {
            ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
        }
        //出错了
        else
            ret = -1;

        return ret;
    }

    void Close()
    {
        if(_epfd != default_epfd)
            close(_epfd);
    }
private:
    int _epfd;//epoll模型的文件描述符
};

err.hpp

#pragma once

#include<iostream>

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR,
    EPOLL_CREATE_ERROR
};

log.hpp

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>

//一个文件用于保存正常运行的日志,一个保存错误日志
#define LOG_FILE "./log.txt"
#define  ERROR_FILE "./error.txt"

//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

//将运行等级转换为字符串
const char* to_string(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;
    }
}

//将固定格式的日志输出到屏幕和文件中
//第一个参数是等级,第二个参数是需要输出的字符串
void logmessage(int level, const char* format, ...)
{
    //输出到屏幕
    char logprefix[1024];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串
    
    char logcontent[1024];
    va_list arg;//可变参数列表
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    std::cout << logprefix << logcontent << std::endl;
    //输出到文件中
    //打开两个文件
    FILE* log = fopen(LOG_FILE, "a");
    FILE* err = fopen(ERROR_FILE, "a");
    if(log != nullptr && err != nullptr)
    {
        FILE* cur = nullptr;
        if(level == DEBUG || level == NORMAL || level == WARNING)
            cur = log;
        if(level == ERROR || level == FATAL)
            cur = err;
        if(cur)
            fprintf(cur, "%s%s\n", logprefix, logcontent);
        fclose(log);
        fclose(err);
    }
}

protocol.hpp

#pragma once
//#define MYSELF
#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>

#define SEP " "
#define SEP_LEN strlen(SEP) //使用sizeof()多了一个\0
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) //使用sizeof()多了一个\0

enum
{
    OK = 0,
    DIV_ZERO,
    MOD_ZERO,
    OP_ERROR
};

//"content" 转化为 "content_len"\r\n"content"\r\n
//加上报头
std::string enlength(const std::string &text)
{
    //按"content_len"\r\n"content"\r\n拼接字符串
    std::string send_string = std::to_string(text.size());
    send_string += LINE_SEP;
    send_string += text;
    send_string += LINE_SEP;
    return send_string;
}

//"content_len"\r\n"content"\r\n 转化为 "content"
//去掉报头
bool delength(const std::string &package, std::string *text)
{
    //查找左侧的\r\n
    auto pos = package.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;//没找到表明数据有问题
    //截取正文长度的字符串并转为整形
    std::string text_len_string = package.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    //截取正文放入储存正文的string里
    *text = package.substr(pos + LINE_SEP_LEN, text_len);
    return true;
}

class Request
{
public:
    Request()
        :_a(0)
        ,_b(0)
        ,_op(0)
    {}

    Request(int a, int b, char op)
        :_a(a)
        ,_b(b)
        ,_op(op)
    {}
    
    //Request序列化
    //Request结构体转化为字符串"a op b"
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        out->clear();//清空
        
        //将变量转为字符串
        std::string a_string = std::to_string(_a);
        std::string b_string = std::to_string(_b);

        *out = a_string;
        *out += SEP;
        *out += _op;
        *out += SEP;
        *out += b_string;
#else
        Json::Value root;
        root["first"] = _a;
        root["second"] = _b;
        root["oper"] = _op;

        Json::FastWriter writer;
        // Json::StyledWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    //Request反序列化
    //字符串"a op b"转化为Request结构体
    bool deserialize(const std::string &in)
    {
#ifdef MYSELF
        auto left = in.find(SEP);//查找左侧的SEP
        auto right = in.rfind(SEP);//查找右侧的SEP
        if (left == std::string::npos || right == std::string::npos)
            return false;//找不到,数据有问题
        if (left == right)
            return false;//只有一个SEP,数据有问题
        if ((right - 1) != (left + SEP_LEN))//在字符串"a op b"中,right - 1和left + SEP_LEN都指向op
            return false;//指向的不是一个位置,数据有问题

        //按左闭右开的方式构造两个数字
        std::string a_string = in.substr(0, left);
        std::string b_string = in.substr(right + SEP_LEN);

        //读取到的数字不能为空
        if (a_string.empty() || b_string.empty())
            return false;

        //填入数据
        _a = std::stoi(a_string);
        _b = std::stoi(b_string);
        _op = in[left + SEP_LEN];
#else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);

        _a = root["first"].asInt();
        _b = root["second"].asInt();
        _op = root["oper"].asInt();
#endif
        return true;
    }

public:
    int _a;
    int _b;
    char _op;
};

class Response
{
public:
    Response()
        :_exitcode(0)
        ,_result(0)
    {}
    
    Response(int exitcode, int result)
        :_exitcode(exitcode)
        ,_result(result)
    {}

    //Response序列化
    //Response结构体转化为字符串"exitcode result"
    bool serialize(std::string *out)
    {
#ifdef MYSELF
        out->clear();//清空

        //将变量转为字符串
        std::string ec_string = std::to_string(_exitcode);
        std::string res_string = std::to_string(_result);

        //拼接字符串
        *out = ec_string;
        *out += SEP;
        *out += res_string;
#else
        Json::Value root;
        root["exitcode"] = _exitcode;
        root["result"] = _result;

        Json::FastWriter writer;
        *out = writer.write(root);
#endif
        return true;
    }

    //Response反序列化
    //字符串"exitcode result"转化为Response结构体
    bool deserialize(const std::string &in)
    {
    #ifdef MYSELF
        auto mid = in.find(SEP);//查找中间的SEP
        if (mid == std::string::npos)
            return false;//找不到,出错

        //按左闭右开的方式构造两个数字
        std::string ec_string = in.substr(0, mid);
        std::string res_string = in.substr(mid + SEP_LEN);

        //读取到的退出码和计算结果不能为空
        if (ec_string.empty() || res_string.empty())
            return false;

        //填入数据
        _exitcode = std::stoi(ec_string);
        _result = std::stoi(res_string);
    #else
        Json::Value root;
        Json::Reader reader;
        reader.parse(in, root);
        
        _exitcode = root["exitcode"].asInt();
        _result = root["result"].asInt();
    #endif
        return true;
    }

public:
    int _exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准
    int _result;   // 计算结果
};

//接收的数据:"content_len"\r\n"a op b"\r\n ......
bool recvpackage(int sock, std::string &inbuffer, std::string *text)
{
    char buffer[1024];//接收网络数据的字符串
    while (true)
    {
        ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0);//接收数据
        if (n > 0)//读到了数据
        {
            buffer[n] = 0;
            inbuffer += buffer;//以追加的方式放入缓冲区ibuffer
            auto pos = inbuffer.find(LINE_SEP);//查找LINE_SEP,就是\r\n
            if (pos == std::string::npos)
                //如果没有找到LINE_SEP,就说明报文"content_len"的部分可能没有读全
                continue;//接着到最开始读数据直到读全
            
            //执行至此时,报文"content_len"\r\n这部分一定读全了
            //可以将"content_len"的部分取出来并转化为整形
            std::string text_len_string = inbuffer.substr(0, pos);
            int text_len = std::stoi(text_len_string);

            //我们现在知道了报文中正文部分的长度,所以就能确定我们读到的是否是一个完整的报文
            //一个报文的组成为:text_len_string + "\r\n" + text + "\r\n"
            //所以,只要缓冲区ibuffer的内容多于一个报文的字符个数,我们就必定保证ibuffer中已经存在了一个完整的报头;
            int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;
            std::cout << "处理前#inbuffer: \n" << inbuffer << std::endl;
            if (inbuffer.size() < total_len)
            {
                std::cout << "输入的消息没有遵守协议,正在等待后续内容" << std::endl;
                continue;//报文没读全继续到上面读取
            }

            //此时缓冲区中就至少有一个完整的报文
            *text = inbuffer.substr(0, total_len);//将一个完整报文构建子串放入输出参数中
            inbuffer.erase(0, total_len);//删除缓冲区中的完整报文
            std::cout << "处理后#inbuffer:\n " << inbuffer << std::endl;
            break;//准备处理报文,跳出循环
        }
        else//没读到数据
            return false;
    }
    return true;
}

bool divonepackage(std::string &inbuffer, std::string *text)
{
    *text = "";
    // 分析处理
    auto pos = inbuffer.find(LINE_SEP);
    if (pos == std::string::npos)
        return false;

    std::string text_len_string = inbuffer.substr(0, pos);
    int text_len = std::stoi(text_len_string);
    int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;

    if (inbuffer.size() < total_len)
        return false;

    // 至少有一个完整的报文
    *text = inbuffer.substr(0, total_len);
    inbuffer.erase(0, total_len);
    return true;
}

sock.hpp

//对原Sock进行修改,使监听套接字只维护在该类中,并增加一些配套成员函数
#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"
#include"err.hpp"

class Sock
{
private:
    static const int backlog = 32;//全链接队列长度为32
    static const int default_sock = -1;//初始化的默认套接字
public:
    Sock()
        :_listensock(default_sock)
    {}

    ~Sock()
    {
        if(_listensock != default_sock)
            close(_listensock);
    }

    void Socket()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
        if(_listensock < 0)//创建套接字失败打印错误原因
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到

        //打开端口复用保证程序退出后可以立即正常启动
        int opt = 1;
        setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    void Bind(int port)
    {
        struct sockaddr_in local;//储存本地网络信息
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
        
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
        {
            logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
            exit(BIND_ERROR);//退出
        }
        logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到
    }

    void Listen()
    {
        //listen设置socket为监听模式
        if(listen(_listensock, backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        logmessage(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);
        printf("sock:%d", sock);
        *err = errno;
        if(sock > 0)
        {
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }

    //返回监听套接字的文件描述符
    int FD()
    {
        return _listensock;
    }

    //关闭监听套接字
    int Close()
    {
        if(_listensock != default_sock)
            close(_listensock);
    }
private:
    int _listensock;
};

tcpserver.hpp

#pragma once
#include<iostream>
#include<poll.h>
#include<string>
#include<unordered_map>
#include<functional>
#include<assert.h>
#include"log.hpp"
#include"epoller.hpp"
#include"err.hpp"
#include"sock.hpp"
#include"util.hpp"

namespace tcpserver
{
    class Connection;
    class TcpServer;//前置声明
    static const int default_port = 8080;//默认端口号为8080
    static const int num = 64;//设置元素数为64,当然epoll可监视的文件描述符无上限,也可以设计令其动态增长
    static const int default_value = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素

    using func_t = std::function<void (Connection*)>;

    class Connection
    {
    public:
        //由于在之前的数据读取中,我们只将缓冲区创建在栈上
        //而且TCP一次数据读取并不能保证能够读到一个完整报文
        //所以我们构建一个Connection类维护每一个链接,也包括缓冲区这些结构
        Connection(int sock, TcpServer *pts)
            :_sock(sock)
            ,_pts(pts)
        {}

        ~Connection()
        {}

        //注册处理方法
        void Register(func_t r, func_t s, func_t e)
        {
            _recver = r;
            _sender = s;
            _excepter = e;
        }
        
        //关闭套接字
        void Close()
        {
            close(_sock);
        }

        int _sock;//套接字
        std::string _inbuffer;//输入缓冲区
        std::string _outbuffer;//输出缓冲区

        func_t _recver;//接收任务的处理函数
        func_t _sender;//发送任务的处理函数
        func_t _excepter;//异常任务的处理函数

        TcpServer* _pts;//指向TcpServer类的指针,方便调用TcpServer的成员函数
    };

    class TcpServer
    {
    private:
        void AddConnection(int sock, uint32_t events, func_t recver, func_t sender, func_t excepter)
        {
            if(events & EPOLLET)//筛选需要关心读事件的描述符,实际上这个判断不写也可以
                Util::SetNonBlock(sock);//设为非阻塞
            Connection* con = new Connection(sock, this);//为链接新建Connection对象
            con->Register(recver, sender, excepter);//将该描述符可用的处理函数加入
            bool ret = _epoller.AddEvent(sock, events);//将文件描述符加入到epoll模型中,允许epoll关心该描述符
            assert(ret);
            _connections.insert(std::pair<int, Connection*>(sock, con));
            //_connections.insert(std::make_pair(sock, con));//将Connection对象插入哈希表管理
            logmessage(NORMAL, "sock number: %d is in epoll and unorder_map", sock);
        }

        //Connection变量是否存在
        bool Connection_exist(int sock)
        {
            auto it = _connections.find(sock);
            if(it == _connections.end())
                return false;
            else
                return true;
        }

        //封装分配任务的代码
        void loop(int timeout)
        {
            int n = _epoller.Wait(_revs, _num, timeout);
            for(int i = 0; i<n; ++i)//遍历_revs数组
            {
                int sock = _revs[i].data.fd;
                uint32_t events = _revs[i].events;

                //由于读写事件的处理函数中就已经能处理这些异常事件
                //所以我们将异常事件全部转化为读写类型的事件,等待读写处理
                if(events & EPOLLERR)//EPOLLERR表示该文件描述符出错
                    events |= (EPOLLIN | EPOLLOUT);
                if(events & EPOLLHUP)//EPOLLERR表示对方已经断开链接
                    events |= (EPOLLIN | EPOLLOUT);

                if(Connection_exist(sock))//该Connection数据得存在
                {
                    //由于Connection变量中都有他们相关的处理函数
                    //所以不同套接字的_recver,_sender,_excepter都可能不一样
                    //这也支持了不同套接字的统一处理
                    if((events & EPOLLIN) && _connections[sock]->_recver)//读事件且有相关函数
                        _connections[sock]->_recver(_connections[sock]);
                    if((events & EPOLLOUT) && _connections[sock]->_sender)//写事件且有相关函数
                        _connections[sock]->_sender(_connections[sock]);
                }
            }
        }

        //出现错误的处理函数
        void Excepter(Connection* con)
        {
            logmessage(DEBUG, "Excepter in");
            //出错了我们直接断开连接
            //将数据从epoll模型中删除
            _epoller.Control(con->_sock, 0, EPOLL_CTL_DEL);
            //将该链接的Connection变量删除
            _connections.erase(con->_sock);
            //将该链接的文件描述符关闭
            con->Close();
            //删该除节点
            delete con;
            logmessage(DEBUG, "Excepter out");
        }

        //接收数据的处理函数
        void Recver(Connection* con)
        {
            logmessage(DEBUG, "Recver in");
            char buffer[1024];//定义缓冲区
            while(1)//由于ET模式只有一次提醒机会,所以我们需要不断读取,直到无数据可读为止
            {
                ssize_t n = recv(con->_sock, buffer, sizeof(buffer)-1, 0);
                if(n > 0)//读到了数据
                {
                    buffer[n] = 0;//在末尾加上/0
                    con->_inbuffer += buffer;//追加到该链接的缓冲区
                    logmessage(NORMAL, "client# %s", buffer);
                    _func(con);//处理该链接的数据
                }
                else if (n == 0)//无数据可读
                {
                    if(con->_excepter)
                    {
                        con->_excepter(con);
                        break;
                    }
                }
                else//出错
                {
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;//没有数据就绪了,直接跳出循环
                    else if(errno == EINTR)
                        continue;//正在进行信号处理,继续读取等待处理完成即可
                    else//出错了,让Excepter处理错误,然后跳出循环
                    {
                        if(con->_excepter)
                        {
                            con->_excepter(con);
                            break;
                        }
                    }
                }
            }
            logmessage(DEBUG, "Recver out");
        }

        //发送数据的处理函数
        void Sender(Connection* con)
        {
            logmessage(DEBUG, "Sender in");
            while(1)//将数据循环全部发送
            {
                int n = send(con->_sock, con->_outbuffer.c_str()
                    , con->_outbuffer.size(), 0);//先发送数据
                if(n > 0)//此次发送数据成功
                {
                    con->_outbuffer.erase(0, n);//删除缓冲区中已发送的数据
                    if(con->_outbuffer.empty())
                    {
                        //此次发送就已经将所有数据发完了
                        break;
                    }
                    //还有数据,继续循环发送
                }
                else//此次发送数据失败
                {
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;//没有数据就绪了,直接跳出循环
                    else if(errno == EINTR)
                        continue;//正在进行信号处理,继续读取等待处理完成即可
                    else//出错了,让Excepter处理错误,然后跳出循环
                    {
                        if(con->_excepter)
                        {
                            con->_excepter(con);
                            break;
                        }
                    }
                }
            }

            //如果数据没发完,需要让epoll对该描述符的写事件进行关心。
            //如果发完了,要关闭对写事件的关心
            if (!con->_outbuffer.empty())
                con->_pts->EnableReadWrite(con, true, true);
            else
                con->_pts->EnableReadWrite(con, true, false);
            logmessage(DEBUG, "Sender out");
        }

        //接收链接的处理函数,只用于listen套接字的读事件处理
        void Accepter(Connection* con)
        {
            logmessage(DEBUG, "Accepter in");
            //循环接收多个链接,直到没有链接接收时退出
            while(1)
            {
                std::string clientip;
                uint16_t clientport;
                int err = 0;
                //epoll负责等,accept直接接收,err负责将本次调用的错误码从函数中带出
                int sock = _sock.Accept(&clientip, &clientport, &err);
                if(sock > 0)//接收成功
                {
                    //将链接管理起来
                    AddConnection(sock, EPOLLIN | EPOLLET, 
                        std::bind(&TcpServer::Recver, this, std::placeholders::_1),
                        std::bind(&TcpServer::Sender, this, std::placeholders::_1),
                        std::bind(&TcpServer::Excepter, this, std::placeholders::_1));
                    //打印成功信息
                    logmessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
                }
                else//接收失败
                {
                    if(err == EAGAIN || err == EWOULDBLOCK)
                        break;//没有数据就绪了,直接跳出循环
                    else if(err == EINTR)
                        continue;//正在进行信号处理,继续读取等待处理完成即可
                    else//出错了,跳出执行
                        break;
                }
            }
            logmessage(DEBUG, "Accepter out");
        }
    public:
        //构造函数
        TcpServer(func_t func, int port = default_port)
            :_func(func)
            ,_port(port)
            ,_revs(nullptr)
        {}

        //析构函数
        ~TcpServer()
        {
            _sock.Close();
            _epoller.Close();
            if(_revs)
                delete[] _revs; 
        }

        void EnableReadWrite(Connection *con, bool readable, bool writeable)
        {
            uint32_t event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;
            _epoller.Control(con->_sock, event, EPOLL_CTL_MOD);
        }

        void initserver()
        {
            //创建监听套接字
            _sock.Socket();
            _sock.Bind(_port);
            _sock.Listen();

            //创建epoll模型
            _epoller.Create();

            //将套接字加入epoll模型,同时也要为它新增一个Connection,并加入哈希表中
            //我们所有新增的描述符统一使用AddConnection加入数据结构进行管理
            AddConnection(_sock.FD(), EPOLLIN | EPOLLET, 
            std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
            //对于listen套接字,我们只需要处理接收链接的读事件,所以我们使用包装的Accepter函数

            //初始化数据
            _num = num;
            _revs = new struct epoll_event[_num];
        }

        //服务器开始运行不断通过Loop函数分发任务
        void dispatcher()
        {
            int timeout = 1000;//设置等待时间
            while(1)
            {
                loop(timeout);//这个分发任务的接口我们不想暴露出来
                //logmessage(DEBUG, "time out...");//超时打印信息
            }
        }
    private:
        uint16_t _port;//进程的端口号
        Sock _sock;//维护套接字的类
        Epoller _epoller;//封装各种epoll接口的类
        std::unordered_map<int, Connection*> _connections;
        //将所有Connection对象管理在哈希表中,key值为文件描述符,value值为其Connection对象的指针
        struct epoll_event* _revs;//epoll_wait返回的所有就绪的节点都放入_revs数组中
        int _num;//数组可接收的最大节点数目
        func_t _func;
    };
}

util.hpp

//将原来写过的将套接字设为非阻塞的代码封装在Util类中
#pragma once
#include<iostream>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>

class Util
{
public:
    //将文件描述符设为非阻塞
    static bool SetNonBlock(int fd)
    {
        int fl = fcntl(fd, F_GETFL);//获取文件描述符的标志,该标志是一个位图结构
        if(fl < 0)//获取失败
        {
            std::cerr << "fctnl:" << strerror(errno) << std::endl;//打印错误码
            return false;
        }
        else
        {
            fcntl(fd, F_SETFL, fl | O_NONBLOCK);//将该文件描述符设为非阻塞
            return true;
        }
    }
};

server.cc

#include"tcpserver.hpp"
#include"protocol.hpp"
#include<memory>

using namespace std;
using namespace tcpserver;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}

//测试函数
// void send_back(Connection* con)
// {
//     con->_outbuffer += con->_inbuffer;
//     con->_inbuffer.erase(con->_inbuffer.begin(), con->_inbuffer.end());
//     if (con->_sender)
//         con->_sender(con);
// }

//计算器函数
const string ops = "+-*/%";
bool calculate(const Request &req, Response &resp)
{
    // req已经有结构化完成的数据啦,你可以直接使用
    resp._exitcode = OK;
    resp._result = 0;
    switch (req._op)
    {
    case '+':
        resp._result = req._a + req._b;
        break;
    case '-':
        resp._result = req._a - req._b;
        break;
    case '*':
        resp._result = req._a * req._b;
        break;
    case '/':
    {
        if (req._b == 0)
            resp._exitcode = DIV_ZERO;
        else
            resp._result = req._a / req._b;
    }
    break;
    case '%':
    {
        if (req._b == 0)
            resp._exitcode = MOD_ZERO;
        else
            resp._result = req._a % req._b;
    }
    break;
    default:
        resp._exitcode = OP_ERROR;
        break;
    }
    return true;
}

//处理数据的函数
void handler(Connection* con)
{
    printf("handler in\n");
    string one_package;
    while(divonepackage(con->_inbuffer, &one_package))//读取完整报文
    {
        string text;
        //去掉报头
        if(!delength(one_package, &text))
            return;
            
        //正文反序列化构造request
        Request req;
        if(!req.deserialize(text))
            return;

        //处理request得到response
        Response resp;
        calculate(req, resp);
        //respone序列化得到正文
        string result;
        resp.serialize(&result);
        //增加报头
        con->_outbuffer += enlength(result);
        //打印结果
        cout << result << endl;
        //发送
        if (con->_sender)
            con->_sender(con);
    }
    printf("handler out\n");
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    unique_ptr<TcpServer> p(new TcpServer(handler));
    p->initserver();
    p->dispatcher();
    return 0;
}

makefile

tcpserver: server.cc
	g++ -o server server.cc -std=c++11 -ljsoncpp
.PHONY:clean
clean:
	rm -f server

2.客户端

client.hpp

#pragma once
#include<iostream>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<errno.h>
#include<string.h>
#include<strings.h>
#include<istream>
#include<stdlib.h>
#include<stdio.h>
#include<memory>
#include<ctype.h>
#include"log.hpp"
#include"protocol.hpp"

#define NUM 1024
enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    CONNECT_ERROR
};

Request ParseLine(const std::string &line);

class tcpclient
{
public:
	//构造函数
	tcpclient(const std::string& ip, const uint16_t& port)
		:_ip(ip)
        ,_port(port)
        ,_sock(-1)
	{}

    void initclient()
    {
        //创建套接字,创建失败打印错误原因
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock == -1)
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _sock);//创建套接字成功,打印让用户观察到
        //客户端不需要显式绑定,该工作交给操作系统完成
    }

    //启动客户端进程
    void run()
    {
        struct sockaddr_in local;
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//填充结构体

        //客户端连接服务器
        if(connect(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)//客户端与服务器进行连接
        {
            logmessage(FATAL, "connect error");//connect失败属于最严重的错误
        }
        else
        {
            std::string line;
            std::string inbuffer;
            while (true)
            {
                std::cout << "mycal>>> ";
                std::getline(std::cin, line);  // 1+1
                Request req = ParseLine(line); // "1+1"
                std::string content;
                req.serialize(&content);
                std::string send_string = enlength(content);
                std::cout << "sendstring:\n" << send_string << std::endl;
                send(_sock, send_string.c_str(), send_string.size(), 0); // bug?? 不管

                std::string package, text;
                //  "content_len"\r\n"exitcode result"\r\n
                if (!recvpackage(_sock, inbuffer, &package))
                    continue;
                if (!delength(package, &text))
                    continue;
                // "exitcode result"
                Response resp;
                resp.deserialize(text);
                std::cout << "exitCode: " << resp._exitcode << std::endl;
                std::cout << "result: " << resp._result << std::endl;
            }
        }
    }

    Request ParseLine(const std::string &line)
    {
        // 建议版本的状态机!
        //"1+1" "123*456" "12/0"
        int status = 0; // 0:操作符之前,1:碰到了操作符 2:操作符之后
        int i = 0;
        int cnt = line.size();
        std::string left, right;
        char op;
        while (i < cnt)
        {
            switch (status)
            {
            case 0:
            {
                if(!isdigit(line[i]))
                {
                    op = line[i];
                    status = 1;
                }
                else left.push_back(line[i++]);
            }
            break;
            case 1:
                i++;
                status = 2;
                break;
            case 2:
                right.push_back(line[i++]);
                break;
            }
        }
        std::cout << std::stoi(left)<<" " << std::stoi(right) << " " << op << std::endl;
        return Request(std::stoi(left), std::stoi(right), op);
    }
    
    //析构函数要释放不使用的文件描述符
    ~tcpclient()
    {
        if( _sock >= 0)
            close(_sock);
    }
private:
	int _sock;//套接字文件描述符
	std::string _ip;//服务器IP地址
	uint16_t _port;//服务器的端口号
};

client.cc 

#include"client.hpp"

using namespace std;

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s server_ip server_port\n\n", proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 3)//如果没输入端口号和目的IP,argc保存的命令参数就不是三个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[2]);
    string ip = argv[1];
    
    unique_ptr<tcpclient> p(new tcpclient(ip, port));

    p->initclient();
    p->run();

    return 0;
}

三、Reator模式

写了这么半天的代码,那到底什么是Reactor模式呢?

Reactor是一种服务器实现模式,它使用实践派发器和连接管理器实现服务器IO,是一种半同步半异步的IO服务器。

实际上我们此次实现的epoll服务器是一个不完全的Reactor服务器,epoll负责将数据放到Connection的_outbuffer中,再由其他函数处理数据。

但实际上的Reactor服务器只负责等待事件就绪,并对就绪的事件进行IO。而实际的数据处理会交给线程池或创建多进程由新进程处理。

它很类似与打地鼠游戏,有地鼠冒头就拿起锤子敲打,相当于有IO事件要处理就进行处理。

地鼠本身是怎么从下面来到这里的,我们不关心。同样,数据经过怎么样的处理,服务器也不关心,它只负责收发数据和异常处理。

总的来说,Reactor模式是目前服务器设计中最常用的模式,在实际开发中的使用非常多。还有一种设计模式叫做Proactor,它应用于少数特殊需求的服务器设计,有兴趣可以查找相关文章查询,我就不再详述了。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值