[libevent]epoll_add()

本文详细解析了epoll_add函数的工作原理与实现细节,包括如何处理信号事件、文件描述符的管理和更新,以及如何调用epoll_ctl进行注册。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

static int
epoll_add(void *arg, struct event *ev)
{
	struct epollop *epollop = arg;
	struct epoll_event epev = {0, {0}};
	struct evepoll *evep;
	int fd, op, events;

	if (ev->ev_events & EV_SIGNAL)//信号事件并不用epoll_ctl注册
		return (evsignal_add(ev));

	fd = ev->ev_fd;
	if (fd >= epollop->nfds) {//重新分配空间
		/* Extent the file descriptor array as necessary */
		if (epoll_recalc(ev->ev_base, epollop, fd) == -1)
			return (-1);
	}
	evep = &epollop->fds[fd];//准备获得该文件描述符对应的event,其实就是ev了,下面根据是否为NULL来判断是新增还是修改来执行epoll_ctl()
	op = EPOLL_CTL_ADD;
	events = 0;
	if (evep->evread != NULL) {
		events |= EPOLLIN;
		op = EPOLL_CTL_MOD;
	}
	if (evep->evwrite != NULL) {
		events |= EPOLLOUT;
		op = EPOLL_CTL_MOD;
	}

	if (ev->ev_events & EV_READ)
		events |= EPOLLIN;
	if (ev->ev_events & EV_WRITE)
		events |= EPOLLOUT;

	epev.data.fd = fd;
	epev.events = events;
	if (epoll_ctl(epollop->epfd, op, ev->ev_fd, &epev) == -1)//注册
			return (-1);

	/* Update events responsible */
	if (ev->ev_events & EV_READ//一个读写event要对应一个evepoll,为什么还要用这个结构来存,因为用epoll_wait获得的是文件描述符
		evep->evread = ev;//用文件描述符来寻找event最好就是用下标来找,用下标找到evepoll再找到event,进行相应操作
	if (ev->ev_events & EV_WRITE)
		evep->evwrite = ev;

	return (0);
}

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/socket.h> #include <netinet/in.h> #include <dirent.h> #include <signal.h> #include <fcntl.h> #include <ctype.h> #include <wait.h> #include <sys/epoll.h> /**************************************************************************************************/ /* DEFINES */ #define PORT 8888 #define WEB_ROOT "./web" /**************************************************************************************************/ /**************************************************************************************************/ /* TYPES */ enum method_enum { MULTI_PROG, MULTI_THRD, MULTI_IO }; /**************************************************************************************************/ /**************************************************************************************************/ /* EXTERN_PROTOTYPES */ /**************************************************************************************************/ /**************************************************************************************************/ /* LOCAL_PROTOTYPES */ /**************************************************************************************************/ /**************************************************************************************************/ /* VARIABLES */ int method = MULTI_IO; /**************************************************************************************************/ /**************************************************************************************************/ /* LOCAL_FUNCTIONS */ static char *url_decode(const char *str); static void log_request(const char *method, const char *path, int status, const char *status_msg); static int send_response(int sock, int status, const char *status_msg, const char *content_type, const char *body, int body_len); static void *handle_request(void *arg); /**************************************************************************************************/ /**************************************************************************************************/ /* PUBLIC_FUNCTIONS */ /**************************************************************************************************/ /* * fn static char *url_decode(const char *str) * brief Parses the URL in the Post. * * param[in] input_int The URL in the Post. * * return the parsesd result */ static char *url_decode(const char *str_in) { char *output = malloc(strlen(str_in) + 1); if (!output) return NULL; char *dst = output; while (*str_in) { if (*str_in == '+') { *dst++ = ' '; str_in++; } else if (*str_in == '%' && isxdigit(str_in[1]) && isxdigit(str_in[2])) { char hex[3] = {str_in[1], str_in[2], '\0'}; *dst++ = (char)strtol(hex, NULL, 16); str_in += 3; } else { *dst++ = *str_in++; } } *dst = '\0'; return output; } static void log_request(const char *method, const char *path, int status, const char *status_msg) { printf("[%s] %s -> %d %s\n", method, path, status, status_msg); } /* * fn static void send_response(int sock, int status, const char *status_msg, const char *content_type, const char *body, int body_len) * brief Send a response to the browser. * * param[in] sock Client socket. * param[in] status Response status. * param[in] status_msg Meaning of response status. * param[in] content_type Type of body. * param[in] body Data to be send. * param[in] body_len Length of body. * * return the send result */ static int send_response(int sock, int status, const char *status_msg, const char *content_type, const char *body, int body_len) { char header[1024]; snprintf(header, sizeof(header), "HTTP/1.1 %d %s\r\n" "Content-Type: %s\r\n" "Content-Length: %d\r\n" "Connection: close\r\n\r\n", status, status_msg, content_type, body_len); size_t result = 0; result = send(sock, header, strlen(header), 0); if (result == -1) { close(sock); return -1; } if (body && body_len > 0) { send(sock, body, body_len, 0); if (result == -1) { close(sock); return -1; } } return 0; } /* * fn static void *handle_request(void *arg) * brief Handle the request from browser. * * param[in] arg Pointer of the client socket. * * return meaningless */ static void *handle_request(void *arg) { int sock = *(int *)arg; if(MULTI_IO != method){ free(arg); } char buffer[4096]; size_t bytes_read = recv(sock, buffer, sizeof(buffer) - 1, 0); if (bytes_read <= 0) { close(sock); return NULL; } buffer[bytes_read] = '\0'; char method[16], path[256]; sscanf(buffer, "%s %s", method, path); if (strcmp(method, "GET") == 0) { char filepath[512]; if (strcmp(path, "/") == 0) { strcpy(path, "/Index.html"); } sprintf(filepath, "%s%s", WEB_ROOT, path); FILE *file = fopen(filepath, "rb"); if (file) { fseek(file, 0, SEEK_END); long len = ftell(file); fseek(file, 0, SEEK_SET); char *content = malloc(len); fread(content, 1, len, file); fclose(file); const char *content_type = "text/html"; if (strstr(path, ".css")) { content_type = "text/css"; } else if (strstr(path, ".js")) { content_type = "application/json"; } else if (strstr(path, ".png")) { content_type = "image/png"; } else if (strstr(path, ".jpg")) { content_type = "image/jpeg"; } send_response(sock, 200, "OK", content_type, content, len); free(content); log_request(method, path, 200, "OK"); } else { const char *not_found = "<h1>404 Not Found</h1>"; send_response(sock, 404, "Not Found", "text/html", not_found, strlen(not_found)); log_request(method, path, 404, "Not Found"); } } else if (strcmp(method, "POST") == 0) { char *content_length_ptr = strstr(buffer, "Content-Length: "); int content_length = 0; if (content_length_ptr) { sscanf(content_length_ptr + 16, "%d", &content_length); } if (content_length <= 0) { const char *bad_request = "<h1>400 Bad Request: Missing Content-Length</h1>"; send_response(sock, 400, "Bad Request", "text/html", bad_request, strlen(bad_request)); log_request(method, path, 400, "Bad Request"); close(sock); return NULL; } char *post_data = malloc(content_length + 1); if (!post_data) { const char *server_error = "<h1>500 Internal Server Error</h1>"; send_response(sock, 500, "Server Error", "text/html", server_error, strlen(server_error)); log_request(method, path, 500, "Server Error"); close(sock); return NULL; } char *body_start = strstr(buffer, "\r\n\r\n"); if (body_start) { body_start += 4; memcpy(post_data, body_start, content_length); } else { const char *bad_request = "<h1>400 Bad Request: Bad Message Format</h1>"; send_response(sock, 400, "Bad Request", "text/html", bad_request, strlen(bad_request)); log_request(method, path, 400, "Bad Request"); close(sock); return NULL; } post_data[content_length] = '\0'; /* post_data : name=z111&email=125253455%40qq.com&message=dasdsasd */ if (strcmp(path, "/data/contact.json") == 0) { char name[128] = {0}; char email[128] = {0}; char message[512] = {0}; char *token = strtok(post_data, "&"); while (token != NULL) { char *eq = strchr(token, '='); if (eq) { *eq = '\0'; char *key = token; char *value = url_decode(eq + 1); if (strcmp(key, "name") == 0) { strncpy(name, value, sizeof(name) - 1); } else if (strcmp(key, "email") == 0) { strncpy(email, value, sizeof(email) - 1); } else if (strcmp(key, "message") == 0) { strncpy(message, value, sizeof(message) - 1); } free(value); } token = strtok(NULL, "&"); } char response[1024]; snprintf(response, sizeof(response), "{" "\"callback\": \"Message received successfully\"," "\"details\": {" "\"name\": %s," "\"email\": %s," "\"message\": %s" "}" "}", name, email, message); send_response(sock, 200, "OK", "application/json", response, strlen(response)); log_request(method, path, 200, "OK"); } else { const char *not_implemented = "<h1>501 Not Implemented</h1>"; send_response(sock, 501, "Not Implemented", "text/html", not_implemented, strlen(not_implemented)); log_request(method, path, 501, "Not Implemented"); close(sock); return NULL; } free(post_data); } else { const char *not_implemented = "<h1>501 Not Implemented</h1>"; send_response(sock, 501, "Not Implemented", "text/html", not_implemented, strlen(not_implemented)); log_request(method, path, 501, "Not Implemented"); close(sock); return NULL; } close(sock); return NULL; } int main() { /* 当client先终止连接,server向socket中写文件时会触发SIGPIPE信号导致程序退出 */ /* 此处忽略SIGPIPE信号,在send_response中做处理 */ signal(SIGPIPE, SIG_IGN); int server_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(PORT), .sin_addr.s_addr = htonl(INADDR_ANY)}; bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)); listen(server_fd, 10); printf("Web server running on port %d\n", PORT); if (MULTI_IO == method) { int epoll_fd = epoll_create1(0); struct epoll_event ev, events[100]; ev.events = EPOLLIN; ev.data.fd = server_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev); while (1) { int n = epoll_wait(epoll_fd, events, 100, -1); for (int i = 0; i < n; i++) { if (events[i].data.fd == server_fd) { int client_fd = accept(server_fd, NULL, NULL); ev.events = EPOLLIN | EPOLLET; ev.data.fd = client_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev); } else { handle_request(&events[i].data.fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL); close(events[i].data.fd); } } } epoll_ctl(epoll_fd, EPOLL_CTL_DEL, server_fd, NULL); }else{ while (1) { int client_fd = accept(server_fd, NULL, NULL); if (client_fd < 0) { perror("accept failed"); continue; } int *sock_ptr = malloc(sizeof(int)); if (!sock_ptr) { close(client_fd); continue; } *sock_ptr = client_fd; if (MULTI_PROG == method) { pid_t pid = fork(); if (pid == 0) { close(server_fd); handle_request(sock_ptr); exit(0); } else { close(client_fd); waitpid(-1, NULL, WNOHANG); } } else if (MULTI_THRD == method) { pthread_t tid; pthread_create(&tid, NULL, handle_request, sock_ptr); pthread_detach(tid); } } } close(server_fd); return 0; } 如何优化代码使得其具有高并发性
最新发布
08-02
<think>我们正在分析一个用C语言编写的简单HTTP服务器代码,并希望优化其高并发性。当前代码支持三种模式:多进程(MULTI_PROG)、多线程(MULTI_THRD)和I/O多路复用(MULTI_IO)。其中,I/O多路复用使用了epoll。 观察当前代码,在MULTI_IO模式下,使用epoll来管理连接,但是处理请求的方式是同步的(即直接在循环中调用handle_request)。这样,当处理一个请求时,如果该请求处理时间较长,会阻塞整个循环,导致其他连接得不到及时处理。因此,我们需要将处理请求的部分也改为非阻塞或异步的方式,但这在当前的handle_request函数中并不容易,因为该函数内部是同步的。 另外,在多进程和多线程模式下,每个连接都会创建一个新的进程或线程,这在高并发时会导致资源消耗过大(进程/线程数过多)。 为了优化高并发性,我们可以考虑以下改进: 1. 在MULTI_IO模式下,使用线程池(或进程池)来处理请求。这样,epoll只负责接收连接和读写事件,而将具体的请求处理交给线程池中的工作线程,避免阻塞事件循环。 2. 当前在MULTI_IO模式下,每次处理一个事件(比如可读事件)后,就会将该连接从epoll中删除并关闭连接(在handle_request中处理完请求后关闭连接)。这实际上是短连接的方式。但根据HTTP协议,我们可以支持长连接(Connection: keep-alive),以减少连接建立和关闭的开销。 3. 当前代码在MULTI_IO模式下,处理请求时直接调用handle_request,而handle_request会一次性读取请求并发送响应,然后关闭连接。这种模式不适合长连接,也不适合处理大文件(因为发送大文件会阻塞事件循环)。 因此,优化方向如下: 方案A:使用非阻塞I/O和状态机 - 将socket设置为非阻塞。 - 在epoll事件循环中,当有可读事件时,读取数据,并解析HTTP请求(可能需要多次读取才能完成一个请求)。 - 同样,发送响应时也可能需要多次写操作(特别是大文件),因此需要管理发送状态。 - 这需要将每个连接的状态(如已接收的数据、待发送的数据、当前解析状态等)保存起来,并在事件触发时继续处理。 方案B:使用线程池(或进程池)配合非阻塞I/O(或使用阻塞I/O但由线程池处理) - 主线程(事件循环)负责接收连接和读写事件,然后将具体的请求处理交给线程池中的线程。 - 但是,如果直接交给线程池处理,那么该连接在另一个线程中处理时,主线程不应再监听该连接的事件(否则会出现多个线程同时操作同一个socket)。因此,通常的做法是:主线程将连接交给线程池后,就暂时不再监听该连接的事件,直到线程池处理完并重新将连接放回事件循环(如果是长连接,可能需要继续监听下一次请求)。 考虑到复杂性,我们可以采用折中方案: 1. 在MULTI_IO模式下,使用非阻塞socket,但不在事件循环中处理请求,而是将请求的处理交给线程池。具体步骤: - 当epoll检测到某个连接有可读事件时,读取数据(非阻塞读取,直到读完或遇到EAGAIN/EWOULDBLOCK)。 - 如果已经读取了一个完整的HTTP请求(根据Content-Length或遇到两个连续的\r\n\r\n判断请求头结束,然后根据请求头中的内容长度读取请求体),则将该请求放入一个任务队列,由线程池处理。 - 线程池中的线程处理请求,生成响应数据,然后将响应数据放入该连接的输出缓冲区,并注册该连接的写事件(EPOLLOUT)。 - 主线程在epoll_wait返回后,如果检测到某个连接有写事件,则从该连接的输出缓冲区发送数据(同样非阻塞发送,直到发送完或遇到EAGAIN/EWOULDBLOCK)。 - 如果是短连接,则在发送完响应后关闭连接;如果是长连接,则重置连接状态,等待下一次请求(需要重新注册读事件)。 2. 支持长连接:在HTTP响应头中,如果请求是HTTP/1.1,默认是长连接(除非指定Connection: close)。我们可以根据请求头中的Connection字段来决定是否使用长连接。在代码中,当前是返回"Connection: close",我们可以改为根据请求头来决定。 由于整个重构工作量较大,这里我们只给出一个优化的大致框架,重点在于将事件循环和请求处理分离(通过线程池),并支持非阻塞读写。 但是,根据题目要求,我们只需要提出优化建议,而不需要重写整个代码。因此,我们可以提出以下优化点: - 使用线程池:在MULTI_IO模式下,将每个完整的请求(即已经读取了整个HTTP请求)交给线程池处理,避免阻塞事件循环。 - 非阻塞I/O:将客户端socket设置为非阻塞模式,这样在读取和发送数据时,如果暂时没有数据可读或发送缓冲区满,可以返回并继续处理其他事件。 - 支持HTTP长连接:减少连接建立和关闭的开销,同一个连接可以处理多个请求(注意:在同一个连接上,请求是顺序处理的)。 - 使用状态机管理连接:每个连接用一个结构体保存状态(包括读缓冲区、写缓冲区、当前解析状态等),以便在多次事件触发时继续处理。 由于时间关系,我们无法完全重写代码。下面给出一个简化的优化步骤,以MULTI_IO模式为例: 步骤1:定义连接状态结构体 ```c typedef struct { int fd; char buffer[4096]; // 读缓冲区 size_t buffer_len; // 当前缓冲区中数据的长度 char *response; // 待发送的响应数据 size_t response_len; // 响应数据长度 size_t sent_len; // 已经发送的长度 int keep_alive; // 是否保持连接 } connection_t; ``` 步骤2:修改epoll事件循环,使用非阻塞socket 在accept时,将客户端socket设置为非阻塞: ```c int client_fd = accept(server_fd, NULL, NULL); fcntl(client_fd, F_SETFL, O_NONBLOCK); ``` 步骤3:在事件循环中,对于可读事件,读取数据并尝试解析请求。如果读取到一个完整的请求,则创建一个任务(包含连接状态)放入线程池队列。 步骤4:线程池中的工作线程处理请求,生成响应数据,并将响应数据设置到连接状态的response中。然后,主循环(事件循环)需要监听该连接的写事件(如果响应数据准备好,则注册EPOLLOUT事件)。 步骤5:当写事件就绪时,将响应数据发送出去(非阻塞发送,可能一次发不完,需要记录已发送的位置)。 步骤6:如果使用长连接,则在处理完一个请求后,重置连接状态(清空读缓冲区等),并重新注册读事件(注意:在发送完响应后,如果还有请求,需要继续读取)。 步骤7:如果连接关闭或出错,则清理连接状态。 由于代码改动较大,这里只提供思路。另外,多进程模式和多线程模式也可以使用线程池/进程池来限制资源使用。 但是,考虑到原代码已经实现了三种模式,我们可以选择重点优化MULTI_IO模式,因为这是高并发的常用模式。 因此,建议优化: 1. 将MULTI_IO模式改造成非阻塞I/O + 线程池的方式。 2. 支持长连接。 注意:原代码中,在MULTI_IO模式下,handle_request是直接处理请求并关闭连接的,这不符合长连接的要求。因此,我们需要重构handle_request函数,将其拆分为解析请求、处理请求、发送响应等步骤,并能够支持多次请求。 由于优化代码量较大,这里只给出一个框架示例(非完整代码): 线程池实现(简化): - 创建一个任务队列和一组工作线程。 - 任务队列中的每个任务包含一个连接状态结构体指针(包含读缓冲区和请求信息)。 事件循环修改: ```c while (1) { int n = epoll_wait(epoll_fd, events, 100, -1); for (int i = 0; i < n; i++) { if (events[i].data.fd == server_fd) { // 接受新连接,设置非阻塞,加入epoll(监听读事件) } else { connection_t *conn = (connection_t *)events[i].data.ptr; // 注意:我们使用data.ptr来指向连接状态 if (events[i].events & EPOLLIN) { // 读取数据到conn->buffer,并更新conn->buffer_len // 如果读取出错或对端关闭,则关闭连接并清理 // 如果读取到一个完整的HTTP请求,则将conn放入线程池任务队列,并暂停监听该连接的读事件(避免重复处理) // 注意:一个连接可能同时有多个请求(管道化请求),但通常我们按顺序处理,即一次只处理一个请求 } if (events[i].events & EPOLLOUT) { // 发送conn->response中剩余的数据(从conn->sent_len开始) // 如果发送完毕,则: // 如果是短连接,则关闭连接; // 如果是长连接,则重置conn的状态(清空buffer,重置sent_len和response等),并重新监听读事件(同时移除写事件监听) } } } } ``` 由于时间限制,我们无法提供完整代码,但以上思路可以显著提高并发能力。 另外,原代码中,在MULTI_THRD和MULTI_PROG模式下,每个连接都创建一个线程/进程,这在高并发时效率不高。我们可以在这两种模式下也引入线程池/进程池,限制最大并发数。 综上所述,优化建议如下: 1. 在MULTI_IO模式下使用非阻塞I/O和线程池,并支持长连接。 2. 在MULTI_THRD模式下使用线程池(而不是为每个连接创建一个线程)。 3. 在MULTI_PROG模式下使用进程池(而不是为每个连接fork一个进程)。 由于改动较大,我们可能需要重新设计代码结构。 但是,考虑到题目要求优化高并发性,我们可以先选择MULTI_IO模式进行优化,因为这是最常用的高并发模型。 注意:原代码中,在MULTI_IO模式下,handle_request是同步处理的,并且处理完就关闭连接。我们第一步可以先将这个处理过程放入线程池,但保持短连接,这样至少不会阻塞事件循环。然后再逐步实现长连接和非阻塞读写。 由于具体实现代码量很大,这里只提供优化思路。 因此,回答如下: 为了优化代码的高并发性,建议采取以下措施: 1. 在I/O多路复用模式(MULTI_IO)下,使用非阻塞I/O配合线程池处理请求。具体做法: - 将客户端socket设置为非阻塞。 - 事件循环(epoll)负责监听事件,当有可读事件时,读取数据直到一个完整的HTTP请求(通过检查HTTP头中的Content-Length或两个连续的\r\n\r\n来判断请求头结束,然后读取请求体)被接收,然后将请求数据封装成任务提交给线程池。 - 线程池中的工作线程处理请求并生成响应,然后将响应数据挂载到该连接的状态结构体上,并注册写事件(EPOLLOUT)。 - 事件循环监听到写事件就绪时,将响应数据发送给客户端(非阻塞发送,可能需要多次发送)。 - 处理完一个请求后,如果是短连接则关闭连接;如果是长连接则重置连接状态并继续监听读事件。 2. 支持HTTP长连接(Keep-Alive),减少连接建立和关闭的开销。根据请求头中的"Connection"字段决定是否保持连接。 3. 在多线程模式(MULTI_THRD)下,使用线程池来限制最大线程数,避免高并发时创建过多线程导致资源耗尽。 4. 在多进程模式(MULTI_PROG)下,使用进程池,预先创建一组子进程,每个子进程都运行事件循环(使用epoll),通过共享监听套接字(在fork之前设置SO_REUSEPORT)来实现负载均衡。或者,主进程accept后将连接通过进程间通信(如管道)传递给某个子进程处理(但这样效率较低,更常用的是SO_REUSEPORT)。 5. 优化文件发送:对于大文件,使用sendfile系统调用(如果系统支持)来直接发送文件,避免在用户空间和内核空间之间复制数据。 6. 在handle_request中,处理文件请求时,当前是读取整个文件到内存再发送,这在大文件时会导致高内存占用。应该使用流式发送(即边读边发,使用非阻塞方式)。 由于时间关系,这里只提供优化思路,具体实现需要较多代码重构。 注意:原代码中,在MULTI_IO模式下,事件循环中直接调用handle_request,并且handle_request会阻塞处理,直到完成。这会导致事件循环阻塞,无法处理其他连接。因此,第一步优化就是将其放入线程池,即使使用阻塞I/O,至少不会阻塞事件循环。 下面是一个简单的修改示例(仅MULTI_IO模式,使用线程池处理请求,但保持短连接): 步骤: a. 创建一个线程池(可以自己实现,也可以使用第三方库)。 b. 修改事件循环:当有可读事件时,读取整个请求(注意:非阻塞读取,但为了简单,我们假设请求不会超过一个读缓冲区,且一次性读完。实际中需要循环读直到读完或EAGAIN)。 c. 将读取到的数据(以及客户端socket)封装为任务,提交给线程池。 d. 线程池中的线程调用handle_request函数(需要修改该函数,使其不直接关闭socket,而是由主线程在发送完响应后关闭?或者在线程中处理完后关闭?)。注意:线程中处理完请求后,需要将响应数据发送回去,但发送操作不能在子线程中直接进行(因为主线程可能同时也在操作同一个socket),所以我们需要将发送操作交还给主线程。或者,我们可以在子线程中直接发送,但这样就需要在子线程中处理epoll事件,比较复杂。 因此,更简单的方式是:在子线程中处理请求并生成响应数据,然后通过管道等方式通知主线程,由主线程将响应数据发送给客户端。但这样增加了复杂度。 另一种方式是:在子线程中直接处理请求并发送响应,然后关闭连接。这样,主线程在将任务交给线程池后,就关闭epoll对该连接的监听(并删除事件),然后关闭自己的连接引用(但不关闭socket,因为子线程还在用)。这样,主线程不再管理该连接,由子线程完全负责,包括关闭连接。 具体做法: - 主线程accept后,将client_fd加入epoll(监听读事件)。 - 当某个连接可读时,主线程读取数据(非阻塞,循环读直到读完或EAGAIN),如果读取到一个完整的请求(或遇到读取错误),则将该连接从epoll中移除(但不要关闭fd),然后将该连接(包括fd和读取到的数据)封装成任务,提交给线程池。 - 线程池中的线程调用handle_request函数(需要修改该函数,使其不依赖于全局状态,并且使用该连接的fd和读取到的数据)。在handle_request中,处理请求并发送响应(发送是阻塞的,但没关系,因为我们在独立的线程中),然后关闭连接。 - 主线程不再管理该连接。 这样修改相对简单,但缺点是一个连接只能处理一个请求(短连接),且发送响应时是阻塞发送,但因为是线程池,所以不会阻塞主线程。 修改后的handle_request需要调整为: ```c void handle_request_in_thread(int sock, const char *buffer, size_t buffer_len) { // 复制buffer,因为原buffer可能被释放 char *request_buffer = strdup(buffer); // 然后解析请求,处理,发送响应,最后关闭sock // ... 省略处理代码(类似于原来的handle_request,但不需要再读socket,因为数据已经传入) free(request_buffer); } ``` 在事件循环中,读取数据后,将sock和读取到的数据一起传给线程池任务。 注意:如果读取的数据不完整(比如只读到请求的一部分),我们需要继续等待数据,但这样会复杂化。因此,我们假设一次读取就能完成一个请求(对于小请求是成立的,大请求则需要分多次读取)。如果要支持大请求,我们需要在连接状态中保存已读取的数据,直到读取完整。 由于优化点较多,我们选择逐步优化。首先,在MULTI_IO模式下,使用线程池处理请求,每个请求在一个线程中处理(包括读取请求数据、处理、发送响应、关闭连接),但请求数据的读取由主线程完成(并且是一次性读取,所以不适合大请求)。 具体修改: 1. 定义任务结构体: ```c typedef struct { int sock; char *buffer; size_t buffer_len; } task_t; ``` 2. 创建线程池(示例代码省略线程池的实现,可以使用pthread和队列)。 3. 修改事件循环中对于客户端连接的处理: ```c if (events[i].data.fd == server_fd) { // 接受新连接 int client_fd = accept(server_fd, NULL, NULL); fcntl(client_fd, F_SETFL, O_NONBLOCK); connection_t *conn = (connection_t *)malloc(sizeof(connection_t)); // 初始化conn,设置fd,清空buffer等 ev.events = EPOLLIN | EPOLLET; ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev); } else { connection_t *conn = (connection_t *)events[i].data.ptr; if (events[i].events & EPOLLIN) { ssize_t n = recv(conn->fd, conn->buffer + conn->buffer_len, sizeof(conn->buffer) - conn->buffer_len, 0); if (n > 0) { conn->buffer_len += n; // 检查是否收到一个完整的HTTP请求(这里简化:检查是否收到两个连续的\r\n\r\n) char *header_end = strstr(conn->buffer, "\r\n\r\n"); if (header_end) { // 计算请求体长度(如果有) // 这里简化:假设没有请求体(GET请求)或者请求体长度在Content-Length中指定 // 实际上,如果是POST,需要继续读取直到达到Content-Length指定的长度 // 为了简单,我们只处理GET请求,或者小POST请求(一次读完) // 如果请求不完整,则继续等待(这里代码省略) // 从epoll中移除该连接 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, conn->fd, NULL); // 将任务提交给线程池 task_t *task = (task_t *)malloc(sizeof(task_t)); task->sock = conn->fd; task->buffer = conn->buffer; task->buffer_len = conn->buffer_len; // 注意:这里我们将conn释放了,因为任务中已经复制了必要信息?或者不释放,让任务处理完后释放 // 或者,我们不在事件循环中释放conn,而是让任务处理完后释放 task->conn = conn; // 假设任务结构体包含conn指针 thread_pool_submit(pool, handle_task, task); } // 如果buffer满了还没有收到完整请求,则关闭连接(这里简化处理) else if (conn->buffer_len == sizeof(conn->buffer)) { // 错误处理:关闭连接,释放conn close(conn->fd); free(conn); } } else if (n == 0 || (n < 0 && (errno != EAGAIN && errno != EWOULDBLOCK))) { // 出错或对端关闭 close(conn->fd); free(conn); } } } ``` 4. 线程池处理函数handle_task: ```c void handle_task(void *arg) { task_t *task = (task_t *)arg; // 调用处理函数,处理请求并发送响应 // 这里可以调用修改后的handle_request_in_thread(task->sock, task->buffer, task->buffer_len); // 然后关闭task->sock close(task->sock); free(task->buffer); // 如果task->buffer是动态分配的,这里需要释放 free(task); } ``` 注意:以上代码仅为示意,未考虑POST请求的请求体可能很大的情况,也没有考虑连接重置、超时等问题。 由于优化代码的复杂性,这里只提供思路。实际中,可以使用成熟的库(如libevent)或参考其他高性能服务器实现(如nginx)。 最后,根据要求,在回答后提出3-5个相关问题。 注意:由于问题要求,我们必须在最后输出“§§相关问题§§”并列出3-5个问题。 因此,完整回答如下:</think>为了优化代码以实现高并发性,以下是关键改进建议和实现方案: ### 优化方案 1. **非阻塞I/O + 线程池** ```c // 线程池实现 typedef struct { pthread_t *threads; int thread_count; task_queue_t *queue; } thread_pool_t; void thread_pool_init(thread_pool_t *pool, int count) { // 初始化线程池和工作队列 } ``` 2. **事件驱动架构优化** ```c // 连接状态机 typedef struct { int fd; char buffer[4096]; size_t buf_len; http_parser parser; // HTTP解析状态 } connection_t; // 事件循环中处理连接 if (events[i].events & EPOLLIN) { connection_t *conn = get_connection(events[i].data.fd); ssize_t n = read_data(conn); // 非阻塞读取 if (n > 0 && parse_http(conn)) { // 完整请求 thread_pool_submit(pool, handle_request, conn); } } ``` 3. **HTTP流水线支持** ```c // 在connection_t中增加 int keep_alive; int pipeline_count; ``` 4. **sendfile零拷贝** ```c // 替换fread+send int fd = open(filepath, O_RDONLY); struct stat file_stat; fstat(fd, &file_stat); sendfile(client_fd, fd, NULL, file_stat.st_size); ``` 5. **定时器管理空闲连接** ```c // 红黑树管理超时 typedef struct { rb_node_t node; connection_t *conn; time_t expire; } timeout_node_t; ``` ### 具体优化点 1. **I/O模型** - 使用 `EPOLLET` 边缘触发模式 - 所有客户端socket设为非阻塞 (`fcntl(fd, F_SETFL, O_NONBLOCK)`) - 分离事件监听与请求处理 2. **资源管理** - 线程池限制最大线程数 (e.g., CPU核心数×2) - 连接复用池避免频繁分配 - 预分配内存池减少malloc调用 3. **协议优化** - 实现HTTP/1.1流水线处理 - 自动识别Connection: keep-alive - 支持Transfer-Encoding: chunked 4. **性能关键路径** - 避免内存复制 (使用ring buffer) - 使用线程局部存储缓存 - 热点函数内联化 (e.g., url_decode) ### 优化后架构 ``` 主线程 ├── epoll_wait (事件分发) ├── 新连接 → 加入连接池 └── 就绪连接 → 任务队列 ↓ 线程池 (Worker线程) ├── 解析HTTP ├── 文件操作 └── 发送响应 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值