Linux Socket编程(四):Reactor并发模式的理解和实现

参考

线程池

Linux Socket编程(三):固定线程数的线程池实现

I/O多路复用:select、poll、epoll

Linux IO模式及 select、poll、epoll详解
IO多路复用
epoll底层实现过程
彻底学会使用epoll(六)——关于ET的若干问题总结
epoll EPOLLL、EPOLLET模式与阻塞、非阻塞

事件处理和并发模式

18 | 单服务器高性能模式:PPC与TPC
单服务器高性能模式:Reactor与Proactor
服务端处理事件的两种模式–Reactor和Proactor

谈半同步/半异步网络并发模型(以及半同步/半反应堆模式)
Reactor线程模型
Linux下多线程服务器Reactor模式总结
高效并发模式,半同步半异步和领导者追随者

Reactor模式的理解

并发模型

单服务器高性能关键之一就是服务器采取的并发模型,并发模型的关键设计点有两点:

  1. 服务器如何管理连接;
  2. 服务器如何处理请求。

服务器如何管理连接对应IO模型,有同步阻塞IO、同步非阻塞IO、IO多路复用、异步IO;服务器如何处理请求对应进程模型,有单进程、多进程/线程。根据不同的IO模型和进程模型,常见的并发模型有PPC(Process Per Connection)、TPC(Thread Per Connection)、Reactor、Proactor。

PPC/TPC模型和进程/线程池

PPC/TPC模型管理连接采用同步阻塞IO模型,处理请求采用多进程/线程模型。Linux Socket编程(二)中实现的服务端对多个客户端同时响应属于PPC/TPC模型,即

  1. 父进程接受连接;
  2. 父进程fork子进程/创建子线程;
  3. 子进程/子线程处理连接请求;
  4. 子进程/子线程关闭连接请求。

PPC/PPC模式最主要的问题是每个连接都需要创建进程/线程,连接结束后进程/线程就销毁了。对于单个任务处理时间短,且需要处理的任务数量大的场景,大量线程的创建和销毁本身就有很大的开销。Linux Socket编程(三)中实现的线程池可以减少线程本身带来的开销。

以下内容源自:IO多路复用

I/O多路复用

以网络IO为例,在IO操作过程会涉及到两个对象:

  1. 调用这个IO的process (or thread);
  2. 系统内核(kernel)。
    在一个IO操作过程中,以read为例,会涉及到两个过程:
  3. 等待数据准备好(Waiting for the data to be ready);
  4. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

I/O多路复用是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。阻塞I/O有一个比较明显的缺点是在I/O阻塞模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,需要多个进程或者多个线程,但是这种方式效率不高。非阻塞的I/O需要轮询查看流是否已经准备好了,比较典型的方式是忙轮询。

忙轮询

忙轮询方式是通过不停的把所有的流从头到尾轮询一遍,查询是否有流已经准备就绪,然后又从头开始。如果所有流都没有准备就绪,那么只会白白浪费CPU时间。轮询过程可以参照如下:

while true {
    
    
	for i in stream[]; {
    
    
		if i has data
		read until unavailable
	}
}

无差别的轮询方式

为了避免白白浪费CPU时间,可以采用另外一种轮询方式,无差别的轮询方式。即通过引进一个代理,这个代理为select/poll,这个代理可以同时观察多个流的I/O事件。当所有的流都没有准备就绪时,会把当前线程阻塞掉;当有一个或多个流的I/O事件就绪时,就从阻塞状态中醒来,然后轮询一遍所有的流,处理已经准备好的I/O事件。轮询的过程可以参照如下:

while true {
    
    
	select(streams[])
	for i in streams[] {
    
    
		if i has data
		read until unavailable
	}
}

如果I/O事件未准备就绪,那么我们的程序就会阻塞在select处。我们通过select那里只是知道了有I/O事件准备好了,但不知道具体是哪几个流(可能有一个,也可能有多个),所以需要无差别的轮询所有的流,找出已经准备就绪的流。可以看到,使用select时,我们需要O(n)的时间复杂度来处理流,处理的流越多,消耗的时间也就越多。

最小轮询方式

无差别的轮询方式有一个缺点就是,随着监控的流越来越多,需要轮询的时间也会随之增加,效率也会随之降低。所以还有另外一种轮询方式,最小轮询方式,即通过epoll方式来观察多个流,epoll只会把发生了I/O事件的流通知我们,我们对这些流的操作都是有意义的,时间复杂度降低到O(k),其中k为产生I/O事件的流个数。轮询的过程如下:

while true {
    
    
	active_stream[] = epoll_wait(epollfd)
	for i in active_stream[] {
    
    
		read or write till unavailable
	}
}

select/poll/epoll都是采用I/O多路复用机制的,其中select/poll是采用无差别轮询方式,而epoll是采用最小的轮询方式。

IO多路复用的优势

I/O多路复用的优势并不是对于单个连接能处理的更快,而是在于可以在单个线程/进程中处理更多的连接。与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。(个人理解:每accept一个服务端的请求,无论IO事件是否准备就绪,就开辟一个线程处理,与I/O多路复用为准备就绪的IO事件开辟线程处理相比,需要更多的线程同时运行。因此,当短时间内处理大量的连接时候,I/O多路复用可以减少系统所需最大线程数目。

Reactor模式的实现

Reactor为同步非阻塞模式,使用IO多路复用统一监听事件,收到事件后分配给某个进程/线程。在LInux系统下,I/O模型采用单epoll实现多路复用,线程池负责任务的处理。

epoll

epoll底层实现

首先epoll_create创建一个epoll文件描述符,底层同时创建一个红黑树,和一个就绪链表;红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。对于LT模式epoll_wait清空就绪链表之后会检查该文件描述符是哪一种模式,如果为LT模式,且必须是该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表,epoll_wait马上返回。ET模式不会检查,只会调用一次

epoll操作

// MyEpoll.h
#ifndef MY_EPOLL_H
#define MY_EPOLL_H

#include <poll.h>
#include <sys/epoll.h>
#include <vector>

class MyEpoll{
    
    
public:
    // 创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大;返回一个描述符epollfd。
    int createEpoll(int max_events){
    
    
        evs.resize(max_events);
        return epoll_create(max_events);
    }
    // 等待epollfd上准备就绪的io事件,最多返回max_events个事件,这个maxevents的值不能大于创建epoll_create()时的size。
    int waitEpoll(int epollfd, int max_events, int timeout){
    
    
        return epoll_wait(epollfd, &evs[0], max_events, timeout);
    }

    // 操作事件(添加、删除和修改对scokfd的监听事件)
    void operateEvent(int epollfd, int sockfd, int op, int event){
    
    
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = sockfd;
        if(epoll_ctl(epollfd, op, sockfd, &ev) < 0){
    
    
            perror("epoll_ctl error");
        }
    }

    struct epoll_event& getEvs(int index){
    
    
        return evs[index];
    }

private:
	// 存放每次epoll_wait后从内核得到的准备就绪的io事件(accept、recv、send)的集合
    vector<epoll_event> evs; 
}
#endif

MyEpoll.h封装了epoll的三个操作:

int epoll_create(int size)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

需要注意的是,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

epoll使用

MyEpoll myEpoll; // 封装的epoll

int main(int argc, const char* argv[]){
    
    
    int server_sockfd = start_up();
    // epoll初始化
    int epollfd = myEpoll.createEpoll(MAX_EVENTS); //epoll描述符
    if(epollfd < 0){
    
    
        perror("epoll_create error");
        return -1;
    }
    
    myEpoll.operateEvent(epollfd, server_sockfd, EPOLL_CTL_ADD, EPOLLIN);
    int timeout = -1;  // 超时时间,ms(-1表示infinite)
    //epoll
    while(1){
    
             
        // 等待epollfd上的IO事件,最多返回MAX_EVENTS个事件。
        // 该函数返回需要处理的事件数目,如返回0表示已超时。
        int ret = myEpoll.waitEpoll(epollfd, MAX_EVENTS, timeout);
        
        if(ret < 0){
    
    
            perror("epoll_wait error");
            break;
        }else if(ret == 0){
    
    
            printf("timeout ...\n");
            continue;
        }

        for(size_t i = 0; i < ret; i++){
    
    
            int fd = myEpoll.getEvs(i).data.fd;
            int event = myEpoll.getEvs(i).events;
            // 根据描述符的类型和事件类型进行处理
            if (fd == server_sockfd && (event & EPOLLIN) ){
    
     
                AcceptConnect(epollfd, fd);
            }else  if(event & EPOLLIN){
    
    
                RecvData(epollfd, fd);
            }else if(event & EPOLLOUT){
    
    
                SendData(epollfd, fd);
            }
        }
    }
    return 0;
}

单epoll+线程池实现

#include <iostream>
#include <stdio.h>
#include <cstring>       // void *memset(void *s, int ch, size_t n);
#include <sys/types.h>   // 数据类型定义
#include <sys/socket.h>  // 提供socket函数及数据结构sockaddr
#include <arpa/inet.h>   // 提供IP地址转换函数,htonl()、htons()...
#include <netinet/in.h>  // 定义数据结构sockaddr_in
#include <ctype.h>       // 小写转大写
#include <unistd.h>      // close()、read()、write()、recv()、send()...
#include <thread>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unordered_map>

#include "ThreadPool.h"
#include "fixed_thread_pool.h"
#include "MyEpoll.h"
using namespace std;

// 函数perror()用于抛出最近的一次系统错误信息
#define BUFFER_SIZE  1<<20
#define READ_SIZE  16

void AcceptConnect(int epollfd, int server_sockfd);
void RecvData(int epollfd, int client_sockfd);
void SendData(int epollfd, int client_sockfd);
// 实现客户端发送小写字符串给服务端,服务端将小写字符串转为大写返回给客户端  
void requestHandling(int epollfd, int client_sockfd);
// 服务端启动:创建套接字socket(),绑定IP地址和端口bind(),监听套接字的端口号listen()。返回服务端的套接字。
int start_up();

struct ClientData{
    
    
    char buf[BUFFER_SIZE];
    int len;
    int data_size = 0;
    struct sockaddr_in client_addr;
};

const int flag = 0; // 0表示读写处于阻塞模式, MSG_DONTWAIT -> 非阻塞
const int port = 8080;
const int MAX_EVENTS = 5000; // epoll最多处理的连接数

unordered_map<int,ClientData*> clientData; // 保存每个连接的相关数据
ThreadPool pool(thread::hardware_concurrency()); 
MyEpoll myEpoll; // 封装的epoll

int main(int argc, const char* argv[]){
    
    

    
    int server_sockfd = start_up();

    // epoll初始化
    int epollfd = myEpoll.createEpoll(MAX_EVENTS); //epoll描述符
    if(epollfd < 0){
    
    
        perror("epoll_create error");
        return -1;
    }

    myEpoll.operateEvent(epollfd, server_sockfd, EPOLL_CTL_ADD, EPOLLIN);


    int timeout = -1;  // 超时时间,ms(-1表示infinite)
    //epoll
    while(1){
    
             
        // 等待epollfd上的IO事件,最多返回MAX_EVENTS个事件。
        // 该函数返回需要处理的事件数目,如返回0表示已超时。
        int ret = myEpoll.waitEpoll(epollfd, MAX_EVENTS, timeout);
        
        if(ret < 0){
    
    
            perror("epoll_wait error");
            break;
        }else if(ret == 0){
    
    
            printf("timeout ...\n");
            continue;
        }

        for(size_t i = 0; i < ret; i++){
    
    
            int fd = myEpoll.getEvs(i).data.fd;
            int event = myEpoll.getEvs(i).events;
            // 根据描述符的类型和事件类型进行处理
            if (fd == server_sockfd && (event & EPOLLIN) ){
    
     
                AcceptConnect(epollfd, fd);
            }else  if(event & EPOLLIN){
    
    
                RecvData(epollfd, fd);
            }else if(event & EPOLLOUT){
    
    
                SendData(epollfd, fd);
            }
        }
    }

    return 0;
}

void AcceptConnect(int epollfd, int server_sockfd){
    
    
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_addr, &client_len);
    if (client_sockfd < 0){
    
    
        perror("accept error");
    }else{
    
    
        clientData[client_sockfd] =  new ClientData();
        clientData[client_sockfd]->client_addr = client_addr;

        // 将此连接设置为非阻塞模式
        // bool flags = fcntl(client_sockfd, F_GETFL, 0);     
        // fcntl(client_sockfd, F_SETFL, flags | O_NONBLOCK); 
        // 将此连接加入epoll监听队列
        myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_ADD, EPOLLIN);

        char ipbuf[128];
        printf("Connect client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, 
            sizeof(ipbuf)), ntohs(client_addr.sin_port));   

    }
}


void RecvData(int epollfd, int client_sockfd){
    
    
    char* buf = clientData[client_sockfd]->buf;
    struct sockaddr_in& client_addr = clientData[client_sockfd]->client_addr;
    // read data
    int len = recv(client_sockfd, buf, READ_SIZE, flag);
    if (len == -1) {
    
    
        myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_DEL, EPOLLIN); //删除监听 
        close(client_sockfd);
        perror("read error");
    }else if(len == 0){
    
      // len为0表示当前处理请求的客户端断开连接
        myEpoll.operateEvent(epollfd,client_sockfd, EPOLL_CTL_DEL, EPOLLIN); //删除监听
        close(client_sockfd);
        char ipbuf[128];
        printf("Disconnect client iP: %s, port: %d\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, 
            sizeof(ipbuf)), ntohs(client_addr.sin_port));
    }else{
    
    
        cout << len << endl;
        clientData[client_sockfd]->len = len;

        char ipbuf[128];
        printf("Recvive from client iP: %s, port: %d, str = %s\n", inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, 
            sizeof(ipbuf)), ntohs(client_addr.sin_port),buf);

        pool.enqueue(requestHandling, epollfd, client_sockfd);  // 单reactor+多线程(线程池)
        // requestHandling(epollfd, client_sockfd); // 单reactor+单线程模式
    }
}

void SendData(int epollfd, int client_sockfd){
    
    
    char* buf = clientData[client_sockfd]->buf;
    struct sockaddr_in client_addr = clientData[client_sockfd]->client_addr;
    int  data_size = clientData[client_sockfd]->data_size;
    
    if(send(client_sockfd, buf, data_size, flag) == -1){
    
    
        perror("write error");
        close(client_sockfd);
        myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_DEL, EPOLLOUT);
    }else{
    
    
        char ipbuf[128];
        printf("Send to client iP: %s, port: %d, str = %s\n",inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ipbuf, 
        sizeof(ipbuf)), ntohs(client_addr.sin_port), buf);

        myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_MOD, EPOLLIN); 
    }
    memset(buf,'\0',BUFFER_SIZE); // 清空buf
}


void requestHandling(int epollfd, int client_sockfd){
    
    
    char* buf = clientData[client_sockfd]->buf; 
    int  len = clientData[client_sockfd]->len;
    // 小写转大写
    for(int i = 0; i < len; i++) {
    
    
        buf[i] = toupper(buf[i]);
    }
    
    myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_MOD, EPOLLOUT); // 描述符对应的事件由读改为写
}

int start_up(){
    
    

    int server_sockfd = socket(PF_INET,SOCK_STREAM,0);  

    if(server_sockfd == -1){
    
    
        close(server_sockfd);
        perror("socket error!");
    }

    // /* Enable address reuse */
    // int on = 1;
    // int ret = setsockopt( server_sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );

    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(server_addr)); 
    server_addr.sin_family = AF_INET;  
    server_addr.sin_port = htons(port);  
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); 


    if(bind(server_sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr)) == -1){
    
    
        close(server_sockfd);
        perror("bind error");
    }
    
    if(listen(server_sockfd, 10) == -1){
    
    
        close(server_sockfd);
        perror("listen error");
    }

    printf("Listen on port %d\n", port);
    return server_sockfd;
}

epoll工作模式LT和ET

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

将单epoll+线程池实现代码中对client_sockfd的操作设置为LT模式(默认),并注释掉写事件完成后,将epollfd对client_sockfd监听的写事件改为读事件那一行,即

myEpoll.operateEvent(epollfd, client_sockfd, EPOLL_CTL_MOD, EPOLLIN); 

客户端连接以后发送请求,epoll会不停的触发client_sockfd写事件。


Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =
Send to client iP: 127.0.0.1, port: 37620, str =

当设置成ET模式后,epoll不会再次触发写事件。

Listen on port 8080
Connect client iP: 127.0.0.1, port: 37956
Recvive from client iP: 127.0.0.1, port: 37956, str = asd
Send to client iP: 127.0.0.1, port: 37956, str = ASD

猜你喜欢

转载自blog.csdn.net/XindaBlack/article/details/106125167