C语言高性能网络编程:epoll并发模型详解与实现
针对传统IO多路复用性能的瓶颈,C语言网络开发常采用EPOLL技术构建高并发服务器。本文将通过完整的服务端(server.c)与客户端(client.c)代码实例,详细拆解从Socket绑定到Epoll事件循环的标准操作流程,帮助开发者快速掌握Linux下高性能网络编程的核心实现。
环境准备与核心头文件
在开始编码前,需确保运行环境为Linux系统。编写基于Epoll的网络程序需要引入特定的系统头文件,以支持套接字(Socket)、异步I/O通知(Epoll)及非阻塞操作(Fcntl)。核心依赖包括:
<sys/socket.h>和<netinet/in.h>:用于创建Socket及管理地址结构。<sys/epoll.h>:提供epoll相关的API接口。<fcntl.h>:用于设置文件描述符的非阻塞属性。
第1步:创建Socket并配置非阻塞模式
首先初始化服务端套接字。我们需要使用socket()创建TCP流式套接字,并利用fcntl()将全局或具体的文件描述符设置为非阻塞(O_NONBLOCK),这是配合ET(边缘触发)模式工作的必要条件,以防止因数据未读完导致的死锁。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <unistd.h>
// 定义端口及连接限制
#define PORT 4444
#define BACKLOG 5
#define EPOLL_SIZE 20
void setnonblocking(int sock_fd) {
int opts = fcntl(sock_fd, F_GETFL);
if(opts == -1) { perror("fcntl"); exit(1); }
opts = opts | O_NONBLOCK;
if(fcntl(sock_fd, F_SETFL, opts) == -1) { perror("set fl"); exit(1); }
}
int main() {
int sock_fd, epfd, nfds, i;
struct sockaddr_in server_addr, client_addr;
socklen_t sin_size = sizeof(struct sockaddr_in);
// 1. 创建监听Socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Creating socket failed.");
exit(1);
}
// 2. 设置端口复用选项,便于调试重启服务
int opt = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 3. 设置非阻塞模式
setnonblocking(sock_fd);
第2步:绑定地址并启动Epoll监控
完成Socket基础配置后,填充本地IP与端口信息进行绑定。同时,创建一个Epoll实例文件描述符,并将监听Socket加入监控列表,关注其读事件(EPOLLIN)。
// 1. 配置本地地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind error");
exit(1);
}
// 2. 开启监听,BACKLOG参数控制等待队列长度
if (listen(sock_fd, BACKLOG) == -1) {
perror("Listen error");
exit(1);
}
// 3. 创建Epoll句柄
epfd = epoll_create(EPOLL_SIZE);
if (epfd == -1) {
perror("Epoll create failed");
exit(1);
}
// 4. 将监听Socket添加到Epoll管理
struct epoll_event ev, events[EPOLL_SIZE];
ev.data.fd = sock_fd;
ev.events = EPOLLIN | EPOLLET; // ET模式下通常配合非阻塞使用
epoll_ctl(epfd, EPOLL_CTL_ADD, sock_fd, &ev);
第3步:主循环与事件分发处理
进入无限循环,使用epoll_wait()等待就绪事件。根据触发事件的类型,分别处理新连接请求、客户端读取及响应写入。
printf("Server started on port %d...\n", PORT);
while (1) {
nfds = epoll_wait(epfd, events, EPOLL_SIZE, -1);
if (nfds == -1) continue;
for (i = 0; i < nfds; i++) {
// 情况A: 有新的客户端连接
if (events[i].data.fd == sock_fd) {
int conn_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &sin_size);
if (conn_fd == -1) {
perror("Accept error");
continue;
}
printf("Got connection from %s\n", inet_ntoa(client_addr.sin_addr));
setnonblocking(conn_fd);
// 将新连接加入Epoll,只关心读事件
ev.data.fd = conn_fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
}
// 情况B: 当前连接有可读数据
else if (events[i].events & EPOLLIN) {
int read_fd = events[i].data.fd;
char buffer[1024];
int nbytes = recv(read_fd, buffer, sizeof(buffer), 0);
if (nbytes == -1) {
perror("Read error");
continue; // 或者关闭连接
} else if (nbytes == 0) {
// 客户端正常断开
close(read_fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, read_fd, NULL);
continue;
}
buffer[nbytes] = '\0';
printf("Client says: %s\n", buffer);
// 切换事件类型为写,准备回应
ev.data.fd = read_fd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, read_fd, &ev);
}
// 情况C: 当前连接有可写能力
else if (events[i].events & EPOLLOUT) {
int write_fd = events[i].data.fd;
const char *response = "Message received!\n";
send(write_fd, response, strlen(response), 0);
// 回应完成后,重新切回关注读事件
ev.data.fd = write_fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, write_fd, &ev);
}
}
}
return 0;
}
客户端测试代码
为了方便验证服务端功能,可以编译以下简单的客户端代码进行联调测试:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
int main(int argc, char **argv) {
int sock_fd;
struct sockaddr_in server_addr;
struct hostent *server;
if (argc < 2) {
fprintf(stderr, "usage: ./client hostname \n");
exit(0);
}
// 解析主机名
if ((server = gethostbyname(argv[1])) == NULL) {
herror("ERROR resolving hostname");
exit(1);
}
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Error creating socket");
exit(1);
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(4444);
memcpy(&(server_addr.sin_addr.s_addr), &(server->h_addr), server->h_length);
if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Connection failed");
exit(1);
}
char buffer[1024];
printf("Connected to server! Enter text ('quit' to exit):\n");
while (fgets(buffer, sizeof(buffer), stdin) != NULL) {
if (strcmp(buffer, "quit\n") == 0) break;
write(sock_fd, buffer, strlen(buffer));
read(sock_fd, buffer, sizeof(buffer));
printf("From Server: %s", buffer);
}
close(sock_fd);
return 0;
}
编译与运行指南
假设上述两段代码保存为server.c和client.c,请按以下步骤执行:
- 编译服务端:
gcc -o server server.c -lpthread(若线程相关扩展可添加该库)。 - 启动服务端:
./server。 - 启动客户端(新开终端):
./client 127.0.0.1。
输入文本后,客户端将收到服务端发回的确认信息,表明Epoll机制成功实现了并发下的读写分离处理。
以上就是C语言利用Epoll构建高性能TCP服务器的详细内容,更多关于网络编程进阶优化的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













