管理客户端在 C 套接字中突然断开连接的好方法

A good way to manage client abruptly disconnecting in C sockets

本文关键字:连接 断开 方法 突然 客户端 套接字 管理      更新时间:2023-10-16

在我目前正在小组中做的一个项目中,我们必须从头开始构建一个使用套接字(Linux)的纸牌游戏。我们还必须建立一个每个玩家都可以使用的聊天室。

目前为止,一切都好。聊天是使用三个单独的线程实现的,一个接收传入连接(最多 50 个)并将其存储在客户端列表中,一个持续等待来自所有连接的客户端的消息,另一个在每次客户端发送消息时创建,将该消息发送到客户端列表中的所有客户端。所有这些都有效,除非单个客户端断开连接。

当客户端断开连接时,我设法使服务器保持活动状态(使用 SIGPIPE 的 sig 处理程序),但现在,当客户端断开连接时,我不断收到错误错误文件描述符。但这不是唯一的问题,因为服务器不断接收空消息并将其发送到其余客户端,有效地在几毫秒内用空消息淹没整个聊天。

我相信,如果我能在服务器端解决问题,客户端就不会有任何问题。

所以我的问题是:在我的情况下,管理错误文件描述符的正确方法(或任何方法)是什么。我已经尝试关闭套接字 FD 并在客户端列表中将值设置为 -1,但这会产生更多问题并且没有解决初始问题。

如有必要,这是代码。最重要的功能(用于聊天)是客户端的reception_thread、chat_thread、receive_string、send_string和connect_to_chat。

这是客户端:

//includes
const int PORT = 2477;
const int CHAT_PORT = 2478;
#define DEBUG
//error()
// Sets up the connection to the server.
//connect_to_server()
int connect_to_chat(char * hostname)
{
#ifdef DEBUG
    printf("[DEBUG] Initiating connection to chat server.n");
#endif 

    struct sockaddr_in serv_addr;
    struct hostent *server;
    // Get a socket.
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        error("Error opening socket for server.");
    // Get the address of the server.
    server = gethostbyname(hostname);
    if (server == NULL) {
        fprintf(stderr, "ERROR, no such hostn");
        exit(0);
    }
    // Zero out memory for server info.
    memset(&serv_addr, 0, sizeof (serv_addr));
    // Set up the server info.
    serv_addr.sin_family = AF_INET;
    memmove(server->h_addr, &serv_addr.sin_addr.s_addr, server->h_length);
    serv_addr.sin_port = htons(CHAT_PORT);
    // Make the connection.
    if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0)
        error("Error connecting to chat server");
#ifdef DEBUG
    printf("[DEBUG] Connected to server.n");
#endif 
    return sockfd;
}
//-------------------------------- Messages ------------------------------------
// Bunch of send/recv functions that are not important to chat
int send_string(int sockfd, std::string myString)
{
#ifdef DEBUG
    printf("[DEBUG] Sending string: %s.n", myString.c_str());
#endif 
    //send size
    uint32_t stringLen = myString.size();
    uint32_t sendLen = htonl(stringLen);
    int n = send(sockfd, &sendLen, sizeof (uint32_t), 0);
    if (n < 0) {
        error("Error sending message (string size). Removing client from list.");
        return -1;
    }
    //send string
    n = send(sockfd, myString.c_str(), stringLen, 0);
    if (n < 0) {
        error("Error sending message (string). Removing client from list.");
        return -1;
    }
    return 0;
}
std::string receive_string(int sockfd)
{
    //get string length
    uint32_t stringLen;
    int n = recv(sockfd, &stringLen, sizeof (uint32_t), 0);
    if (n < 0) {
        perror("Error receiving message(string size).");
    }
    stringLen = ntohl(stringLen);
    std::vector<uint8_t> buffer;
    buffer.resize(stringLen, 0x00);
    //get string
    n = recv(sockfd, &(buffer[0]), stringLen, 0);
    if (n < 0) {
        perror("Error receiving message(string).");
    }

    std::string returnString;
    returnString.assign(reinterpret_cast<const char*> (&(buffer[0])), buffer.size()); //might be a bad idea, but it works
#ifdef DEBUG
    printf("[DEBUG] Received message: %sn", returnString.c_str());
#endif
    return returnString;
}

//----------------------------- Printing functions------------------------------
void print_menu_guest()
{
    // some visual function
}
void print_menu_user()
{
    // some visual function
}
void print_info()
{
    std::cout << " No information available on the game yet." << std::endl;
}

//---------------------------- Account functions -------------------------------
// Not necessary for chat functions
//--------------------------- Chat thread functions ----------------------------
void reception_thread(int sockfd)
{
#ifdef DEBUG
    printf("[DEBUG] Reception thread started.n");
#endif 
    std::string stringToPrint;
    while (1) {
        stringToPrint = receive_string(sockfd);
        std::cout << stringToPrint << std::endl;
    }
}
void chat_thread(int sockfd, char* host)
{
#ifdef DEBUG
    printf("[DEBUG] Chat thread started.n");
#endif 
    std::string myString, myUsername, blank;
    std::cout << "Enter your username (NO SPACES): ";
    std::cin >> myUsername;
    myUsername += ": ";
    int chat_sockfd = connect_to_chat(host);
    std::thread reception_thr(reception_thread, chat_sockfd);
    reception_thr.detach();
    while (1) {
        getline(std::cin, myString);
        if (!myString.empty()) {
            if (myString != "/quit") {
                send_string(chat_sockfd, (myUsername + myString));
            }
            else {
                printf("On peut pas encore quitter :( ");
            }
        }
    }

}
//---------------------- Menu management functions -----------------------------
// Main menu function
//---------------------------- Main function -----------------------------------
int main(int argc, char** argv)
{
    /* Make sure host and port are specified. */
    if (true) {
        char* hostname = "localhost";
        /* Connect to the server. */
        int sockfd = connect_to_server(hostname);

#ifdef DEBUG
        printf("[DEBUG] Client ID: Not yet implemented. ");
#endif
        login_prompt(sockfd);
        user_menu_loop(sockfd);
    }
    return 0;
}

这是服务器:它最重要的功能(用于聊天)是setup_user_fetcher、message_receiver、send_string_to_all、receive_string、send_string、get_chat_user、setup_chat_listener。

// Bunch of includes
const int PORT = 2477;
const int CHAT_PORT = 2478;
const int BACKLOG = 10;
const int MAX_CLIENTS = 20;
int clients_list[50] = {-1};
#define DEBUG
void error(const char *msg)
{
    perror(msg);
}
/* Catch Signal Handler functio */
void signal_callback_handler(int signum){
        printf("Caught signal SIGPIPE %dn",signum);
}

//-------------------------- Server set-up functions ---------------------------
// Not necessary for chat
//--------------------------- Chat server functions  ---------------------------
int setup_chat_listener()
{
    int sockfd;
    struct sockaddr_in serv_addr;
    // Get a socket to listen on
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        error("ERROR opening listener socket.");
    // Zero out the memory for the server information
    memset(&serv_addr, 0, sizeof (serv_addr));
    // set up the server info
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(CHAT_PORT);
    // Bind the server info to the listener socket.
    if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0)
        error("Error binding listener socket.");
#ifdef DEBUG
    printf("[DEBUG] Chat listener set.n");
#endif 
    // Return the socket number.
    return sockfd;
}
int get_chat_user(int sockfd)
{
#ifdef DEBUG
    printf("[DEBUG] Getting chat user.n");
#endif 
    struct sockaddr_in their_addr;
    socklen_t sin_size;
    if (listen(sockfd, BACKLOG) < 0) {
        perror("Error while listening.");
        exit(EXIT_FAILURE);
    }
    sin_size = sizeof (struct sockaddr_in);
    // Mise a zero de la memoire pour le client.
    memset(&their_addr, 0, sin_size);
    int new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &sin_size);
    if (new_fd < 0)
        error("Error while accepting.");
    printf("Chat server: Connection received from: %sn",
           inet_ntoa(their_addr.sin_addr));

    return new_fd;
}
int send_string(int sockfd, std::string myString)
{
#ifdef DEBUG
    printf("[DEBUG] Sending string to client %d.n", sockfd);
#endif 
    uint32_t stringLen = myString.size();
    uint32_t sendLen = htonl(stringLen);
    int n = send(sockfd, &sendLen, sizeof (uint32_t), 0);
    if (n < 0) {
        error("Error sending message (string size). Removing client from list.");
        return -1;
    }
    //send string
    n = send(sockfd, myString.c_str(), stringLen, 0);
    if (n < 0) {
        error("Error sending message (string). Removing client from list.");
        return -1;
    }
    return 0;
}
std::string receive_string(int sockfd)
{
#ifdef DEBUG
    printf("[DEBUG] Receiving string.n");
    printf("Current chat user sockfd: %dn", sockfd);
#endif 
    uint32_t stringLen;
    int n = recv(sockfd, &stringLen, sizeof (uint32_t), 0);
    #ifdef DEBUG
    printf("[DEBUG] String size received: %d.n", stringLen);
#endif 
    if (n < 0) {
        perror("Error receiving message(string size).");
    }
    stringLen = ntohl(stringLen);
    std::vector<uint8_t> buffer;
    buffer.resize(stringLen, 0x00);
    //get string
    n = recv(sockfd, &(buffer[0]), stringLen, 0);
    if (n < 0) {
        perror("Error receiving message(string).");
        close(sockfd);
    }

    std::string returnString;
    returnString.assign(reinterpret_cast<const char*> (&(buffer[0])), buffer.size()); //might be a bad idea, but it works
#ifdef DEBUG
    printf("[DEBUG] Received message: %sn", returnString.c_str());
#endif
    return returnString;
}
void send_string_to_all(std::string myString)
{
#ifdef DEBUG
    printf("[DEBUG] Sending string to all clients.n");
#endif 
    int n;
    for (int i = 0; i < 50; ++i) {
        if (clients_list[i] != -1) {
            n = send_string(clients_list[i], myString);
            if (n < 0) {
                close(clients_list[i]);
                clients_list[i] = -1;
            }
        }
    }
}
void message_receiver(int sockfd)
{
#ifdef DEBUG
    printf("[DEBUG] Setting up message receiver.n");
    printf("Current chat user sockfd: %d", sockfd);
#endif 
    std::string message;
    int n;
    while (1) {
        message = receive_string(sockfd);
        std::thread t1(send_string_to_all, message);
        t1.detach();
    }
}


//------------------------------------------------------------------------------
// Bunch of send/recv functions, not necessary to chat

//----------------------------Account Functions---------------------------------
// Not necessary to chat
//------------------------------------------------------------------------------
// Main menu function
void setup_user_fetcher(int lis_chat_sockfd)
{
#ifdef DEBUG
    printf("[DEBUG] Gotta catch'em all.n");
#endif 
    while (1) {
        int chat_user_sockfd = get_chat_user(lis_chat_sockfd);
        for (int i = 0; i < 50; ++i)
            if (clients_list[i] == -1) {
                clients_list[i] = chat_user_sockfd;
                break;
            }
        std::thread message_receiver_thread(message_receiver, chat_user_sockfd);
        message_receiver_thread.detach();
    }
}
int main(int argc, char** argv)
{
    signal(SIGPIPE, signal_callback_handler);
    int lis_sockfd = setup_listener();
    int lis_chat_sockfd = setup_chat_listener();
    std::thread chat_thread(setup_user_fetcher, lis_chat_sockfd);
    chat_thread.detach();
    while (1) {
        int user_sockfd = get_user(lis_sockfd);
        int* user_sockfd_ptr = (int*) malloc(sizeof (int));
        memset(user_sockfd_ptr, 0, sizeof (int));
        user_sockfd_ptr[0] = user_sockfd;
#ifdef DEBUG
        printf("[DEBUG] Starting main menu...n");
#endif
        pthread_t thread;
        int result = pthread_create(&thread, NULL, main_menu,
                                    (void *) user_sockfd_ptr);
        if (result) {
            printf("Thread creation failed with return code %dn", result);
            exit(-1);
        }
#ifdef DEBUG
        printf("[DEBUG] New main menu thread started.n");
#endif
    }
    close(lis_sockfd);
    pthread_exit(NULL);
    return 0;
}

如果要重现错误,可以使用以下行编译代码

g++ client.cpp -o client -std=c++14 -pthread
g++ server.cpp -o server -std=c++14 -pthread

并在没有任何参数的情况下运行两者。客户端设置为在"本地主机"上连接。

如果有人能帮助我解决这个问题,我会很高兴。

我重新评论摆脱SIGPIPE信号本身。

signal(SIGPIPE, SIG_IGN);

现在,被杀死的套接字上的write()s将简单地返回 -1。处理起来应该比异步信号更容易。

如果出于其他原因需要SIGPIPE,请将 write() s 替换为 sendto() s,并带有 MSG_NOSIGNAL 选项。有关详细信息,请参阅sendto(2)手册页。

你有 UB。 如果读取的字节数0&(buffer[0])将失败(我相信如果客户端断开连接,就会发生这种情况)。在构建字符串之前,您应该测试0并尽早返回。

此外,在发现错误后您不会返回,因此在发生错误时从错误数据构建字符串。

也许更像:

std::string receive_string(int sockfd)
{
    uint32_t stringLen;
    int n = recv(sockfd, &stringLen, sizeof (uint32_t), 0);
    if (n < 0) {
        close(sockfd);
        // exit early
        throw std::runtime_error("Error receiving message(string size): "
            + std::string(std::strerror(errno)));
    }
    // test for zero
    if(!n)
        return {}; // empty string
    stringLen = ntohl(stringLen);
    std::vector<uint8_t> buffer(stringLen);
    // buffer.resize(stringLen, 0x00);
    //get string
    n = recv(sockfd, &(buffer[0]), stringLen, 0);
    if (n < 0) {
        close(sockfd);
        // exit early
        throw std::runtime_error("Error receiving message(string): "
            + std::string(std::strerror(errno)));
   }
    // only build string if no errors
    return {buffer.begin(), buffer.begin() + n};
}