本文章承接上一篇文章2.1.1网络io与select,poll,epoll,使用epoll
以及reactor
实现服务器百万并发。
calloc()
在动态分配完内存后,自动初始化该内存空间为零,而malloc()
不初始化,里边数据是随机的垃圾数据。因此使用malloc()
后需要调用memset()
进行初始化而使用calloc()则不需要。
#include
#include
#include
#include
#include // struct sockaddr_in
#include
#include // close()函数
#include // memcpy()函数
#include
#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
进行进一步的封装链表:适合查找有序、有规律的数据
修改代码如下:
#include
#include
#include
#include
#include // struct sockaddr_in
#include
#include // close()函数
#include // memcpy()函数
#include
#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,目的端口,协议),五元组确定一个唯一的连接。
为了达到百万连接,源端口不够的解决方法(对客户端而言):
其中客户端的源端口范围为0-65535,最大值为固定值,不可改变。
这里采用增加目的端口的方案,即增加服务器端口。
修改代码如下:
#include
#include
#include
#include
#include // struct sockaddr_in
#include
#include // close()函数
#include // memcpy()函数
#include
#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);
}
}
}
}
流程:
通过测试,该代码可实现百万并发。
需要准备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 异常,解决方法