如何将Reactor模式应用于服务器的开发

前言
之前翻阅了大量的资料,查看有关于Reactor模式和Proactor模式的原理及两者的应用场景,但是始终不得其解;前两天看了一位资深开发工程师的博客后,方才豁然开朗;本文是在借鉴技术大牛文章的基础上,结合自身的理解才得以完成,并非原创。写技术博客的目的是为了加深自己的理解,将书本上的精华转化为自己的知识。下面以Epoll模型为例,介绍如何将Reactor模式应用于服务器的开发中。
Reactor模式的原理
现实生活中,凡是有稳定架构和一定规模的单位内部都会有明确的分工,老板负责去外面洽谈业务,员工负责将老板接下来的订单完成。在服务器开发中也是如此,主线程只负责监听套接字上所发生的事情(连接事件、读写事件等),当有指定的事件发生时便通知工作线程去处理相应的事件。就好比餐厅的服务员只负责接待客人、填写菜单的工作,具体做菜则由后房厨师负责完成。下面以epoll模型为例阐述Reactor模式的工作原理。
1、主线程通过epoll_create()创建一个Epoll例程并注册socket上的读就绪事件。
2、主线程调用epoll_wait()等待读事件的发生,当socket上有数据可读时,则通知主线程。
3、主线程将有数据可读的套接字放入读请求队列中。
4、睡眠在请求队列上的某个工作线程被唤醒,从请求队列中拿出有数据可读的套接字,并将数据读取出。
5、工作线程读取完数据并处理数据,一般情况下TCP窗口是有空闲资源的(滑动窗口的本意就是为了体现TCP是一种稳定可靠的传输协议,如果TCP窗口没空余资源了,那么数据的传输也会终止),本端可以直接向对端发送数据。
6、当调用send函数无法发送数据且错误码为EWOULDBLOCK时,主线程才向操作系统申请创建一个epoll例程,并注册写就绪事件;同时将未发送出去的数据暂存在发送缓冲区中。
7、主线程调用epoll_wait()监听写就绪事件,当套接字可写时,便在指定的工作线程中继续发送之前未发送出去的数据,发送完数据,即刻移除写事件。

Reactor模式的流程图如下:
Reactor模式流程图
上图的Reactor模式中涉及到注册写就绪事件,在理论上是可行的,但是在实际的开发环境下并非如此,下面以一张简图加以说明。
实际开发中发送数据的流程图
在通信过程中,TCP窗口一般都是有空闲资源的,如此epoll_wait便会不断地监听到写就绪事件。如果出现一种情况,对端一直不接收数据,那么本端持续地发数据,数据发不出去便存储到发送缓冲区,久而久之发送缓冲区会被填满;因此为了避免这种情况的发生,需要加一个逻辑判断;给发送缓冲区设定一个水位线,如果发送缓冲区中存储的数据量超过了水位线,清空发送缓冲区中的数据,同时关闭这一条连接。
但这种思路同样存在效率问题,每次发不出去数据时才去检测发送缓冲区中的数据量,再决定要不要关闭这条连接;其实我们可以做一个定时器,不定时去检测发送缓冲区中的数据量是否超过水位线,同时发送一条数据给对端,如果仍然发不出去则关闭这条连接。
Reactor模式的应用

#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>  
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <signal.h>   
#include <pthread.h>
#include <semaphore.h>
#include <list>
#include <errno.h>
#include <time.h>
#include <sstream>
#include <iomanip>
#include <stdlib.h>
#define WORKER_THREAD_NUM   5
#define min(a, b) ((a <= b) ? (a) : (b)) 

int g_epollfd = 0;
bool g_bStop = false;
int g_listenfd = 0;
pthread_t g_acceptthreadid = 0;
pthread_t g_threadid[WORKER_THREAD_NUM] = { 0 };

//当listenfd上有连接请求时,多个线程会去争取这个连接请求,并和客户端建立连接,单纯地使用互斥锁会比较浪费资源,结合条件变量会提高效率
pthread_cond_t  g_acceptcond;
pthread_mutex_t  g_acceptmutex;

pthread_cond_t g_cond;
pthread_mutex_t g_mutex;

pthread_mutex_t g_clientmutex;
//全局队列g_listClients用于存储可写或可读套接字
std::list<int> g_listClients;
//退出或者发生异常中断事件时,执行资源的清除工作
void prog_exit(int signo)
{
	//注册相应的信号对应的处理事件
    ::signal(SIGINT, SIG_IGN);
    ::signal(SIGTERM, SIG_IGN);
     std::cout << "program recv signal " << signo << " to exit." << std::endl;
     g_bStop = true;
    ::epoll_ctl(g_epollfd, EPOLL_CTL_DEL, g_listenfd, NULL);
    ::shutdown(g_listenfd, SHUT_RDWR);
    ::close(g_listenfd);
    ::close(g_epollfd);
    ::pthread_cond_destroy(&g_acceptcond);
    ::pthread_mutex_destroy(&g_acceptmutex);
    ::pthread_cond_destroy(&g_cond);
    ::pthread_mutex_destroy(&g_mutex);
    ::pthread_mutex_destroy(&g_clientmutex);
}
bool create_server_listener(const char* ip, short port)
{
    g_listenfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
    if (g_listenfd == -1)
        return false;
	//设置端口和地址可重用,服务器重启时不会出现地址或端口绑定失败,及大量的time_wait状态
    int on = 1;
    ::setsockopt(g_listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
    ::setsockopt(g_listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr)); 
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(ip);
    servaddr.sin_port = htons(port);
    if (::bind(g_listenfd, (sockaddr *)&servaddr, sizeof(servaddr)) == -1)
        return false;
    //构建一个大小为50的连接请求队列
    if (::listen(g_listenfd, 50) == -1)
        return false;
	//向操作系统请求创建一个epoll例程
    g_epollfd = ::epoll_create(1);
    if (g_epollfd == -1)
        return false;
    struct epoll_event e;
    memset(&e, 0, sizeof(e));
	//注册事件类型
    e.events = EPOLLIN | EPOLLRDHUP;
    e.data.fd = g_listenfd;
    //将g_listenfd加到epoll例程g_epollfd中监听e中注册的事件
    if (::epoll_ctl(g_epollfd, EPOLL_CTL_ADD, g_listenfd, &e) == -1)
        return false;
    return true;
}
//从对列中移除客户端的套接字
void release_client(int clientfd)
{
    if (::epoll_ctl(g_epollfd, EPOLL_CTL_DEL, clientfd, NULL) == -1)
        std::cout << "release client socket failed as call epoll_ctl failed" << std::endl;
    ::close(clientfd);
}
//接收线程函数
void* accept_thread_func(void* arg)
{   
    while (!g_bStop)
    {
       //条件变量和互斥锁搭配使用
        ::pthread_mutex_lock(&g_acceptmutex);
        ::pthread_cond_wait(&g_acceptcond, &g_acceptmutex);
        struct sockaddr_in clientaddr;
        socklen_t addrlen;
        int newfd = ::accept(g_listenfd, (struct sockaddr *)&clientaddr, &addrlen);
        ::pthread_mutex_unlock(&g_acceptmutex);
        if (newfd == -1)
            continue;
        std::cout << "new client connected: " << ::inet_ntoa(clientaddr.sin_addr) <<
			":" << ::ntohs(clientaddr.sin_port) << std::endl;

        //将客户端套接字newfd设置为non-blocking
        int oldflag = ::fcntl(newfd, F_GETFL, 0);
        int newflag = oldflag | O_NONBLOCK;
        if (::fcntl(newfd, F_SETFL, newflag) == -1)
        {
            std::cout << "fcntl error, oldflag =" << oldflag << ", newflag = " << newflag << std::endl;
            continue;
        }

        struct epoll_event e;
        memset(&e, 0, sizeof(e));
        //将event事件类型设置成边缘触发的模式
        e.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
        e.data.fd = newfd;
        if (::epoll_ctl(g_epollfd, EPOLL_CTL_ADD, newfd, &e) == -1)
            std::cout << "epoll_ctl error, fd =" << newfd << std::endl;
    }
    return NULL;
}
//工作线程
void* worker_thread_func(void* arg)
{   
    while (!g_bStop)
    {
        int clientfd;
        ::pthread_mutex_lock(&g_clientmutex);
        //防止工作线程出现虚假唤醒
        while (g_listClients.empty())
            ::pthread_cond_wait(&g_cond, &g_clientmutex);
   
        clientfd = g_listClients.front();
        g_listClients.pop_front();  
        pthread_mutex_unlock(&g_clientmutex);
        std::string strclientmsg;
        char buff[256];
        bool bError = false;
        while (true)
        {
            memset(buff, 0, sizeof(buff));
            int nRecv = ::recv(clientfd, buff, 256, 0);
            //对非阻塞套接字执行Send或recv函数时,返回值分为三种情况:大于0、小于0、等于0;小于0时还要结合errno进行判断
            if (nRecv == -1)
            {
                if (errno == EWOULDBLOCK)
                {
                  //错误码为EWOULDBLOCK表示资源不可用,需要重试或者等待
                    break;
                }
                else
                {
                    std::cout << "recv error, client disconnected, fd = "
					<< clientfd << std::endl;
                    release_client(clientfd);
                    bError = true;
                    break;
                } 
            }
            //对端关闭了socket,这端也关闭。
            else if (nRecv == 0)
            {
                std::cout << "peer closed, client disconnected, fd = " << clientfd << std::endl;
                release_client(clientfd);
                bError = true;
                break;
            }
            strclientmsg += buff;
        }
        //出错了,就不要再继续往下执行了
        if (bError)
            continue;
        std::cout << "client msg: " << strclientmsg;

        //将消息加上时间标签后发回
        time_t now = time(NULL);
        struct tm* nowstr = localtime(&now);
        std::ostringstream ostimestr;
        ostimestr << "[" << nowstr->tm_year + 1900 << "-" 
                  << std::setw(2) << std::setfill('0') << nowstr->tm_mon + 1 << "-" 
                  << std::setw(2) << std::setfill('0') << nowstr->tm_mday << " "
                  << std::setw(2) << std::setfill('0') << nowstr->tm_hour << ":" 
                  << std::setw(2) << std::setfill('0') << nowstr->tm_min << ":" 
                  << std::setw(2) << std::setfill('0') << nowstr->tm_sec << "]server reply: ";
        strclientmsg.insert(0, ostimestr.str());
        while (true)
        {
            int nSent = ::send(clientfd, strclientmsg.c_str(), strclientmsg.length(), 0);
            if (nSent == -1)
            {
                if (errno == EWOULDBLOCK)
                {
                //当数据发送不出去时,此时需要向全局的epoll例程中注册写就绪事件,再次循环执行写操作
                     struct epoll_event e;
					 memset(&e, 0, sizeof(e));
					 e.events = EPOLLOUT | EPOLLRDHUP | EPOLLET;
					 e.data.fd = clientfd;
					 //向全局的epoll例程中注册写就绪事件
					 if(::epoll_ctl(g_epollfd, EPOLL_CTL_ADD, clientfd, &e) == -1)
					 {
						std::cout<<"epoll_ctl error,fd =" <<clientfd << std::endl;	
					 }
					 continue;
                }
                else
                {
                    std::cout << "send error, fd = " << clientfd << std::endl;
                    release_client(clientfd);
                    break;
                }  
            }          
            std::cout << "send: " << strclientmsg;
            strclientmsg.erase(0, nSent);
            if (strclientmsg.empty())
                break;
        }
    }
    return NULL;
}

void daemon_run()
{
    int pid;
    signal(SIGCHLD, SIG_IGN);
    pid = fork();
    if (pid < 0)
    {
        std:: cout << "fork error" << std::endl;
        exit(-1);
    }
    //父进程退出,子进程独立运行
    else if (pid > 0) {
        exit(0);
    }
    //之前parent和child运行在同一个session里,parent是会话(session)的领头进程, parent进程作为会话的领头进程;如果exit结束执行的话,那么子进程会成为孤儿进程,会被操作系统的init进程收养。
    //执行setsid()之后,child将重新获得一个新的会话(session),并成为新的Session的领头进程,同时脱离和原Session 和Parent进程间的关系
    setsid();
    int fd;
    fd = open("/dev/null", O_RDWR, 0);
    if (fd != -1)
    {
      //将标准输入、标准输出、标准错误指向的file结构体释放掉,使得这三个文件描述符指向fd指向的file结构体
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
    }
    if (fd > 2)
        close(fd);
}
int main(int argc, char* argv[])
{  
    short port = 0;
    int ch;
    bool bdaemon = false;
    while ((ch = getopt(argc, argv, "p:d")) != -1)
    {
        switch (ch)
        {
        case 'd':
            bdaemon = true;
            break;
        case 'p':
            port = atol(optarg);
            break;
        }
    }
    if (bdaemon)
        daemon_run();
    if (port == 0)
        port = 12345
    if (!create_server_listener("0.0.0.0", port))
    {
        std::cout << "Unable to create listen server: ip=0.0.0.0, port=" << port << "." << std::endl;
        return -1;
    }
    //设置信号处理
    signal(SIGCHLD, SIG_DFL);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, prog_exit);
    signal(SIGTERM, prog_exit);

    ::pthread_cond_init(&g_acceptcond, NULL);
    ::pthread_mutex_init(&g_acceptmutex, NULL);

    ::pthread_cond_init(&g_cond, NULL);
    ::pthread_mutex_init(&g_mutex, NULL);

    ::pthread_mutex_init(&g_clientmutex, NULL);
     
    ::pthread_create(&g_acceptthreadid, NULL, accept_thread_func, NULL);
    //启动工作线程,五个工作线程去处理业务
    for (int i = 0; i < WORKER_THREAD_NUM; ++i)
    {
        ::pthread_create(&g_threadid[i], NULL, worker_thread_func, NULL);
    }
    while (!g_bStop)
    {       
        struct epoll_event ev[1024];
        //主线程调用epoll_wait去监听套接字上相应事件的发生
        int n = ::epoll_wait(g_epollfd, ev, 1024, 10);
        if (n == 0)
            continue;
        else if (n < 0)
        {
            std::cout << "epoll_wait error" << std::endl;
            continue;
        }
        int m = min(n, 1024);
        for (int i = 0; i < m; ++i)
        {
            //通知接收连接线程接收新连接
            if (ev[i].data.fd == g_listenfd)
                pthread_cond_signal(&g_acceptcond);
            else
            {        
                pthread_mutex_lock(&g_clientmutex);              
                g_listClients.push_back(ev[i].data.fd);
                pthread_mutex_unlock(&g_clientmutex);
                //通知工作线程从队列中取出套接字,并对取出的套接字执行读或写操作
                pthread_cond_signal(&g_cond);
            }     
        }
    }
    return 0;
}

重难点剖析
1、关于虚假唤醒的处理方法
由于Linux中futex原则,信号的发送一般遵循“宁可错发,也不可漏发”的规律,当请求队列中只有一个套接字可用时,可能会唤醒多个线程去抢夺该套接字;顺利抢到该套接字的线程便可执行相应的业务,但是未获得套接字资源的线程便处于虚假唤醒的状态;为了避免这种情况的发生,一般采用一个while循环来判断请求队列中是否有资源,没有资源时,便继续等待。处理虚假唤醒的代码如下:

 while (g_listClients.empty())
            ::pthread_cond_wait(&g_cond, &g_clientmutex);

2、pthread_cond_wait()函数的使用方法
使用pthread_cond_wait函数时,必须先获得与该条件变量相关的互斥锁,也即是如下一段代码:

::pthread_mutex_lock(&g_acceptmutex);
::pthread_cond_wait(&g_acceptcond,&g_acceptmutex);
::pthread_mutex_unlock(&g_acceptmutex);

如果等待条件变量唤醒和线程解锁操作不是原子性的操作,那么假设当线程A解锁后cpu的时间片用完了,线程B获得cpu时间片并获得互斥锁资源,此时线程B发现条件变量得到满足并发出条件满足的信号;但此时线程A依然没获得cpu的时间片,就无法得知条件已经满足,当线程A再次获得cpu时间片进入临界区后,便会一直阻塞在pthread_cond_wait函数上。
因此等待条件变量唤醒和释放互斥锁资源必须是原子性的,也即是当线程释放了互斥锁,那么代表该线程已经在条件变量上被唤醒。

3、设置地址和端口可重用
在socket通信中,客户端和服务器端在通信过程中,无论哪一方先关闭连接,都会进入time_wait状态,为什么会有time_wait状态的出现?因为数据包在网络环境中有2MSL的生存周期,当端口被其他进程给迅速地占用了,那么会出现数据包错发给其他进程的情况,因此关闭的端口会进入Time_Wait状态。但是高性能的服务器追求效率,当服务器重启时,能够迅速地再次绑定之前的端口号而不是要等待2MSL的时长。设置端口和地址可重用的代码如下:

   int on =1;
   //设置地址可重用
	setsockopt(g_listenfd, SOL_SOCKET, SOCK_REUSEADDR, (char *)&on, sizeof(on));
	//设置端口可重用
	setsockopt(g_listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));

4、主线程使用epoll_wait()监听套接字可读事件时采用边缘触发模式
如果epoll_wait采用水平触发模式,当scoket上有数据可读时便会通知主线程调用工作线程去读取数据,如果工作线程没有把socket上的数据读取完,那么epoll_wait()会再次通知主线程调用工作线程去读取socket上的数据;如此会造成多个线程去读取一个socket上的数据,造成数据读取紊乱。
epoll_wait()采用水平触发的模式,只有socket上的数据被读取完了,当客户端发送新的数据过来时才会通知主线程调用工作线程读取socket上的数据。将event事件设置成边缘触发的方法如下:
struct event e;
memset(&e, 0, sizeof(e));
e.events = EPOLLET | EPOLLRDHUP | EPOLLIN
结束语
在Linux上完成编译生成可执行文件,通过&符号将该可执行文件运行在后台,通过telnet命令或nc命令模拟客户端与该服务器进行通信。
telnet 127.0.0.1 12345
或者
nc -v 127.0.0.1 12345
向服务器发送一条数据,服务器接收数据并加上时间戳返回给客户端。由于本机没有安装Linux环境,程序的运行结果位于另外一台主机上,这里就不附上程序的运行结果了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值