I/O多路复用:基于epoll实现Reactor高性能TCP服务器

目录

前言

一、I/O多路复用

1、select

(1)fd_set

(2)timeval

(3)原理

(4)特点

(5)缺点

(6)实现

[<1> common](#<1> common)

[<2> inetaddr](#<2> inetaddr)

[<3> mutex](#<3> mutex)

[<4> log](#<4> log)

[<5> socket](#<5> socket)

[<6> selectserver](#<6> selectserver)

[6.1 构造函数](#6.1 构造函数)

[6.2 printfd](#6.2 printfd)

[6.3 accepter](#6.3 accepter)

[6.4 recver](#6.4 recver)

[6.5 handlerevent](#6.5 handlerevent)

[6.6 start](#6.6 start)

[<7> main](#<7> main)

2、poll

(1)原理

(2)优点

(3)缺点

(4)实现

[<1> common](#<1> common)

[<2> inetaddr](#<2> inetaddr)

[<3> mutex](#<3> mutex)

[<4> log](#<4> log)

[<5> socket](#<5> socket)

[<6> pollserver](#<6> pollserver)

[6.1 构造函数](#6.1 构造函数)

[6.2 printfd](#6.2 printfd)

[6.3 accepter](#6.3 accepter)

[6.4 recver](#6.4 recver)

[6.5 handlerevent](#6.5 handlerevent)

[6.6 start](#6.6 start)

[<7> main](#<7> main)

3、epoll

(1)接口

[<1> epoll_create](#<1> epoll_create)

[<2> epoll_ctl](#<2> epoll_ctl)

[<3> epoll_wait](#<3> epoll_wait)

(2)原理

(3)优点

(4)工作方式

[<1> LT](#<1> LT)

[<2> ET](#<2> ET)

(5)应用

二、Reactor

三、Epoll+Reactor实现

1、common

2、inetaddr

3、mutex

4、log

5、socket

6、epoller

(1)构造函数

(2)modeventhelper

(3)addevent

(4)delevent

(5)modevent

(6)waitevent

(7)析构函数

7、protocol

(1)decode

[(2) excute](#(2) excute)

8、connection

9、Reactor

(1)构造函数

[(2) 查找连接](#(2) 查找连接)

(3)connectionexist

(4)connectionempty

[(5) looponce](#(5) looponce)

[(6) dispatcher](#(6) dispatcher)

[(7) printconnection](#(7) printconnection)

[(8) loop](#(8) loop)

[(9) addconnection](#(9) addconnection)

[(10) enablereadwrite](#(10) enablereadwrite)

[(11) delconnection](#(11) delconnection)

[(12) stop](#(12) stop)

10、channel

(1)构造函数

(2)recver

(3)sender

(4)excepter

(5)getsockfd

(6)inbuffer

(7)appendoutbuffer

11、listener

(1)构造函数

(2)recver

(3)getsockfd

12、main

结语


前言

Linux下的I/O多路复用技术是构建高并发网络服务器的核心,多路复用经历了从select/poll/epoll的发展演进。select和poll采取轮询机制,每次调用都需将文件描述符集合从用户态拷贝到内核态,时间复杂度为O(n),当连接数增大时性能将线性下降。epoll则解决了select/poll性能这方面的问题,epoll底层通过红黑树来管理已注册的文件描述符,使用就绪链表来存储活跃连接,通过回调机制实现O(1)的事件通知,避免了无效的文件描述符遍历。本文将围绕I/O多路复用展开介绍,并在I/O多路复用的基础上,基于Reactor设计模式,以epoll作为底层事件通知机制,实现一个高性能的TCP服务器。基于epoll的Reactor模式,使得I/O事件与业务处理分离,以事件循环驱动整个服务器,是Linux下高性能网络服务器实现的经典范式。

一、I/O多路复用

1、select

select用于实现多路复用输入/输出模型,select可以监视多个文件描述符的状态变化,调用select后,程序将停在select等待,直到被监视的文件描述符有一个或多个发生了状态改变。

nfds:需要监视的最大文件描述符值+1;

readfds、writefds、exceptfds分别为需要检测的可读文件描述符集合、可写文件描述符集合及异常文件描述符集合;

参数timeout为结构timeval,用来设置select的等待时间:

NULL:则表示select没有timeout,select将一直阻塞,直到某个文件描述符上发生了事件;

0:仅检测文件描述符集合的状态,然后立即返回,并不等待外部事件的发生;

特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

(1)fd_set

关于select参数类型fd_set的结构,fd_set为位图结构,通过使用位图中对应的位来表示要监视的文件描述符。通过fd_set的接口,可以比较方便的操作位图。

cpp 复制代码
void FD_CLR(int fd, fd_set *set); // ⽤来清除描述词组set中相关fd 的位
int  FD_ISSET(int fd, fd_set *set);// ⽤来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set);// ⽤来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set);// ⽤来清除描述词组set的全部位

(2)timeval

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

select返回值:

执行成功则返回文件描述符状态已改变的个数;

如果返回0代表在文件描述符状态改变前已超过timeout时间,没有返回;

当有错误发生时则返回-1。

(3)原理

理解select模型的关键在于理解fd_set,为方便说明,取fd_set为1字节,fd_set中的每一个bit对应一个文件描述符fd,则1字节的fd_set对应8个fd。

执行FD_ZERO:

cpp 复制代码
fd_set set;
FD_ZERO(&set);

则set位图表示是0000,0000;

若fd=5,执行FD_SET:

cpp 复制代码
FD_SET(fd,&set);

set第5位将置为1,则set变为0001,0000;

若再加入fd=2、fd=1,则set变为0001,0011;

cpp 复制代码
select(6,&set,0,0,0)

执行select阻塞等待;

若fd=1、fd=2上都发生可读事件,则select返回,此时set将变为0000,0011,没有事件发生的fd=5将被清空。

(4)特点

select可监控的文件描述符个数取决于sizeof(fd_set)的值,将fd加入select监控集的同时,还需使用一个array保存放到select监控集中的fd,array的作用可概括为两个方面:

一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。

二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入,扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(5)缺点

每次调用select,都需要手动设置fd集合,从接口使用角度来说不是很方便;

每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;

同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也会很大;

select支持的文件描述符数量太小。

(6)实现

下面实现一个基于select的TCP回显服务器,该服务器主要包括了网络通信、日志、线程同步、事件驱动等模块。

<1> common

common模块包含了服务器通用的头文件、错误码定义以及一个禁止拷贝的基类。

common.hpp

cpp 复制代码
#ifndef _COMMON_HPP_
#define _COMMON_HPP_
#include<iostream>
#include<functional>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
enum exitcode
{
    OK=0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    OPEN_ERR
};
class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy& )=delete;
    const nocopy& operator=(const nocopy& )=delete;
    ~nocopy(){}
};
#define CONV(addr) ((struct sockaddr*)&addr)
#endif

exitcode枚举类型定义了一组程序退出时的状态码,每个枚举值对应一种可能的错误场景,nocopy为不可拷贝基类,通过将拷贝构造、赋值重载函数删除来禁止派生类拷贝,便于后续文件描述符这种唯一资源的管理,CONV为地址转换宏,用于将sockaddr_in强制类型转化为sockaddr。

<2> inetaddr

inetaddr为网络地址封装模块,用于将底层的sockaddr_in结构体与IP字符串和端口号进行统一管理,为上层网络提供便利的地址操作接口。

inetaddr.hpp

cpp 复制代码
#ifndef _INETADDR_HPP_
#define _INETADDR_HPP_
#include"common.hpp"
using namespace std;
class inetaddr
{
public:
    inetaddr(){}
    inetaddr(struct sockaddr_in& addr)
    {
        setaddr(addr);
    }
    inetaddr(const string& ip,uint16_t port)
    :_ip(ip)
    ,_port(port)
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family=AF_INET;
        inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
        _addr.sin_port=htons(_port);
    }
    inetaddr(uint16_t port)
    :_port(port)
    ,_ip()
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family=AF_INET;
        _addr.sin_port=htons(_port);
        _addr.sin_addr.s_addr=INADDR_ANY;
    }
    void setaddr(struct sockaddr_in& addr)
    {
        _addr=addr;
        _port=ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(_addr));
        _ip=ipbuffer;
    }
    uint16_t port()
    {
        return _port;
    }
    string ip() 
    {
        return _ip;
    }
    const struct sockaddr_in& netaddr()
    {
        return _addr;
    }
    const struct sockaddr* netaddrptr()
    {
        return CONV(_addr);
    }
    socklen_t netaddrlen()
    {
        return sizeof(_addr);
    }
    bool operator==(const inetaddr& addr)
    {
        return _ip==addr._ip && _port==addr._port;
    }
    string StringAddr()
    {
        return _ip+":"+to_string(_port);
    }
    ~inetaddr()
    {}
private:
    struct sockaddr_in _addr;
    uint16_t _port;
    string _ip;
};
#endif

inetaddr通过多个构造函数来满足不同场景,无参构造函数用于创建一个空地址对象;接收sockaddr_in的构造函数从系统底层地址结构初始化;接收IP字符串和端口的构造函数用于连接远程服务器时指定目标地址;接收端口的构造函数用于服务器创建监听地址,IP设置为INADDR_ANY用于监听本机所有网络接口。setaddr负责在设置地址结构的同时自动填充字符串形式的IP、端口;port、ip分别返回端口号和IP字符串;netaddr返回sockaddr_in结构的常量引用;netaddrptr返回可用于系统调用的sockaddr指针;内部使用CONV完成类型转换;netaddrlen返回地址结构体长度;StringAddr返回IP:端口格式的字符串,便于日志打印和调试;operator==通过比较IP、端口来判断两个地址是否相同。

<3> mutex

mutex为线程同步模块,实现了对互斥锁的封装。

mutex.hpp

cpp 复制代码
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
namespace mutexmodule
{
    class mutex
    {
    public:
        mutex()
        {
            pthread_mutex_init(&_mutex,nullptr);
        }
        void lock()
        {
            int n=pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void unlock()
        {
            int n=pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
    private:
        pthread_mutex_t _mutex;
    };
    class lockguard
    {
    public:
        lockguard(mutex& mutex):_mutex(mutex)
        {
            _mutex.lock();
        }
        ~lockguard()
        {
            _mutex.unlock();
        }
    private:
        mutex& _mutex;
    };
}

mutex构造函数通过调用pthread_mutex_init对互斥锁进行初始化,lock、unlock分别调用pthread_mutex_lock、pthread_mutex_unlock进行加锁、解锁,析构函数调用pthread_mutex_destroy销毁锁。lockguard构造函数通过接收mutex对象调用lock进行加锁,析构函数自动调用unlock解锁。

<4> log

log是一个支持策略模式、线程安全的日志系统,定义了日志策略的抽象基类,并派生出控制台日志输出策略、文件日志输出策略。

log.hpp

cpp 复制代码
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include<iostream>
#include<string>
#include<cstdio>
#include<filesystem>
#include<fstream>
#include<sstream>
#include<memory>
#include<ctime>
#include<unistd.h>
#include"mutex.hpp"
using namespace std;
namespace logmodule
{
    using namespace mutexmodule;
    const string gsep ="\r\n";
    class logstrategy
    {
    public:
        virtual ~logstrategy()=default;
        virtual void synclog(const string& message)=0;
    };
    class consolelogstrategy:public logstrategy
    {
    public:
        consolelogstrategy(){}
        void synclog(const string& message) override
        {
            lockguard guard(_mutex);
            cout<<message<<gsep;
        }
        ~consolelogstrategy(){}
    private:
        mutex _mutex;
    };
    const string defaultpath="/var/log/";
    const string defaultfile="my.log";
    class filelogstrategy:public logstrategy
    {
    public:
        filelogstrategy(const string& path=defaultpath,const string& file=defaultfile)
                        :_path(path),
                        _file(file)
                        {
                            lockguard guard(_mutex);
                            if(filesystem::exists(_path))
                            {
                                return;
                            }
                            try
                            {
                              filesystem::create_directories(_path);
                            }
                            catch(const filesystem::filesystem_error& e)
                            {
                                cerr<<e.what()<<endl;
                            }
                        }
        void synclog(const string& message) override
        {
            lockguard guard(_mutex);
            string filename=_path+(_path.back()=='/'?"":"/")+_file;
            ofstream out(filename,ios::app);
            if(!out.is_open())
            {
                return;
            }
            out<<message<<gsep;
            out.close();
        }
        ~filelogstrategy(){}
    private:
        string _path;
        string _file;
        mutex _mutex;
    };
    enum class loglevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    string levelstr(loglevel lev)
    {
        switch(lev)
        {
            case loglevel::DEBUG: return "DEBUG";
            case loglevel::INFO: return "INFO";
            case loglevel::WARNING: return "WARNING";
            case loglevel::ERROR: return "ERROR";
            case loglevel::FATAL: return "FATAL";
            default: return "UNKNOWN";
        }
    }
    string gettimestamp()
    {
        time_t t=time(nullptr);
        struct tm curr_tm;
        localtime_r(&t,&curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
            curr_tm.tm_year+1900,
            curr_tm.tm_mon+1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );
        return timebuffer;
    }
    class logger
    {
    public:
        logger():_ptr(nullptr)
        {
            enableconsolelogstrategy();
        }
        void enableconsolelogstrategy()
        {
            _ptr=make_unique<consolelogstrategy>();
        }
        void enablefilelogstrategy()
        {
            _ptr=make_unique<filelogstrategy>();
        }
        class logmessage
        {
        public:
            logmessage(const string& src,loglevel level,int num,logger& logger)
            :_pid(getpid())
            ,_src(src)
            ,_num(num)
            ,_curr_time(gettimestamp())
            ,_level(level)
            ,_logger(logger)
            {
                stringstream ss;
                ss<<"["<<_curr_time<<"]"<<"["<<levelstr(_level)<<"]"<<"["<<_pid<<"]"<<"["<<_src<<"]"<<"["<<_num<<"]"<<"-";
                _loginfo=ss.str();
            }
            template<class K>
            logmessage& operator<<(const K& info)
            {
                stringstream ss;
                ss<<info;
                _loginfo+=ss.str();
                return *this;
            }
            ~logmessage()
            {
                if(_logger._ptr)
                {
                    _logger._ptr->synclog(_loginfo);
                }
            }
        private:
            string _curr_time;
            loglevel _level;
            pid_t _pid;
            string _src;
            int _num;
            string _loginfo;
            logger& _logger;
        };
        logmessage operator()(loglevel lev,const string& name,int line)
        {
            return logmessage(name,lev,line,*this);
        }
        ~logger()
        {
            
        }
    private:
        unique_ptr<logstrategy> _ptr;
    };
    static logger log;
    #define LOG(level) logmodule::log(level,__FILE__,__LINE__)
    #define Enable_Console_Log_Strategy() logmodule::log.enableconsolelogstrategy()
    #define Enable_File_Log_Strategy() logmodule::log.enablefilelogstrategy()
}
#endif

consolelogstrategy控制台策略在输出时使用互斥锁保证线程安全,filelogstrategy文件策略则在构造时检查并创建日志目录,每次写入时以追加模式打开文件并加锁。日志级别定义了DEBUG、INFO、WARNING、ERROR、FATAL五个等级,并提供了等级转字符串的函数。gettimestamp函数用于获取当前时间的格式化字符串。logger类为日志器,内部持有一个策略对象的智能指针,默认使用控制台策略,也可通过enableconsolelogstrategy和enablefilelogstrategy动态切换策略。logger内部定义了一个嵌套类logmessage,logmessage构造函数通过接收文件名、日志级别、行号、logger引用,在构造时生成带有时间戳、进程ID、文件名、行号等信息的日志前缀;重载operator<<通过流式方式输入日志内容,析构时将日志内容通过logger持有的策略对象输出。logger类还重载了operator(),返回一个临时的logmessage对象,这样就可以实现通过LOG以流的方式输出日志。静态logger、宏,用于日志记录、并根据需要启用控制台、文件输出策略。

<5> socket

socket模块是对套接字的网络封装层,位于socketmodule命名空间中,为上层提供了面向对象的socket操作接口。

cpp 复制代码
#ifndef _SOCKET_HPP_
#define _SOCKET_HPP_
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstdlib>
#include"common.hpp"
#include"log.hpp"
#include"inetaddr.hpp"
using namespace std;
namespace socketmodule
{
    using namespace logmodule;
    const static int gbacklog=16;
    class socket
    {
    public:
        virtual ~socket(){}
        virtual void socketordie()=0;
        virtual void bindordie(uint16_t port)=0;
        virtual void listenordie(int backlog)=0;
        virtual void Close()=0;
        virtual int recv(string* out)=0;
        virtual int send(const string& message)=0;
        virtual int accept(inetaddr* addr)=0;
        virtual int connect(const string& ip,uint16_t port)=0;
        virtual int fd()=0;
    public:
        void buildtcpsocket(uint16_t port,int backlog=gbacklog)
        {
            socketordie();
            bindordie(port);
            listenordie(backlog);
        }
        void buildclient()
        {
            socketordie();
        }
    };
    const static int defaultfd=-1;
    class tcpsocket:public socket
    {
    public:
        tcpsocket()
        :_sockfd(defaultfd)
        {}
        tcpsocket(int fd)
        :_sockfd(fd)
        {}
        tcpsocket(const string& ip,uint16_t port)
        {}
        ~tcpsocket()
        {}
        void socketordie() override
        {
            _sockfd=::socket(AF_INET,SOCK_STREAM,0);
            if(_sockfd<0)
            {
                LOG(loglevel::FATAL)<<"socket error";
                exit(SOCKET_ERR);
            }
            LOG(loglevel::INFO)<<"sock success";
        }
        void bindordie(uint16_t port) override
        {
            inetaddr addr(port);
            int n=::bind(_sockfd,addr.netaddrptr(),addr.netaddrlen());
            if(n<0)
            {
                LOG(loglevel::FATAL)<<"bind error";
                exit(BIND_ERR);
            }
            LOG(loglevel::INFO)<<"bind success";
        }
        void listenordie(int backlog) override
        {
            int n=listen(_sockfd,backlog);
            if(n<0)
            {
                LOG(loglevel::FATAL)<<"listen error";
                exit(LISTEN_ERR);
            }
            LOG(loglevel::INFO)<<"listen success";
        }
        int accept(inetaddr* addr) override
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int fd=::accept(_sockfd,CONV(peer),&len);
            if(fd<0)
            {
                LOG(loglevel::WARNING)<<"accept error";
                return -1;
            }
            return fd;
        }
        int recv(string* out) override
        {
            char buffer[4096*2];
            ssize_t n=::recv(_sockfd,buffer,sizeof(buffer)-1,0);
            if(n>0)
            {
                buffer[n]=0;
                *out+=buffer;
            }
            return n;
        }
        int send(const string& message) override
        {
            return ::send(_sockfd,message.c_str(),message.size(),0);
        }
        void Close() override
        {
            if(_sockfd>=0)
            {
                ::close(_sockfd);
            }
        }
        int connect(const string& ip,uint16_t port) override
        {
            inetaddr addr(ip,port);
            return ::connect(_sockfd,addr.netaddrptr(),addr.netaddrlen());
        }
        int fd(){return _sockfd;}
    private:
        int _sockfd;
    };
}
#endif

socket为抽象基类,声明了一系列的纯虚函数,socketordie用于创建套接字、bindordie用于绑定端口、listenordie用于监听、Close用于关闭、recv和send用于数据收发、accept用于接受连接、connect用于连接远程服务器、以及fd用于获取文件描述符。buildtcpsocket用于服务器依次执行创建、绑定和监听三步操作,buildclient用于客户端只创建套接字。tcpsocket类继承自socket,在tcpsocket中,socketordie调用系统socket函数创建TCP套接字,bindordie通过inetaddr类将端口转换为网络地址结构后调用bind;listenordie调用listen开始监听,backlog为默认连接数,accept负责接收客户端连接,返回新的套接字文件描述符,recv用于读取数据,将读取到的内容追加到传入的字符串参数中,send通过调用系统send进行消息的发送,Close用于关闭套接字,connect用于客户端连接服务器,通过inetaddr构建地址后调用connect。

<6> selectserver

selectserver模块是一个基于select实现的一个TCP服务器:

6.1 构造函数
cpp 复制代码
#ifndef _SELECTSERVER_HPP_
#define _SELECTSERVER_HPP_
#include<iostream>
#include<unistd.h>
#include<memory>
#include"socket.hpp"
#include"log.hpp"
using namespace socketmodule;
using namespace logmodule;
using namespace std;
class selectserver
{
    const static int size=sizeof(fd_set)*8;
    const static int defaultfd=-1;
public:
    selectserver(int port)
    :_listensock(make_unique<tcpsocket>())
    ,_isrunning(false)
    {
        _listensock->buildtcpsocket(port);
        for(int i=0;i<size;i++)
            fdarr[i]=defaultfd;
        fdarr[0]=_listensock->fd();
    }
    void stop()
    {
        _isrunning=false;
    }
    ~selectserver()
    {}
private:
    unique_ptr<socketmodule::socket> _listensock;
    bool _isrunning;
    int fdarr[size];
};
#endif

_listensock(make_unique<tcpserver>()),_listensock->buildtcpsocket(port),构造函数selectserver通过接收一个端口号,通过make_unique创建tcpsocket对象并调用buildtcpsocket完成套接字的创建、绑定和监听。fdarr[i]=defaultfd,遍历fdarr数组将所有槽位设为defaultfd,fdarr[0]=_listensock->fd(),将监听套接字的文件描述符存入数组的第一个位置。

6.2 printfd
cpp 复制代码
void printfd()
{
    cout<<"fdarr[]: ";
    for(int i=0;i<size;i++)
    {
        if(fdarr[i]==defaultfd)
            continue;
        cout<<fdarr[i]<<" ";
    }
    cout<<"\r\n";
}

printfd用于调试输出,通过for循环遍历fdarr数组,if(fdarr[i]==defaultfd) continue,跳过无效槽位,cout<<fdarr[i]<<" ",输出所有有效的文件描述符,cout<<"\r\n",最后输出换行。

6.3 accepter
cpp 复制代码
void accepter()
{
    inetaddr addr;
    int sockfd=_listensock->accept(&addr);
    if(sockfd>=0)
    {
        LOG(loglevel::INFO)<<"get a new link,sockfd: "<<sockfd;
        int pos=0;
        for(;pos<size;pos++)
        {
            if(fdarr[pos]==defaultfd)
            break;
        }
        if(pos==size)
        {
            LOG(loglevel::WARNING)<<"server full";
            close(sockfd);
        }
        else
        {
            fdarr[pos]=sockfd;
        }
    }
}

accepter负责接收新连接,int sockfd=_listensock->accept(&addr),调用accept接收新连接,返回新的套接字文件描述符,通过for循环从fdarr的起始位置寻找第一个空闲位置,fdarr[pos]=sockfd,并将sockfd存入到fdarr中。

6.4 recver
cpp 复制代码
void recver(int fd,int pos)
{
    char buffer[1024];
    ssize_t n=recv(fd,buffer,sizeof(buffer)-1,0);
    if(n>0)
    {
        buffer[n]=0;
        cout<<"client say@: "<<buffer<<endl;
    }
    else if(n==0)
    {
        LOG(loglevel::INFO)<<"client quit...";
        fdarr[pos]=defaultfd;
        close(fd);
    }
    else
    {
        LOG(loglevel::ERROR)<<"recv error";
    }
}

recver接收文件描述符和它在数组中的索引,char buffer[1024],buffer为字符缓冲区,ssize_t n=recv(fd,buffer,sizeof(buffer)-1,0),调用recv读取数据,若n>0,则读到数据,buffer[n]=0,在buffer末尾添加字符串结束符后输出到控制台,若n==0,则表示客户端关闭连接,fdarr[pos]=defaultfd,close(fd),将数组对应位置的槽位设为defaultfd,并关闭套接字,如果n<0,LOG(loglevel::ERROR)<<"recv error";记录错误日志。

6.5 handlerevent
cpp 复制代码
void handlerevent(fd_set& fdset)
{
    for(int i=0;i<size;i++)
    {
        if(fdarr[i]==defaultfd)
            continue;
        if(FD_ISSET(fdarr[i],&fdset))
        {
            if(fdarr[i]==_listensock->fd())
            {
                accepter();
            }
            else
            {
                recver(fdarr[i],i);
            }
        }
    }
}

handlerevent接收一个fd_set引用,用于处理就绪的事件。通过for循环遍历fdarr,跳过无效槽位,FD_ISSET(fdarr[i],&fdset),对每个有效文件描述符调用FD_ISSET判断是否就绪。fdarr[i]==_listensock->fd(),如果就绪的描述符等于监听套接字的描述符,则调用accepter接收新连接;recver(fdarr[i],i),else则调用recver处理客户端数据。

6.6 start
cpp 复制代码
void start()
{
    _isrunning=true;
    while(_isrunning)
    {
        fd_set fset;
        FD_ZERO(&fset);
        int maxfd=defaultfd;
        for(int i=0;i<size;i++)
        {
            if(fdarr[i]==defaultfd)
                continue;
            FD_SET(fdarr[i],&fset);
            if(maxfd<fdarr[i])
            {
                maxfd=fdarr[i];
            }
        }
        printfd();
        int n=select(maxfd+1,&fset,nullptr,nullptr,nullptr);
        switch(n)
        {
        case -1:
            LOG(loglevel::ERROR)<<"select error";
            break;
        case 0:
            LOG(loglevel::INFO)<<"time out...";
            break;
        default:
            LOG(loglevel::DEBUG)<<"有事件就绪了...,n: "<<n;
            handlerevent(fset);
            break;
        }
    }
    _isrunning=false;
}

start是服务器的主循环,fd_set fset,FD_ZERO(&fset),声明一个fd_set类型的集合变量fset并调用FD_ZERO清空,通过for循环遍历fdarr,FD_SET(fdarr[i],&fset),将有效的文件描述符添加到fset集合中,maxfd=fdarr[i],同时更新maxfd为当前最大值。printfd(),调用printfd输出当前所有有效的文件描述符。int n=select(maxfd+1,&fset,nullptr,nullptr),调用select,传入maxfd+1、fset集合,只监听读事件且阻塞等待,select根据返回值n通过switch语句进行处理,当有事件就绪了,n为就绪事件的数量,调用handlerevent对就绪事件进行处理。

<7> main

main模块为整个select服务器的入口点:

cpp 复制代码
#include"selectserver.hpp"
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        cout<<"usage:"<<argv[0]<<"port"<<endl;
        exit(USAGE_ERR);
    }
    Enable_Console_Log_Strategy();
    uint16_t port=stoi(argv[1]);
    unique_ptr<selectserver> up=make_unique<selectserver>(port);
    up->start();
    return 0;
}

Enable_Console_Log_Strategy(),启用控制台日志策略,uint16_t port=stoi(argv[1]),将命令行第二个参数转换为端口号,unique_ptr<selectserver> up=make_unique<selectserver>(port),通过端口号构造selectserver,up->start(),调用start启动服务器。

Makefile:

bash 复制代码
selectserver:main.cc
	g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
	rm -rf selectserver

通过Makefile即可实现一键化编译,g++ -o @ ^ -std=c++17,采用c++17标准,服务器运行如下所示:

Makefile编译通过后,./selectserver 8922,绑定端口号8922,服务器开始运行,打开另一个终端,telnet 127.0.0.1 8922,进行连接,客户端向服务器输入消息,服务器可回显相应内容,结果如下所示:

客户端:

服务器:

2、poll

(1)原理

poll也是Linux I/O多路复用的一种机制,可以用于同时监控多个文件描述符的事件。与select类似,但克服了select的一些限制。

poll接口如下:

fds是一个poll函数监听的结构列表,pollfd结构包含了三部分内容,如下所示:

cpp 复制代码
//pollfd结构
struct pollfd
{
    int fd;
    short events;
    short revents;
};

fd为文件描述符,events为监听的事件集合,revents为返回的事件集合。

events、revents的取值如下:

nfds表示fds数组的长度,timeout表示poll函数的超时时间,以毫秒为单位。

poll返回值大于0,表示poll由于监听的文件描述符就绪而返回;

poll返回值等于0,表示poll等待超时;

poll返回值小于0,表示出错。

(2)优点

不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现;

pollfd结构包含了要监视的event和发生的event,不再使用select参数-值传递的方式,接口使用比select更加方便;

poll并没有文件描述符最大数量限制。

(3)缺点

当poll中监听的文件描述符数目增多时:

和select一样,poll返回后,需要轮询pollfd来获取就绪的文件描述符;

每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中;

同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的文件描述符数量增多,poll的效率也会随之线性下降。

(4)实现

将上面实现的select TCP回显服务器稍加修改,即可改造成一个基于poll的TCP服务器。

<1> common

common.hpp

cpp 复制代码
#ifndef _COMMON_HPP_
#define _COMMON_HPP_
#include<iostream>
#include<functional>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
enum exitcode
{
    OK=0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    OPEN_ERR
};
class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy& )=delete;
    const nocopy& operator=(const nocopy& )=delete;
    ~nocopy(){}
};
#define CONV(addr) ((struct sockaddr*)&addr)
#endif

common模块与上面select服务器实现一致,提供了退出状态码枚举、不可拷贝基类和socket地址转化宏,为整个服务器提供通用的基础设施。

<2> inetaddr

inetaddr.hpp

cpp 复制代码
#ifndef _INETADDR_HPP_
#define _INETADDR_HPP_
#include"common.hpp"
using namespace std;
class inetaddr
{
public:
    inetaddr(){}
    inetaddr(struct sockaddr_in& addr)
    {
        setaddr(addr);
    }
    inetaddr(const string& ip,uint16_t port)
    :_ip(ip)
    ,_port(port)
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family=AF_INET;
        inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
        _addr.sin_port=htons(_port);
    }
    inetaddr(uint16_t port)
    :_port(port)
    ,_ip()
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family=AF_INET;
        _addr.sin_port=htons(_port);
        _addr.sin_addr.s_addr=INADDR_ANY;
    }
    void setaddr(struct sockaddr_in& addr)
    {
        _addr=addr;
        _port=ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(_addr));
        _ip=ipbuffer;
    }
    uint16_t port()
    {
        return _port;
    }
    string ip() 
    {
        return _ip;
    }
    const struct sockaddr_in& netaddr()
    {
        return _addr;
    }
    const struct sockaddr* netaddrptr()
    {
        return CONV(_addr);
    }
    socklen_t netaddrlen()
    {
        return sizeof(_addr);
    }
    bool operator==(const inetaddr& addr)
    {
        return _ip==addr._ip && _port==addr._port;
    }
    string StringAddr()
    {
        return _ip+":"+to_string(_port);
    }
    ~inetaddr()
    {}
private:
    struct sockaddr_in _addr;
    uint16_t _port;
    string _ip;
};
#endif

inetaddr模块与select服务器实现一致,对网络地址进行了封装,支持从IP、端口或仅从端口构造,并能返回原生地址结构和IP字符串。

<3> mutex

mutex.hpp

cpp 复制代码
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
namespace mutexmodule
{
    class mutex
    {
    public:
        mutex()
        {
            pthread_mutex_init(&_mutex,nullptr);
        }
        void lock()
        {
            int n=pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void unlock()
        {
            int n=pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
    private:
        pthread_mutex_t _mutex;
    };
    class lockguard
    {
    public:
        lockguard(mutex& mutex):_mutex(mutex)
        {
            _mutex.lock();
        }
        ~lockguard()
        {
            _mutex.unlock();
        }
    private:
        mutex& _mutex;
    };
}

mutex模块与select服务器实现一致,对互斥锁进行了封装,提供了RAII风格的lockguard类。

<4> log

log.hpp

cpp 复制代码
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include<iostream>
#include<string>
#include<cstdio>
#include<filesystem>
#include<fstream>
#include<sstream>
#include<memory>
#include<ctime>
#include<unistd.h>
#include"mutex.hpp"
using namespace std;
namespace logmodule
{
    using namespace mutexmodule;
    const string gsep ="\r\n";
    class logstrategy
    {
    public:
        virtual ~logstrategy()=default;
        virtual void synclog(const string& message)=0;
    };
    class consolelogstrategy:public logstrategy
    {
    public:
        consolelogstrategy(){}
        void synclog(const string& message) override
        {
            lockguard guard(_mutex);
            cout<<message<<gsep;
        }
        ~consolelogstrategy(){}
    private:
        mutex _mutex;
    };
    const string defaultpath="/var/log/";
    const string defaultfile="my.log";
    class filelogstrategy:public logstrategy
    {
    public:
        filelogstrategy(const string& path=defaultpath,const string& file=defaultfile)
                        :_path(path),
                        _file(file)
                        {
                            lockguard guard(_mutex);
                            if(filesystem::exists(_path))
                            {
                                return;
                            }
                            try
                            {
                              filesystem::create_directories(_path);
                            }
                            catch(const filesystem::filesystem_error& e)
                            {
                                cerr<<e.what()<<endl;
                            }
                        }
        void synclog(const string& message) override
        {
            lockguard guard(_mutex);
            string filename=_path+(_path.back()=='/'?"":"/")+_file;
            ofstream out(filename,ios::app);
            if(!out.is_open())
            {
                return;
            }
            out<<message<<gsep;
            out.close();
        }
        ~filelogstrategy(){}
    private:
        string _path;
        string _file;
        mutex _mutex;
    };
    enum class loglevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    string levelstr(loglevel lev)
    {
        switch(lev)
        {
            case loglevel::DEBUG: return "DEBUG";
            case loglevel::INFO: return "INFO";
            case loglevel::WARNING: return "WARNING";
            case loglevel::ERROR: return "ERROR";
            case loglevel::FATAL: return "FATAL";
            default: return "UNKNOWN";
        }
    }
    string gettimestamp()
    {
        time_t t=time(nullptr);
        struct tm curr_tm;
        localtime_r(&t,&curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
            curr_tm.tm_year+1900,
            curr_tm.tm_mon+1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );
        return timebuffer;
    }
    class logger
    {
    public:
        logger():_ptr(nullptr)
        {
            enableconsolelogstrategy();
        }
        void enableconsolelogstrategy()
        {
            _ptr=make_unique<consolelogstrategy>();
        }
        void enablefilelogstrategy()
        {
            _ptr=make_unique<filelogstrategy>();
        }
        class logmessage
        {
        public:
            logmessage(const string& src,loglevel level,int num,logger& logger)
            :_pid(getpid())
            ,_src(src)
            ,_num(num)
            ,_curr_time(gettimestamp())
            ,_level(level)
            ,_logger(logger)
            {
                stringstream ss;
                ss<<"["<<_curr_time<<"]"<<"["<<levelstr(_level)<<"]"<<"["<<_pid<<"]"<<"["<<_src<<"]"<<"["<<_num<<"]"<<"-";
                _loginfo=ss.str();
            }
            template<class K>
            logmessage& operator<<(const K& info)
            {
                stringstream ss;
                ss<<info;
                _loginfo+=ss.str();
                return *this;
            }
            ~logmessage()
            {
                if(_logger._ptr)
                {
                    _logger._ptr->synclog(_loginfo);
                }
            }
        private:
            string _curr_time;
            loglevel _level;
            pid_t _pid;
            string _src;
            int _num;
            string _loginfo;
            logger& _logger;
        };
        logmessage operator()(loglevel lev,const string& name,int line)
        {
            return logmessage(name,lev,line,*this);
        }
        ~logger()
        {
            
        }
    private:
        unique_ptr<logstrategy> _ptr;
    };
    static logger log;
    #define LOG(level) logmodule::log(level,__FILE__,__LINE__)
    #define Enable_Console_Log_Strategy() logmodule::log.enableconsolelogstrategy()
    #define Enable_File_Log_Strategy() logmodule::log.enablefilelogstrategy()
}
#endif

log日志模块与select服务器实现一致,采用策略模式,控制台策略、文件策略都通过互斥锁保证线程安全,logger类通过宏提供流式日志输出,每条日志都包含时间戳、日志级别、进程ID、源文件名和行号。

<5> socket
cpp 复制代码
#ifndef _SOCKET_HPP_
#define _SOCKET_HPP_
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstdlib>
#include"common.hpp"
#include"log.hpp"
#include"inetaddr.hpp"
using namespace std;
namespace socketmodule
{
    using namespace logmodule;
    const static int gbacklog=16;
    class socket
    {
    public:
        virtual ~socket(){}
        virtual void socketordie()=0;
        virtual void bindordie(uint16_t port)=0;
        virtual void listenordie(int backlog)=0;
        virtual void Close()=0;
        virtual int recv(string* out)=0;
        virtual int send(const string& message)=0;
        virtual int accept(inetaddr* addr)=0;
        virtual int connect(const string& ip,uint16_t port)=0;
        virtual int fd()=0;
    public:
        void buildtcpsocket(uint16_t port,int backlog=gbacklog)
        {
            socketordie();
            bindordie(port);
            listenordie(backlog);
        }
        void buildclient()
        {
            socketordie();
        }
    };
    const static int defaultfd=-1;
    class tcpsocket:public socket
    {
    public:
        tcpsocket()
        :_sockfd(defaultfd)
        {}
        tcpsocket(int fd)
        :_sockfd(fd)
        {}
        tcpsocket(const string& ip,uint16_t port)
        {}
        ~tcpsocket()
        {}
        void socketordie() override
        {
            _sockfd=::socket(AF_INET,SOCK_STREAM,0);
            if(_sockfd<0)
            {
                LOG(loglevel::FATAL)<<"socket error";
                exit(SOCKET_ERR);
            }
            LOG(loglevel::INFO)<<"sock success";
        }
        void bindordie(uint16_t port) override
        {
            inetaddr addr(port);
            int n=::bind(_sockfd,addr.netaddrptr(),addr.netaddrlen());
            if(n<0)
            {
                LOG(loglevel::FATAL)<<"bind error";
                exit(BIND_ERR);
            }
            LOG(loglevel::INFO)<<"bind success";
        }
        void listenordie(int backlog) override
        {
            int n=listen(_sockfd,backlog);
            if(n<0)
            {
                LOG(loglevel::FATAL)<<"listen error";
                exit(LISTEN_ERR);
            }
            LOG(loglevel::INFO)<<"listen success";
        }
        int accept(inetaddr* addr) override
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int fd=::accept(_sockfd,CONV(peer),&len);
            if(fd<0)
            {
                LOG(loglevel::WARNING)<<"accept error";
                return -1;
            }
            return fd;
        }
        int recv(string* out) override
        {
            char buffer[4096*2];
            ssize_t n=::recv(_sockfd,buffer,sizeof(buffer)-1,0);
            if(n>0)
            {
                buffer[n]=0;
                *out+=buffer;
            }
            return n;
        }
        int send(const string& message) override
        {
            return ::send(_sockfd,message.c_str(),message.size(),0);
        }
        void Close() override
        {
            if(_sockfd>=0)
            {
                ::close(_sockfd);
            }
        }
        int connect(const string& ip,uint16_t port) override
        {
            inetaddr addr(ip,port);
            return ::connect(_sockfd,addr.netaddrptr(),addr.netaddrlen());
        }
        int fd(){return _sockfd;}
    private:
        int _sockfd;
    };
}
#endif

socket模块与select服务器实现一致,定义了抽象基类socket,tcpsocket实现了具体的socket操作。

<6> pollserver

pollserver模块是整个服务器的核心,实现了一个基于poll的TCP服务器,该模块负责管理监听套接字和所有客户端连接,通过poll系统调用同时监控多个文件描述符上的读事件。

pollserver.hpp

6.1 构造函数
cpp 复制代码
#ifndef _POLLSERVER_HPP_
#define _POLLSERVER_HPP_
#include<iostream>
#include<unistd.h>
#include<memory>
#include<sys/poll.h>
#include"socket.hpp"
#include"log.hpp"
using namespace socketmodule;
using namespace logmodule;
using namespace std;
class pollserver
{
    const static int size=4096;
    const static int defaultfd=-1;
public:
    pollserver(int port)
    :_listensock(make_unique<tcpsocket>())
    ,_isrunning(false)
    {
        _listensock->buildtcpsocket(port);
        for(int i=0;i<size;i++)
        {
            fds[i].fd=defaultfd;
            fds[i].events=0;
            fds[i].revents=0;
        }
        fds[0].fd=_listensock->fd();
        fds[0].events=POLLIN;
    }
    void stop()
    {
        _isrunning=false;
    }
    ~pollserver()
    {}
private:
    unique_ptr<socketmodule::socket> _listensock;
    bool _isrunning;
    struct pollfd fds[size];
};
#endif

构造函数pollserver接收一个端口号作为参数,_listensock(make_unique<tcpsocket>()),通过make_unique创建一个tcpsocket对象,_listensock->buildtcpsocket(port),调用buildtcpsocket完成套接字的创建、绑定和监听操作,通过for循环遍历pollfd,fds[0].fd=_listensock->fd(),fds[0].events=POLLIN,将数组第一个元素的fd设为监听套接字的文件描述符,并设置其events为POLLIN,关心该文件描述符的可读事件。

6.2 printfd
cpp 复制代码
void printfd()
{
    cout<<"fds[]: ";
    for(int i=0;i<size;i++)
    {
        if(fds[i].fd==defaultfd)
            continue;
        cout<<fds[i].fd<<" ";
    }
    cout<<"\r\n";
}

printfd通过for循环遍历pollfd,cout<<"fds[i].fd<<" ";输出所有有效文件描述符的编号,便于观察当前服务器管理的连接情况。

6.3 accepter
cpp 复制代码
void accepter()
{
    inetaddr addr;
    int sockfd=_listensock->accept(&addr);
    if(sockfd>=0)
    {
        LOG(loglevel::INFO)<<"get a new link,sockfd: "<<sockfd;
        int pos=0;
        for(;pos<size;pos++)
        {
            if(fds[pos].fd==defaultfd)
            break;
        }
        if(pos==size)
        {
            LOG(loglevel::WARNING)<<"server full";
            close(sockfd);
        }
        else
        {
            fds[pos].fd=sockfd;
            fds[pos].events=POLLIN;
            fds[pos].revents=0;
        }
    }
}

accepter负责接收客户端连接,int sockfd=_listensock->accept(&addr),调用_listensock的accept接收连接,通过for循环寻找第一个空闲位置,fds[pos].fd=sockfd,fds[pos].events=POLLIN,fds[pos].revents=0,如果成功找到,则将新的套接字文件描述符放入该槽位,将events设为POLLIN,revents清零;if(pos==size),如果数组已满,LOG(loglevel::WARNING)<<"server full";close(sockfd),则记录日志并关闭新连接。

6.4 recver
cpp 复制代码
void recver(int pos)
{
    char buffer[1024];
    ssize_t n=recv(fds[pos].fd,buffer,sizeof(buffer)-1,0);
    if(n>0)
    {
        buffer[n]=0;
        cout<<"client say@: "<<buffer<<endl;
    }
    else if(n==0)
    {
        LOG(loglevel::INFO)<<"client quit...";
        fds[pos].fd=defaultfd;
        fds[pos].events=0;
        fds[pos].revents=0;
        close(fds[pos].fd);
    }
    else
    {
        LOG(loglevel::ERROR)<<"recv error";
        fds[pos].fd=defaultfd;
        fds[pos].events=0;
        fds[pos].revents=0;
        close(fds[pos].fd);
    }
}

recver通过接收一个数组索引作为参数,表示要处理哪个客户端连接。char buffer[1024],ssize_t n=recv(fds[pos].fd,buffer,sizeof(buffer)-1,0),通过调用recv从对应的套接字读取数据。若n>0,表示成功读取到数据,buffer[n]=0,在缓冲区末尾添加字符串结束符后输出到控制台。若n==0,表示客户端主动关闭连接,fds[pos].fd=defaultfd,fds[pos].events=0,fds[pos].revents=0,close(fds[pos].fd),将对应槽位的fd设为defaultfd,events和revents清零,关闭套接字。如果n<0,表示读取错误,记录错误日志,并执行相同的清理操作。

6.5 handlerevent
cpp 复制代码
void handlerevent()
{
    for(int i=0;i<size;i++)
    {
        if(fds[i].fd==defaultfd)
            continue;
        if(fds[i].revents & POLLIN)
        {
            if(fds[i].fd==_listensock->fd())
            {
                accepter();
            }
            else
            {
                recver(i);
            }
        }
    }
}

handlerevent用于处理所有就绪的事件,通过for循环遍历pollfd,if(fds[i].fd==defaultfd),continue跳过无效槽位,if(fds[i].revents & POLLIN),对每个有效的文件描述符,检查其revents是否包含POLLIN事件。if(fds[i].fd==_listensock->fd()),accepter(),如果当前就绪的文件描述符为监听套接字,则调用accepter接收新连接,recver(i),else则调用recver处理客户端数据。

6.6 start
cpp 复制代码
void start()
{
    int timeout=-1;
    _isrunning=true;
    while(_isrunning)
    {
        int n=poll(fds,size,timeout);
        switch(n)
        {
            case -1:
            LOG(loglevel::ERROR)<<"poll error";
            break;
            case 0:
            LOG(loglevel::INFO)<<"poll time out...";
            break;
            default:
            LOG(loglevel::DEBUG)<<"有事件就绪了...,n: "<<n;
            handlerevent();
            break;
        }
    }
    _isrunning=false;
}

start是服务器的主循环,int timeout=-1,将超时时间设为-1表示无限等待,通过while循环,int n=poll(fds,size,timeout),调用poll传入整个fds数组、数组大小和超时时间。通过switch对返回值n进行处理,当n==-1时表示出错,LOG(loglevel::ERROR)<<"poll error",记录错误日志;n==0表示超时,LOG(loglevel::INFO)<<"poll time out...",记录超时信息;当n>0时表示有事件就绪,handlerevent(),调用handlerevent处理就绪事件。

<7> main

main模块为整个pollserver服务器的入口点:

cpp 复制代码
#include"pollserver.hpp"
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        cout<<"usage:"<<argv[0]<<"port"<<endl;
        exit(USAGE_ERR);
    }
    Enable_Console_Log_Strategy();
    uint16_t port=stoi(argv[1]);
    unique_ptr<pollserver> up=make_unique<pollserver>(port);
    up->start();
    return 0;
}

Enable_Console_Log_Strategy(),启用控制台日志策略,uint16_t port=stoi(argv[1]),将命令行第二个参数转换为端口号,unique_ptr<pollserver> up=make_unique<pollserver>(port),通过make_unique创建pollserver对象,up->start(),调用start启动服务器。

Makefile:

bash 复制代码
pollserver:main.cc
	g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
	rm -rf pollserver

通过Makefile即可实现一键化编译,g++ -o @ ^ -std=c++17,采取c++17标准,服务器运行如下所示:

Makefile编译通过后,./pollserver 8923,绑定端口号8923,服务器开始运行,打开另一个终端,telnet 127.0.0.1 8923,进行连接,客户端向服务器发送消息,服务器可回显相应内容,结果如下所示:

客户端:

服务器:

3、epoll

epoll被公认为Linux下性能最好的多路I/O就绪通知方法,epoll几乎具备了之前所说的一切优点,epoll是为处理大批量句柄而作了改进的poll。

(1)接口

epoll有3个相关的系统调用:

<1> epoll_create

epoll_create用于创建一个epoll的句柄,从Linux2.6.8之后,size参数被忽略,调用完epoll_create之后,必须调用close()关闭。

<2> epoll_ctl

epoll_ctl为epoll的事件注册函数,它不同于select是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

第一个参数epfd是epoll_create的返回值;

第二个参数op表示动作,用三个宏表示,取值如下:

EPOLL_CTL_ADD:注册新的fd到epfd中;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数fd是需要监听的fd;

第四个参数event告诉内核需要监听什么事件。

struct epoll_event的结构如下:

events的取值如下:

EPOLLIN:表示对应的文件描述符可以读;

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读;

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET:将EPOLL设为边缘触发模式;

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket,需要再次把这个socket加入到EPOLL红黑树里。

<3> epoll_wait

epoll_wait用于收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中,events不能为空;maxevents用于告诉内核这个events有多大,maxevents不能大于epoll_create的size;参数timeout表示超时时间,以毫秒为单位,0会立即返回,-1表示永久阻塞;如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,返回0表示已超时,返回小于0表示函数调用失败。

(2)原理

当某一进程调用epoll_create时,内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。

cpp 复制代码
struct eventpoll{
....
/*红⿊树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root  rbr;
/*双链表中则存放着将要通过epoll_wait返回给⽤⼾的满⾜条件的事件*/
struct list_head rdlist;
....
};

每个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl向epoll对象中添加进来的事件。这些事件都会被挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来

而所有添加到epoll中的事件都会与设备、驱动程序建立回调关系,当响应的事件发生时会调用这个回调方法。这个回调方法在内核中称为ep_poll_callback,它会将发生的事件添加到rdlist双链表中。在epoll中,对于每一个事件,都会建立一个epitem结构体。

cpp 复制代码
struct epitem{
    struct rb_node  rbn;//红⿊树节点
    struct list_head rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep; //指向其所属的eventpoll对象
    struct epoll_event event; //期待发⽣的事件类型
}

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可;如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,时间复杂度为O(1)。

epoll的使用就是三部曲:

调用epoll_create创建一个epoll句柄;

调用epoll_ctl,将要监控的文件描述符进行注册;

调用epoll_wait,等待文件描述符就绪。

(3)优点

接口使用方便:虽然拆分成了三个函数,但使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开;

数据拷贝轻量:只在合适的时候调用EPOLL_CTL_ADD将文件描述符结构拷贝到内核中,这个操作并不频繁,而select/poll都是每次循环都需要进行拷贝;

事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪,时间复杂度为O(1),即使文件描述符数目很多,效率也不会受到影响;

没有数量限制:文件描述符数目无上限。

(4)工作方式

epoll有2种工作方式:水平触发LT和边缘触发ET。

<1> LT

epoll默认状态下就是LT工作模式:

当epoll检测到socket上事件就绪时,可以不立刻处理,或者只处理一部分。

LT支持阻塞读写和非阻塞读写。

<2> ET

将socket添加到epoll文件描述符的时候使用了EPOLLET标志,epoll将进入ET工作模式。

当epoll检测到socket上事件就绪时,必须立刻处理;

ET模式下,文件描述符上的事件就绪后,只有一次处理机会;

ET的性能相比LT更高,epoll_wait的返回次数会少很多;

ET只支持非阻塞的读写;

使用ET模式的epoll,需要将文件描述符设置为非阻塞。

(5)应用

对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll;

如一个需要处理上万个客户端的服务器,各种互联网APP的入口服务器,这样的服务器适合使用epoll。

二、Reactor

Reactor是一种事件驱动的编程模式,用于高效地处理并发I/O操作。它通过一个或多个事件循环来监听和处理各种事件,从而实现高效的并发处理,而无需为每个连接创建一个线程或进程。

事件循环:事件循环是异步编程的核心,负责监听事件并触发相应的回调函数。它通常是一个单线程的执行模型,通过多路复用技术select/poll/epoll高效地管理多个I/O操作。

三、Epoll+Reactor实现

下面实现一个基于epoll的Reactor高性能TCP服务器,该服务器主要包含了网络通信、日志、线程同步、事件驱动等模块。

1、common

common模块是整个服务器的基础,包含了服务器通用的头文件、错误码以及一个禁止拷贝的基类等。

common.hpp

cpp 复制代码
#ifndef _COMMON_HPP_
#define _COMMON_HPP_
#include<iostream>
#include<functional>
#include<fcntl.h>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
using namespace std;
enum exitcode
{
    OK=0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    OPEN_ERR,
    EPOLL_CREATE_ERR,
    EPOLL_CTL_ERR
};
class nocopy
{
public:
    nocopy(){}
    nocopy(const nocopy& )=delete;
    const nocopy& operator=(const nocopy& )=delete;
    ~nocopy(){}
};
int defaultport=8915;
void setnonblock(int fd)
{
    int fl=fcntl(fd,F_GETFL);
    if(fl<0)
    {
        return;
    }
    fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}
#define CONV(addr) ((struct sockaddr*)&addr)
#endif

程序退出时的状态码枚举exitcode、不可拷贝的基类nocopy、以及sockaddr类型转换宏与前面实现的select/poll服务器一致。int fl=fcntl(fd,F_GETFL),setnonblock通过fcntl获取当前文件状态标志,fcntl(fd,F_SETFL,fl | O_NONBLOCK),设置O_NONBLOCK将套接字变为非阻塞模式。

2、inetaddr

inetaddr.hpp

cpp 复制代码
#ifndef _INETADDR_HPP_
#define _INETADDR_HPP_
#include"common.hpp"
using namespace std;
class inetaddr
{
public:
    inetaddr(){}
    inetaddr(struct sockaddr_in& addr)
    {
        setaddr(addr);
    }
    inetaddr(const string& ip,uint16_t port)
    :_ip(ip)
    ,_port(port)
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family=AF_INET;
        inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);
        _addr.sin_port=htons(_port);
    }
    inetaddr(uint16_t port)
    :_port(port)
    ,_ip()
    {
        memset(&_addr,0,sizeof(_addr));
        _addr.sin_family=AF_INET;
        _addr.sin_port=htons(_port);
        _addr.sin_addr.s_addr=INADDR_ANY;
    }
    void setaddr(struct sockaddr_in& addr)
    {
        _addr=addr;
        _port=ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(_addr));
        _ip=ipbuffer;
    }
    uint16_t port()
    {
        return _port;
    }
    string ip() 
    {
        return _ip;
    }
    const struct sockaddr_in& netaddr()
    {
        return _addr;
    }
    const struct sockaddr* netaddrptr()
    {
        return CONV(_addr);
    }
    socklen_t netaddrlen()
    {
        return sizeof(_addr);
    }
    bool operator==(const inetaddr& addr)
    {
        return _ip==addr._ip && _port==addr._port;
    }
    string StringAddr()
    {
        return _ip+":"+to_string(_port);
    }
    ~inetaddr()
    {}
private:
    struct sockaddr_in _addr;
    uint16_t _port;
    string _ip;
};
#endif

inetaddr模块与前面实现的select/poll服务器一致,对网络地址进行了封装,提供了从IP和端口构造、从已有地址结构构造以及仅从端口构造三种构造函数,还提供了获取端口号、IP字符串、原生地址结构及其指针的方法,重载operator==用于地址比较。

3、mutex

mutex.hpp

cpp 复制代码
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
namespace mutexmodule
{
    class mutex
    {
    public:
        mutex()
        {
            pthread_mutex_init(&_mutex,nullptr);
        }
        void lock()
        {
            int n=pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void unlock()
        {
            int n=pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
    private:
        pthread_mutex_t _mutex;
    };
    class lockguard
    {
    public:
        lockguard(mutex& mutex):_mutex(mutex)
        {
            _mutex.lock();
        }
        ~lockguard()
        {
            _mutex.unlock();
        }
    private:
        mutex& _mutex;
    };
}

mutex模块与前面实现的select/poll服务器一致,对互斥锁进行了封装,mutex用于初始化和销毁互斥锁,lockguard采用RAII机制自动管理锁的生命周期,在构造时加锁,析构时解锁。

4、log

log.hpp

cpp 复制代码
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include<iostream>
#include<string>
#include<cstdio>
#include<filesystem>
#include<fstream>
#include<sstream>
#include<memory>
#include<ctime>
#include<unistd.h>
#include"mutex.hpp"
using namespace std;
namespace logmodule
{
    using namespace mutexmodule;
    const string gsep ="\r\n";
    class logstrategy
    {
    public:
        virtual ~logstrategy()=default;
        virtual void synclog(const string& message)=0;
    };
    class consolelogstrategy:public logstrategy
    {
    public:
        consolelogstrategy(){}
        void synclog(const string& message) override
        {
            lockguard guard(_mutex);
            cout<<message<<gsep;
        }
        ~consolelogstrategy(){}
    private:
        mutex _mutex;
    };
    const string defaultpath="/var/log/";
    const string defaultfile="my.log";
    class filelogstrategy:public logstrategy
    {
    public:
        filelogstrategy(const string& path=defaultpath,const string& file=defaultfile)
                        :_path(path),
                        _file(file)
                        {
                            lockguard guard(_mutex);
                            if(filesystem::exists(_path))
                            {
                                return;
                            }
                            try
                            {
                              filesystem::create_directories(_path);
                            }
                            catch(const filesystem::filesystem_error& e)
                            {
                                cerr<<e.what()<<endl;
                            }
                        }
        void synclog(const string& message) override
        {
            lockguard guard(_mutex);
            string filename=_path+(_path.back()=='/'?"":"/")+_file;
            ofstream out(filename,ios::app);
            if(!out.is_open())
            {
                return;
            }
            out<<message<<gsep;
            out.close();
        }
        ~filelogstrategy(){}
    private:
        string _path;
        string _file;
        mutex _mutex;
    };
    enum class loglevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    string levelstr(loglevel lev)
    {
        switch(lev)
        {
            case loglevel::DEBUG: return "DEBUG";
            case loglevel::INFO: return "INFO";
            case loglevel::WARNING: return "WARNING";
            case loglevel::ERROR: return "ERROR";
            case loglevel::FATAL: return "FATAL";
            default: return "UNKNOWN";
        }
    }
    string gettimestamp()
    {
        time_t t=time(nullptr);
        struct tm curr_tm;
        localtime_r(&t,&curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
            curr_tm.tm_year+1900,
            curr_tm.tm_mon+1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );
        return timebuffer;
    }
    class logger
    {
    public:
        logger():_ptr(nullptr)
        {
            enableconsolelogstrategy();
        }
        void enableconsolelogstrategy()
        {
            _ptr=make_unique<consolelogstrategy>();
        }
        void enablefilelogstrategy()
        {
            _ptr=make_unique<filelogstrategy>();
        }
        class logmessage
        {
        public:
            logmessage(const string& src,loglevel level,int num,logger& logger)
            :_pid(getpid())
            ,_src(src)
            ,_num(num)
            ,_curr_time(gettimestamp())
            ,_level(level)
            ,_logger(logger)
            {
                stringstream ss;
                ss<<"["<<_curr_time<<"]"<<"["<<levelstr(_level)<<"]"<<"["<<_pid<<"]"<<"["<<_src<<"]"<<"["<<_num<<"]"<<"-";
                _loginfo=ss.str();
            }
            template<class K>
            logmessage& operator<<(const K& info)
            {
                stringstream ss;
                ss<<info;
                _loginfo+=ss.str();
                return *this;
            }
            ~logmessage()
            {
                if(_logger._ptr)
                {
                    _logger._ptr->synclog(_loginfo);
                }
            }
        private:
            string _curr_time;
            loglevel _level;
            pid_t _pid;
            string _src;
            int _num;
            string _loginfo;
            logger& _logger;
        };
        logmessage operator()(loglevel lev,const string& name,int line)
        {
            return logmessage(name,lev,line,*this);
        }
        ~logger()
        {
            
        }
    private:
        unique_ptr<logstrategy> _ptr;
    };
    static logger log;
    #define LOG(level) logmodule::log(level,__FILE__,__LINE__)
    #define Enable_Console_Log_Strategy() logmodule::log.enableconsolelogstrategy()
    #define Enable_File_Log_Strategy() logmodule::log.enablefilelogstrategy()
}
#endif

log为日志模块,与select/poll服务器实现一致,采取策略模式,定义了日志策略的抽象基类,并派生出控制台输出和文件输出两种具体策略。logger为日志器,内部持有策略对象的智能指针,嵌套了logmessage类,构造时将生成带有时间戳、进程ID、文件名和行号的日志前缀,通过operator<<流式输出,析构时将日志内容输出。

5、socket

socket.hpp

cpp 复制代码
#ifndef _SOCKET_HPP_
#define _SOCKET_HPP_
#include<iostream>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstdlib>
#include"common.hpp"
#include"log.hpp"
#include"inetaddr.hpp"
using namespace std;
namespace socketmodule
{
    using namespace logmodule;
    const static int gbacklog=16;
    class socket
    {
    public:
        virtual ~socket(){}
        virtual void socketordie()=0;
        virtual void bindordie(uint16_t port)=0;
        virtual void listenordie(int backlog)=0;
        virtual void Close()=0;
        virtual int recv(string* out)=0;
        virtual int send(const string& message)=0;
        virtual int accept(inetaddr* addr)=0;
        virtual int connect(const string& ip,uint16_t port)=0;
        virtual int fd()=0;
    public:
        void buildtcpsocket(uint16_t port,int backlog=gbacklog)
        {
            socketordie();
            bindordie(port);
            listenordie(backlog);
        }
        void buildclient()
        {
            socketordie();
        }
    };
    const static int defaultfd=-1;
    class tcpsocket:public socket
    {
    public:
        tcpsocket()
        :_sockfd(defaultfd)
        {}
        tcpsocket(int fd)
        :_sockfd(fd)
        {}
        tcpsocket(const string& ip,uint16_t port)
        {}
        ~tcpsocket()
        {}
        void socketordie() override
        {
            _sockfd=::socket(AF_INET,SOCK_STREAM,0);
            if(_sockfd<0)
            {
                LOG(loglevel::FATAL)<<"socket error";
                exit(SOCKET_ERR);
            }
            LOG(loglevel::INFO)<<"sock success";
        }
        void bindordie(uint16_t port) override
        {
            inetaddr addr(port);
            int n=::bind(_sockfd,addr.netaddrptr(),addr.netaddrlen());
            if(n<0)
            {
                LOG(loglevel::FATAL)<<"bind error";
                exit(BIND_ERR);
            }
            LOG(loglevel::INFO)<<"bind success";
        }
        void listenordie(int backlog) override
        {
            int n=listen(_sockfd,backlog);
            if(n<0)
            {
                LOG(loglevel::FATAL)<<"listen error";
                exit(LISTEN_ERR);
            }
            LOG(loglevel::INFO)<<"listen success";
        }
#define ACCEPT_ERR -3
#define ACCEPT_DONE -1
#define ACCEPT_CONTINUE -2
        int accept(inetaddr* addr) override
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int fd=::accept(_sockfd,CONV(peer),&len);
            if(fd<0)
            {
                if(errno==EAGAIN || errno==EWOULDBLOCK)
                {
                    return -1;
                }
                else if(errno==EINTR)
                {
                    return -2;
                }
                else
                {
                    LOG(loglevel::WARNING)<<"accept error";
                    return -3;
                }
            }
            return fd;
        }
        int recv(string* out) override
        {
            char buffer[4096*2];
            ssize_t n=::recv(_sockfd,buffer,sizeof(buffer)-1,0);
            if(n>0)
            {
                buffer[n]=0;
                *out+=buffer;
            }
            return n;
        }
        int send(const string& message) override
        {
            return ::send(_sockfd,message.c_str(),message.size(),0);
        }
        void Close() override
        {
            if(_sockfd>=0)
            {
                ::close(_sockfd);
            }
        }
        int connect(const string& ip,uint16_t port) override
        {
            inetaddr addr(ip,port);
            return ::connect(_sockfd,addr.netaddrptr(),addr.netaddrlen());
        }
        int fd(){return _sockfd;}
    private:
        int _sockfd;
    };
}
#endif

socket模块与select/poll服务器实现一致,对socket套接字进行了封装,定义了抽象基类socket,声明了创建、绑定、监听、关闭、收发、接受连接、连接服务器等纯虚函数。tcpsocket继承socket并实现了所有方法,提供了构建TCP服务器和客户端的辅助方法,是非阻塞模式的基础。

6、epoller

epoller模块是对epoll的完整封装,为上层Reactor实现提供了高效的事件管理接口。

epoller.hpp

(1)构造函数

cpp 复制代码
#pragma once
#include<iostream>
#include<unistd.h>
#include<sys/epoll.h>
#include"common.hpp"
#include"log.hpp"
using namespace std;
using namespace logmodule;
class epoller
{
public:
    epoller()
    :_epfd(-1)
    {
        _epfd=epoll_create(128);
        if(_epfd<0)
        {
            LOG(loglevel::FATAL)<<"epoll error";
            exit(EPOLL_CREATE_ERR);
        }
        LOG(loglevel::INFO)<<"epoll success"<<_epfd;
    }
private:
    int _epfd;
};

_epfd=epoll_create(128),构造函数通过调用epoll_create创建一个epoll实例,返回值由_epfd接收。

(2)modeventhelper

cpp 复制代码
void modeventhelper(int sockfd,uint32_t event,int oper)
{
    struct epoll_event ev;
    ev.events=event;
    ev.data.fd=sockfd;
    int n=epoll_ctl(_epfd,oper,sockfd,&ev);
    if(n<0)
    {
        LOG(loglevel::ERROR)<<"epoll_ctl error";
        return;
    }
    LOG(loglevel::INFO)<<"epoll_ctl success: "<<sockfd;
}

modeventhelper为核心函数,接收套接字描述符、事件类型和操作类型三个参数,struct epoll_event ev,ev.events=event,ev.data.fd=sockfd,声明一个epoll_event结构体,将events字段设置为传入的事件标志,data.fd设置为套接字文件描述符。int n=epoll_ctl(_epfd,oper,sockfd,&ev),随后调用epoll_ctl执行操作。

(3)addevent

cpp 复制代码
void addevent(int sockfd,uint32_t events)
{
    modeventhelper(sockfd,events,EPOLL_CTL_ADD);
}

addevent用于将事件添加到epoll中,modeventhelper(sockfd,events,EPOLL_CTL_ADD),通过调用modeventhelper并传入EPOLL_CTL_ADD操作码,用于首次将套接字加入epoll事件监听。

(4)delevent

cpp 复制代码
void delevent(int sockfd)
{
    int n=epoll_ctl(_epfd,EPOLL_CTL_DEL,sockfd,nullptr);
    (void)n;
}

delevent用于从epoll中删除事件,int n=epoll_ctl(_epfd,EPOLL_CTL_DEL,sockfd,nullptr),通过调用epoll_ctl传入EPOLL_CTL_DEL,并将最后一个参数置为nullptr,表示删除该文件描述符的所有事件,这个操作用于关闭连接。

(5)modevent

cpp 复制代码
void modevent(int sockfd,uint32_t event)
{
    modeventhelper(sockfd,event,EPOLL_CTL_MOD);
}

modevent用于修改已注册的事件,modeventhelper(sockfd,event,EPOLL_CTL_MOD),通过调用modeventhelper并传入EPOLL_CTL_MOD操作码,用于根据缓冲区状态动态调整监听写事件。

(6)waitevent

cpp 复制代码
int waitevent(struct epoll_event revs[],int maxnum,int timeout)
{
    int n=epoll_wait(_epfd,revs,maxnum,timeout);
    if(n<0)
    {
        LOG(loglevel::WARNING)<<"epoll_wait error";
    }
    else if(n==0)
    {
        LOG(loglevel::WARNING)<<"epoll wait timeout";
    }
    else
    {
            
    }
    return n;
}

waitevent对epoll_wait进行了封装,接收事件数组、数组大小和超时时间三个参数,int n=epoll_wait(_epfd,revs,maxnum,timeout),调用epoll_wait阻塞等待事件发生,n用于接收就绪事件的数量,return n,返回就绪文件描述符的数量。

(7)析构函数

cpp 复制代码
~epoller()
{
    if(_epfd>=0)
    {
        close(_epfd);
    }
}

析构函数负责清理资源,if(_epfd>=0),close(_epfd),如果_epfd有效则调用close关闭epoll实例,释放内核资源。

7、protocol

protocol模块负责协议解码和业务计算。

protocol.hpp

(1)decode

cpp 复制代码
#ifndef _PROTOCOL_HPP_
#define _PROTOCOL_HPP_
#include<iostream>
#include<string>
#include"log.hpp"
using namespace std;
using namespace logmodule;
const string sep="\r\n";
class protocol
{
public:
    protocol()
    {}
    bool decode(string& buffer,string* package)
    {
        ssize_t pos=buffer.find(sep);
        if(pos==string::npos) return false;
        *package=buffer.substr(0,pos);
        buffer.erase(0,pos+sep.size());
        return true;
    }
};
#endif

decode负责从缓冲区中提取一个完整的包,ssize_t pos=buffer.find(sep),首先在缓冲区中查找换行符的位置,if(pos==string::npos) 如果找不到说明缓冲区中没有完整的行,return false,返回false。*package=buffer.substr(0,pos),如果找到了,就从缓冲区开头截取到换行符之前的部分作为完整的包存入package,buffer.erase(0,pos+sep.size()),然后从原缓冲区中删除这一行包括换行符。

(2) excute

cpp 复制代码
string excute(string& package)
{
    cout << "excute: " << package << endl; 
    size_t pos = string::npos;
    char op = 0;
    for(char c : {'+','-','*','/','%'}) {
        pos = package.find(c);
        if(pos != string::npos) {
            op = c;
            break;
        }
    }
    if(pos == string::npos) return "ERROR\r\n";
    int x = stoi(package.substr(0, pos));
    int y = stoi(package.substr(pos + 1));
    int result = 0;
    switch(op)
    {
        case '+': result = x + y; break;
        case '-': result = x - y; break;
        case '*': result = x * y; break;
        case '/': 
            if(y == 0) return "ERROR: division by zero\r\n";
            result = x / y; 
            break;
        case '%':
            if(y == 0) return "ERROR: modulo by zero\r\n";
            result = x % y;
            break;
    }
    return to_string(result) + "\r\n";
}

excute是业务处理的核心,通过for循环查找表达式中的操作符,pos=package.find(c),遍历加减乘除取模五个字符,找到第一个出现的位置。找到操作符后,将表达式分割成左右两部分,int x=stoi(package.substr(0,pos)),int y=stoi(package.substr(pos+1)),使用stoi函数转换为整数,通过switch语句根据操作符进行计算。to_string(result)+"\r\n",计算完成后,将结果转换为字符串并加上换行符返回。

8、connection

connection模块是所有连接类的抽象基类,定义了网络连接的统一接口和公共功能,为多态处理不同类型的连接提供了基础。

connection.hpp

cpp 复制代码
#ifndef _CONNECTION_HPP_
#define _CONNECTION_HPP_
#include<iostream>
#include<string>
#include<functional>
#include"inetaddr.hpp"
using namespace std;
class Reactor;
using handler_t=function<string(string&)>;
class connection
{
public:
    connection()
    :_events(0)
    ,_owner(nullptr)
    ,_sockfd(-1)
    {

    }
    virtual void recver()=0;
    virtual void sender()=0;
    virtual void excepter()=0;
    virtual int getsockfd()=0;
    void setevent(const uint32_t& events)
    {
        _events=events;
    }
    uint32_t getevent()
    {
        return _events;
    }
    void setfd(int sockfd)
    {
        _sockfd=sockfd;
    }
    int getfd()
    {
        return _sockfd;
    }
    void setowner(Reactor* owner)
    {
        _owner=owner;
    }
    Reactor* getowner()
    {
        return _owner;
    }
    void appendoutbuffer(const string& out)
    {
    }
    string& inbuffer()
    {
        return _inbuffer;
    }
    void registerhandler(handler_t handler)
    {
        _handler=handler;
    }
    ~connection()
    {}
private:
    Reactor *_owner;
    uint32_t _events;
    int _sockfd;
    string _inbuffer;
    string _outbuffer;
public:
    handler_t _handler;
};
#endif

connection为抽象基类,recver、sender、excepter分别对应接收数据、发送数据和异常处理三种核心操作,getsockfd用于获取文件描述符。setevent、getevent用于设置和获取事件标志。setfd、getfd用于设置和获取套接字文件描述符。setowner、getowner用于设置和获取所属的Reactor对象。inbuffer用于返回输入缓冲区的引用,允许外部直接操作缓冲区。registerhandler用于注册业务处理回调函数,这是连接层与业务层解耦的关键。

9、Reactor

Reactor模块是整个服务器的核心,实现了Reactor设计模式,负责事件循环、事件分发和连接管理。Reactor将epoll的事件通知机制与业务处理逻辑完全解耦,是事件的中枢调度器。

Reactor.hpp

(1)构造函数

cpp 复制代码
#ifndef _REACTOR_HPP_
#define _REACTOR_HPP_
#include<iostream>
#include<memory>
#include<unordered_map>
#include"epoller.hpp"
#include"connection.hpp"
#include"log.hpp"
using namespace std;
using namespace logmodule;
class Reactor
{
    static const int revsnum=128;
public:
    Reactor()
    :_up(make_unique<epoller>())
    ,_isrunning(false)
    {}
    ~Reactor()
    {}
private:
    unique_ptr<epoller> _up;
    unordered_map<int,shared_ptr<connection>> _connections;
    bool _isrunning;
    struct epoll_event _revs[revsnum];
};
#endif

unique_ptr<epoller> _up,_up是一个指向epoller对象的智能指针,封装了epoll的系统调用,unordered_map<int,shared_ptr<connection>> _connections,_connections用于将文件描述符映射到对应的连接,_isrunning为运行状态标志,控制事件循环的启停。_revs为epoll_event数组,用于存放epoll_wait返回的就绪事件列表。_up(make_unique<epoller>()),_isrunning(false),构造函数通过make_unique初始化epoller对象并将运行标志设为false。

(2) 查找连接

cpp 复制代码
#ifndef _REACTOR_HPP_
#define _REACTOR_HPP_
#include<iostream>
#include<memory>
#include<unordered_map>
#include"epoller.hpp"
#include"connection.hpp"
#include"log.hpp"
using namespace std;
using namespace logmodule;
class Reactor
{
    static const int revsnum=128;
private:
    bool func(int sockfd)
    {
        auto iter=_connections.find(sockfd);
        if(iter==_connections.end())
        {
            return false;
        }
        else
        {
            return true;
        }
    }
public:
    Reactor()
    :_up(make_unique<epoller>())
    ,_isrunning(false)
    {}
    ~Reactor()
    {}
private:
    unique_ptr<epoller> _up;
    unordered_map<int,shared_ptr<connection>> _connections;
    bool _isrunning;
    struct epoll_event _revs[revsnum];
};
#endif

auto iter=_connections.find(sockfd),func用于根据文件描述符在_connections中查找是否存在对应连接,if(iter==_connections.end()),表示查找失败,return false,else查找成功,return true。

(3)connectionexist

cpp 复制代码
bool connectionexist(const shared_ptr<connection>& conn)
{
    return func(conn->getsockfd());
}
bool connectionexist(int sockfd)
{
    return func(sockfd);
}

connectionexist有两个重载版本,func(conn->getsockfd()),func(sockfd),分别接收共享指针和文件描述符,调用func完成存在性检查。

(4)connectionempty

cpp 复制代码
bool connectionempty()
{
    return _connections.empty();
}

return _connections.empty(),connectionempty用于判断连接映射表_connections是否为空。

(5) looponce

cpp 复制代码
int looponce(int timeout)
{
    return _up->waitevent(_revs,revsnum,timeout);
}

_up->waitevent(_revs,revsnum,timeout),looponce通过调用epoller的waitevent,等待事件发生并返回就绪事件数量。

(6) dispatcher

cpp 复制代码
void dispatcher(int n)
{
    for(int i=0;i<n;i++)
    {
        int sockfd=_revs[i].data.fd;
        uint32_t revents=_revs[i].events;
        if(revents & EPOLLERR)
        {
            revents|=(EPOLLIN | EPOLLOUT);
        }
        if(revents & EPOLLHUP)
        {
            revents|=(EPOLLIN | EPOLLOUT);
        }
        if(revents & EPOLLIN)
        {
            if(connectionexist(sockfd))
            _connections[sockfd]->recver();
        }
        if(revents & EPOLLOUT)
        {
            if(connectionexist(sockfd))
            _connections[sockfd]->sender();
        }
    }
}

dispatcher是核心的事件分发函数,int sockfd=_revs[i].data.fd,uint32_t revents=_revs[i].events,通过for循环遍历就绪事件数组,revents & EPOLLERR,revents & EPOLLHUP,对于错误和挂起事件,revents |=(EPOLLIN | EPOLLOUT),会主动添加读写事件标记。revents & EPOLLIN,对于读事件,_connections[sockfd]->recver(),调用连接的recver方法;revents & EPOLLOUT,对于写事件,_connections[sockfd]->sender(),调用连接的sender方法。

(7) printconnection

cpp 复制代码
void printconnection()
{
    for(auto& conn:_connections)
    {
        cout<<conn.second->getsockfd()<<" ";
    }
    cout<<"\r\n";
}

cout<< conn.second->getsockfd()<<" ",printconnection通过范围for输出当前所有连接的文件描述符列表。

(8) loop

cpp 复制代码
void loop()
{
    if(connectionempty())
    return;
    _isrunning=true;
    int timeout=-1;
    while(_isrunning)
    {
        printconnection();
        int n=looponce(timeout);
        dispatcher(n);
    }
    _isrunning=false;
}

loop是Reactor的主事件循环,int timeout=-1,timeout为超时时间,设为-1表示无限等待,通过while循环,printconnection(),int n=looponce(timeout),dispatcher(n),每次循环调用printconnection输出当前所有连接的文件描述符,然后调用looponce等待事件,最后调用dispatcher处理就绪事件。

(9) addconnection

cpp 复制代码
void addconnection(shared_ptr<connection>& conn)
{
    if(connectionexist(conn))
    {
        LOG(loglevel::WARNING)<<"connection exist"<<conn->getsockfd();
        return;
    }
    uint32_t events=conn->getevent();
    int sockfd=conn->getsockfd();
    _up->addevent(sockfd,events);
    conn->setowner(this);
    _connections[sockfd]=conn;
}

addconnection用于添加新连接,uint32_t events=conn->getevent(),int sockfd=conn->getsockfd(),获取连接关心的事件类型和文件描述符,_up->addevent(sockfd,events),调用epoller的addevent将事件加入epoll监听,conn->setowner(this),_connections[sockfd]=conn,接着设置连接的所有者为当前Reactor对象,最后将连接存入_connections中。

(10) enablereadwrite

cpp 复制代码
void enablereadwrite(int sockfd,bool read,bool write)
{
    if(!connectionexist(sockfd))
    {
        LOG(loglevel::WARNING)<<"sockfd exist"<<sockfd;
        return;
    }
    uint32_t newevent=(EPOLLET | (read?EPOLLIN:0) | (write?EPOLLOUT:0));
    _connections[sockfd]->setevent(newevent);
    _up->modevent(sockfd,newevent);
}

enablereadwrite用于动态修改某个连接的事件监听状态,uint32_t newevent=(EPOLLET | (read?EPOLLIN:0) | (write?EPOLLOUT:0)),根据read和write参数组合新的事件标志,_connections[sockfd]->setevent(newevent),_up->modevent(sockfd,newevent),最后更新连接的事件标志并调用epoller的modevent修改监听事件。

(11) delconnection

cpp 复制代码
void delconnection(int sockfd)
{
    _up->delevent(sockfd);
    _connections.erase(sockfd);
    close(sockfd);
}

delconnection用于删除连接,_up->delevent(sockfd),_connections.erase(sockfd),close(sockfd),首先从epoll删除该文件描述符的事件,然后从_connections中移除,最后关闭套接字文件描述符。

(12) stop

cpp 复制代码
void stop()
{
    _isrunning=false;
}

_isrunning=false,stop通过将_isrunning运行标志设为false,从而终止事件循环。

10、channel

channel模块继承自connection抽象基类,用于封装一个非阻塞TCP连接的具体行为。

channel.hpp

(1)构造函数

cpp 复制代码
#ifndef _CHANNEL_HPP_
#define _CHANNEL_HPP_
#include"Reactor.hpp"
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<memory>
#include<functional>
#include"common.hpp"
#include"connection.hpp"
#include"log.hpp"
#include"inetaddr.hpp"
using namespace std;
using namespace logmodule;
#define SIZE 1024
class channel:public connection
{
public:
    channel(int sockfd,const inetaddr& addr)
    :_sockfd(sockfd)
    ,_addr(addr)
    {
        setnonblock(_sockfd);
    }
private:
    int _sockfd;
    inetaddr _addr;
    string _inbuffer;
    string _outbuffer;
};
#include"Reactor.hpp"
#endif

构造函数通过接收套接字文件描述符sockfd和地址对象addr,setnonblock(_sockfd),调用setnonblock将套接字设置为非阻塞模式。

(2)recver

cpp 复制代码
void recver() override
{
    char buffer[SIZE];
    while(true)
    {
        buffer[0]=0;
        ssize_t n=recv(_sockfd,buffer,sizeof(buffer)-1,0);
        if(n>0)
        {
            buffer[n]=0;
            _inbuffer+=buffer;
        }
        else if(n==0)
        {
            excepter();
            return;
        }
        else
        {
            if(errno==EAGAIN || errno==EWOULDBLOCK)
            {
                break;
            }
            else if(errno==EINTR)
            {
                continue;
            }
            else
            {
                excepter();
                return;
            }
        }
    }
    if(!_inbuffer.empty() && _handler)
    {
        _outbuffer+=_handler(_inbuffer);
        _inbuffer.clear();
    }
    if(!_outbuffer.empty())
    {
        sender();
        if(!_outbuffer.empty())
        {
            getowner()->enablereadwrite(_sockfd,true,true);
        }
    }
}

ssize_t n=recv(_sockfd,buffer,sizeof(buffer)-1,0),recver通过while循环调用recv读取数据直到缓冲区无新数据或遇到EAGAIN错误,buffer[n]=0,_inbuffer+=buffer,将读取到的内容追加到_inbuffer中,if(!_inbuffer.empty() && _handler),如果输入缓冲区非空且存在回调处理函数_handler,_outbuffer+=_handler(_inbuffer),_inbuffer.clear(),则调用_handler处理输入数据,将生成的响应内容存入_outbuffer并清空输入缓冲区,sender(),随后调用sender发送响应数据,if(!_outbuffer.empty()),如果输出缓冲区不为空,getowner()->enablereadwrite(_sockfd,true,true),则调整Reactor监听读写事件。

(3)sender

cpp 复制代码
void sender() override
{
    while(true)
    {
        ssize_t n=send(_sockfd,_outbuffer.c_str(),_outbuffer.size(),0);
        if(n>0)
        {
            _outbuffer.erase(0,n);
            if(_outbuffer.empty())
                break;
        }
        else if(n==0)
        {
            break;
        }
        else
        {
            if(errno==EAGAIN || errno==EWOULDBLOCK)
            {
                break;
            }
            if(errno==EINTR)
            {
                continue;
            }
            else
            {
                excepter();
                return;
            }
        }
        }
        if(!_outbuffer.empty())
        {
            getowner()->enablereadwrite(_sockfd,true,true);
        }
        else
        {
            getowner()->enablereadwrite(_sockfd,true,false);
        }
    }
}

ssize_t n=send(_sockfd,_outbuffer.c_str(),_outbuffer.size(),0),sender通过while循环调用send发送_outbuffer中的数据,直到全部发完或发送缓冲区满,if(!_outbuffer.empty()),发送完毕后根据是否还有待发数据,通过getowner()->enablereadwrite动态调整Reactor监听的事件类型,getowner()->enablereadwrite(_sockfd,true,true),如果还有数据未发完就同时监听读写事件,

getowner()->enablereadwrite(_sockfd,true,false),else只监听读事件。

(4)excepter

cpp 复制代码
void excepter() override
{
    getowner()->delconnection(_sockfd);
}

excepter()用于关闭连接,当连接异常、对端关闭或发生错误时,getowner()->delconnection(_sockfd),excepter会调用Reactor的delconnection移除并关闭连接。

(5)getsockfd

cpp 复制代码
int getsockfd() override
{
    return _sockfd;
}

return _sockfd,getsockfd用于获取_sockfd套接字文件描述符。

(6)inbuffer

cpp 复制代码
string& inbuffer() 
{
    return _inbuffer;
}

return _inbuffer,inbuffer用于返回输入缓冲区的内容。

(7)appendoutbuffer

cpp 复制代码
void appendoutbuffer(const string& out) 
{
    _outbuffer+=out;
}

_outbuffer+=out,appendoutbuffer用于实现将数据追加到输出缓冲区_outbuffer中。

11、listener

listener模块继承自connection基类,用于监听端口并接受新的客户端连接。

listener.hpp

(1)构造函数

cpp 复制代码
#ifndef _LISTENER_HPP_
#define _LISTENER_HPP_
#include<iostream>
#include<memory>
#include"epoller.hpp"
#include"socket.hpp"
#include"connection.hpp"
#include"common.hpp"
#include"channel.hpp"
using namespace std;
using namespace socketmodule;
class listener:public connection
{
public:
    listener(int port=defaultport)
    :_port(port)
    ,_listensock(make_unique<tcpsocket>())
    {
        _listensock->buildtcpsocket(_port);
        setevent(EPOLLIN | EPOLLET);
        setnonblock(_listensock->fd());
        setfd(_listensock->fd());
    }
private:
    int _port;
    unique_ptr<socketmodule::socket> _listensock;
};
#include"Reactor.hpp"
#endif

_listensock(make_unique<tcpsocket>()),通过make_unique创建tcpsocket对象,_listensock->buildtcpsocket(_port),调用buildtcpsocket绑定_port端口开始监听,setevent(EPOLLIN | EPOLLET),调用setevent将套接字设置为非阻塞边缘触发,setfd(_listensock->fd()),通过setfd将文件描述符记录到基类中。

(2)recver

cpp 复制代码
void recver() override
{
    inetaddr addr;
    while(true)
    {
        int sockfd=_listensock->accept(&addr);
        if(sockfd==ACCEPT_ERR)
            break;
        else if(sockfd==ACCEPT_CONTINUE)
            continue;
        else if(sockfd==ACCEPT_DONE)
            break;
        else
        {
            shared_ptr<connection> sp=make_shared<channel>(sockfd,addr);
            sp->setevent(EPOLLIN | EPOLLET);
            if(_handler!=nullptr)
            sp->registerhandler(_handler);
            getowner()->addconnection(sp);
        }
    }
}

int sockfd=_listensock->accept(&addr),recver通过while循环调用accept尽可能多地接收连接,shared_ptr<connection> sp=make_shared<channel>(sockfd,addr),每成功接收一个新的客户端套接字,就创建一个channel对象来封装这个连接,sp->setevent(EPOLLIN | EPOLLET),设置其事件为边缘可读,if(_handler!=nullptr),sp->registerhandler(_handler),如果外部通过registerhandler设置了回调函数则一并传递给新连接,getowner()->addconnection(sp),最后通过getowner将新连接注册到Reactor中统一管理。

(3)getsockfd

cpp 复制代码
int getsockfd() override
{
    return _listensock->fd();
}

return _listensock->fd(),getsockfd用于获取_listensock对应的文件描述符fd。

12、main

main模块实现了将该服务器的所有组件串联起来,启动了一个基于Reactor模式的事件驱动服务器。

main.cc

cpp 复制代码
#include<iostream>
#include<string>
#include"connection.hpp"
#include"listener.hpp"
#include"channel.hpp"
#include"log.hpp"
#include"common.hpp"
#include"protocol.hpp"
using namespace std;
static void usage(string proc)
{
    cerr<<"usage:"<<proc<<"port"<<endl;
}
int main(int argc,char*argv[])
{
    if(argc!=2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    Enable_Console_Log_Strategy();
    uint16_t port=stoi(argv[1]);
    shared_ptr<protocol> pp=make_shared<protocol>();
    shared_ptr<connection> sp=make_shared<listener>(port);
    sp->registerhandler([pp](string& inbuffer)->string{
        cout<<"handler called"<<endl;
        string repstr;
        while(true)
        {
            string package;
            if(!pp->decode(inbuffer,&package))
                break;
            repstr+=pp->excute(package);
        }
        return repstr;
    });
    unique_ptr<Reactor> rp=make_unique<Reactor>();
    rp->addconnection(sp);
    rp->loop();
    return 0;
}

Enable_Console_Log_Strategy(),启动控制台日志输出,shared_ptr<protocol> pp=make_shared<protocol>(),通过make_shared创建protocol对象用来处理协议的编解码和执行逻辑,uint16_t port=stoi(argv[1]),shared_ptr<connection> sp=make_shared<listener>(port),通过make_shared创建listener对象并绑定到指定端口。通过调用sp->registerhandler为listener注册一个回调函数,当有数据到达时,这个回调函数将被调用,if(!pp->decode(inbuffer,&package)),通过while循环调用protocol的decode从输入缓冲区中提取完整的数据包,repstr+=pp->excute(package),每提取出一个包就调用excute执行相应的业务逻辑并将结果拼接起来,return repstr,最后返回响应字符串。unique_ptr<Reactor> rp=make_unique<Reactor>(),rp->addconnection(sp),rp->loop(),通过make_unique创建一个Reactor对象,将listener作为第一个连接添加进去,最后调用loop启动主循环。由于listener本身也是connection的子类,Reactor会监听它的读事件,每当有新客户端连接进来时,listener的recver会被触发,它会接受新连接并自动创建对应的channel对象添加到Reactor中,同时把listener上的回调函数也注册到这些新channel上,从而实现对所有客户端连接的统一处理。通过一个监听器加上一个Reactor事件循环,调用回调函数将协议处理逻辑注入到连接中。

Makefile:

bash 复制代码
Reactorserver:main.cc
	g++ -o $@ $^  -std=c++17
.PHONY:clean
clean:
	rm -rf Reactorserver

通过Makefile即可实现一键化编译,g++ -o @ ^ -std=c++17,采用c++17标准,服务器运行如下所示:

Makefile编译通过后,./Reactorserver 8925,绑定端口号8925,服务器开始运行,打开另一个终端,telnet 127.0.0.1 8925,进行连接,客户端向服务器发送消息,服务器可执行相应的计算功能,并将结果发送给客户端,结果如下所示:

客户端:

服务器:

结语

I/O多路复用的核心思想就是让一个线程同时监控多个文件描述符,只有当文件描述符就绪时才去处理,而不是每个文件描述符都阻塞等待。select/poll/epoll是Linux下经典的I/O多路复用方式,select有文件描述符数量上限,且每次调用都需要重新传递整个文件描述符集合,效率随文件描述符数量增多而线性下降。poll突破了数量限制,但仍需线性扫描所有文件描述符找出就绪者。epoll是高效的I/O复用方式,采用事件驱动机制,通过内核维护一个就绪列表,用户只需要索取就绪事件而不需要每次都重新传递全部文件描述符,同时也支持边缘触发和水平触发两种模式,非常适合大规模高并发场景。Reactor是一种事件驱动的编程模式,通过一个或多个事件循环来监听和处理各种事件,从而实现高效的并发处理。在基于epoll的Reactor服务器实现过程中,从最底层的epoll事件驱动机制出发,构建了一个非阻塞的事件处理框架,多路复用使得用少量的线程就能管理大量的连接,而Reactor模式则将连接的建立、读写、异常处理等操作抽象为对事件的响应,这种设计模式改变了程序的执行流程,不再是主动调用函数去读写,而是等待事件到来触发处理逻辑,这种反转正是高并发服务器实现的核心所在。

相关推荐
楼田莉子1 小时前
仿Muduo的高并发服务器:基于HTTP的HTTP服务器及其测试
运维·服务器·http
kyle~1 小时前
Linux时间系统3---时间同步控制机制(step、slew、offset、frequency)
linux·运维·服务器
葱卤山猪1 小时前
【自用】解析http post表单数据,将其中的二进制数据保存到csv文件且加载到内存
网络·网络协议·http
Mike117.1 小时前
GBase 8c 序列用在业务流水号上要留几道边界
服务器·数据库
零壹AI实验室1 小时前
DeepSeek本地部署:从零开始,把大模型跑在自己电脑上
服务器·网络·人工智能·电脑
西柚小萌新1 小时前
【计算机常识】--使用 Gitea 在本地/内网搭建 Git 私有服务器
服务器·git·gitea
铅笔小新z1 小时前
【Linux】进程间通信(IPC)
java·linux·运维
WL_Aurora1 小时前
Shell编程从入门到实战
linux
stanleyrain1 小时前
Windows 实现 Linux 风格“选中即复制,中键即粘贴”操作指南
linux·运维·windows