【问题标题】:C++ sockets client disconnectC++ 套接字客户端断开连接
【发布时间】:2019-04-14 15:26:17
【问题描述】:

出于学习目的,我正在制作自己的 TCP Socket 类。 该类旨在处理多个客户端。每个客户端都存储在vector 中。当客户端断开连接时,我遇到了从向量中正确删除客户端的问题。 如何在与vector 断开连接时正确删除客户端以及如何相应地处理传入数据? (见其他分支)。 目前,控制台在断开连接时收到std::cout else case 的垃圾邮件。

bool socks::start() {
    if (listen(this->master_socket, this->backlog) !=0){
        std::cerr << "Failed to start listening." << std::endl;
        return false;
    }

    std::cout << "Listening for connections on port " << this->listening_port << std::endl;

    int max_sd;
    addrlen = sizeof(address);

    while (true) {

        //clear the socket set
        FD_ZERO( & readfds);

        //add master socket to set
        FD_SET(master_socket, & readfds);
        max_sd = master_socket;

        // Add child sockets to set
        for (int i = 0; i < this->clients.size();
        i++){

            //socket descriptor
            int sd = clients[i];

            // If valid socket descriptor then add to read list
            if (sd > 0)
                FD_SET(sd, & readfds);

            //highest file descriptor number, need it for the select function
            if (sd > max_sd)
                max_sd = sd;
        }

        // Wait indefinitely for an activity on one of the sockets
        int activity = select(max_sd + 1, & readfds, NULL, NULL, NULL);
        if ((activity < 0) && (errno != EINTR)) {
            std::cerr << "select() failed" << std::endl;
            return false;
        }

        // Handle incoming connections
        if (FD_ISSET(master_socket, & readfds)){
            if ((new_socket = accept(master_socket, (struct sockaddr *) & address,(socklen_t *) & addrlen)) <0){
                std::cerr << "Failed to accept incoming connection." << std::endl;
                return false;
            }

            // Information about the new connection
            std::cout << "New connection : "
                << "[SOCKET_FD : " << new_socket
                << " , IP : " << inet_ntoa(address.sin_addr)
                << " , PORT : " << ntohs(address.sin_port)
                << "]" << std::endl;
            // Add connection to vector
            this->clients.push_back(new_socket);
        }
        // Hande client disconnections / incoming data?
        else{
            std::cout << "Disconnect??? Or what happens here?" << std::endl;
        }
    }
}

编辑:我将此添加到 else 案例中:

else {
    for (int j = 0; j < this->clients.size(); ++j) {
        if (this->clients.at(j) == -1) {
            continue; // eventually vector.erase() ?
        }
        if (FD_ISSET(this->clients.at(j), &this->readfds)) {
            char buf[256];
            ssize_t rc = recv(this->clients.at(j), buf, 256, 0);
            if (rc == 0) {
                std::cout << "Client disconnected! [SOCKET_FD: "
                    << this->clients.at(j) << "]"
                    << std::endl;
                close(this->clients.at(j));
                this->clients.erase(this->clients.begin() + j);
            } else {
                std::cout << "Client " << this->clients.at(j)
                    << " sent: " << buf << std::endl;
            }
        }
    }
}

【问题讨论】:

  • 啊,好吧。以“良好”方式断开连接的套接字将变得可读,read(或recv)返回零。它有据可查。
  • 您已经知道如何检查套接字是否可读。您可以使用被动侦听套接字来执行此操作。正如我所说,如果readrecv 返回零,那么这与“连接结束”相同(对应于文件的“文件结束”)。互联网上肯定有数以百万计的示例和教程。快速搜索会更早告诉您。
  • @Kyu96:“您将迭代集合中的所有套接字,检查哪个是可读的(如何?)” - 您的 select() 调用要求可读性,因此它将修改readfds 的条目以删除所有不可读的套接字。因此,您只需迭代您的clients 列表,在每个列表上调用FD_ISSET(),就像您对master_socket 所做的那样。 “然后删除它?” - 一旦你确定给定的客户端是否可读,你就可以recv()来自该客户端的数据,如果recv失败或返回0,@987654337 @ 该客户端并将其从 clients 列表中删除,否则根据需要对数据进行操作。
  • @RemyLebeau 感谢您的建议。请看我的编辑。我设法在断开连接时删除客户端并处理传入数据。我想知道是否还有一些我遗漏的东西,需要完成的重要检查或其他我尚未考虑的事情。我想确保我没有遗漏任何重要的东西;)
  • @Kyu96: 1) 你的clients 列表中不应该有值为-1 的项目。如果是这样,您的代码中还有其他错误。 2)不要在你的循环中使用at(),这是浪费开销。请改用operator[]。 3) 如果您想在循环中修改clients,请不要在每次循环迭代时增加j,否则您将在删除客户端时跳过客户端。否则,使用迭代器而不是索引,因为erase() 将迭代器返回到列表中的下一个元素。 4) 你没有处理 recv() 错误返回 -1 的情况。你也需要close()这些客户。

标签: c++ sockets tcp


【解决方案1】:

您的select() 调用仅要求可读套接字,因此在退出时它将修改您的readfds 以删除所有非可读套接字。因此,您只需遍历您的clients 列表,在每个套接字上调用FD_ISSET(),就像您对master_socket 所做的那样。无论如何,您不应该在 else 块中进行该迭代,因为侦听套接字可能会在已建立的客户端也在接收数据的同时接收新的入站客户端。

一旦您确定给定客户端是否可读,您就可以recv() 来自该客户端的数据,如果recv 调用返回 -1(错误)或 0(对等体正常断开连接),close()客户端并将其从clients 列表中删除。否则,根据需要对数据采取行动。

其他需要考虑的事项:

  1. 您的 clients 列表中不应包含值为 -1 的项目。如果是这样,您的代码就有更大的问题需要修复。

  2. 不要在循环中使用clients.at(),这只是浪费开销。请改用列表的operator[]

  3. 1234563否则,使用迭代器而不是索引,因为erase() 将迭代器返回到列表中的下一个元素。无论如何都要考虑使用迭代器,因为您要擦除的是迭代器,而不是索引。
  4. 您没有处理 recv() 可能在错误时返回 -1 的情况。您需要close() 并删除失败的客户端,而不仅仅是断开连接的客户端。

  5. 您假设recv() 返回以空值结尾的数据,即使发送方实际发送以空值结尾的数据也无法保证。 TCP 是一种流传输,任何给定的读取都可能返回比请求更少的字节。您必须注意recv() 的返回值才能知道实际收到了多少字节,否则您可能会超出缓冲区的范围。

试试这样的:

bool socks::start() {
    if (listen(master_socket, backlog) < 0) {
        std::cerr << "Failed to start listening." << std::endl;
        return false;
    }

    std::cout << "Listening for connections on port " << listening_port << std::endl;

    fd_set readfds;
    char buf[256];

    while (true) {

        //clear the socket set
        FD_ZERO(&readfds);

        //add master socket to set
        FD_SET(master_socket, &readfds);
        int max_sd = master_socket;

        // Add child sockets to set
        for (size_t i = 0; i < clients.size(); ++i) {
            //socket descriptor
            int sd = clients[i];    
            FD_SET(sd, &readfds);

            //highest file descriptor number, need it for the select function
            if (sd > max_sd)
                max_sd = sd;
        }

        // Wait indefinitely for an activity on one of the sockets
        int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
        if (activity < 0) {
            if (errno == EINTR) continue;
            std::cerr << "select() failed" << std::endl;
            return false;
        }

        // Handle incoming connections
        if (FD_ISSET(master_socket, &readfds)) {
            sockaddr_in address;
            socklen_t addrlen = sizeof(address);
            int new_socket = accept(master_socket, (sockaddr *) &address, &addrlen);
            if (new_socket < 0) {
                std::cerr << "Failed to accept incoming connection." << std::endl;
                return false;
            }

            // Information about the new connection
            std::cout << "New connection : "
                      << "[SOCKET_FD : " << new_socket
                      << " , IP : " << inet_ntoa(address.sin_addr)
                      << " , PORT : " << ntohs(address.sin_port)
                      << "]" << std::endl;

            // Add connection to vector
            clients.push_back(new_socket);    
        }

        // Handle client disconnections / incoming data?
        size_t j = 0;
        while (j < clients.size()) {
            int sd = clients[j];
            if (FD_ISSET(sd, &readfds)) {
                ssize_t rc = recv(sd, buf, sizeof(buf), 0);
                if (rc <= 0) {
                    std::cout << "Client " << (rc < 0) ? "read error" : "disconnected" << "! [SOCKET_FD: " << sd << "]" << std::endl;
                    close(sd);
                    clients.erase(clients.begin() + j);
                    continue;
                }
                std::cout << "Client " << sd << " sent: ";
                std::cout.write(buf, rc);
                std::cout << std::endl;
            }
            ++j;
        }
    }

    return true;
}

请注意select() 一次可以处理的最大套接字数。如果您最终获得的客户端数量超过了select() 可以处理的数量,您将不得不将列表分解为对select() 的多个调用(可能在工作线程中调用它们以进行并行处理),或者改用(e)poll()

bool socks::start() {
    if (listen(master_socket, backlog) < 0) {
        std::cerr << "Failed to start listening." << std::endl;
        return false;
    }

    std::cout << "Listening for connections on port " << listening_port << std::endl;

    std::vector<pollfd> readfds;
    char buf[256];
    pollfd pfd;

    //add master socket to set
    pfd.fd = master_socket;
    pfd.events = POLLIN;
    pfd.revents = 0;
    readfds.push_back(pfd);

    while (true) {

        // Wait indefinitely for an activity on one of the sockets
        int activity = poll(&readfds[0], readfds.size(), -1);
        if (activity < 0) {
            if (errno == EINTR) continue;
            std::cerr << "poll() failed" << std::endl;
            return false;
        }

        // Handle incoming connections, client disconnections, and incoming data
        size_t j = 0;
        while (j < readfds.size()) {
            if (readfds[j].revents == 0) {
                ++j;
                continue;
            }

            int sd = readfds[j].fd;

            if (readfds[j].revents & POLLIN) {
                if (sd == master_socket) {
                    sockaddr_in address;
                    socklen_t addrlen = sizeof(address);
                    int new_socket = accept(master_socket, (struct sockaddr *) &address, &addrlen);
                    if (new_socket < 0) {
                        std::cerr << "Failed to accept incoming connection." << std::endl;
                        return false;
                    }

                    // Information about the new connection
                    std::cout << "New connection : "
                              << "[SOCKET_FD : " << new_socket
                              << " , IP : " << inet_ntoa(address.sin_addr)
                              << " , PORT : " << ntohs(address.sin_port)
                              << "]" << std::endl;

                    // Add connection to vectors

                    clients.push_back(new_socket);    

                    pfd.fd = new_socket;
                    pfd.events = POLLIN | POLLRDHUP;
                    pfd.revents = 0;
                    readfds.push_back(pfd);
                }
                else {
                    ssize_t rc = recv(sd, buf, sizeof(buf), 0);
                    if (rc > 0) {
                        std::cout << "Client " << sd << " sent: ";
                        std::cout.write(buf, rc);
                        std::cout << std::endl;
                    }
                    else if (rc == 0) {
                        readfds[j].revents |= POLLHUP;
                    } else {
                        readfds[j].revents |= POLLERR;
                    }
                }
            }

            if (readfds[j].revents != POLLIN) {
                if (sd == master_socket) {
                    ...
                }
                else {
                    std::cout << "Client " << (readfds[j].revents & POLLERR) ? "read error" : "disconnected" << "! [SOCKET_FD: " << sd << "]" << std::endl;
                    close(sd);
                    clients.erase(std::find(clients.begin(), clients.end(), sd));
                    readfds.erase(readfds.begin() + j);
                    continue;
                }
            }

            ++j;
        }
    }

    return true;
}

【讨论】:

  • 感谢您提供的非常有用的建议!它绝对帮助我了解我可以在我的代码中改进什么以及还有什么需要研究的。关于select() 可以处理的最大值 - 如果我没记错的话,不是 1024 吗?如果更多的客户端连接会发生什么?这会导致崩溃吗?
  • @Kyu96 "执行 fd 值为负数或等于或大于 FD_SETSIZE 的 FD_CLR() 或 FD_SET() 将导致未定义的行为。"
  • 将您的示例更改为基于投票的示例是否复杂?似乎poll 会比select 更好的选择,因为据我了解它更快且不限于一定数量的客户?
  • 感谢感谢您的帮助!案例我想将数据从服务器发送到客户端,是否需要添加另一个std::vector&lt;pollfd&gt;进行写入?
  • @Kyu96 没有。只需将POLLOUT 包含在您在现有向量中轮询的事件中。这相当于select() 中的writefds