如何修复C++多客户端Web服务器中select()与send()的Error 141问题
问题描述
我正在开发一款支持多客户端同时连接且无阻塞的C++ Web服务器,采用select()函数实现该功能。当客户端在我发送响应(此处为一段长视频)时关闭连接,程序会以状态码141退出。经LLDB调试发现,该错误恰好发生在send()函数尝试向已被客户端关闭的socket发送数据时。请问在这种场景下,我是否遗漏了某些处理步骤,或是需要调整处理方式?
我的代码实现如下:
void Error(const char *msg) { perror(msg); exit(1); } void Server::Init() { memset(&server_infos, 0, sizeof(server_infos)); server_infos.ai_family = AF_INET; server_infos.ai_protocol = SOCK_STREAM; getaddrinfo(LOCALHOST, PORT, &server_infos, &sinfo_ptr); } void Server::CreateServer() { Init(); if ((server_socket = socket(sinfo_ptr->ai_family, sinfo_ptr->ai_protocol, 0)) == -1) Error("Error: Creating socket failed\n"); int optval = 1; setsockopt(server_socket, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); if (bind(server_socket, sinfo_ptr->ai_addr, sinfo_ptr->ai_addrlen) == -1) Error("Error: Binding failed\n"); if (listen(server_socket, FD_SETSIZE) == -1) Error("Error: Listening failed\n"); } void Server::SendResponseHeader(int clt_skt) { char response_header[] = "HTTP/1.1 200 OK\r\n" "Server: Allah Y7ssen L3wan\r\n" "Content-Length: 82013359\r\n" "Content-Type: video/mp4\r\n\r\n"; if (send(clt_skt, response_header, strlen(response_header), 0) == -1) Error("Error (Send) -> "); } void Server::DropClient() { close(active_clt); FD_CLR(active_clt, &readfds); FD_CLR(active_clt, &writefds); _clients.erase(itb); close(itb->GetCltFd()); std::cerr << "Connection Closed\n"; } int Server::AcceptAddClientToSet() { int newconnection = accept(server_socket, (struct sockaddr *)&storage_sock, &clt_addr); if (newconnection == -1) Error("Error (Accept) -> "); const char *path = "/Users/me/Desktop/video.mp4"; _clients.push_back(Client(newconnection, open(path, O_RDONLY))); client_write_ready = false; FD_SET(_clients.back().GetCltSocket(), &readfds); FD_SET(_clients.back().GetCltSocket(), &writefds); if (_clients.back().GetCltSocket() > maxfds) maxfds = _clients.back().GetCltSocket(); return (newconnection); } void Server::Start() { CreateServer(); FD_ZERO(&readfds); FD_ZERO(&writefds); FD_SET(server_socket, &readfds); FD_SET(server_socket, &writefds); maxfds = server_socket; bytesreceived = 0; while (TRUE) { tmpfdsread = readfds; tmpfdswrite = writefds; activity = select(maxfds + 1, &tmpfdsread, &tmpfdswrite, NULL, NULL); if (activity == -1) Error("Error (Select) -> "); if (FD_ISSET(server_socket, &tmpfdsread)) client_socket = AcceptAddClientToSet(); for (itb = _clients.begin(); itb != _clients.end(); itb++) { active_clt = itb->GetCltSocket(); if (FD_ISSET(active_clt, &tmpfdsread) && !client_write_ready) { bytesreceived = recv(active_clt, requested_data, sizeof(requested_data), 0); if (bytesreceived <= 0) { DropClient(); std::cerr << "Connection Closed\n"; } else { client_write_ready = true; SendResponseHeader(active_clt); } } if (FD_ISSET(active_clt, &tmpfdswrite) && client_write_ready) { bytesread = read(itb->GetCltFd(), buffer, sizeof(buffer)); if (bytesread == -1) Error("Error (Read) -> "); bytessent = send(active_clt, buffer, bytesread, 0); if (bytessent == -1) { std::cout << "Cannot Send\n"; Error("Error (Send) -> "); } if (bytesread == 0) { DropClient(); std::cout << "Done With This Client ->" << itb->GetCltFd() << "\n"; } } } } close(server_socket); }
问题分析与解决方案
状态码141对应EPIPE错误(管道破裂),本质是向已断开的TCP连接写入数据时触发的信号,默认会直接终止程序。你的代码存在几个核心问题:
核心问题点
- 全局状态变量冲突:
client_write_ready是全局变量,多客户端连接时会被所有连接共享,导致状态混乱。 - 错误处理逻辑粗暴:
send()出错直接调用Error()退出程序,没有区分"客户端断开"这类非致命错误。 - 迭代器失效问题:
DropClient()中执行_clients.erase(itb)后,itb已失效,后续调用itb->GetCltFd()会触发未定义行为。 - select集合管理不当:初始就把所有客户端socket加入写集合,导致select频繁触发无意义的写就绪。
- 循环迭代器处理错误:erase客户端后,原迭代器失效,继续循环会出错。
针对性修复方案
1. 为每个客户端维护独立状态
把client_write_ready改成Client类的成员变量,每个连接单独管理自己的可写状态:
class Client { private: int clt_socket; int file_fd; bool write_ready; public: Client(int sock, int fd) : clt_socket(sock), file_fd(fd), write_ready(false) {} int GetCltSocket() const { return clt_socket; } int GetCltFd() const { return file_fd; } bool IsWriteReady() const { return write_ready; } void SetWriteReady(bool ready) { write_ready = ready; } };
2. 优化send错误处理,避免程序退出
遇到EPIPE/ECONNRESET时,仅清理当前连接,不终止程序:
#include <errno.h> void Server::SendResponseHeader(int clt_skt) { char response_header[] = "HTTP/1.1 200 OK\r\n" "Server: Allah Y7ssen L3wan\r\n" "Content-Length: 82013359\r\n" "Content-Type: video/mp4\r\n\r\n"; ssize_t sent = send(clt_skt, response_header, strlen(response_header), 0); if (sent == -1) { if (errno == EPIPE || errno == ECONNRESET) { std::cerr << "Client disconnected while sending header\n"; return; } Error("Error (Send Header) -> "); } }
3. 修复DropClient的迭代器失效问题
让DropClient返回erase后的新迭代器,避免循环出错:
std::list<Client>::iterator Server::DropClient(std::list<Client>::iterator it) { int clt_skt = it->GetCltSocket(); int file_fd = it->GetCltFd(); close(clt_skt); FD_CLR(clt_skt, &readfds); FD_CLR(clt_skt, &writefds); close(file_fd); std::cerr << "Connection Closed\n"; return _clients.erase(it); }
4. 优化select集合管理
仅当客户端有数据要发送时,才将其socket加入写集合:
int Server::AcceptAddClientToSet() { socklen_t clt_addr_len = sizeof(storage_sock); int newconnection = accept(server_socket, (struct sockaddr *)&storage_sock, &clt_addr_len); if (newconnection == -1) Error("Error (Accept) -> "); const char *path = "/Users/me/Desktop/video.mp4"; int file_fd = open(path, O_RDONLY); if (file_fd == -1) { Error("Error opening video file -> "); close(newconnection); return -1; } _clients.emplace_back(newconnection, file_fd); FD_SET(newconnection, &readfds); if (newconnection > maxfds) maxfds = newconnection; return newconnection; }
5. 修复主循环的迭代器处理
用while循环替代for循环,处理erase后的迭代器:
void Server::Start() { CreateServer(); FD_ZERO(&readfds); FD_ZERO(&writefds); FD_SET(server_socket, &readfds); maxfds = server_socket; ssize_t bytesreceived, bytesread, bytessent; char requested_data[1024]; char buffer[4096]; while (true) { fd_set tmpfdsread = readfds; fd_set tmpfdswrite = writefds; int activity = select(maxfds + 1, &tmpfdsread, &tmpfdswrite, NULL, NULL); if (activity == -1) Error("Error (Select) -> "); if (FD_ISSET(server_socket, &tmpfdsread)) AcceptAddClientToSet(); auto it = _clients.begin(); while (it != _clients.end()) { int active_clt = it->GetCltSocket(); bool client_write_ready = it->IsWriteReady(); if (FD_ISSET(active_clt, &tmpfdsread)) { bytesreceived = recv(active_clt, requested_data, sizeof(requested_data), 0); if (bytesreceived <= 0) { it = DropClient(it); continue; } else { it->SetWriteReady(true); SendResponseHeader(active_clt); FD_SET(active_clt, &writefds); } } if (FD_ISSET(active_clt, &tmpfdswrite) && client_write_ready) { bytesread = read(it->GetCltFd(), buffer, sizeof(buffer)); if (bytesread == -1) Error("Error (Read) -> "); if (bytesread == 0) { it = DropClient(it); std::cout << "Done With This Client -> " << active_clt << "\n"; continue; } bytessent = send(active_clt, buffer, bytesread, 0); if (bytessent == -1) { if (errno == EPIPE || errno == ECONNRESET) { std::cout << "Client disconnected while sending data\n"; it = DropClient(it); continue; } std::cout << "Cannot Send\n"; Error("Error (Send) -> "); } } ++it; } } close(server_socket); }
额外优化建议
可以在程序初始化时忽略SIGPIPE信号,这样send()遇到EPIPE时会返回-1而不是直接终止程序:
#include <signal.h> // 在CreateServer或Init中添加 signal(SIGPIPE, SIG_IGN);
内容的提问来源于stack exchange,提问作者TouFa7
相关产品推荐
相关产品推荐

