一、服务端
下面是一个使用epoll机制在Linux上编写的简单套接字程序示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define MAX_BUFFER_SIZE 1024
int main() {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_size;
char buffer[MAX_BUFFER_SIZE];
struct epoll_event event, events[MAX_EVENTS];
// 创建套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket < 0) {
perror("Error creating socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(6868);
//server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //绑定127.0.0.1
memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));
// 绑定套接字到地址
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Error binding socket");
exit(EXIT_FAILURE);
}
// 监听
if (listen(server_socket, 5) < 0) {
perror("Error listening");
exit(EXIT_FAILURE);
}
// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
perror("Error creating epoll instance");
exit(EXIT_FAILURE);
}
// 设置event结构体
event.events = EPOLLIN;
event.data.fd = server_socket;
// 将socket添加到epoll实例中
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) < 0) {
perror("Error adding socket to epoll instance");
exit(EXIT_FAILURE);
}
while (1) {
int num_ready = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (num_ready < 0) {
perror("Error waiting for events");
exit(EXIT_FAILURE);
}
for (int i = 0; i < num_ready; i++) {
if (events[i].data.fd == server_socket) {
// 检测到新的客户端连接请求
addr_size = sizeof(client_addr);
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_size);
// 设置client_socket为非阻塞
int flags = fcntl(client_socket, F_GETFL, 0);
fcntl(client_socket, F_SETFL, flags | O_NONBLOCK);
event.events = EPOLLIN | EPOLLET;
event.data.fd = client_socket;
// 将客户端socket添加到epoll实例中
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) < 0) {
perror("Error adding client socket to epoll instance");
exit(EXIT_FAILURE);
}
printf("New client connected: %s\n", inet_ntoa(client_addr.sin_addr));
} else {
// 处理客户端发送的数据
int client_fd = events[i].data.fd;
memset(buffer, 0, MAX_BUFFER_SIZE);
int num_bytes = recv(client_fd, buffer, MAX_BUFFER_SIZE, 0);
if (num_bytes < 0) {
perror("Error receiving data");
close(client_fd);
continue;
} else if (num_bytes == 0) {
// 客户端连接关闭
printf("Client disconnected\n");
close(client_fd);
continue;
}
// 处理接收到的数据
printf("Received data from client: %s\n", buffer);
// 将数据发送回客户端
send(client_fd, buffer, num_bytes, 0);
}
}
}
// 关闭套接字和epoll实例
close(server_socket);
close(epoll_fd);
return 0;
}
这个程序创建了一个服务器套接字,使用epoll机制监听连接请求和处理客户端发送的数据。它首先创建了一个套接字 server_socket
,并将其绑定到地址。然后通过 listen
函数开始监听连接请求。
接下来,程序创建了一个epoll实例 epoll_fd
,并使用 epoll_create1
函数进行创建。然后,将服务器套接字添加到epoll实例中,通过 epoll_ctl
函数实现。接下来,程序进入一个无限循环中,使用 epoll_wait
函数等待事件发生。一旦有事件发生,通过遍历 events
数组处理每个事件。
当检测到一个新的客户端连接请求时,程序通过 accept
函数接受新的客户端连接,并将新的客户端套接字设置为非阻塞模式。然后,将客户端套接字添加到epoll实例中。
当客户端发送数据时,程序通过 recv
函数接收数据,并处理接收到的数据。然后,将数据发送回客户端,使用 send
函数。
最后,在循环结束时,程序关闭服务器套接字和epoll实例。
在C语言中,`struct sockaddr_in` 是一个用于存储网络地址的结构体,其中包括IP地址和端口号。它定义在 <netinet/in.h> 头文件中。这个结构体的定义如下:
struct sockaddr_in {
short sin_family; // e.g. AF_INET, AF_INET6
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // Zero padding to make the struct the same size as struct sockaddr
};
其中最后一个成员 sin_zero 是一个填充字段,其目的是为了保证 struct sockaddr_in 结构体的总大小和 struct sockaddr 结构体的大小相同,因为在socket API中,地址通常是通过 struct sockaddr 类型来传递的。为了确保类型兼容和内存布局的一致性,`sin_zero` 成员被添加到 struct sockaddr_in 结构体中,当使用这个结构体时,通常需要将此字段设置为全 0。
memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero)); 使用 memset 函数将 sin_zero 字段的内存设置为0。这里的 '\0' 是空字符(null terminator),用于表示字符串的结束,在内存中其值为0。这行代码保证了填充字段没有留下任何未定义的数据,满足某些系统和库对结构体初始化的要求。
在许多实现中,这个填充可能并不是严格必要的,因为sockaddr_in和sockaddr的转换通常都能正常工作,但按照好的编程习惯,仍然建议对这部分内存进行清零处理。
请注意,此示例程序是一个简单的示例,为了简洁起见,没有进行错误处理和边界检查。在实际编程中,您需要根据需求进行适当的错误处理和边界检查。此外,此示例使用了阻塞的 recv
和 send
函数,您可以根据需要使用非阻塞的I/O函数。
二、客户端
在Linux系统中,`epoll` 是一个高效的多路复用IO接口,它可以用于同时监控多个文件描述符,来检测它们是否有IO事件发生。在网络编程中,`epoll` 常用于接收端来管理多个客户端连接。然而,`epoll` 也同样适用于发送端,特别是当程序需要管理大量的出站连接时。
在发送端使用 epoll 有若干优势:
1. 非阻塞 I/O: 可以将套接字设置为非阻塞模式,然后使用 epoll 来检测何时可以在不阻塞的情况下发送数据。
2. 效率: 当有大量的套接字需要同时发送数据时,使用 epoll 可以减少CPU时间片的浪费,并减少上下文切换,因为可以仅在写入操作能够进行时才尝试发送数据。
3. 可扩展性: epoll 比传统的多路I/O复用方法(如 select 和 poll)具有更好的可扩展性,并且当监控的文件描述符数量增加时,其性能不会显著下降。
下面是一个简单的例子代码,演示了如何在Linux环境下使用epoll来监控socket的发送情况:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#define MAX_EVENTS 10
#define SERVER_PORT 6868
#define SERVER_IP "127.0.0.1"
int set_non_blocking(int sockfd) {
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags == -1) {
return -1;
}
flags |= O_NONBLOCK;
if (fcntl(sockfd, F_SETFL, flags) == -1) {
return -1;
}
return 0;
}
int main() {
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
exit(EXIT_FAILURE);
}
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd == -1) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字为非阻塞模式
if (set_non_blocking(socket_fd) == -1) {
perror("set_non_blocking failed");
exit(EXIT_FAILURE);
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
// 连接到服务器
if (connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
if (errno != EINPROGRESS) { // 非阻塞socket在连接时会返回EINPROGRESS
perror("connect failed");
exit(EXIT_FAILURE);
}
}
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLOUT | EPOLLET; // 关注可写事件,使用边缘触发模式
ev.data.fd = socket_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &ev) == -1) {
perror("epoll_ctl: socket_fd");
exit(EXIT_FAILURE);
}
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == socket_fd && (events[i].events & EPOLLOUT)) {
// 套接字准备好写入,发送数据
const char *msg = "Hello, Server!";
ssize_t bytes_sent = send(socket_fd, msg, strlen(msg), 0);
if (bytes_sent < 0) {
// 发送失败的处理
perror("send failed");
close(socket_fd);
exit(EXIT_FAILURE);
} else {
printf("Message sent: %s\n", msg);
// 为了简化示例,发送成功后就退出循环
close(socket_fd);
close(epoll_fd);
exit(EXIT_SUCCESS);
}
}
}
}
close(epoll_fd);
return 0;
}
注意:这个示例假设与服务器的连接已经建立,并准备发送数据。如果服务器没有运行在端口 6868 或者服务器拒绝连接,那么 connect 调用将失败。
在运行这个代码前,确保本地的服务器正在监听端口 6868,否则 connect 调用将不会成功。此外,该例子只发送一次数据并在发送成功后立即关闭socket和epoll文件描述符,这只是为了示范目的。实际使用中,可能希望保持连接并继续根据需要进行数据发送。
三、编译运行
1. 服务端
gcc server.c -o server
./server
New client connected: 127.0.0.1
Received data from client: Hello, Server!
Client disconnected
2.客户端
gcc client.c -o client
./client
Message sent: Hello, Server!
四、多个客户端
想要在一个进程中管理多个到同一个服务器的连接,不需要为每个连接创建新的进程。而是在同一个进程中打开多个套接字,并将它们全部注册到同一个`epoll`实例。如下所示:
#include <sys/epoll.h>
#include <sys/socket.h>
// 其他必要的头文件...
#define MAX_EVENTS 10
int main() {
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
exit(EXIT_FAILURE);
}
struct epoll_event ev, events[MAX_EVENTS];
int socket_fds[2]; // 假设我们有两个连接
// 对每个套接字重复连接和设置过程
for (int i = 0; i < 2; i++) {
socket_fds[i] = /* 这里是创建套接字并连接到服务器的代码 */;
ev.events = EPOLLOUT;
ev.data.fd = socket_fds[i];
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fds[i], &ev) == -1) {
perror("epoll_ctl: socket_fds[i]");
exit(EXIT_FAILURE);
}
}
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < nfds; ++i) {
if (events[i].events & EPOLLOUT) {
// 在这里根据events[i].data.fd判断是哪个套接字准备好了,然后发送数据
send(events[i].data.fd, /* data */, /* size */, /* flags */);
// 处理发送逻辑
}
}
}
close(epoll_fd);
for (int i = 0; i < 2; i++) {
close(socket_fds[i]); // 关闭套接字
}
return 0;
}
在这个示例中,`socket_fds` 数组用来存储两个套接字描述符,并且都被添加到同一个`epoll`实例中。这样,在主循环中使用`epoll_wait`时可以同时监控两个套接字的事件状态。当套接字准备好写数据时,`epoll_wait`会返回并且通过检查`events[i].events` 来确定是哪个套接字准备好,并执行相应的`send`操作。
上述代码是一个示意性的框架,其中需要填充创建套接字并连接到服务器的代码,以及进行实际数据发送的代码。此外,异常处理和清理操作(如关闭套接字)在实际应用中也需要妥善处理。
五、动态添加和删除客户端套接字
1. 动态添加
为每个客户端都维护一个 epoll
实例并不是一个可扩展或高效的解决方案。事实上,`epoll` 的主要优势之一就是能够使用单个 epoll
实例来监控多个文件描述符(如socket连接)。这样,使用单个线程或者进程就能够管理大量的客户端连接,从而显著减少系统资源的使用和上下文切换的开销。
正确的做法是为所有的客户端连接使用同一个 epoll
实例。当有新的客户端连接时,可以把新的socket文件描述符添加到这个 epoll
实例中去。这个 epoll
实例会告诉哪些socket有事件需要处理,比如数据准备好读取或socket准备好写入数据。
下面是一个简单的例子,展示了如何使用单个 epoll
实例来处理来自多个客户端的连接:
#define MAX_EVENTS 1024
// 创建并初始化epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
// 处理错误
}
struct epoll_event event, events[MAX_EVENTS];
// 通过某种方式获取到一个监听socket_fd,例如bind和listen之后的socket
event.events = EPOLLIN; // 监控可读事件
event.data.fd = listen_socket_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_socket_fd, &event) == -1) {
// 处理错误
}
while (1) {
// 等待事件发生
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
// 处理错误
}
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_socket_fd) {
// 接受新的连接
int client_fd = accept(listen_socket_fd, NULL, NULL);
if (client_fd == -1) {
// 处理错误
}
// 设置新的socket为非阻塞模式...
// 将新的客户端socket添加到epoll实例中
event.events = EPOLLIN | EPOLLET; // 边缘触发模式
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
// 处理错误
}
} else {
// 处理客户端socket的事件:
// 如果是EPOLLIN事件,读取数据
// 如果是EPOLLOUT事件,发送数据
// 如果有EPOLLERR或EPOLLHUP,处理断开连接
}
}
}
// 清理资源
close(epoll_fd);
// 关闭其他打开的sockets
这个例子中,我们通过对每个新接受的客户端连接调用 epoll_ctl
,让单个 epoll
实例监控多个客户端连接。在服务端程序运行期间,`epoll_wait` 调用返回准备好的事件,然后我们根据事件类别(可读、可写、错误等)来处理每个客户端的socket。
使用这种方式,可以高效、可靠和可扩展地管理成千上万个并发连接。
2. 动态删除
在同一个 epoll
实例中动态地删除多个客户端套接字,可以通过调用 epoll_ctl
函数并指定 EPOLL_CTL_DEL
操作来实现。当决定不再监控某个文件描述符时,需要从 epoll
的监控列表中移除它,以避免无用的资源占用和可能的错误触发。
以下是一个简单的示例,说明如何删除多个套接字:
#include <sys/epoll.h>
// 其他必要的头文件...
int main() {
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1 failed");
exit(EXIT_FAILURE);
}
// 假设我们有一个socket_fds数组,包含了要监控的所有客户端套接字文件描述符
int socket_fds[] = { /* ... 客户端套接字文件描述符列表 ... */ };
int num_sockets = sizeof(socket_fds) / sizeof(socket_fds[0]);
// 将所有客户端套接字添加到epoll监控
for (int i = 0; i < num_sockets; ++i) {
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = socket_fds[i];
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fds[i], &ev) == -1) {
perror("epoll_ctl: ADD");
exit(EXIT_FAILURE);
}
}
// ... 在这里进行一些IO操作 ...
// 假设现在要移除多个客户端套接字
for (int i = 0; i < num_sockets; ++i) {
if (需要删除的条件) { // 这里应该是具体的逻辑条件,用来判断哪些套接字需要被删除
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fds[i], NULL) == -1) {
perror("epoll_ctl: DEL");
// 即使删除失败,你可能也希望继续尝试删除其他套接字
}
}
}
// 清理并关闭epoll实例
close(epoll_fd);
return 0;
}
在上面的示例中,通过循环遍历一个包含多个套接字的数组,并使用条件来判断是否应该删除某个套接字。满足条件的套接字会通过 epoll_ctl
调用与 EPOLL_CTL_DEL
操作来从 epoll
实例中移除。在 EPOLL_CTL_DEL
操作中,事件参数可以是 NULL
,因为删除操作不需要事件结构的信息。
在实际的并发服务器应用程序中,可能需要对资源访问进行同步,以防止出现竞态条件。如果应用程序是多线程的,确保在访问和修改与 epoll
实例相关的共享资源时使用适当的锁机制。