新连接的建立
- 创建套接字:使用
socket()
函数创建一个监听套接字,例如:
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
- 绑定地址:将监听套接字绑定到指定的地址和端口,使用
bind()
函数:
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(listen_fd);
exit(EXIT_FAILURE);
}
- 监听连接:使用
listen()
函数使套接字进入监听状态:
if (listen(listen_fd, BACKLOG) == -1) {
perror("listen");
close(listen_fd);
exit(EXIT_FAILURE);
}
- 添加到epoll:创建一个epoll实例,使用
epoll_create1()
函数,然后将监听套接字添加到epoll实例中,使用 epoll_ctl()
函数:
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(listen_fd);
exit(EXIT_FAILURE);
}
struct epoll_event event;
event.data.fd = listen_fd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) == -1) {
perror("epoll_ctl: listen_fd");
close(listen_fd);
close(epoll_fd);
exit(EXIT_FAILURE);
}
- 接受新连接:在epoll_wait() 返回且事件为监听套接字的可读事件时,使用
accept()
函数接受新连接,并将新连接的套接字也添加到epoll实例中:
if (events[i].data.fd == listen_fd) {
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd == -1) {
perror("accept");
continue;
}
setnonblocking(client_fd);
event.data.fd = client_fd;
event.events = EPOLLIN | EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd");
close(client_fd);
}
}
数据的读写
- 读数据:当epoll_wait() 返回且事件为某个客户端套接字的可读事件时,使用
read()
函数读取数据。由于采用边缘触发模式,需要循环读取直到 read()
返回 EAGAIN
或 EWOULDBLOCK
:
ssize_t read_bytes;
char buffer[BUFFER_SIZE];
while ((read_bytes = read(client_fd, buffer, sizeof(buffer))) > 0) {
// 处理读取到的数据
}
if (read_bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 数据读完,退出循环
} else if (read_bytes == -1) {
perror("read");
close(client_fd);
} else if (read_bytes == 0) {
// 对方关闭连接
close(client_fd);
}
- 写数据:当需要向客户端发送数据时,先将数据准备好,然后使用
write()
函数发送。同样在边缘触发模式下,可能需要循环写入直到数据全部发送完毕:
ssize_t write_bytes;
while ((write_bytes = write(client_fd, response_buffer, response_length)) < response_length) {
if (write_bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
// 设置写事件等待,下次epoll_wait返回时继续写
event.data.fd = client_fd;
event.events = EPOLLOUT | EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd for write");
close(client_fd);
}
break;
} else if (write_bytes == -1) {
perror("write");
close(client_fd);
break;
}
}
事件的分发机制
- epoll_wait:使用
epoll_wait()
函数等待事件发生,该函数会阻塞直到有事件就绪:
int num_events = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_events == -1) {
perror("epoll_wait");
close(epoll_fd);
close(listen_fd);
exit(EXIT_FAILURE);
}
- 事件处理:遍历
epoll_wait()
返回的事件数组 events
,根据事件类型(EPOLLIN
可读、EPOLLOUT
可写等)和对应的文件描述符,调用相应的处理函数,如上述的新连接建立处理、数据读写处理等。
常见问题及解决方案
- 惊群问题:在多个进程或线程同时监听同一个套接字时,可能会出现多个进程或线程同时被唤醒去处理同一个连接的情况。解决方案是使用
epoll
的边缘触发模式,并且在接受连接后及时将监听套接字从epoll实例中移除,处理完连接后再重新添加。
- 高并发下的性能问题:随着并发连接数的增加,可能会出现性能瓶颈。解决方案包括优化代码逻辑,减少不必要的系统调用,合理设置缓冲区大小,采用高效的内存管理策略,以及使用多线程或多进程来分担负载。
- 连接异常关闭:客户端可能异常关闭连接,导致服务器端的读或写操作出错。在读写操作出错时,需要检查错误码,对于连接关闭相关的错误(如
EPIPE
、ECONNRESET
等),及时关闭对应的套接字,清理相关资源。
- 内存泄漏:在处理连接和数据时,如果内存分配后没有正确释放,会导致内存泄漏。需要仔细管理内存,例如在关闭连接时,释放与该连接相关的所有已分配内存。