高并发服务端程序的演进--从10k到100w

什么是服务端程序

    按照我个人的理解,通常服务端程序是指运行在一个拥有公网ip的主机上,处理客户端程序通过网络连接(通常是socket)发送的请求,返回相应的结果数据的一类程序。我们通常说的C/S结构中的S也通常指运行在Server上的某一个程序。这种处理方式很明显是一个中心化的结构,即服务端程序(server)为中心,各个客户端统一连接到这个中心获取自己关心的数据。这种结构就会无可避免的遇到一个问题,在同一时刻,非常多的客户端来请求数据,所以处理高并发连接请求是服务端程序面临最大挑战。这篇文章只讨论单台机器处理并发请求的程序模型演进,一步步的演示服务程序是如何最高效的利用机器性能来处理最高几十万甚至百万的客户端请求。

简单的socket server程序

    下面我们先看看一个最基本实现响应客户端请求的服务端程序

    void exec()
    {
        int m_sock = ::socket(PF_INET, SOCK_STREAM, 0);
        if (m_sock < 0)
        {
            std::cout << "socket()err:" << errno << std::endl;
            return;
        }

        int on = 1;
        if (::setsockopt(m_sock, SOL_SOCKET, SO_REUSEADDR, (const char *) &on, sizeof(on)) == -1)
        {
            std::cout << "setsockopt()err:" << errno << std::endl;
            return;
        }

#if defined(SO_NOSIGPIPE) && !defined(MSG_NOSIGNAL)//MACOS || IOS
        if (::setsockopt(m_sock, SOL_SOCKET, SO_NOSIGPIPE, (const char *) &on, sizeof(on)) == -1)
        {
            std::cout << "setsockopt()pipe err:" << errno << std::endl;
            return;
        }
#endif
        sockaddr_in m_addr = {0};
        m_addr.sin_family = AF_INET;
        m_addr.sin_addr.s_addr = INADDR_ANY;
        m_addr.sin_port = htons (666);

        int n_ret = ::bind(m_sock, (struct sockaddr *) &m_addr, sizeof(m_addr));
        if (n_ret == -1)
        {
            std::cout << "bind()err:" << errno << std::endl;
            return;
        }


        int n_et = ::listen(m_sock, 5);

        if (n_et == -1)
        {
            std::cout << "listen()err:" << errno << std::endl;
            return;
        }

        int addr_length = sizeof(m_addr);
        while (true)
        {
            int socket_data = ::accept(m_sock, (sockaddr *) &m_addr, (socklen_t *) &addr_length);
            if (socket_data > 0)
            {
                //read write through socket_data
                char buf[256] = {0};
                ssize_t status = ::recv(socket_data, buf, 256, 0);
                if (status > 0)
                {
                    ::send(socket_data, buf, status, 0);
                }

                ::close(socket_data);
            }
        }

    }

    上面的代码实现了最基本的socket TCP连接,通过创建socket,绑定端口,监听,accept等步骤完成服务端的初始化;这时候客户端如果有连接到达,那么阻塞的accept函数就会返回,它返回一个socketfd,我们就可以在这个fd上读取客户端发来的信息,进而将信息写回给这个fd,远端的客户端就会收到相应的信息。
    显然这个极端简单的服务端程序出了细节和边界条件没有处理外,完全谈不到任何性能。因为程序一直等待客户端的连接到来,连接到来后程序进行读写,但是在读写的过程中由于网络情况完全有可能造成read的阻塞,这样一来整个程序被阻塞,后续的其他客户端程序连接无法被相应,即便整个系统的负载极低,但也无法处理后续的连接,直到之前的连接数据顺利读取完毕。
    有点经验的程序员应该马上就会想到用线程。下面看一下用线程处理每一个读写,这样就不会阻塞整个系统的运作,让整个程序每时每刻都在处理连接读写。

        while (true)
        {
            int socket_data = ::accept(m_sock, (sockaddr *) &m_addr, (socklen_t *) &addr_length);
            if (socket_data > 0)
            {
                std::thread t([socket_data]()-> void
                {
                    //read write through socket_data
                    char buf[256] = {0};
                    ssize_t status = ::recv(socket_data, buf, 256, 0);
                    if (status > 0)
                    {
                        ::send(socket_data, buf, status, 0);
                    }

                    ::close(socket_data);
                });
                t.detach();
            }
        }

    只需要将刚才代码中的while循环中的代码替换成上述代码,即可实现我们的诉求。现在再来看程序执行情况,每一个连接进来的socket的读写都会在一个单独线程中被处理,程序不会因为一个socket的读写阻塞而影响其他socket的读写。这样看来这个修改明显改善了之前程序的处理能力。但是问题在于当有大量的连接请求来到时,瞬间会开启大量线程,而线程是系统核心资源,大量线程会造成系统资源耗尽(不同操作系统对于进程能开辟的线程数量也有限制),另外大量线程竞争会造成cpu频繁切换时间片,使整个系统的整体效率降低。
    更好的一种实现是“线程池”,用固定的线程(或者有增长数量上线的线程)去处理请求。这样就可以尽量避免上述程序的缺点。

        stdx::ThreadPool threadpool(4, 10);
        while (true)
        {
            int socket_data = ::accept(m_sock, (sockaddr *) &m_addr, (socklen_t *) &addr_length);
            if (socket_data > 0)
            {
                threadpool.commit([](int fd)-> void
                                  {
                                      //read write through socket_data
                                      char buf[256] = {0};
                                      ssize_t status = ::recv(fd, buf, 256, 0);
                                      if (status > 0)
                                      {
                                          ::send(fd, buf, status, 0);
                                      }

                                      ::close(fd);
                                  },
                                  socket_data);

            }
        }

    上面代码示例展示了将连接过来的socket分发到线程池中执行,线程池如果均处于工作状态,那么任务会在一个队列中等待空闲线程的出现。这样的程序模型已经将处理效率提升了不止百倍了,然而还是有可以挖掘的性能空间。设想一下如果线程池中的某一条线程处理一个连接的过程中,对于该连接的socket读写出现的阻塞,那么这条工作线程就陷入了等待,等待意味着整个系统的性能出现了闲置,所以它也没有榨干系统的所有性能。
    上述的程序演进同样可以将线程模型变为进程模型,即fork出n个进程或者固定几个进程来处理连接。用进程的好处在于进程间彼此隔离,可以提升程序稳定性。坏处在于进程的开销比线程要高,进程间协同也需要系统级进程通讯的方案,比较繁琐。

C10K问题

    即便我们用线程池改进了程序性能,那么时间回退到20年前,以当时的系统性能这样的程序框架能撑住多少连接呢?恐怕不到1万个,这就是C10K问题。
https://en.wikipedia.org/wiki/C10k_problem
    这个问题简而言之就是当时的条件下一方面受制于硬件条件,另一方面操作系统(最广泛引用的linux)还没有epoll这样的高效多路复用系统函数可供使用(2003年附近引入)。所以单台主机往往很难 承受并发上万个请求。一个具体例子,早期qq的服务器就是由于此种原因,不得不使用了UDP协议来规避掉这个问题。
    正是由于存在这样的瓶颈,所以以linux为例,引入了高效的IO多路复用API,大大提升了系统的并发能力,让TCP高并发成为可能。下面我们看一下IO多路复用的几个典型API,它们分别是select,poll和epoll。这些API统一可以成为事件驱动的,我们只需要将需要关心读写的文件描述符设置给这些函数,那么当这个文件描述符有可读或者可写数据时候,操作系统会回调给应用层,我们在做相应处理即可。这样的一个好处就是我们可以不必再等待一个socket读事件读取全部业务需要相关的数据,而是可以中断这样的读取,等待系统回调。这样就可以节省下等待某一个socket数据的时间,用来处理其他连接。

6930270-2ab17ce3afc14923.png
polling.png

上图是IO多路复用在应用层和kernel层的交互关系图。

IO多路复用--select、poll、epoll

以下select代码示例

    fd_set m_read_fdset;
    fd_set m_all_fdset;

    void polling_select()
    {
        FD_ZERO(&m_all_fdset);
        FD_SET(m_sock, &m_all_fdset);
        while (true)
        {
            m_read_fdset = m_all_fdset;
            int total = ::select(FD_SETSIZE, &m_read_fdset, nullptr, nullptr, nullptr);
            if (total > 0)
            {
                if (FD_ISSET(m_sock, &m_read_fdset)) //accept
                {
                    int addr_length = sizeof(m_addr);
                    int socket_data = ::accept(m_sock, (sockaddr *) &m_addr, (socklen_t *) &addr_length);

                    if (socket_data <= 0)
                    {
                        std::cout << "polling_select()accept err:" << std::strerror(errno) << std::endl;
                        continue;
                    }

                    FD_SET(socket_data, &m_all_fdset);
                }
                else //read && write back
                {
                    for (int i = 0; i < FD_SETSIZE; ++i)
                    {
                        if (FD_ISSET(i, &m_read_fdset))
                        {
                            ServerSocket s;
                            s.m_sock = i;
                            std::string str_bff;
                            if (!(s >> str_bff))
                            {
                                s.close();
                                FD_CLR(i, &m_all_fdset);
                                std::cout << "one client offline" << std::endl;
                                break;
                            }
                            std::cout << str_bff << std::endl;

                            if (std::string::npos != str_bff.find("exit"))
                            {
                                s.close();
                                FD_CLR(i, &m_all_fdset);
                                std::cout << "one client exit" << std::endl;
                                break;
                            }

                            std::string strbuff;
                            std::string strinput("server recieve client!");
                            SimpleTransDataUtil::build_trans_data(strbuff,
                                                                  (unsigned char *) strinput.c_str(),
                                                                  strinput.size());
                            s << strbuff;
                            s.m_sock = -1;
                        }
                    }
                }
            }
            else
            {
                if (errno == EINTR)
                {
                    //忽略此信号
                }
                else
                {
                    std::cout << "polling_select()err:" << std::strerror(errno) << std::endl;
                    break;
                }
            }
        }
    }

    select优势在于目前几乎在所有的平台上都有良好的支持。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。另外的缺点是每次系统回调应用层是需要遍历所有fd,来比对具体哪一个fd有了读写事件,这样造成了低效率。

以下是poll的代码示例

   void polling_poll()
    {
        std::vector<pollfd> vec_pollfd;
        pollfd pfd_accept = {0};
        pfd_accept.fd = m_sock;
        pfd_accept.events = POLLIN;
        vec_pollfd.push_back(pfd_accept);

        while (true)
        {
            int nready = ::poll(&vec_pollfd[0], vec_pollfd.size(), -1);
            if (nready < 0)
            {
                if (errno == EINTR)
                {
                    //忽略此信号
                    continue;
                }
                else
                {
                    std::cout << "polling_poll()err:" << std::strerror(errno) << std::endl;
                    break;
                }
            }


            if (vec_pollfd[0].revents & POLL_IN)
            {
                int addr_length = sizeof(m_addr);
                int socket_data = ::accept(m_sock, (sockaddr *) &m_addr, (socklen_t *) &addr_length);

                if (socket_data <= 0)
                {
                    std::cout << "polling_select()accept err:" << std::strerror(errno) << std::endl;
                    continue;
                }

                pollfd pfd_rwfd = {0};
                pfd_rwfd.fd = socket_data;
                pfd_rwfd.events = POLL_IN;
                vec_pollfd.push_back(pfd_rwfd);
                continue;
            }


            for (auto item = vec_pollfd.begin(); item != vec_pollfd.end(); ++item)
            {
                if ((item->revents & POLL_IN) && item->fd != m_sock)
                {
                    ServerSocket s;
                    s.m_sock = item->fd;

                    std::string str_bff;

                    if (!(s >> str_bff))
                    {
                        s.close();
                        item->fd = -1;

                        std::cout << "one client offline" << std::endl;
                        continue;
                    }
                    std::cout << str_bff << std::endl;

                    if (std::string::npos != str_bff.find("exit"))
                    {
                        s.close();
                        item->fd = -1;
                        std::cout << "one client exit" << std::endl;
                        continue;
                    }

                    std::string strbuff;
                    std::string strinput("server recieve client!");
                    SimpleTransDataUtil::build_trans_data(strbuff,
                                                          (unsigned char *) strinput.c_str(),
                                                          strinput.size());
                    s << strbuff;
                    s.m_sock = -1;
                }
            }
        }


    }

    poll在select基础上去除了监控fd数量上的限制,但是同样也需要做遍历这种低效的比对来判断具体哪一个fd需要进行读写处理。

以下是epoll的代码示例

#define IPADDRESS   "127.0.0.1"
#define PORT        8787
#define MAXSIZE     1024
#define LISTENQ     5
#define FDSIZE      1000
#define EPOLLEVENTS 100

//创建socket、绑定、监听
listenfd = socket_bind(IPADDRESS,PORT);

struct epoll_event events[EPOLLEVENTS];

//创建一个描述符
epollfd = epoll_create(FDSIZE);

//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);

//循环等待
for ( ; ; ){
    //该函数返回已经准备好的描述符事件数目
    ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
    //处理接收到的连接
    handle_events(epollfd,events,ret,listenfd,buf);
}

//事件处理函数
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
     int i;
     int fd;
     //进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。
     for (i = 0;i < num;i++)
     {
         fd = events[i].data.fd;
        //根据描述符的类型和事件类型进行处理
         if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
         else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
         else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
     }
}

//添加事件
static void add_event(int epollfd,int fd,int state){
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd){
     int clifd;     
     struct sockaddr_in cliaddr;     
     socklen_t  cliaddrlen;     
     clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);     
     if (clifd == -1)         
     perror("accpet error:");     
     else {         
         printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);                       //添加一个客户描述符和事件         
         add_event(epollfd,clifd,EPOLLIN);     
     } 
}

//读处理
static void do_read(int epollfd,int fd,char *buf){
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)     {         
        perror("read error:");         
        close(fd); //记住close fd        
        delete_event(epollfd,fd,EPOLLIN); //删除监听 
    }
    else if (nread == 0)     {         
        fprintf(stderr,"client close.\n");
        close(fd); //记住close fd       
        delete_event(epollfd,fd,EPOLLIN); //删除监听 
    }     
    else {         
        printf("read message is : %s",buf);        
        //修改描述符对应的事件,由读改为写         
        modify_event(epollfd,fd,EPOLLOUT);     
    } 
}

//写处理
static void do_write(int epollfd,int fd,char *buf) {     
    int nwrite;     
    nwrite = write(fd,buf,strlen(buf));     
    if (nwrite == -1){         
        perror("write error:");        
        close(fd);   //记住close fd       
        delete_event(epollfd,fd,EPOLLOUT);  //删除监听    
    }else{
        modify_event(epollfd,fd,EPOLLIN); 
    }    
    memset(buf,0,MAXSIZE); 
}

//删除事件
static void delete_event(int epollfd,int fd,int state) {
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

//修改事件
static void modify_event(int epollfd,int fd,int state){     
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

    终于有了最终极的一个解决方案,来把IO效率提升到最高,这就是epoll。它克服了之前两种方式的缺点,它的明显劣势在于平台强依赖,只在linux上才能使用。值得一提的是,epoll函数有两种触发模式Level-triggered和edge-triggered,edge-triggered需要关联的fd是非阻塞的(non-blocking)。

阻塞IO和非阻塞IO

blockingIO

    在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:


6930270-cd2a4b87d6b3505a.gif
blockingio.gif

我们前面的所有例子都是基于阻塞的IO模型。通过上图可以看到,阻塞的过程分成两个阶段,都在kernel中,第一阶段是等待数据到来(一个可以返回给应用层的阈值);第二阶段是从内核缓冲拷贝到用户缓冲。显而易见第一阶段是阻塞耗时的关键。

non-blockingIO

    non-blockingIO流程如下:


6930270-84ea0b1e2b525de4.gif
nonblocking.gif

    从上图我们可以看出,非阻塞IO需要不断轮询相关fd是否有数据已经准备好。轮询效率通常都很低,所以搭配IO多路复用,让系统以事件的方式回调给应用层是最高效的方式。所以非阻塞IO应该搭配IO多路复用来使用,反过来IO多路复用既可以搭配阻塞IO,又可以搭配非阻塞IO。前面提到的,epoll中的edge-triggered必须搭配非阻塞IO,这种模式通常被认为是比Level-triggered方式更高效,看一下linux手册对他们的描述:

     Level-triggered and edge-triggered
       The epoll event distribution interface is able to behave both as
       edge-triggered (ET) and as level-triggered (LT).  The difference
       between the two mechanisms can be described as follows.  Suppose that
       this scenario happens:

       1. The file descriptor that represents the read side of a pipe (rfd)
          is registered on the epoll instance.

       2. A pipe writer writes 2 kB of data on the write side of the pipe.

       3. A call to epoll_wait(2) is done that will return rfd as a ready
          file descriptor.

       4. The pipe reader reads 1 kB of data from rfd.

       5. A call to epoll_wait(2) is done.

       If the rfd file descriptor has been added to the epoll interface
       using the EPOLLET (edge-triggered) flag, the call to epoll_wait(2)
       done in step 5 will probably hang despite the available data still
       present in the file input buffer; meanwhile the remote peer might be
       expecting a response based on the data it already sent.  The reason
       for this is that edge-triggered mode delivers events only when
       changes occur on the monitored file descriptor.  So, in step 5 the
       caller might end up waiting for some data that is already present
       inside the input buffer.  In the above example, an event on rfd will
       be generated because of the write done in 2 and the event is consumed
       in 3.  Since the read operation done in 4 does not consume the whole
       buffer data, the call to epoll_wait(2) done in step 5 might block
       indefinitely.

       An application that employs the EPOLLET flag should use nonblocking
       file descriptors to avoid having a blocking read or write starve a
       task that is handling multiple file descriptors.  The suggested way
       to use epoll as an edge-triggered (EPOLLET) interface is as follows:

              i   with nonblocking file descriptors; and

              ii  by waiting for an event only after read(2) or write(2)
                  return EAGAIN.

    通俗的来解释一下这段话,就是ET模式系统通知一次调用方后就不负责维护这一次调用的缓冲区数据了,如果应用不把缓冲区数据读取完,那么系统不会再回调给你这个事件。与之相对的,LT模式,如果系统回调你一个读写事件,应用层不一次性读取完成的话,系统仍然会继续将读写事件回调给你,让你有机会继续读取本次数据传输的剩余部分。
    所以ET模式必须用(实际上是墙裂建议)non-blockingIO模式的fd,这样可以循环读取一个fd,直到返回错误码EAGAIN为止,这样就能保证一次系统回调,我们把应用层缓冲区的数据全部读取完成。实际上这里要关注的核心问题是系统一次回调应用方必须将所有buffer中的数据读取完毕,所以如果用阻塞IO的话,很难做到,要么读取不彻底,让系统再一次陷入等待事件回调,要么某一次读取已经没有数据,阻塞在读取等待中。

进一步挖掘性能提升空间

    上面对代码一步步进行优化演进,并且介绍了系统层面最高效的IO多路复用API,那么下一步可以结合高效的IO多路复用+多线程(进程)模型,让整个系统能够充分发掘硬件潜力,每时每刻不存在空闲的处理客户端连入请求。另外,为了抹平系统的差异,我们可以采用libevent这个开源框架来隐藏各个系统最高效IO多路复用的差异(linux下epoll,windows下IOCP,BSD下的kqueue)。下面是整体程序的一个概览:

6930270-e7d9a68512abff4c.png
1548300737233-image.png

    上面的结构实际在著名的项目中都有应用,如memcached,nginx。不同的是ngnix采用的是工作进程。简单说一下这个系统的运行,主线程负责polling监听中的socket,有客户端连入请求会生成用于读写的socketfd,主线程负责将这个fd通过事先已经在运行的若干条工作线程中的某一条中的管道以数据形式传送过去,从而触发已经在工作线程polling的事件回调,读出这个fd,加入到当前工作线程的polling队列中,这样主线程负责分发有读写状态的fd,工作线程负责读取/写入数据。整个系统工作线程的开启数量大体对应机器的cpu核心数,最大限度利用每一个核心的处理能力并且避免过多的频繁的线程上下文切换。此外整个系统的所有io操作均是非阻塞io操作,这样所有工作线程,只要在系统有连接处理的时候,都能随时接受任务,最大化整个系统的效率。

下面是利用libevent实现的完整代码:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/fcntl.h>
#include <event2/event.h>
#include <event2/listener.h>
#include <event2/bufferevent.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>
#include <vector>
#include <list>
#include <atomic>
#include <map>
#include "../utils/string_x.h"
#include "transdata.h"
#include <iostream>

#define DEBUG_SERVER
//#undef DEBUG_SERVER



class SocketServer;

/**
 * 简化子线程数据结构,用计数器来简单判定当前线程工作的负荷。
 */
struct EventThreadInfo
{
    int thread_id;        /* unique ID of this thread */
    struct event_base *base;    /* libevent handle this thread uses */
    struct event *notify_event;         /* listen event for notify pipe */
    int notify_receive_fd;      /* receiving end of notify pipe */
    int notify_send_fd;         /* sending end of notify pipe */
    std::atomic_int sock_counter;
    SocketServer *server;
    //存储传输未完成的socket fd, 用map来简单实现,高负载真是情况下这里应该改用更高效的数据结构。
    std::map<evutil_socket_t, stringxa> m_fd_trans_buff;

    //for emplace_back
    EventThreadInfo(int id, struct event_base *base_out, struct event *event_out,
                    int receive_fd, int send_fd, SocketServer *socket_server) : thread_id(id), base(base_out),
                                                                                notify_event(event_out),
                                                                                notify_receive_fd(receive_fd),
                                                                                notify_send_fd(send_fd),
                                                                                sock_counter(0), server(socket_server)
    {
    }

    EventThreadInfo(const EventThreadInfo &) = delete;

    EventThreadInfo(EventThreadInfo &&info) = delete;

    EventThreadInfo &operator=(const EventThreadInfo &) = delete;

    EventThreadInfo &operator=(EventThreadInfo &&) = delete;

};

/**
 * 利用IO多路复用(libevent进行平台无关封装),在固定的n条线程内(4-8条,视cpu核心数而定)实现高性能的socket连接&&数据处理。
 * 可以是长连接TCP(并不适合服务端消息实时推送)也可以是短链接TCP,线程模型和多路复用的应用大体来源于memcache。
 */

#ifndef FUNCTION_BEGIN
#define FUNCTION_BEGIN do{
#endif

#ifndef FUNCTION_LEAVE
#define FUNCTION_LEAVE break
#endif

#ifndef FUNCTION_END
#define FUNCTION_END }while(false)
#endif


class SocketServer
{
public:
    enum
    {
        READ_BUFF_LEN = 512,
        SOCKETSERVER_PORT = 19840
    };

    SocketServer(int n_count) : m_n_working_thread_count(n_count <= 0 ? 1 : n_count), main_base(nullptr),
                                m_n_threadid(0)
    {

    }

    ~SocketServer()
    {
        uninit();
    }

    /**
     * 初始化主线程io多路,检测一个本地监听的socket,将连接socket fd 分发到各个workthread处理
     * @return
     */
    bool start_server()
    {
        bool ret = true;
        evconnlistener *listener = nullptr;
        FUNCTION_BEGIN ;
            main_base = event_base_new();
            if (!main_base)
            {
                ret = false;
                FUNCTION_LEAVE;
            }

            struct sockaddr_in sin;
            memset(&sin, 0, sizeof(struct sockaddr_in));
            sin.sin_family = AF_INET;
            sin.sin_addr.s_addr = INADDR_ANY;
            sin.sin_port = htons(SOCKETSERVER_PORT);

            listener = evconnlistener_new_bind(main_base, SocketServer::listener_cb, this,
                                               LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE,
                                               -1, (struct sockaddr *) &sin,
                                               sizeof(struct sockaddr_in));
            if (!listener)
            {
                ret = false;
                FUNCTION_LEAVE;
            }

            //init working thread
            bool init_working = init_working_threads();
            if (!init_working)
            {
                ret = false;
                FUNCTION_LEAVE;
            }

            event_base_dispatch(main_base);
        FUNCTION_END;
        if (listener)
        {
            evconnlistener_free(listener);
        }
        return ret;
    }

    static void listener_cb(evconnlistener *listener, evutil_socket_t fd,
                            struct sockaddr *sock, int socklen, void *arg)
    {
#ifdef DEBUG_SERVER
//        sockaddr_in addr_data_sock = {0};
//        int len_data_sock = sizeof(addr_data_sock);
//        ::getsockname(fd, (sockaddr *) &addr_data_sock, (socklen_t *) &len_data_sock);
//        std::cout << "data sock host addr:" << ::inet_ntoa(addr_data_sock.sin_addr) << " port:"
//                  << ntohs(addr_data_sock.sin_port) << std::endl;
//
//        sockaddr_in addr_data_sock_peer = {0};
//        int len_data_sock_peer = sizeof(addr_data_sock_peer);
//        ::getpeername(fd, (sockaddr *) &addr_data_sock_peer, (socklen_t *) &len_data_sock_peer);
//        std::cout << "data sock peer addr:" << ::inet_ntoa(addr_data_sock_peer.sin_addr) << " port:"
//                  << ntohs(addr_data_sock_peer.sin_port) << std::endl;

#endif


        auto server = (SocketServer *) arg;
        server->dispatch(fd);
    }


private:
    /**
    * runs on mainthread,
    * use pipe write socket fd to one of the workingthread event_base
    * @param fd
    */
    void dispatch(evutil_socket_t fd)
    {
        int counter = 0;
        int index = -1;
        int min = -1;
        for (ITERATOR it = m_list_threadinfos.begin(); it != m_list_threadinfos.end(); ++it)
        {
            int tmp = it->sock_counter.load();
            if (min == -1 || tmp <= min)
            {
                min = tmp;
                index = counter;
            }
            counter++;
        }

        if (index < 0 || index >= m_list_threadinfos.size())
        {
            return;
        }

        counter = 0;
        for (ITERATOR it = m_list_threadinfos.begin(); it != m_list_threadinfos.end(); ++it)
        {
            if (index == counter)
            {
                it->sock_counter++;
                write(it->notify_send_fd, &fd, sizeof(evutil_socket_t));
                break;
            }
            counter++;
        }

#ifdef DEBUG_SERVER
        int n_counter = 0;
        for (ITERATOR it = m_list_threadinfos.begin(); it != m_list_threadinfos.end(); ++it)
        {
            n_counter += it->sock_counter.load();
        }
        std::cout << "connected count is:" << n_counter << std::endl;
#endif
    }

    event_base *main_base;
    int m_n_working_thread_count;
    int m_n_threadid;

    //write before init , won't read write at same time
    //read multi-thread safe
    using ITERATOR = std::list<EventThreadInfo>::iterator;
    std::list<EventThreadInfo> m_list_threadinfos;

    bool init_working_threads()
    {
        bool ret = true;
        FUNCTION_BEGIN ;

            for (int i = 0; i < m_n_working_thread_count; ++i)
            {
                int thread_id = ++m_n_threadid;
                int fd[2];
                if (pipe(fd) != 0)
                {
                    ret = false;
                    break;
                }

                m_list_threadinfos.emplace_back(thread_id, nullptr, nullptr, fd[0], fd[1], this);

                event_base *base = event_base_new();
                struct event *ev = event_new(base, fd[0]/*read pipe*/, EV_READ | EV_PERSIST,
                                             indicate_sock_come_cb,
                                             reinterpret_cast<void *>(&(m_list_threadinfos.back())));

                m_list_threadinfos.back().base = base;
                m_list_threadinfos.back().notify_event = ev;

                if (0 != event_add(ev, nullptr))
                {
                    ret = false;
                    break;
                }

                std::thread t([base]() -> void
                              {
                                  event_base_dispatch(base);
                              });


                t.detach();
            }


        FUNCTION_END;
        return ret;
    }

    static void indicate_sock_come_cb(evutil_socket_t fd, short, void *info)
    {
        EventThreadInfo *threadinfo = reinterpret_cast<EventThreadInfo *>(info);
        if (fd == threadinfo->notify_receive_fd)
        {
            evutil_socket_t sock_fd = -1;
            if (read(fd, &sock_fd, sizeof(evutil_socket_t)) <= 0)
            {
                std::cerr << "indicate_sock_come_cb read data error" << std::endl;
            }
            if (sock_fd != -1)
            {
                //这里后续没有清理,如果是一个纯粹的服务端程序确实不需要清理,因为只会重启这个进程
                bufferevent *bev = bufferevent_socket_new(threadinfo->base, sock_fd,
                                                          BEV_OPT_CLOSE_ON_FREE);

                bufferevent_setcb(bev, socket_read_cb, socket_write_cb, socket_event_cb, info);
                bufferevent_enable(bev, EV_READ | EV_PERSIST | EV_ET);
            }
            else
            {
                std::cerr << "indicate_sock_come_cb read data is invalid" << std::endl;
            }
        }
        else
        {
            std::cerr << "indicate_sock_come_cb fd is not pipe read fd" << std::endl;
        }

    }

    static void socket_event_cb(bufferevent *bev, short events, void *arg)
    {
        EventThreadInfo *threadinfo = (EventThreadInfo *) arg;
        threadinfo->sock_counter--;
#if defined(DEBUG_SERVER)
        std::cerr << "socket disconnected from server:" << events << std::endl;
#endif
        //这将自动close套接字和free读写缓冲区
        bufferevent_free(bev);
    }

    static
    void socket_read_cb(bufferevent *bev, void *arg)
    {
        EventThreadInfo *threadinfo = (EventThreadInfo *) arg;
        evutil_socket_t socket_fd = bufferevent_getfd(bev);
        char msg[READ_BUFF_LEN];
        stringxa str_data;
        if (threadinfo->m_fd_trans_buff.find(socket_fd) !=
            threadinfo->m_fd_trans_buff.end())
        {
            str_data = std::move(threadinfo->m_fd_trans_buff[socket_fd]);
        }
        while (true)
        {
            size_t len = bufferevent_read(bev, msg, READ_BUFF_LEN);
            if (len)
            {
                len = len > READ_BUFF_LEN ? READ_BUFF_LEN : len;
                str_data.append(msg, len);
            }
            if (SimpleTransDataUtil::check_data(str_data))
            {
                if (threadinfo->m_fd_trans_buff.find(socket_fd) !=
                    threadinfo->m_fd_trans_buff.end())
                {
                    threadinfo->m_fd_trans_buff.erase(socket_fd);
                }
                break;
            }
            else if (len == 0) //data trans incomplete
            {
                if (socket_fd != -1)
                {
                    if (threadinfo->m_fd_trans_buff.find(socket_fd) !=
                        threadinfo->m_fd_trans_buff.end())
                    {
                        threadinfo->m_fd_trans_buff[socket_fd] = std::move(str_data);
                    }
                    else
                    {
                        threadinfo->m_fd_trans_buff.insert(
                                std::pair<evutil_socket_t, stringxa>(socket_fd, str_data));
                    }
                }
#if defined(DEBUG_SERVER)
                std::cout << "read data incomplete" << std::endl;
#endif
                return;
            }
        }
#if defined(DEBUG_SERVER)
//        std::cout << "server read the data:" << str_data << std::endl;
        std::cout << "read alldata in server thread:" << threadinfo->thread_id << std::endl;
#endif


        //这里涉及到实际场景的优化问题,整个框架都是非阻塞高性能处理,如果业务回复端数据的过程有耗时,这里应该做一些特殊的逻辑
        //如,没有在缓存中的数据可能要经过耗时运算或者IO,这样可能扔到线程池中不阻塞整个框架为好。
        stringxa str_rep;
        str_rep.format_multitype("I has read your data in thread", threadinfo->thread_id);
        stringxa str_final_rep;
        SimpleTransDataUtil::build_trans_data(str_final_rep, (unsigned char *) str_rep.c_str(), str_rep.size());
        if (bufferevent_write(bev, str_final_rep.c_str(), str_final_rep.size()) == -1)
        {
#if defined(DEBUG_SERVER)
            std::cout << "write socket ret is not 0" << std::endl;
#endif
        }
    }

    static
    void socket_write_cb(bufferevent *bev, void *arg)
    {
        //call after bufferevent_write, 如果数据量很大, 那么可以将缓冲分片调用,如果不是发送文件这种大数据,一般应该不用
#if defined(DEBUG_SERVER)
        std::cout << "socket_write_cb" << std::endl;
#endif
    }


    void uninit()
    {
        if (main_base)
        {
            event_base_free(main_base);
            main_base = nullptr;
        }
        //其余资源不清里了, 大多数情况情况下是直接杀进城重启的
    }
};

最后

    一个高并发的服务端程序,还有很多细节需要处理,这里只是展示了最基本的多线程模型+IO多路复用来处理连接的最基本功能。一个真实可用的网络框架远复杂的多,另外还要关注服务端配置参数的一些优化,比如进程可以处理的fd数量上限,对time_wait状态socket的处理等等。希望可以对没有接触过服务端程序的同学有所帮助。

猜你喜欢

转载自blog.csdn.net/weixin_34392435/article/details/87229403