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(¤t, 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, ¤t, 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(¤t, NULL);
//计算耗时
int time_used = TIME_SUB_MS(current, begin);
//更新每次时间开始值
memcpy(&begin, ¤t, 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;
}