为了实现一个更精细和具体需求的多线程epoll
服务器,我们将专注于以下几个方面:
- 多线程处理:使用线程池来优化资源使用和提高性能。
- 协议包处理:实现一个简单的协议包处理逻辑,包括包头和包体,包头包含包体长度信息。
- 并发连接管理:使用
epoll
以边缘触发(ET)模式来高效管理并发连接。
设计协议包格式
为了简化,我们定义协议包格式如下:
- 包头:4字节,表示包体的长度(不包括包头自身)。
- 包体:变长,根据包头定义的长度读取。
服务器实现
服务器的主要任务是接收客户端连接,读取数据,根据协议解析数据包,然后进行相应的处理。这里,我们将简单地将接收到的数据包原样返回给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <pthread.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#define MAX_EVENTS 1024
#define PORT 8080
#define BUF_SIZE 4096
void *worker_thread(void *arg);
int handle_new_connection(int server_fd, int epoll_fd);
int read_data(int client_fd);
int main() {
int server_fd, epoll_fd;
struct epoll_event event, events[MAX_EVENTS];
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项,允许重用地址和端口
int opt = 1;
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 绑定socket到地址和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听socket
if (listen(server_fd, 10) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 创建epoll实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl: server_fd");
exit(EXIT_FAILURE);
}
// 事件循环
while (1) {
int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
if (events[i].data.fd == server_fd) {
handle_new_connection(server_fd, epoll_fd);
} else {
if (events[i].events & EPOLLIN) {
if (read_data(events[i].data.fd) <= 0) {
close(events[i].data.fd); // 关闭连接
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); // 从epoll实例中移除
}
}
}
}
}
return 0;
}
int handle_new_connection(int server_fd, int epoll_fd) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd < 0) {
perror("accept");
return -1;
}
// 设置为非阻塞模式
int flags = fcntl(client_fd, F_GETFL, 0);
fcntl(client F_GETFL, 0);
fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
struct epoll_event event;
event.events = EPOLLIN | EPOLLET; // 边缘触发
event.data.fd = client_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
perror("epoll_ctl: client_fd");
close(client_fd);
return -1;
}
return 0;
}
int read_data(int client_fd) {
char buf[BUF_SIZE];
int total_bytes_read = 0;
while (1) {
ssize_t bytes_read = read(client_fd, buf + total_bytes_read, BUF_SIZE - total_bytes_read);
if (bytes_read < 0) {
// 如果非阻塞read没有数据可读,则退出循环
if (errno == EAGAIN || errno == EWOULDBLOCK) {
break;
} else {
perror("read");
return -1;
}
} else if (bytes_read == 0) {
// 客户端关闭了连接
return 0;
} else {
total_bytes_read += bytes_read;
// 假设这里是一个完整的数据包,实际应用中你需要根据协议来处理粘包问题
// 这里简单地回显收到的数据
write(client_fd, buf, total_bytes_read);
total_bytes_read = 0; // 重置计数器,准备读取下一个数据包
}
}
return total_bytes_read;
}
这段代码完成了从上一部分的断点继续。它实现了非阻塞读取客户端发送的数据,并简单地将数据回显给客户端。这里的示例假设每次读取到的数据都是完整的数据包,但在实际应用中,你需要根据自己的协议来处理可能出现的粘包或拆包问题。
注意,这个示例使用了边缘触发(ET)模式的epoll,这意味着每次事件被触发后,你需要读取所有可用的数据,直到返回EAGAIN错误,因为在边缘触发模式下,只有状态变化时才会触发事件,如果不把数据读完,可能会错过后续的数据。
此外,示例中的错误处理非常基础,实际应用中可能需要更详细的错误处理逻辑来确保服务器的稳定运行。
为了与上述服务器端代码配合,客户端需要能够连接到服务器,发送数据包,并接收服务器的回显数据。这里,我们同样采用非阻塞模式,并使用epoll
来处理可读事件。客户端的实现相对简单,因为它通常只处理一个连接。
客户端的基本逻辑如下:
- 创建socket并连接到服务器。
- 使用
epoll
监控socket的可读事件。 - 发送数据到服务器。
- 接收服务器回显的数据。
下面是客户端的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUF_SIZE 4096
int main() {
int sock_fd, epoll_fd;
struct sockaddr_in server_addr;
struct epoll_event event, events[1]; // 客户端只需处理一个事件
// 创建socket
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0) {
perror("socket");
exit(EXIT_FAILURE);
}
// 设置服务器地址
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(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("connect");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 创建epoll实例
epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 添加socket到epoll
event.events = EPOLLIN; // 只关心可读事件
event.data.fd = sock_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event) == -1) {
perror("epoll_ctl: sock_fd");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 发送数据
char message[] = "Hello, server!";
if (write(sock_fd, message, sizeof(message)) < 0) {
perror("write");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 等待并处理事件
while (1) {
int n = epoll_wait(epoll_fd, events, 1, -1);
if (n < 0) {
perror("epoll_wait");
break;
}
if (events[0].data.fd == sock_fd) {
char buf[BUF_SIZE];
int bytes_read = read(sock_fd, buf, BUF_SIZE);
if (bytes_read > 0) {
printf("Received from server: %s\n", buf);
break; // 假设收到回显后退出
} else if (bytes_read == 0) {
printf("Server closed connection\n");
break;
} else if (errno != EAGAIN) {
perror("read");
break;
}
}
}
close(sock_fd);
return 0;
}
这个客户端程序尝试连接到服务器,发送一条消息,然后等待接收服务器的回显数据。一旦接收到数据或遇到错误(例如服务器关闭连接),客户端就会退出。
请注意,这个示例假设服务器在处理完客户端的请求后会关闭连接。在实际应用中,客户端和服务器之间的交互可能更复杂,需要根据具体的应用协议来设计数据的发送和接收逻辑。