Reactor 模式实现:从 epoll 到高并发调试

1.reactor实现

对epoll多路复用实现的TCP服务器中的各功能进行封装,从而提高其代码可读性,可扩展性,可维护性,复用性

关键之处在于从对io操作的管理转向对事件的管理,实现"事件注册->等待->分发->响应->更新"的流程

1.1全局变量

epoll实例的全局变量,各函数中操作同一个epoll

cpp 复制代码
int epfd = 0;

声明处理事件的回调函数类型,统一接口便于分发事件

cpp 复制代码
typedef int (*RCALLBACK)(int fd);

具体回调函数的声明

cpp 复制代码
int recv_cb(int fd);
int accept_cb(int fd);
int send_cb(int fd);

连接信息结构体,将不同fd的数据与行为进行封装

cpp 复制代码
struct conn{
    //套接字,客户端fd或者监听fd
    int fd;
    
    //读写数据的缓冲区数组和大小
    char rbuffer[BUFFER_LENGTH];
    int rlength;

    char wbuffer[BUFFER_LENGTH];
    int wlength;

    //把回调函数的指针加入结构体
    RCALLBACK send_callback;
    //互斥状态的两个回调函数指针,共同体的形式加入结构体(客户端fd调用recv_callback,监听fd调用accept_callback)
    union{
        RCALLBACK recv_callback;
        RCALLBACK accept_callback;
    } r_action;
};

共同体,所有成员共用同一块内存空间,节省内存

用数组存储所有连接

cpp 复制代码
struct conn conn_list[CONNECTION_SIZE] = {0};

1.2封装函数

创建服务器,创建套接字,绑定地址,启动监听fd

cpp 复制代码
int init_server(unsigned short port){

    //创建TCP流式套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //设置服务器地址信息,IPv4,绑定所有本地网卡,绑定端口
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
    servaddr.sin_port = htons(port); //0-1023

    //绑定套接字到服务器地址和端口
    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
        printf("bind failed: %s\n", strerror(errno));
    }

    //开启监听套接字,最大等待队列为10
    listen(sockfd, 10);
    printf("listen finished: %d\n", sockfd);

    return sockfd;
}

添加/修改epoll事件

cpp 复制代码
int set_event(int fd, int event, int flag){
    //flag非零时,添加epoll事件
    if(flag){//no-zero add
        //创建epoll_event类型变量
        struct epoll_event ev;
        //设定关注的事件类型
        ev.events = event;
        //将当前fd存入ev的data.fd对象中
        ev.data.fd = fd;
        //添加到epfd的epoll实例中
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    }else{//zero mod
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        //修改epfd中的epoll实例
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
}

接收客户端连接请求,创建客户端fd并完成初始化

cpp 复制代码
//listen(sockfd) --> EPOLLIN --> accept_cb
int accept_cb(int fd){

    //定义客户端地址结构体,计算长度
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    //调用accept函数,从监听fd接收数据并创建对应地址的客户端fd
    int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
    printf("accept finished: %d\n", clientfd);

    //调用event_register函数初始化连接信息,关注可读事件
    event_register(clientfd, EPOLLIN);

    return 0;
}

为epoll实例中添加新的客户端连接

cpp 复制代码
int event_register(int fd, int event){

    //初始化连接信息,绑定fd,设置回调函数
    conn_list[fd].fd = fd;
    //添加连接,共同体中选择recv_cb回调函数
    conn_list[fd].r_action.recv_callback = recv_cb;
    conn_list[fd].send_callback = send_cb;

    //初始化缓冲区
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].rlength = 0;

    memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].wlength = 0;

    //调用set_event函数添加事件,并监控可读事件
    set_event(fd, EPOLLIN, 1);
}

客户端fd触发EPOLLIN事件的回调函数,接收客户端发送的数据

cpp 复制代码
int recv_cb(int fd){

    //调用recv接收客户端数据,存入该连接的接收缓冲区,通过接收数据长度判断接收状态
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    //状态为零时客户端主动断开连接
    if(count == 0){
        printf("clientfd disconnect: %d\n", fd);
        //关闭断开的客户端fd
        close(fd);
        //将该fd从epfd的epoll实例中删除,不再监控
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);// 无需ev结构体

        return 0;
    }
    //存储数据长度
    conn_list[fd].rlength  = count;

    //打印接收数据
    printf("RRECV: %s\n", conn_list[fd].rbuffer);

#if 1 //echo  回声模式开关,1开启

    //将接收缓冲区的数据和数据长度存储到发送缓冲区中,用于send函数
    conn_list[fd].wlength = conn_list[fd].rlength;
    memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);

#endif

    //调用set_event函数修改该fd的关注事件为EPOLLOUT可写
    //让epoll后续触发可写事件,调用send_cb发送缓冲区中数据
    set_event(fd, EPOLLOUT, 0);

    return count;
}

客户端fd触发可写事件后,发送缓冲区中的数据

cpp 复制代码
int send_cb(int fd){

    //调用send函数将发送缓冲区中的数据发送至客户端fd
    int count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);

    //调用set_event函数将该fd连接信息中的关注事件修改为可读,从而再次接收数据
    set_event(fd, EPOLLIN, 0);

    return count;
}

1.3主函数

设置端口

cpp 复制代码
unsigned short port = 2000;

初始化服务器,创建套接字,绑定地址,开始监听,返回监听套接字

cpp 复制代码
int sockfd = init_server(port);

调用epoll_create函数创建一个epoll实例

cpp 复制代码
epfd = epoll_create(1);

将监听套接字存入连接列表中

cpp 复制代码
conn_list[sockfd].fd = sockfd;

设置accept_cb为共同体回调函数的处理,即监听到可读事件后调用accept_cb函数

cpp 复制代码
conn_list[sockfd].r_action.recv_callback = accept_cb;

调用set_event函数将监听套接字加入到epoll实例中,监控其可读事件

cpp 复制代码
set_event(sockfd, EPOLLIN, 1);

主循环,处理新连接,收发客户端数据

cpp 复制代码
while(1){
    ...
}

创建数组存储就绪事件,初始化为0

cpp 复制代码
struct epoll_event events[1024] = {0};

调用epoll_wait阻塞等待,直到监控的fd触发了就绪事件,并统计数量

cpp 复制代码
int nready = epoll_wait(epfd, events, 1024, -1);

循环遍历所有就绪事件(时间复杂度为O(k))

cpp 复制代码
int i = 0;
for(i = 0;i < nready;i ++){
    ...
}

获取当前就绪事件对应的fd,存入connfd中,简化操作

cpp 复制代码
int connfd = events[i].data.fd;

当就绪事件触发可读事件时,执行连接列表中该fd的recv_callback回调函数,添加新连接或读取数据;当就绪事件触发可写事件时,执行连接列表中该fd的send_callback回调函数,将发送缓冲区的数据发送至客户端

cpp 复制代码
if(events[i].events & EPOLLIN){
    conn_list[connfd].r_action.recv_callback(connfd);
}

if(events[i].events & EPOLLOUT){
    conn_list[connfd].send_callback(connfd);
}

1.4方法总结

将epoll模式的TCP服务器中多个关键函数进行封装处理:init_server处理服务器的创建,套接字的创建,绑定地址,开启监听和返回监听fd;set_event处理epoll的添加修改;accept_cb接收新连接并创建客户端fd;event_register完善客户端的连接信息结构体形式,并添加到epoll实例中;recv_cb接收并存储客户端发送的数据,处理断开连接的客户端;send_cb实现向客户端回发数据操作;主函数中的循环通过epoll_wait实现对所有fd的监控,根据关注事件类型调用对应的回调函数。

epoll作为事件多路分发器,conn_list作为事件处理器,主循环是事件分离器,契合reactor模式的一个分发器+多个处理器的模式

具体流程为:init_server->主函数循环

若为可读事件

若为监听fd->accept_cb->event_register->set_event

若为客户端fd->recv_cb->set_event

若为可写事件->send_cb->set_event

1.5完整代码

cpp 复制代码
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<pthread.h>
#include<unistd.h>
#include<poll.h>
#include<sys/epoll.h>

//宏定义设定缓冲区和连接列表的大小
#define BUFFER_LENGTH       1024
#define CONNECTION_SIZE          1048576

//声明处理事件的回调函数类型,统一接口便于分发时间
typedef int (*RCALLBACK)(int fd);

//epoll实例的全局变量,各函数中操作同一个epoll
int epfd = 0;
//具体回调函数的声明
int recv_cb(int fd);
int accept_cb(int fd);
int send_cb(int fd);

//连接信息结构体
struct conn{
    //套接字,客户端fd或者监听fd
    int fd;
    
    //读写数据的缓冲区数组和大小
    char rbuffer[BUFFER_LENGTH];
    int rlength;

    char wbuffer[BUFFER_LENGTH];
    int wlength;

    //把回调函数的指针加入结构体
    RCALLBACK send_callback;
    //互斥状态的两个回调函数指针,共同体的形式加入结构体(客户端fd调用recv_callback,监听fd调用accept_callback)
    //共同体,所有成员公用同一块内存空间,节省内存
    union{
        RCALLBACK recv_callback;
        RCALLBACK accept_callback;
    } r_action;

};

//用数组存储所有连接
struct conn conn_list[CONNECTION_SIZE] = {0};

//添加/修改epoll事件
int set_event(int fd, int event, int flag){
    //flag非零时,添加epoll事件
    if(flag){//no-zero add
        //创建epoll_event类型变量
        struct epoll_event ev;
        //设定关注的事件类型
        ev.events = event;
        //将当前fd存入ev的data.fd对象中
        ev.data.fd = fd;
        //添加到epfd的epoll实例中
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    }else{//zero mod
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        //修改epfd中的epoll实例
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
}

//为epoll实例中添加新的客户端连接
int event_register(int fd, int event){

    //初始化连接信息,绑定fd,设置回调函数
    conn_list[fd].fd = fd;
    //添加连接,共同体中选择recv_cb回调函数
    conn_list[fd].r_action.recv_callback = recv_cb;
    conn_list[fd].send_callback = send_cb;

    //初始化缓冲区
    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].rlength = 0;

    memset(conn_list[fd].wbuffer, 0, BUFFER_LENGTH);
    conn_list[fd].wlength = 0;

    //调用set_event函数添加事件,并监控可读事件
    set_event(fd, EPOLLIN, 1);
}

//listen(sockfd) --> EPOLLIN --> accept_cb
//接收客户端连接请求,创建客户端fd并完成初始化
int accept_cb(int fd){

    //定义客户端地址结构体,计算长度
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    //调用accept函数,从监听fd接收数据并创建对应地址的客户端fd
    int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
    printf("accept finished: %d\n", clientfd);

    //调用event_register函数初始化连接信息,关注可读事件
    event_register(clientfd, EPOLLIN);

    return 0;
}

//客户端fd触发EPOLLIN事件的回调函数,接收客户端发送的数据
int recv_cb(int fd){

    //调用recv接收客户端数据,存入该连接的接收缓冲区,通过接收数据长度判断接收状态
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    //状态为零时客户端主动断开连接
    if(count == 0){
        printf("clientfd disconnect: %d\n", fd);
        //关闭断开的客户端fd
        close(fd);
        //将该fd从epfd的epoll实例中删除,不再监控
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);// 无需ev结构体

        return 0;
    }
    //存储数据长度
    conn_list[fd].rlength  = count;

    //打印接收数据
    printf("RRECV: %s\n", conn_list[fd].rbuffer);

#if 1 //echo  回声模式开关,1开启

    //将接收缓冲区的数据和数据长度存储到发送缓冲区中,用于send函数
    conn_list[fd].wlength = conn_list[fd].rlength;
    memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);

#endif

    //调用set_event函数修改该fd的关注事件为EPOLLOUT可写
    //让epoll后续触发可写事件,调用send_cb发送缓冲区中数据
    set_event(fd, EPOLLOUT, 0);

    return count;
}

//客户端fd触发可写事件后,发送缓冲区中的数据
int send_cb(int fd){

    //调用send函数将发送缓冲区中的数据发送至客户端fd
    int count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);

    //调用set_event函数将该fd连接信息中的关注事件修改为可读,从而再次接收数据
    set_event(fd, EPOLLIN, 0);

    return count;
}

//创建服务器,开启监听fd
int init_server(unsigned short port){

    //创建TCP流式套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //设置服务器地址信息,IPv4,绑定所有本地网卡,绑定端口
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
    servaddr.sin_port = htons(port); //0-1023

    //绑定套接字到服务器地址和端口
    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
        printf("bind failed: %s\n", strerror(errno));
    }

    //开启监听套接字,最大等待队列为10
    listen(sockfd, 10);
    printf("listen finished: %d\n", sockfd);

    return sockfd;
}

int main(){

    //设置端口
    unsigned short port = 2000;
    //初始化服务器,创建套接字,绑定地址,开始监听
    //返回监听套接字
    int sockfd = init_server(port);

    //调用epoll_create函数创建一个epoll实例
    epfd = epoll_create(1);

    //将监听套接字存入连接列表中
    conn_list[sockfd].fd = sockfd;
    //设置accept_cb为共同体回调函数的处理,即监听到可读事件后调用accept_cb函数
    conn_list[sockfd].r_action.recv_callback = accept_cb;

    //调用set_event函数将监听套接字加入到epoll实例中,监控其可读事件
    set_event(sockfd, EPOLLIN, 1);

    //主循环,处理新连接,收发客户端数据
    while(1){//mainloop

        //创建数组存储就绪事件,初始化为0
        struct epoll_event events[1024] = {0};
        //调用epoll_wait阻塞等待,直到监控的fd触发了就绪事件,并统计数量
        int nready = epoll_wait(epfd, events, 1024, -1);

        //循环遍历所有就绪事件(时间复杂度为O(k))
        int i = 0;
        for(i = 0;i < nready;i ++){

            //获取当前就绪事件对应的fd,存入connfd中,简化操作
            int connfd = events[i].data.fd;

            //当就绪事件触发可读事件时,执行连接列表中该fd的recv_callback回调函数,添加新连接或读取数据
            if(events[i].events & EPOLLIN){
                conn_list[connfd].r_action.recv_callback(connfd);
            }
            //当就绪事件触发可写事件时,执行连接列表中该fd的send_callback回调函数,将发送缓冲区的数据发送至客户端
            if(events[i].events & EPOLLOUT){
                conn_list[connfd].send_callback(connfd);
            }
        }
    }
}

2.实现百万并发

reactor实现部分作为作为服务器的服务端,再开启三个虚拟机作为客户端,并运行客户端代码(见2.)

2.1启动服务器和客户端

修改服务器的连接上限

cpp 复制代码
#define CONNECTION_SIZE          1048576

修改客户端的端口数量,先测试一个端口

cpp 复制代码
#define MAX_PORT		1

启服务器端,输出

listen finished: 3

启动客户端,输出

connections: 999, sockfd:1002, time_used:1936

socket: Too many open files

error : Too many open files

2.2调试代码

服务器端显示1031个连接成功

尝试修改客户端主机open files的打开上限

ulimit -a 查看 显示数据为

open files (-n) 1024

ulimit -n 1048576 进行修改

查看修改结果

open files (-n) 1048576

增加服务器端代码的返回值判断

cpp 复制代码
int event_register(int fd, int event){

    if(fd < 0) return -1;
    ...
}

int accept_cb(int fd){
    
    ...
    printf("accept finished: %d\n", clientfd);
    if(clientfd < 0){
        printf("accept error: %d\n", errno);
        return -1;
    }
    event_register(clientfd, EPOLLIN);

    return 0;
}

再次运行

客户端达到28000连接,报错提示客户端端口耗尽,五元组不足

connections: 27999, sockfd:28002, time_used:6554

connect: Cannot assign requested address

error : Cannot assign requested address

服务器端建立20个端口

cpp 复制代码
#define MAX_PORT            20

主函数中添加对端口的循环初始化

cpp 复制代码
for(int i = 0;i < MAX_PORT;i ++){
    
    //新的端口
    int sockfd = init_server(port + i);

    conn_list[sockfd].fd = sockfd;
    conn_list[sockfd].r_action.recv_callback = accept_cb;

    set_event(sockfd, EPOLLIN, 1);
}

简化服务器端send输出,每一千条打印一次数据和所耗时间,不再显示recv和listen结果

创建开始时时间结构体变量

cpp 复制代码
struct timeval begin;

获取开始时的时间

cpp 复制代码
gettimeofday(&begin,NULL);

获取每建立1000个连接时的时间

cpp 复制代码
struct timeval current;
gettimeofday(&current, NULL);

宏定义耗时计算方法

cpp 复制代码
#define TIME_SUB_MS(tv1, tv2)   ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

计算耗时并更新每次时间开始值

cpp 复制代码
int time_used = TIME_SUB_MS(current, begin);
memcpy(&begin, &current, sizeof(timeval));

打印结果

cpp 复制代码
printf("accept finished: %d, time_used: %d\n", clientfd, time_used);

再次运行

服务器端连接提高至132000

accept finished: 132000, time_used: 787

客户端在达到45000左右时报错

connections: 46999, sockfd:47002, time_used:2254

RecvBuffer:Hello Server: client --> 46998

data from 4

Error clientfd:43002, errno:11

connect: Connection refused

error : Connection refused

尝试调大服务器TCP队列参数,提高监听队列

全连接队列上限 sudo sysctl -w net/core/somaxconn=4096

半连接队列上限 sudo sysctl -w net.ipv4.tcp_max_syn_backlog=4096

cpp 复制代码
listen(sockfd, 4096);

再次运行

服务器端连接提高至666000

accept finished: 666000, time_used: 5542

客户端连接提高至201999

connections: 201999, sockfd:202002, time_used:2326

此时连接断开,受硬件设备限制,暂无法达到百万并发

2.3完整代码

服务器端

cpp 复制代码
#include<stdio.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<pthread.h>
#include<unistd.h>
#include<poll.h>
#include<sys/epoll.h>
#include<sys/time.h>

//宏定义设定缓冲区和连接列表的大小
#define BUFFER_LENGTH       1024
#define CONNECTION_SIZE     1048576
#define MAX_PORTS            20

#define TIME_SUB_MS(tv1, tv2)   ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

//声明处理事件的回调函数类型,统一接口便于分发时间
typedef int (*RCALLBACK)(int fd);

//epoll实例的全局变量,各函数中操作同一个epoll
int epfd = 0;
//具体回调函数的声明
int recv_cb(int fd);
int accept_cb(int fd);
int send_cb(int fd);

//创建开始时时间结构体变量
struct timeval begin;

//连接信息结构体
struct conn{
    //套接字,客户端fd或者监听fd
    int fd;
    
    //读写数据的缓冲区数组和大小
    char rbuffer[BUFFER_LENGTH];
    int rlength;

    char wbuffer[BUFFER_LENGTH];
    int wlength;

    //把回调函数的指针加入结构体
    RCALLBACK send_callback;
    //互斥状态的两个回调函数指针,共同体的形式加入结构体(客户端fd调用recv_callback,监听fd调用accept_callback)
    //共同体,所有成员公用同一块内存空间,节省内存
    union{
        RCALLBACK recv_callback;
        RCALLBACK accept_callback;
    } r_action;

};

//用数组存储所有连接
struct conn conn_list[CONNECTION_SIZE] = {0};

//添加/修改epoll事件
int set_event(int fd, int event, int flag){
    //flag非零时,添加epoll事件
    if(flag){//no-zero add
        //创建epoll_event类型变量
        struct epoll_event ev;
        //设定关注的事件类型
        ev.events = event;
        //将当前fd存入ev的data.fd对象中
        ev.data.fd = fd;
        //添加到epfd的epoll实例中
        epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
    }else{//zero mod
        struct epoll_event ev;
        ev.events = event;
        ev.data.fd = fd;
        //修改epfd中的epoll实例
        epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
    }
}

//为epoll实例中添加新的客户端连接
int event_register(int fd, int event){

    if(fd < 0) return -1;

    //初始化连接信息,绑定fd,设置回调函数
    conn_list[fd].fd = fd;
    //添加连接,共同体中选择recv_cb回调函数
    conn_list[fd].r_action.recv_callback = recv_cb;
    conn_list[fd].send_callback = send_cb;

    //初始化缓冲区
    conn_list[fd].rlength = 0;
    conn_list[fd].wlength = 0;

    //调用set_event函数添加事件,并监控可读事件
    set_event(fd, EPOLLIN, 1);
}

//listen(sockfd) --> EPOLLIN --> accept_cb
//接收客户端连接请求,创建客户端fd并完成初始化
int accept_cb(int fd){

    //定义客户端地址结构体,计算长度
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    //调用accept函数,从监听fd接收数据并创建对应地址的客户端fd
    int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
    //printf("accept finished: %d\n", clientfd);
    if(clientfd < 0){
        printf("accept error: %d\n", errno);
        return -1;
    }

    //调用event_register函数初始化连接信息,关注可读事件
    event_register(clientfd, EPOLLIN);

    if(clientfd % 1000 == 0){
        
        //获取每建立1000个连接时的时间
        struct timeval current;
        gettimeofday(&current, NULL);

        //计算耗时
        int time_used = TIME_SUB_MS(current, begin);
        //更新每次时间开始值
        memcpy(&begin, &current, sizeof(struct timeval));

        printf("accept finished: %d, time_used: %d\n", clientfd, time_used);
    }

    return 0;
}

//客户端fd触发EPOLLIN事件的回调函数,接收客户端发送的数据
int recv_cb(int fd){

    memset(conn_list[fd].rbuffer, 0, BUFFER_LENGTH );
    //调用recv接收客户端数据,存入该连接的接收缓冲区,通过接收数据长度判断接收状态
    int count = recv(fd, conn_list[fd].rbuffer, BUFFER_LENGTH, 0);
    //状态为零时客户端主动断开连接
    if(count == 0){
        printf("clientfd disconnect: %d\n", fd);
        //关闭断开的客户端fd
        close(fd);
        //将该fd从epfd的epoll实例中删除,不再监控
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);// 无需ev结构体

        return 0;
    }
    //存储数据长度
    conn_list[fd].rlength  = count;

    //打印接收数据
    //printf("RRECV: %s\n", conn_list[fd].rbuffer);

#if 1 //echo  回声模式开关,1开启

    //将接收缓冲区的数据和数据长度存储到发送缓冲区中,用于send函数
    conn_list[fd].wlength = conn_list[fd].rlength;
    memcpy(conn_list[fd].wbuffer, conn_list[fd].rbuffer, conn_list[fd].wlength);

#endif

    //调用set_event函数修改该fd的关注事件为EPOLLOUT可写
    //让epoll后续触发可写事件,调用send_cb发送缓冲区中数据
    set_event(fd, EPOLLOUT, 0);

    return count;
}

//客户端fd触发可写事件后,发送缓冲区中的数据
int send_cb(int fd){

    //调用send函数将发送缓冲区中的数据发送至客户端fd
    int count = send(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);

    //调用set_event函数将该fd连接信息中的关注事件修改为可读,从而再次接收数据
    set_event(fd, EPOLLIN, 0);

    return count;
}

//创建服务器,开启监听fd
int init_server(unsigned short port){

    //创建TCP流式套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    //设置服务器地址信息,IPv4,绑定所有本地网卡,绑定端口
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
    servaddr.sin_port = htons(port); //0-1023

    //绑定套接字到服务器地址和端口
    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))){
        printf("bind failed: %s\n", strerror(errno));
    }

    //开启监听套接字,最大等待队列为10
    listen(sockfd, 4096);
    //printf("listen finished: %d\n", sockfd);

    return sockfd;
}

int main(){

    //设置端口
    unsigned short port = 2000;

    //调用epoll_create函数创建一个epoll实例
    epfd = epoll_create(1);

    for(int i = 0;i < MAX_PORTS;i ++){
    
        //初始化服务器,创建套接字,绑定地址,开始监听
        //返回监听套接字
        int sockfd = init_server(port + i);

        //将监听套接字存入连接列表中
        conn_list[sockfd].fd = sockfd;
        //设置accept_cb为共同体回调函数的处理,即监听到可读事件后调用accept_cb函数
        conn_list[sockfd].r_action.recv_callback = accept_cb;

        //调用set_event函数将监听套接字加入到epoll实例中,监控其可读事件
        set_event(sockfd, EPOLLIN, 1);
    }

    //获取开始时的时间
    gettimeofday(&begin,NULL);

    //主循环,处理新连接,收发客户端数据
    while(1){//mainloop

        //创建数组存储就绪事件,初始化为0
        struct epoll_event events[1024] = {0};
        //调用epoll_wait阻塞等待,直到监控的fd触发了就绪事件,并统计数量
        int nready = epoll_wait(epfd, events, 1024, -1);

        //循环遍历所有就绪事件(时间复杂度为O(k))
        int i = 0;
        for(i = 0;i < nready;i ++){

            //获取当前就绪事件对应的fd,存入connfd中,简化操作
            int connfd = events[i].data.fd;

            //当就绪事件触发可读事件时,执行连接列表中该fd的recv_callback回调函数,添加新连接或读取数据
            if(events[i].events & EPOLLIN){
                conn_list[connfd].r_action.recv_callback(connfd);
            }
            //当就绪事件触发可写事件时,执行连接列表中该fd的send_callback回调函数,将发送缓冲区的数据发送至客户端
            if(events[i].events & EPOLLOUT){
                conn_list[connfd].send_callback(connfd);
            }
        }
    }
}

客户端

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/time.h> 
#include <unistd.h> 


#define MAX_BUFFER		128
#define MAX_EPOLLSIZE	(384*1024)
#define MAX_PORT		100

#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)

int isContinue = 0;

static int ntySetNonblock(int fd) {
	int flags;

	flags = fcntl(fd, F_GETFL, 0);
	if (flags < 0) return flags;
	flags |= O_NONBLOCK;
	if (fcntl(fd, F_SETFL, flags) < 0) return -1;
	return 0;
}

static int ntySetReUseAddr(int fd) {
	int reuse = 1;
	return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}



int main(int argc, char **argv) {
	if (argc <= 2) {
		printf("Usage: %s ip port\n", argv[0]);
		exit(0);
	}

	const char *ip = argv[1];
	int port = atoi(argv[2]);
	int connections = 0;
	char buffer[128] = {0};
	int i = 0, index = 0;

	struct epoll_event events[MAX_EPOLLSIZE];
	
	int epoll_fd = epoll_create(MAX_EPOLLSIZE);
	
	strcpy(buffer, " Data From MulClient\n");
		
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(struct sockaddr_in));
	
	addr.sin_family = AF_INET;
	addr.sin_addr.s_addr = inet_addr(ip);

	struct timeval tv_begin;
	gettimeofday(&tv_begin, NULL);

	while (1) {
		if (++index >= MAX_PORT) index = 0;
		
		struct epoll_event ev;
		int sockfd = 0;

		if (connections < 340000 && !isContinue) {
			sockfd = socket(AF_INET, SOCK_STREAM, 0);
			if (sockfd == -1) {
				perror("socket");
				goto err;
			}

			//ntySetReUseAddr(sockfd);
			addr.sin_port = htons(port+index);

			if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
				perror("connect");
				goto err;
			}
			ntySetNonblock(sockfd);
			ntySetReUseAddr(sockfd);

			sprintf(buffer, "Hello Server: client --> %d\n", connections);
			send(sockfd, buffer, strlen(buffer), 0);

			ev.data.fd = sockfd;
			ev.events = EPOLLIN | EPOLLOUT;
			epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev);
		
			connections ++;
		}
		//connections ++;
		if (connections % 1000 == 999 || connections >= 340000) {
			struct timeval tv_cur;
			memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
			
			gettimeofday(&tv_begin, NULL);

			int time_used = TIME_SUB_MS(tv_begin, tv_cur);
			printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used);

			int nfds = epoll_wait(epoll_fd, events, connections, 100);
			for (i = 0;i < nfds;i ++) {
				int clientfd = events[i].data.fd;

				if (events[i].events & EPOLLOUT) {
					sprintf(buffer, "data from %d\n", clientfd);
					send(sockfd, buffer, strlen(buffer), 0);
				} else if (events[i].events & EPOLLIN) {
					char rBuffer[MAX_BUFFER] = {0};				
					ssize_t length = recv(sockfd, rBuffer, MAX_BUFFER, 0);
					if (length > 0) {
						printf(" RecvBuffer:%s\n", rBuffer);

						if (!strcmp(rBuffer, "quit")) {
							isContinue = 0;
						}
						
					} else if (length == 0) {
						printf(" Disconnect clientfd:%d\n", clientfd);
						connections --;
						close(clientfd);
					} else {
						if (errno == EINTR) continue;

						printf(" Error clientfd:%d, errno:%d\n", clientfd, errno);
						close(clientfd);
					}
				} else {
					printf(" clientfd:%d, errno:%d\n", clientfd, errno);
					close(clientfd);
				}
			}
		}

		usleep(1 * 1000);
	}

	return 0;

err:
	printf("error : %s\n", strerror(errno));
	return 0;
	
}
相关推荐
00后程序员张4 小时前
【Python】基于 PyQt6 和 Conda 的 PyInstaller 打包工具
运维·服务器·数据库
·心猿意码·4 小时前
C++Lambda 表达式与函数对象
开发语言·c++
jiajixi4 小时前
go-swagger学习笔记
笔记·学习·golang
❀͜͡傀儡师4 小时前
使用docker 安装dragonfly带配置文件(x86和arm)版本
运维·docker·容器
我言秋日胜春朝★4 小时前
【Linux网络编程】多路转接reactor——ET模式的epoll
linux·服务器·网络
Mingze03145 小时前
C语言四大排序算法实战
c语言·数据结构·学习·算法·排序算法
东风西巷5 小时前
STranslate(翻译工具OCR工具) 中文绿色版
学习·ocr·电脑·软件需求
程序员东岸6 小时前
学完顺序表后,用 C 语言写了一个通讯录
数据结构·笔记·学习
拱-卒6 小时前
VB.NET入门学习教程
学习