You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复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连接写入数据时触发的信号,默认会直接终止程序。你的代码存在几个核心问题:

核心问题点

  1. 全局状态变量冲突:client_write_ready是全局变量,多客户端连接时会被所有连接共享,导致状态混乱。
  2. 错误处理逻辑粗暴:send()出错直接调用Error()退出程序,没有区分"客户端断开"这类非致命错误。
  3. 迭代器失效问题:DropClient()中执行_clients.erase(itb)后,itb已失效,后续调用itb->GetCltFd()会触发未定义行为。
  4. select集合管理不当:初始就把所有客户端socket加入写集合,导致select频繁触发无意义的写就绪。
  5. 循环迭代器处理错误: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 16:07:04