2.2.1使用epoll及reactor实现服务器百万并发

前言

本文章承接上一篇文章2.1.1网络io与select,poll,epoll,使用epoll以及reactor实现服务器百万并发。

为什么需要实现一个sock_item?

  • 每个fd都对应一个sock_item
  • sock_item包含rbuffer, wbuffer, rlength, wlength, events, callback,可使每个fd拥有独立的缓冲区

内存分配

calloc()在动态分配完内存后,自动初始化该内存空间为零,而malloc()不初始化,里边数据是随机的垃圾数据。因此使用malloc()后需要调用memset()进行初始化而使用calloc()则不需要。

百万并发尝试1

#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>  // struct sockaddr_in
#include <fcntl.h>
#include <unistd.h>      // close()函数
#include <string.h>      // memcpy()函数
#include <stdlib.h>

#define NONBLOCK 0
#define BUFFER_LENGTH 128
#define EVENTS_LENGTH 128

// listenfd, clientfd
struct sock_item  // conn_item
{
    int fd; // clientfd

    char* rbuffer;
    int rlength;

    char* wbuffer;
    int wlength;

    int event;

    void (*recv_cb)(int fd, char* buffer, int length);
    void (*send_cb)(int fd, char* buffer, int length);

    void (*accept_cb)(int fd, char* buffer, int length);
};

struct reactor
{
    int epfd;  // epoll

    struct sock_item* items;
};


int main()
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    // 第一个参数domain为协议簇
    // (AF_UNIX:本机通信;AF_INET:TCP/IP-IPv4;AF_INET6:TCP/IP-IPv6)
    // 第二个参数type为套接字类型常用类型有:
    // (SOCK_STREAM:TCP流;SOCK_DGRAM:UDP数据报;SOCK_RAW:原始套接字)
    // 第三个参数protocol,一般设置为0
    // (
    //  当套接字使用的协议簇和类型确定时,该参数值为0;
    //  有时创建原始套接字时,不知道要使用的协议簇和类型的情况下,protocol参数可确定协议的种类
    // )
    // socket函数成功返回int类型的值,从3开始,依次递增(0对应stdin,1对应stdout,2对应stderr)
    // 失败返回"-1",错误代码写入"errno"中
    if(listenfd == -1) return -1;

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    // INADDR_ANY 为 0.0.0.0
    // 127.0.0.1      用于本机通信(回环地址)
    // 192.168.0.123  特定对外通信
    // 0.0.0.0        任意都可通信(包括本机及对外)

    // htonl (host to net long)
    // 将long类型变量从主机字节顺序转变成网络字节顺序
    // 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,
    // 从而可以保证数据在不同主机之间传输时能够被正确解释,
    // 网络字节顺序采用big-endian(大端)排序方式。
    servaddr.sin_port = htons(9999);
    // htons (host to net short)

    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
    {
        return -2;
    }

#if NONBLOCK
    int flag = fcntl(listenfd, F_GETFL, 0);  // 获取listenfd的flag(F_GETFL:get flag)
    flag |= O_NONBLOCK;                      // 使用位或操作设置flag为非阻塞的
    fcntl(listenfd, F_SETFL, flag);          // 设置listenfd的flag(F_SETFL:set flag)
#endif
    
    listen(listenfd, 10);

    struct reactor* r = (struct reactor*)calloc(1, sizeof(struct reactor));
    if(r == NULL) return -3;

    r->items = (struct sock_item*)calloc(EVENTS_LENGTH, sizeof(struct sock_item));
    if(r->items == NULL) return -4;

    r->epfd = epoll_create(1);  // r->epfd值为4
    // 新建一个epoll描述符
    // size参数只需为大于0的数,该参数为历史遗留,现在没有意义

    struct epoll_event ev, events[EVENTS_LENGTH];
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(r->epfd, EPOLL_CTL_ADD, listenfd, &ev);
    // 对epoll描述符进行操作(添加或者删除所有待监控的连接等)
    // 第一个参数为epfd:epoll描述符
    // 第二个参数为op:  指定操作类型
    // (
    //  EPOLL_CTL_ADD 往事件表中注册fd上的事件
    //  EPOLL_CTL_DEL 删除fd上的注册事件
    //  EPOLL_CTL_MOD 修改fd上的注册事件
    // )
    // 第三个参数为fd:   要操作的文件描述符
    // 第四个参数为event:指定事件,它是epoll_event结构指针类型
    // (
    //  epoll_event定义:
    //  events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)
    //  data成员:存储用户数据
    // )

    while(1)
    {
        int nready = epoll_wait(r->epfd, events, EVENTS_LENGTH, -1);
        // 第一个参数为epfd:epoll描述符
        // 第二个参数events:   检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中
        // 第三个参数maxevents:指定最多监听多少个事件
        // 第四个参数timeout:  指定epoll的超时时间,单位是毫秒
        // (当timeout为-1是,epoll_wait调用将永远阻塞,直到某个事件发生。当timeout为0时,epoll_wait调用将立即返回)
        // 返回值:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
        printf("---------- %d\n", nready);
        
        int i = 0;
        for(i = 0;i < nready;i++)
        {
            int clientfd = events[i].data.fd;
            if(clientfd == listenfd)  // 若触发事件的fd为listenfd,说明有客户端连接请求,执行accpet()
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int connfd = accept(listenfd, (struct sockaddr*)&client, &len);

                printf("accept: %d\n",connfd);
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(r->epfd, EPOLL_CTL_ADD, connfd, &ev);

                // 初始化connfd对应的sock_item
                r->items[connfd].fd = connfd;

                r->items[connfd].rbuffer = calloc(1, BUFFER_LENGTH);
                r->items[connfd].rlength = 0;

                r->items[connfd].wbuffer = calloc(1, BUFFER_LENGTH);
                r->items[connfd].wlength = 0;

                r->items[connfd].event = EPOLLIN; 
            }
            else if(events[i].events & EPOLLIN) // clientfd
            {
                char* rbuffer = r->items[clientfd].rbuffer;
                char* wbuffer = r->items[clientfd].wbuffer;

                int n = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
                // 默认为水平触发(LT),即一次发送的数据,若一次接收无法全部接收,会继续接收多次,直到接收完发送的数据
                // 将 ev.events 设置为 EPOLLIN | EPOLLET,可改为边沿触发(ET)
                // 边沿触发(ET):只会接收一次,若无法接收完发送的数据,剩余数据会被留在内核协议栈的缓冲区,下次接收时,从剩余数据开始接收
                // 小数据倾向于LT(水平触发),大数据倾向于ET(边沿触发)
                // 水平触发可以一次性接收所有数据,而边沿触发需要通过循环才能接收所有数据(假设不能一次接收完数据)
                if(n > 0)
                {
                    //rbuffer[n] = '\0';
                    printf("recv : %s\n", rbuffer);

                    memcpy(wbuffer, rbuffer, BUFFER_LENGTH);


                    ev.events = EPOLLOUT;
                    ev.data.fd = clientfd;
                    epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
                }
                else if(n == 0)  // 客户端断开时,需要调用服务器的close()函数
                {
                    free(rbuffer);
                    free(wbuffer);

                    r->items[clientfd].fd = 0;

                    close(clientfd);
                    //events[i].data.fd = -1;
                }
            }
            else if(events[i].events & EPOLLOUT)
            // 每次send之前判断IO是否可写,在可写时进行send
            // 当send返回值小于buffer长度时,说明还有数据未发出,需要等IO可写时再次发出剩余数据
            {
                char* wbuffer = r->items[clientfd].wbuffer;

                int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0);
                printf("sent : %d\n", sent);

                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
            }
        }
    }
}

当前问题:

  • 无法满足百万并发的需求

解决方法:

  • 将数据结构由数组改为链表加数组,并且对reactor进行进一步的封装

在这里插入图片描述

链表:适合查找有序、有规律的数据

百万并发尝试2

修改代码如下:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>  // struct sockaddr_in
#include <fcntl.h>
#include <unistd.h>      // close()函数
#include <string.h>      // memcpy()函数
#include <stdlib.h>

#define BUFFER_LENGTH 128
#define EVENTS_LENGTH 128

#define ITEM_LENGTH 1024

// listenfd, clientfd
struct sock_item  // conn_item
{
    int fd; // clientfd

    char* rbuffer;
    int rlength;

    char* wbuffer;
    int wlength;

    int event;

    void (*recv_cb)(int fd, char* buffer, int length);
    void (*send_cb)(int fd, char* buffer, int length);

    void (*accept_cb)(int fd, char* buffer, int length);
};

struct eventblock
{
    struct sock_item* items;
    struct eventblock* next;
};

struct reactor
{
    int epfd;  // epoll
    int blkcnt;

    struct eventblock* evblk;
};

int reactor_resize(struct reactor* r) // new eventblock
{
    if(r == NULL) return -1;

    struct eventblock* blk = r->evblk;

    while(blk!=NULL && blk->next!=NULL)
    {
        blk = blk->next;
    }

    struct sock_item* item = (struct sock_item*)malloc(ITEM_LENGTH * sizeof(struct sock_item));
    if(item == NULL) return -4;
    memset(item, 0, ITEM_LENGTH* sizeof(struct sock_item));  // 初始化分配的内存

    printf("-------------\n");
    struct eventblock* block = (struct eventblock*)malloc(sizeof(struct eventblock));
    if(block == NULL)
    {
        free(item);
        return -5;
    }
    memset(block, 0, sizeof(struct eventblock));

    block->items = item;
    block->next = NULL;

    if(blk == NULL)
    {
        r->evblk = block;
    }
    else
    {
        blk->next = block;
    }
    ++r->blkcnt;

    return 0;
}

struct sock_item* reactor_lookup(struct reactor* r, int sockfd)
{
    if(r == NULL) return NULL;
    //if(r->evblk == NULL) return NULL;
    if(sockfd <= 0) return NULL;

    printf("reactor_lookup --> %d\n", r->blkcnt);
    int blkidx = sockfd / ITEM_LENGTH;
    while(blkidx >= r->blkcnt)
    {
        reactor_resize(r);
    }

    int i = 0;
    struct eventblock* blk = r->evblk;
    while(i++ < blkidx && blk != NULL)
    {
        blk = blk->next;
    }

    return &blk->items[sockfd % ITEM_LENGTH];
}

int main()
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    // 第一个参数domain为协议簇
    // (AF_UNIX:本机通信;AF_INET:TCP/IP-IPv4;AF_INET6:TCP/IP-IPv6)
    // 第二个参数type为套接字类型常用类型有:
    // (SOCK_STREAM:TCP流;SOCK_DGRAM:UDP数据报;SOCK_RAW:原始套接字)
    // 第三个参数protocol,一般设置为0
    // (
    //  当套接字使用的协议簇和类型确定时,该参数值为0;
    //  有时创建原始套接字时,不知道要使用的协议簇和类型的情况下,protocol参数可确定协议的种类
    // )
    // socket函数成功返回int类型的值,从3开始,依次递增(0对应stdin,1对应stdout,2对应stderr)
    // 失败返回"-1",错误代码写入"errno"中
    if(listenfd == -1) return -1;

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    // INADDR_ANY 为 0.0.0.0
    // 127.0.0.1      用于本机通信(回环地址)
    // 192.168.0.123  特定对外通信
    // 0.0.0.0        任意都可通信(包括本机及对外)

    // htonl (host to net long)
    // 将long类型变量从主机字节顺序转变成网络字节顺序
    // 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,
    // 从而可以保证数据在不同主机之间传输时能够被正确解释,
    // 网络字节顺序采用big-endian(大端)排序方式。
    servaddr.sin_port = htons(9999);
    // htons (host to net short)

    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
    {
        return -2;
    }
    
    listen(listenfd, 10);

    struct reactor* r = (struct reactor*)calloc(1, sizeof(struct reactor));
    if(r == NULL)
    {
        return -3;
    }

    r->epfd = epoll_create(1);  // r->epfd值为4
    // 新建一个epoll描述符
    // size参数只需为大于0的数,该参数为历史遗留,现在没有意义

    struct epoll_event ev, events[EVENTS_LENGTH];
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(r->epfd, EPOLL_CTL_ADD, listenfd, &ev);
    // 对epoll描述符进行操作(添加或者删除所有待监控的连接等)
    // 第一个参数为epfd:epoll描述符
    // 第二个参数为op:  指定操作类型
    // (
    //  EPOLL_CTL_ADD 往事件表中注册fd上的事件
    //  EPOLL_CTL_DEL 删除fd上的注册事件
    //  EPOLL_CTL_MOD 修改fd上的注册事件
    // )
    // 第三个参数为fd:   要操作的文件描述符
    // 第四个参数为event:指定事件,它是epoll_event结构指针类型
    // (
    //  epoll_event定义:
    //  events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)
    //  data成员:存储用户数据
    // )

    while(1)
    {
        int nready = epoll_wait(r->epfd, events, EVENTS_LENGTH, -1);
        // 第一个参数为epfd:epoll描述符
        // 第二个参数events:   检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中
        // 第三个参数maxevents:指定最多监听多少个事件
        // 第四个参数timeout:  指定epoll的超时时间,单位是毫秒
        // (当timeout为-1是,epoll_wait调用将永远阻塞,直到某个事件发生。当timeout为0时,epoll_wait调用将立即返回)
        // 返回值:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
        printf("---------- %d\n", nready);
        
        int i = 0;
        for(i = 0;i < nready;i++)
        {
            int clientfd = events[i].data.fd;
            if(clientfd == listenfd)  // 若触发事件的fd为listenfd,说明有客户端连接请求,执行accpet()
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int connfd = accept(listenfd, (struct sockaddr*)&client, &len);

                printf("accept: %d\n",connfd);
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(r->epfd, EPOLL_CTL_ADD, connfd, &ev);
				
                // 使用reactor_lookup()函数查找fd对应的sock_item,并对其进行初始化
                struct sock_item* item = reactor_lookup(r, connfd);
                item->fd = connfd;
                item->rbuffer = calloc(1, BUFFER_LENGTH);
                item->rlength = 0;

                item->wbuffer = calloc(1, BUFFER_LENGTH);
                item->wlength = 0;
            }
            else if(events[i].events & EPOLLIN) // clientfd
            {
                struct sock_item* item = reactor_lookup(r, clientfd);

                char* rbuffer = item->rbuffer;
                char* wbuffer = item->wbuffer;

                int n = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
                // 默认为水平触发(LT),即一次发送的数据,若一次接收无法全部接收,会继续接收多次,直到接收完发送的数据
                // 将 ev.events 设置为 EPOLLIN | EPOLLET,可改为边沿触发(ET)
                // 边沿触发(ET):只会接收一次,若无法接收完发送的数据,剩余数据会被留在内核协议栈的缓冲区,下次接收时,从剩余数据开始接收
                // 小数据倾向于LT(水平触发),大数据倾向于ET(边沿触发)
                // 水平触发可以一次性接收所有数据,而边沿触发需要通过循环才能接收所有数据(假设不能一次接收完数据)
                if(n > 0)
                {
                    //rbuffer[n] = '\0';
                    printf("recv : %s\n", rbuffer);

                    memcpy(wbuffer, rbuffer, BUFFER_LENGTH);


                    ev.events = EPOLLOUT;
                    ev.data.fd = clientfd;
                    epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
                }
                else if(n == 0)  // 客户端断开时,需要调用服务器的close()函数
                {
                    free(rbuffer);
                    free(wbuffer);

                    item->fd = 0;

                    close(clientfd);
                    //events[i].data.fd = -1;
                }
            }
            else if(events[i].events & EPOLLOUT)
            // 每次send之前判断IO是否可写,在可写时进行send
            // 当send返回值小于buffer长度时,说明还有数据未发出,需要等IO可写时再次发出剩余数据
            {
                struct sock_item* item = reactor_lookup(r, clientfd);

                char* wbuffer = item->wbuffer;

                int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0);
                printf("sent : %d\n", sent);

                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
            }
        }
    }
}

我们使用send和recv对fd进行操作,操作的底层为五元组(sip,sport,dip,dport,proto)即(源ip,源端口,目的ip,目的端口,协议),五元组确定一个唯一的连接。

为了达到百万连接,源端口不够的解决方法(对客户端而言):

  • 增加源ip(增加网卡,绑定不同ip地址,使用多进程)
  • 增加目的ip
  • 增加目的端口

其中客户端的源端口范围为0-65535,最大值为固定值,不可改变。

这里采用增加目的端口的方案,即增加服务器端口。

百万并发尝试3

修改代码如下:

#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <netinet/in.h>  // struct sockaddr_in
#include <fcntl.h>
#include <unistd.h>      // close()函数
#include <string.h>      // memcpy()函数
#include <stdlib.h>

#define BUFFER_LENGTH 128
#define EVENTS_LENGTH 128

#define PORT_COUNT  100
#define ITEM_LENGTH 1024

// listenfd, clientfd
struct sock_item  // conn_item
{
    int fd; // clientfd

    char* rbuffer;
    int rlength;

    char* wbuffer;
    int wlength;

    int event;

    void (*recv_cb)(int fd, char* buffer, int length);
    void (*send_cb)(int fd, char* buffer, int length);

    void (*accept_cb)(int fd, char* buffer, int length);
};

struct eventblock
{
    struct sock_item* items;  // 数组,含ITEM_LENGTH个sock_item
    struct eventblock* next;
};

struct reactor
{
    int epfd;  // epoll
    int blkcnt;

    struct eventblock* evblk;  // 链表
};

int reactor_resize(struct reactor* r) // new eventblock
{
    if(r == NULL) return -1;

    struct eventblock* blk = r->evblk;

    while(blk!=NULL && blk->next!=NULL)
    {
        blk = blk->next;
    }

    struct sock_item* items = (struct sock_item*)malloc(ITEM_LENGTH * sizeof(struct sock_item));
    if(items == NULL) return -4;
    memset(items, 0, ITEM_LENGTH * sizeof(struct sock_item));  // 初始化分配的内存

    //printf("-------------\n");
    struct eventblock* block = (struct eventblock*)malloc(sizeof(struct eventblock));
    if(block == NULL)
    {
        free(items);
        return -5;
    }
    memset(block, 0, sizeof(struct eventblock));

    block->items = items;
    block->next = NULL;

    if(blk == NULL)
    {
        r->evblk = block;
    }
    else
    {
        blk->next = block;
    }
    ++r->blkcnt;

    return 0;
}

struct sock_item* reactor_lookup(struct reactor* r, int sockfd)
{
    if(r == NULL) return NULL;
    //if(r->evblk == NULL) return NULL;
    if(sockfd <= 0) return NULL;

    //printf("reactor_lookup --> %d\n", r->blkcnt);
    int blkidx = sockfd / ITEM_LENGTH;
    while(blkidx >= r->blkcnt)  // 因为sockfd不可能突增,所以该循环只执行一次,也可使用if
    {
        reactor_resize(r);
    }

    int i = 0;
    struct eventblock* blk = r->evblk;
    while(i++ < blkidx && blk != NULL)
    {
        blk = blk->next;
    }

    return &blk->items[sockfd % ITEM_LENGTH];
}

int init_server(short port)
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    // 第一个参数domain为协议簇
    // (AF_UNIX:本机通信;AF_INET:TCP/IP-IPv4;AF_INET6:TCP/IP-IPv6)
    // 第二个参数type为套接字类型常用类型有:
    // (SOCK_STREAM:TCP流;SOCK_DGRAM:UDP数据报;SOCK_RAW:原始套接字)
    // 第三个参数protocol,一般设置为0
    // (
    //  当套接字使用的协议簇和类型确定时,该参数值为0;
    //  有时创建原始套接字时,不知道要使用的协议簇和类型的情况下,protocol参数可确定协议的种类
    // )
    // socket函数成功返回int类型的值,从3开始,依次递增(0对应stdin,1对应stdout,2对应stderr)
    // 失败返回"-1",错误代码写入"errno"中
    if(listenfd == -1) return -1;

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    // INADDR_ANY 为 0.0.0.0
    // 127.0.0.1      用于本机通信(回环地址)
    // 192.168.0.123  特定对外通信
    // 0.0.0.0        任意都可通信(包括本机及对外)

    // htonl (host to net long)
    // 将long类型变量从主机字节顺序转变成网络字节顺序
    // 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,
    // 从而可以保证数据在不同主机之间传输时能够被正确解释,
    // 网络字节顺序采用big-endian(大端)排序方式。
    servaddr.sin_port = htons(port);
    // htons (host to net short)

    if (bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)
    // 绑定本地的ip和端口,还有协议
    {
        return -2;
    }
    
    listen(listenfd, 10);

    return listenfd;
}

int is_listenfd(int *fds, int connfd)
{
    int i=0;
    for(i=0;i<PORT_COUNT;i++)
    {
        if(fds[i] == connfd)
        {
            return 1;
        }
    }
    return 0;
}

int main()
{
    struct reactor* r = (struct reactor*)calloc(1, sizeof(struct reactor));
    if(r == NULL)
    {
        return -3;
    }

    r->epfd = epoll_create(1);  // r->epfd值为4
    // 新建一个epoll描述符
    // size参数只需为大于0的数,该参数为历史遗留,现在没有意义
    struct epoll_event ev, events[EVENTS_LENGTH];

    int sockfds[PORT_COUNT] = {0};
    int i = 0;
    // 创建多个listenfd,增加服务器端口
    for(i=0;i<PORT_COUNT;i++)
    {
        sockfds[i] = init_server(9999+i);

        ev.events = EPOLLIN;
        ev.data.fd = sockfds[i];

        epoll_ctl(r->epfd, EPOLL_CTL_ADD, sockfds[i], &ev);
        // 对epoll描述符进行操作(添加或者删除所有待监控的连接等)
        // 第一个参数为epfd:epoll描述符
        // 第二个参数为op:  指定操作类型
        // (
        //  EPOLL_CTL_ADD 往事件表中注册fd上的事件
        //  EPOLL_CTL_DEL 删除fd上的注册事件
        //  EPOLL_CTL_MOD 修改fd上的注册事件
        // )
        // 第三个参数为fd:   要操作的文件描述符
        // 第四个参数为event:指定事件,它是epoll_event结构指针类型
        // (
        //  epoll_event定义:
        //  events:描述事件类型,和poll支持的事件类型基本相同(两个额外的事件:EPOLLET和EPOLLONESHOT,高效运作的关键)
        //  data成员:存储用户数据
        // )
    }

    while(1)
    {
        int nready = epoll_wait(r->epfd, events, EVENTS_LENGTH, -1);
        // 第一个参数为epfd:epoll描述符
        // 第二个参数events:   检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中
        // 第三个参数maxevents:指定最多监听多少个事件
        // 第四个参数timeout:  指定epoll的超时时间,单位是毫秒
        // (当timeout为-1是,epoll_wait调用将永远阻塞,直到某个事件发生。当timeout为0时,epoll_wait调用将立即返回)
        // 返回值:成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno
        //printf("---------- %d\n", nready);
        
        int i = 0;
        for(i = 0;i < nready;i++)
        {
            int clientfd = events[i].data.fd;
            if(is_listenfd(sockfds, clientfd))  // 若触发事件的fd为listenfd,说明有客户端连接请求,执行accpet()
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int connfd = accept(clientfd, (struct sockaddr*)&client, &len);

                if(connfd % 1000 == 999)
                {

                    printf("accept: %d\n",connfd);
                }
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                epoll_ctl(r->epfd, EPOLL_CTL_ADD, connfd, &ev);
				
                // 使用reactor_lookup()函数查找fd对应的sock_item,并对其进行初始化
                struct sock_item* item = reactor_lookup(r, connfd);
                item->fd = connfd;
                item->rbuffer = calloc(1, BUFFER_LENGTH);
                item->rlength = 0;

                item->wbuffer = calloc(1, BUFFER_LENGTH);
                item->wlength = 0;
            }
            else if(events[i].events & EPOLLIN) // clientfd
            {
                struct sock_item* item = reactor_lookup(r, clientfd);

                char* rbuffer = item->rbuffer;
                char* wbuffer = item->wbuffer;

                int n = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
                // 默认为水平触发(LT),即一次发送的数据,若一次接收无法全部接收,会继续接收多次,直到接收完发送的数据
                // 将 ev.events 设置为 EPOLLIN | EPOLLET,可改为边沿触发(ET)
                // 边沿触发(ET):只会接收一次,若无法接收完发送的数据,剩余数据会被留在内核协议栈的缓冲区,下次接收时,从剩余数据开始接收
                // 小数据倾向于LT(水平触发),大数据倾向于ET(边沿触发)
                // 水平触发可以一次性接收所有数据,而边沿触发需要通过循环才能接收所有数据(假设不能一次接收完数据)
                if(n > 0)
                {
                    //rbuffer[n] = '\0';
                    //printf("recv : %s\n", rbuffer);

                    memcpy(wbuffer, rbuffer, BUFFER_LENGTH);


                    ev.events = EPOLLOUT;
                    ev.data.fd = clientfd;
                    epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
                }
                else if(n == 0)  // 客户端断开时,需要调用服务器的close()函数
                {
                    free(rbuffer);
                    free(wbuffer);

                    item->fd = 0;

                    close(clientfd);
                    //events[i].data.fd = -1;
                }
            }
            else if(events[i].events & EPOLLOUT)
            // 每次send之前判断IO是否可写,在可写时进行send
            // 当send返回值小于buffer长度时,说明还有数据未发出,需要等IO可写时再次发出剩余数据
            {
                struct sock_item* item = reactor_lookup(r, clientfd);

                char* wbuffer = item->wbuffer;

                int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0);
                //printf("sent : %d\n", sent);

                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                epoll_ctl(r->epfd, EPOLL_CTL_MOD, clientfd, &ev);
            }
        }
    }
}

流程:

  • 创建reactor
  • 创建epfd,赋给reactor->epfd
  • 创建PORT_COUNT个listenfd,并与PORT_COUNT个端口(从9999开始)连接
  • 将所有listenfd对应的epoll_event->events设置为EPOLLIN(即监听listenfd的输入事件)并加入epoll监听列表
  • 执行循环:
    • 使用epoll_wait返回就绪的epoll_event
    • 检测epoll_event对应的fd是否为listenfd,若是,则执行accpet()与客户端连接,并将clientfd加入epoll监听列表,调用reactor_lookup寻找clientfd,返回对应的sock_item,对sock_item进行初始化(赋值fd,分配缓冲区)
    • 若为clientfd且为输入事件,执行recv()从客户端接收数据
    • 若为clientfd且为输出事件,执行send()向客户端发送数据

通过测试,该代码可实现百万并发。

百万并发测试前注意事项:

  • 需要准备4台虚拟机,1台作为服务器(内存8G),3台作为客户端(内存2G)

  • 修改最大文件打开数,因为fd也属于文件。

    • ulimit -a          // 查看限制
      ulimit -n 1000000  // 将最大文件打开数改为1000000(临时的,重启后重置)
      
  • 修改/etc/sysctl.conf文件

    • 打开文件

      • vim /etc/sysctl.conf
        
    • 在文件最下方添加以下内容

      • net.ipv4.tcp_mem = 262144 524288 786432
        net.ipv4.tcp_wmem = 1024 1024 2048
        net.ipv4.tcp_rmem = 1024 1024 2048
        fs.file-max = 1048576
        net.ipv4.tcp_max_orphans = 16384
        net.ipv4.tcp_mem = 252144 524288 786432
        net.ipv4.tcp_wmem = 2048 2048 4096
        net.ipv4.tcp_rmem = 2048 2048 4096
        fs.file-max = 1048576
        net.nf_conntrack_max = 1048576
        net.netfilter.nf_conntrack_tcp_timeout_established = 1200
        
    • 从/etc/sysctl.conf中加载系统参数

      • sysctl -p
        modprobe nf_conntrack
        

参考博客:

ulimit命令用法详解

Linux系统中sysctl命令详解

高并发访问时,提示: Cannot assign requested address 异常,解决方法

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值