1、epoll
epoll
是Linux内核提供的一种高效的事件通知机制,用于I/O多路复用。它允许一个线程监视多个文件描述符(如套接字),并在这些文件描述符上有事件发生时通知应用程序。epoll
相比于传统的select
和poll
机制,提供了更高的性能和可扩展性,特别适用于高并发和高吞吐量的网络服务器。
1.1、epoll
的基本概念
- 文件描述符(File Descriptor, fd):在 Linux 系统中,每个打开的文件或套接字都有一个唯一的文件描述符。
epoll
通过文件描述符来监听特定的事件。 - epoll 实例:通过
epoll_create
或epoll_create1
创建的一个 epoll 实例,用于管理和监听一组文件描述符。 - 事件(Event):表示文件描述符上的特定操作,如读(
EPOLLIN
)、写(EPOLLOUT
)、错误(EPOLLERR
)等。 - 事件监听:通过
epoll_ctl
函数向 epoll 实例注册或修改文件描述符的事件监听。 - 事件通知:通过
epoll_wait
函数等待事件的发生,当注册的事件发生时,epoll_wait
会返回,应用程序可以进行相应的处理。
1.2、创建epoll实例
-
epoll_create
- 创建一个新的epoll(7)实例。
-
函数原型
-
int epoll_create(int size);
-
size
:这是一个历史遗留参数,最初用于告诉内核预期要监听的文件描述符的数量。从Liunx2.6.8开始,这个参数在 Linux 内核中已经被忽略,但为了兼容性,仍然需要传递一个大于 0 的值。
-
-
返回值
-
成功时返回一个非负的文件描述符(epfd),表示新创建的
epoll
实例。 -
失败时返回 -1,并设置
errno
以指示错误。
-
-
epoll_create1
- 创建一个新的epoll(7)实例。
-
函数原型
-
int epoll_create1(int flags);
-
flags:这是一个位掩码参数,用于指定额外的选项。目前只有一个标志位有效:
EPOLL_CLOEXEC
:在调用exec
系列函数时自动关闭epoll
实例的文件描述符,避免文件描述符泄漏到子进程中。
-
-
返回值
-
成功时返回一个非负的文件描述符(epfd),表示新创建的
epoll
实例。 -
失败时返回 -1,并设置
errno
以指示错误。
-
-
总结
-
如果你不需要额外的选项,并且希望兼容较旧的 Linux 系统,可以使用
epoll_create
。 -
如果你需要使用
EPOLL_CLOEXEC
选项,或者你确定你的代码只会在支持epoll_create1
的较新 Linux 系统上运行,那么使用epoll_create1
是更好的选择。
-
1.3、epoll_ctl
-
epoll_ctl是一个系统调用,向epoll实例中添加、删除或修改文件描述符及相关事件。
-
函数原型
-
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-
epfd
:epoll
实例的文件描述符(通过epoll_create
或epoll_create1
创建)。 -
op
:操作类型,指定要执行的操作。可以是以下三种之一:EPOLL_CTL_ADD
:将目标文件描述符fd
添加到epoll
实例中,并关联事件event
。EPOLL_CTL_MOD
:修改与目标文件描述符fd
关联的事件。EPOLL_CTL_DEL
:从epoll
实例中删除目标文件描述符fd
。
-
fd
:目标文件描述符,通常是一个套接字描述符。 -
event
:指向epoll_event
结构的指针,描述要监听的事件。-
epoll_event
结构-
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; // 事件类型 epoll_data_t data; // 用户数据 };
-
events
:这是一个位掩码,指定要监听的事件类型。常见的事件类型包括:EPOLLIN
:可读事件。EPOLLOUT
:可写事件。EPOLLRDHUP
:对端关闭连接或关闭写入(用于边缘触发模式)。EPOLLPRI
:紧急数据到达(通常用于带外数据)。EPOLLERR
:错误事件(总是会触发,无需显式指定)。EPOLLHUP
:挂断事件(总是会触发,无需显式指定)。EPOLLET
:启用边缘触发模式(默认是水平触发模式)。EPOLLONESHOT
:一次性事件,触发后自动禁用。
-
data
:这是一个联合体,用于存储用户数据。通常情况下,fd
字段会被使用,用于存储文件描述符。
-
-
-
-
返回值
-
成功时返回 0。
-
失败时返回 -1,并设置
errno
以指示错误。
-
-
示例
-
int sockfd = socket(AF_INET, SOCK_STREAM, 0); struct epoll_event ev; ev.events = EPOLLIN; // 监听可读事件 ev.data.fd = sockfd; // 存储文件描述符 // 添加事件 epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 修改事件 ev.events = EPOLLOUT; epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); // 删除事件 epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL)
-
-
总结
epoll_ctl
是管理epoll
实例的核心函数,通过它可以灵活地添加、修改和删除文件描述符及其相关事件。理解并熟练使用epoll_ctl
对于编写高性能的网络服务器应用程序至关重要。通过合理配置epoll_event
结构,可以实现对特定事件的高效监听和处理。
1.4、epoll_wait
-
epoll_wait
是系统调用, Linux 系统中用于等待epoll
实例中的文件描述符事件发生的函数。通过epoll_wait
,应用程序可以阻塞等待多个文件描述符上的事件,直到有事件发生或超时。 -
函数原型
-
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
epfd
:epoll
实例的文件描述符(通过epoll_create
或epoll_create1
创建)。 -
events
:指向epoll_event
数组的指针,用于存储返回的事件。 -
maxevents
:events
数组的大小,表示最多可以返回的事件数量。 -
timeout
:超时时间,单位为毫秒。可以设置为以下值:-1
:无限期等待,直到有事件发生。0
:立即返回,无论是否有事件发生。>0
:等待指定的毫秒数,超时后返回。
-
-
返回值
- 成功时返回发生的事件数量(即
events
数组中实际存储的事件数量)。 - 超时返回 0。
- 失败时返回 -1,并设置
errno
以指示错误。
- 成功时返回发生的事件数量(即
1.5、触发方式
-
在
epoll
机制中,事件触发模式有两种:水平触发(Level Triggered,LT)和边沿触发(Edge Triggered,ET)。这两种触发模式决定了epoll
如何通知应用程序文件描述符上的事件。理解这两种触发模式的差异对于优化 I/O 多路复用系统的性能至关重要。 -
水平触发(Level Triggered,LT)
-
水平触发 是
epoll
的默认触发模式。在这种模式下,只要文件描述符上存在未处理的事件,epoll
就会持续通知应用程序。具体来说:- 如果文件描述符处于可读状态(例如接收缓冲区中有数据可读),
epoll
会持续通知应用程序,直到应用程序处理完所有数据。 - 如果文件描述符处于可写状态(例如发送缓冲区有空闲空间可写),
epoll
会持续通知应用程序,直到应用程序写入足够的数据。
- 如果文件描述符处于可读状态(例如接收缓冲区中有数据可读),
-
优点:
-
易于使用,不易发生漏读或漏写的情况。
-
对应用程序的编写要求较低,开发者无需担心一次性读取或写入所有数据。
-
-
缺点:
-
可能会产生不必要的通知,增加系统的开销。
-
如果应用程序处理事件的速度较慢,可能会导致大量的通知积压。
-
-
-
边沿触发(Edge Triggered,ET)
-
边沿触发 模式下,
epoll
仅在文件描述符的状态发生变化时通知应用程序。具体来说:-
如果文件描述符从不可读变为可读(例如接收缓冲区中有新数据到达),
epoll
会通知应用程序。 -
如果文件描述符从不可写变为可写(例如发送缓冲区有空闲空间),
epoll
会通知应用程序。
-
-
优点:
-
大大减少了不必要的通知,提高了系统的效率。
-
适用于高并发、高性能的场景,能够更好地利用系统资源。
-
-
缺点:
- 编程复杂度较高,应用程序必须一次性读取或写入所有可用数据,否则可能会漏掉事件。
- 如果应用程序未能及时处理事件,可能导致事件丢失(例如一次读取未能读取所有数据,剩余数据将不再通知)。
-
-
使用示例
水平触发模式(默认)
在水平触发模式下,以下代码每调用一次 epoll_wait
就会返回可读事件,直到所有的数据都被读取:
struct epoll_event ev;
ev.events = EPOLLIN; // 默认是水平触发
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
char buf[1024];
ssize_t num_bytes = read(events[i].data.fd, buf, sizeof(buf));
// 处理读取的数据
}
}
}
边沿触发模式
在边沿触发模式下,需要在 epoll_event
中显式设置 EPOLLET
标志:
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 启用边沿触发
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
char buf[1024];
while (1) {
ssize_t num_bytes = read(events[i].data.fd, buf, sizeof(buf));
if (num_bytes < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break; // 数据读取完毕
}
perror("read");
break;
} else if (num_bytes == 0) {
// 对端关闭连接
break;
}
// 处理读取的数据
}
}
}
}
-
总结
-
水平触发(LT):默认模式,容易使用,不易漏读或漏写。
-
边沿触发(ET):效率高,适用于高并发场景,但编程复杂度较高,需要一次性处理所有事件。
- 选择哪种触发模式取决于具体的应用需求和性能考虑。对于大多数应用来说,水平触发模式已经足够,但在需要极致性能的场景下,边沿触发模式可能更适合。
-
1.6、epoll简单示例
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/epoll.h>
#include <string.h>
#include <unistd.h>
int main()
{
// 创建tcp套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(2000);
// 绑定地址
if(-1 == bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
// 监听连接
listen(tcp_socket, 20);
// 创建epoll实例
int epfd = epoll_create(1);
struct epoll_event ev;
ev.data.fd = tcp_socket;
ev.events = EPOLLIN;
// 添加epoll监听事件
epoll_ctl(epfd, EPOLL_CTL_ADD, tcp_socket, &ev);
int nready = 0;
struct epoll_event events[20] = {0};
char buffer[1024] = {0};
for(;;)
{
// 等待事件到来
nready = epoll_wait(epfd, events, 20, -1);
for(int i = 0; i < nready; i++)
{
// 如果事件可读
if(events[i].events & EPOLLIN)
{
// 判断是否是连接到来
if(events[i].data.fd == tcp_socket)
{
int client_fd = accept(tcp_socket, NULL, NULL);
ev.data.fd = client_fd;
// 添加epoll监听事件
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
continue;
}
else
{
memset(buffer, 0, 1024);
int len = recv(events[i].data.fd, buffer, 1024, 0);
if(0 == len)
{
printf("%d:连接断开\n", events[i].data.fd);
// 删除监听事件
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
continue;
}
else if(-1 == len)
{
perror("recv");
break;
}
printf("recv:%s\n", buffer);
}
}
}
}
close(tcp_socket);
close(epfd);
return 0;
}
2、百万并发(仅连接)
1.1、Reactor
- 在网络编程中,Reactor模式(反应器模式)是一种用于事件驱动架构的设计模式。它的主要目的是处理多个并发输入/输出(I/O)操作,通常用于构建高性能、可扩展的网络服务器。Reactor模式的核心思想是将I/O操作的事件处理和实际的业务逻辑分离,以提高系统的并发处理能力和响应速度。
1.2、并发实现
-
头文件
-
reactor.h
-
#ifndef _REACTOR_H_ #define _REACTOR_H_ #define PORT 1 // 端口 #define IP INADDR_ANY // IP #define EVENT_SIZE 10 // epoll一次返回事件的大小 #define CONN_SIZE 1048576 #define EPOLLIN_BUFFER_LEN 1024 #define EPOLLOUT_BUFFER_LEN 1024 typedef int(*CALLBACK)(int); // 存储连接数据 struct conn{ int fd; // EPOLLIN int in_len; char epollin_buffer[EPOLLIN_BUFFER_LEN]; CALLBACK epollin_cb; // EPOLLOUT int out_len; char epollout_buffer[EPOLLOUT_BUFFER_LEN]; CALLBACK epollout_cb; }; #endif
- conn用于存储连接的fd与接收发送的数据,因为fd是唯一的,所有可以使用fd作为conn数组的下标
-
-
设置事件
-
向epoll添加事件,或修改现有事件
-
int set_event(int fd, int event, int flag) { struct epoll_event ev; ev.data.fd = fd; ev.events = event; // flag不等于0,添加事件,否则改变。 if (flag) { epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); } else { epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev); } }
-
-
注册事件
-
当有连接到来时,需要向epoll实例中注册事件。
-
int event_register(int fd, int event) { if(fd < 0) return -1; conn_list[fd].fd = fd; conn_list[fd].epollin_cb = recv_cb; conn_list[fd].epollout_cb = send_cb; conn_list[fd].in_len = 0; conn_list[fd].out_len = 0; memset(conn_list[fd].epollin_buffer, 0, EPOLLIN_BUFFER_LEN); memset(conn_list[fd].epollout_buffer, 0, EPOLLOUT_BUFFER_LEN); set_event(fd, event, 1); return 0; }
-
-
接收连接
-
接收客户端的连接,并向epoll中添加该连接
-
int accept_cb(int fd) { int new_fd = accept(fd, NULL, NULL); if (-1 == new_fd) { perror("accept"); } // printf("%d : 建立连接\n", new_fd); event_register(new_fd, EPOLLIN); if ((new_fd % 1000) == 0) { struct timeval current; gettimeofday(¤t, NULL); int time_used = TIME_SUB_MS(current, begin); memcpy(&begin, ¤t, sizeof(struct timeval)); printf("连接完成: %d, 用时: %d\n", new_fd, time_used); } }
-
-
接收数据
-
接收客户端发送的数据,并将事件修改为EPOLLOUT,向客户端发送一条数据
- 因为已经建立的连接fd一直都是处于可写状态,所有将事件设置为EPOLLOUT即可立即向客户端发送数据
-
int recv_cb(int fd) { printf("等待数据到来\n"); conn_list[fd].in_len = 0; memset(conn_list[fd].epollin_buffer, 0, EPOLLIN_BUFFER_LEN); int len = recv(fd, conn_list[fd].epollin_buffer, EPOLLIN_BUFFER_LEN, 0); if(-1 == len) { perror("recv"); return -1; } if(0 == len) { printf("%d : 连接关闭!\n", fd); epoll_ctl(fd, EPOLL_CTL_DEL, fd, NULL); close(fd); return 0; } // printf("recv_cb:%s\n", conn_list[fd].epollin_buffer); conn_list[fd].in_len = len; set_event(fd, EPOLLOUT, 0); }
-
-
发送数据
-
当事件处于EPOLLOUT时,发送数据
-
int send_cb(int fd) { conn_list[fd].out_len = conn_list[fd].in_len; memset(conn_list[fd].epollout_buffer, 0, EPOLLOUT_BUFFER_LEN); memcpy(conn_list[fd].epollout_buffer, conn_list[fd].epollin_buffer, conn_list[fd].in_len); if (-1 == send(fd, conn_list[fd].epollout_buffer, conn_list[fd].out_len, 0)) { perror("send"); } set_event(fd, EPOLLIN, 0); }
-
-
初始化服务器
-
成功返回fd
-
int init_server(int port) { // 创建TCP套接字 int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); if(-1 == tcp_socket) // 错误判断 { perror("socket"); } // 端口复用 int val = 1; if(-1 == setsockopt(tcp_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val))) { perror("setsockopt()"); return -1; } // 设置监听地址 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(IP); // 监听所有可用的网络接口(即所有IP地址) server_addr.sin_port = htons(port); // 服务端绑定地址 if(-1 == bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) { perror("bind"); close(tcp_socket); return -1; } // 监听 listen(tcp_socket, 20); return tcp_socket; }
-
-
main函数
-
主函数一直处于循环状态
-
int main() { int nready; struct epoll_event ev; struct epoll_event events[EVENT_SIZE] = {0}; // 初始化套接字,创建套接字、绑定地址,监听套接字 int tcp_socket = init_server(); if (-1 == tcp_socket) { return -1; } conn_list[tcp_socket].fd = tcp_socket; conn_list[tcp_socket].epollin_cb = accept_cb; epfd = epoll_create(1); ev.data.fd = tcp_socket; ev.events = EPOLLIN; if(-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, tcp_socket, &ev)) { perror("epoll_ctl"); return -1; } gettimeofday(&begin, NULL); // 获取时间 // 主循环 for(;;) { nready = epoll_wait(epfd, events, EVENT_SIZE, -1); if(-1 == nready) { perror("epoll_wait"); return -1; } for(int i = 0; i < nready; i++) { int fd = events[i].data.fd; if(EPOLLIN & events[i].events) { conn_list[fd].epollin_cb(fd); } if(EPOLLOUT & events[i].events) { conn_list[fd].epollout_cb(fd); } } } close(epfd); return 0; }
-
-
整体程序
-
reactor.c
-
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <sys/epoll.h> #include <string.h> #include <unistd.h> #include <sys/time.h> #include "reactor.h" #define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000) struct conn conn_list[CONN_SIZE] = {0}; int epfd; int recv_cb(int); int send_cb(int); int set_event(int, int, int); struct timeval begin; int event_register(int fd, int event) { if(fd < 0) return -1; conn_list[fd].fd = fd; conn_list[fd].epollin_cb = recv_cb; conn_list[fd].epollout_cb = send_cb; conn_list[fd].in_len = 0; conn_list[fd].out_len = 0; memset(conn_list[fd].epollin_buffer, 0, EPOLLIN_BUFFER_LEN); memset(conn_list[fd].epollout_buffer, 0, EPOLLOUT_BUFFER_LEN); set_event(fd, event, 1); return 0; } int set_event(int fd, int event, int flag) { struct epoll_event ev; ev.data.fd = fd; ev.events = event; // flag不等于0,添加事件,否则改变。 if (flag) { epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); } else { epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev); } } int accept_cb(int fd) { int new_fd = accept(fd, NULL, NULL); if (-1 == new_fd) { perror("accept"); } // printf("%d : 建立连接\n", new_fd); event_register(new_fd, EPOLLIN); if ((new_fd % 1000) == 0) { struct timeval current; gettimeofday(¤t, NULL); int time_used = TIME_SUB_MS(current, begin); memcpy(&begin, ¤t, sizeof(struct timeval)); printf("连接完成: %d, 用时: %d\n", new_fd, time_used); } } int recv_cb(int fd) { conn_list[fd].in_len = 0; memset(conn_list[fd].epollin_buffer, 0, EPOLLIN_BUFFER_LEN); int len = recv(fd, conn_list[fd].epollin_buffer, EPOLLIN_BUFFER_LEN, 0); if(-1 == len) { perror("recv"); return -1; } if(0 == len) { printf("%d : 连接关闭!\n", fd); epoll_ctl(fd, EPOLL_CTL_DEL, fd, NULL); close(fd); return 0; } // printf("recv_cb:%s\n", conn_list[fd].epollin_buffer); conn_list[fd].in_len = len; set_event(fd, EPOLLOUT, 0); } int send_cb(int fd) { conn_list[fd].out_len = conn_list[fd].in_len; memset(conn_list[fd].epollout_buffer, 0, EPOLLOUT_BUFFER_LEN); memcpy(conn_list[fd].epollout_buffer, conn_list[fd].epollin_buffer, conn_list[fd].in_len); if (-1 == send(fd, conn_list[fd].epollout_buffer, conn_list[fd].out_len, 0)) { perror("send"); } set_event(fd, EPOLLIN, 0); } int init_server(int port) { // 创建TCP套接字 int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); if(-1 == tcp_socket) // 错误判断 { perror("socket"); } // 端口复用 int val = 1; if(-1 == setsockopt(tcp_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val))) { perror("setsockopt()"); return -1; } // 设置监听地址 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(IP); // 监听所有可用的网络接口(即所有IP地址) server_addr.sin_port = htons(port); // 服务端绑定地址 if(-1 == bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) { perror("bind"); close(tcp_socket); return -1; } // 监听 listen(tcp_socket, 20); return tcp_socket; } int main() { int nready; struct epoll_event ev; struct epoll_event events[EVENT_SIZE] = {0}; // 初始化套接字,创建套接字、绑定地址,监听套接字 int tcp_socket = init_server(PORT); if (-1 == tcp_socket) { return -1; } conn_list[tcp_socket].fd = tcp_socket; conn_list[tcp_socket].epollin_cb = accept_cb; epfd = epoll_create(1); ev.data.fd = tcp_socket; ev.events = EPOLLIN; if(-1 == epoll_ctl(epfd, EPOLL_CTL_ADD, tcp_socket, &ev)) { perror("epoll_ctl"); return -1; } gettimeofday(&begin, NULL); // 获取时间 // 主循环 for(;;) { nready = epoll_wait(epfd, events, EVENT_SIZE, -1); if(-1 == nready) { perror("epoll_wait"); return -1; } for(int i = 0; i < nready; i++) { int fd = events[i].data.fd; if(EPOLLIN & events[i].events) { conn_list[fd].epollin_cb(fd); } if(EPOLLOUT & events[i].events) { conn_list[fd].epollout_cb(fd); } } } close(epfd); return 0; }
-
-
客户端程序
-
服务端的实现是本片文章的重点内容,在此不对客户端进行讨论
-
epoll_client.c
-
#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 1 #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); int sockfd = 0; while (1) { if (++index >= MAX_PORT) index = 0; struct epoll_event ev; 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 || errno == EAGAIN || errno == ENOTSOCK) continue; printf(" Error clientfd:%d, errno:%d\n", clientfd, errno); close(clientfd); } } else { printf(" clientfd:%d, errno:%d\n", clientfd, errno); close(clientfd); } } } usleep(500); } return 0; err: printf("error : %s\n", strerror(errno)); return 0; }
-
1.3、并发问题解决
1.3.1、编译报错
-
编译程序
-
gcc reactor.c -o reactor // 报错 collect2: fatal error: ld terminated with signal 11 [段错误], core dumped compilation terminated. /tmp/cc3xbSaQ.o:在函数‘set_event’中: reactor.c:(.text+0x19c): 截断重寻址至相符: R_X86_64_PC32 针对在
-
-
上述程序中创建了一个conn_list的数据,数组大小为1048576,因为数据空间过大,所有编译出错。
-
解决方法:编译时加上
-mcmodel=medium
-
gcc reactor.c -o reactor -mcmodel=medium
-
在编译C程序时使用
-mcmodel
选项是针对不同的内存模型,主要影响代码和数据的内存布局。不同的内存模型适用于不同的应用场景,尤其是当程序的大小超过一定限制时,选择合适的内存模型可以避免编译器或链接器错误。-mcmodel=medium
:适用于中等规模的程序,代码段和数据段的大小在 2GB 以内,代码段是位置独立的,数据段是位置依赖的。-mcmodel=large
:适用于大型程序,代码段和数据段的大小可以超过 2GB,但它们都是位置依赖的,可能会影响性能。
-
1.3.2、客户端报错“ Too many open files”
- 打开的文件太多
- 该错误是因为Liunx中对文件描述符的限制
-
使用
ulimit -a
查看文件描述的限制 -
-
使用
ulimit -n 1048576
修改文件描述符限制 -
-
1.3.3、Cannot assign requested address
-
无法分配请求的地址
-
在TCP连接中,当客户端尝试与服务器建立连接时,如果出现 “Cannot assign requested address” 错误,通常意味着客户端无法分配到一个有效的本地端口号或IP地址来进行连接。
-
客户端在短时间内尝试了大量的TCP连接,导致可用的本地端口号耗尽。
-
解决方法:
- 查看端口范围:sysctl net.ipv4.ip_local_port_range
- sudo sysctl -w net.ipv4.ip_local_port_range=“1024 65000”
-
当第二次进行上述设置后再次出现**“无法分配请求地址”**此时应该整加服务端和客户端的端口数量
-
-
此时连接数已经到达62999
-
-
解决方法:
-
在服务端增加端口的数量
-
在reactor.h中添加**#define PORTS 20**
-
主函数修改为
-
int main() { int nready; struct epoll_event ev; struct epoll_event events[EVENT_SIZE] = {0}; epfd = epoll_create(1); for(int i = 0; i < MAX_PORTS;i++) { int server_fd = init_server(PORT+i); conn_list[server_fd].fd = server_fd; conn_list[server_fd].epollin_cb = accept_cb; set_event(server_fd, EPOLLIN, 1); } gettimeofday(&begin, NULL); // 获取时间 // 主循环 for(;;) { nready = epoll_wait(epfd, events, EVENT_SIZE, -1); if(-1 == nready) { perror("epoll_wait"); return -1; } for(int i = 0; i < nready; i++) { int fd = events[i].data.fd; if(EPOLLIN & events[i].events) { conn_list[fd].epollin_cb(fd); } if(EPOLLOUT & events[i].events) { conn_list[fd].epollout_cb(fd); } } } close(epfd); return 0; }
-
-
-
1.3.4、完整代码
-
reactor.h
-
#ifndef _REACTOR_H_ #define _REACTOR_H_ #define PORT 2000 // 端口 #define IP INADDR_ANY // IP #define MAX_PORTS 20 #define EVENT_SIZE 1024 // epoll一次返回事件的大小 #define CONN_SIZE 1048576 #define EPOLLIN_BUFFER_LEN 1024 #define EPOLLOUT_BUFFER_LEN 1024 typedef int(*CALLBACK)(int); // 存储连接数据 struct conn{ int fd; // EPOLLIN int in_len; char epollin_buffer[EPOLLIN_BUFFER_LEN]; CALLBACK epollin_cb; // EPOLLOUT int out_len; char epollout_buffer[EPOLLOUT_BUFFER_LEN]; CALLBACK epollout_cb; }; #endif
-
-
reactor.c
-
#include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <sys/epoll.h> #include <string.h> #include <unistd.h> #include <sys/time.h> #include "reactor.h" #define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000) struct conn conn_list[CONN_SIZE] = {0}; int epfd; int recv_cb(int); int send_cb(int); int set_event(int, int, int); struct timeval begin; int event_register(int fd, int event) { if(fd < 0) return -1; conn_list[fd].fd = fd; conn_list[fd].epollin_cb = recv_cb; conn_list[fd].epollout_cb = send_cb; conn_list[fd].in_len = 0; conn_list[fd].out_len = 0; memset(conn_list[fd].epollin_buffer, 0, EPOLLIN_BUFFER_LEN); memset(conn_list[fd].epollout_buffer, 0, EPOLLOUT_BUFFER_LEN); set_event(fd, event, 1); return 0; } int set_event(int fd, int event, int flag) { struct epoll_event ev; ev.data.fd = fd; ev.events = event; // flag不等于0,添加事件,否则改变。 if (flag) { epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev); } else { epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev); } } int accept_cb(int fd) { int new_fd = accept(fd, NULL, NULL); if (-1 == new_fd) { perror("accept"); } // printf("%d : 建立连接\n", new_fd); event_register(new_fd, EPOLLIN); if ((new_fd % 1000) == 0) { struct timeval current; gettimeofday(¤t, NULL); int time_used = TIME_SUB_MS(current, begin); memcpy(&begin, ¤t, sizeof(struct timeval)); printf("连接完成: %d, 用时: %d\n", new_fd, time_used); } } int recv_cb(int fd) { conn_list[fd].in_len = 0; memset(conn_list[fd].epollin_buffer, 0, EPOLLIN_BUFFER_LEN); int len = recv(fd, conn_list[fd].epollin_buffer, EPOLLIN_BUFFER_LEN, 0); if(-1 == len) { perror("recv"); return -1; } if(0 == len) { printf("%d : 连接关闭!\n", fd); epoll_ctl(fd, EPOLL_CTL_DEL, fd, NULL); close(fd); return 0; } // printf("recv_cb:%s\n", conn_list[fd].epollin_buffer); conn_list[fd].in_len = len; set_event(fd, EPOLLOUT, 0); } int send_cb(int fd) { conn_list[fd].out_len = conn_list[fd].in_len; memset(conn_list[fd].epollout_buffer, 0, EPOLLOUT_BUFFER_LEN); memcpy(conn_list[fd].epollout_buffer, conn_list[fd].epollin_buffer, conn_list[fd].in_len); if (-1 == send(fd, conn_list[fd].epollout_buffer, conn_list[fd].out_len, 0)) { perror("send"); } set_event(fd, EPOLLIN, 0); } int init_server(int port) { // 创建TCP套接字 int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); if(-1 == tcp_socket) // 错误判断 { perror("socket"); } // 端口复用 int val = 1; if(-1 == setsockopt(tcp_socket, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val))) { perror("setsockopt()"); return -1; } // 设置监听地址 struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(IP); // 监听所有可用的网络接口(即所有IP地址) server_addr.sin_port = htons(port); // 服务端绑定地址 if(-1 == bind(tcp_socket, (struct sockaddr *)&server_addr, sizeof(struct sockaddr))) { perror("bind"); close(tcp_socket); return -1; } // 监听 listen(tcp_socket, 20); return tcp_socket; } int main() { int nready; struct epoll_event ev; struct epoll_event events[EVENT_SIZE] = {0}; epfd = epoll_create(1); for(int i = 0; i < MAX_PORTS;i++) { int server_fd = init_server(PORT+i); conn_list[server_fd].fd = server_fd; conn_list[server_fd].epollin_cb = accept_cb; set_event(server_fd, EPOLLIN, 1); } gettimeofday(&begin, NULL); // 获取时间 // 主循环 for(;;) { nready = epoll_wait(epfd, events, EVENT_SIZE, -1); if(-1 == nready) { perror("epoll_wait"); return -1; } for(int i = 0; i < nready; i++) { int fd = events[i].data.fd; if(EPOLLIN & events[i].events) { conn_list[fd].epollin_cb(fd); } if(EPOLLOUT & events[i].events) { conn_list[fd].epollout_cb(fd); } } } close(epfd); return 0; }
-
-
epoll_client.c
-
#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 20 #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); int sockfd = 0; while (1) { if (++index >= MAX_PORT) index = 0; struct epoll_event ev; 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 || errno == EAGAIN || errno == ENOTSOCK) continue; printf(" Error clientfd:%d, errno:%d\n", clientfd, errno); close(clientfd); } } else { printf(" clientfd:%d, errno:%d\n", clientfd, errno); close(clientfd); } } } usleep(500); } return 0; err: printf("error : %s\n", strerror(errno)); return 0; }
-