【问题标题】:A good way to manage client abruptly disconnecting in C sockets一种管理客户端在 C 套接字中突然断开连接的好方法
【发布时间】:2016-06-09 01:36:51
【问题描述】:

在我目前在小组中进行的一个项目中,我们必须从头开始构建一个使用套接字 (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 host\n");
        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: %s\n", 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 %d\n",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: %s\n",
           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: %d\n", 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: %s\n", 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 %d\n", 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

并在没有任何参数的情况下运行两者。客户端设置为在“localhost”上连接。

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

【问题讨论】:

  • 只有 900 行代码? Methinx 你需要缩小你的问题范围......
  • @John3136 我绝对可以。如果有任何不清楚的地方(因为很多功能使用其他功能),请随时提问。我现在将对其进行编辑。
  • 这是 C++,不是 C 代码!不同的语言。
  • @Olaf 实际上,几乎所有服务器/客户端部分都是用 C 库编写的(相同的语法等),所以是的,从技术上讲它是 C++,但服务器/客户端部分是 C。
  • 编辑了问题,只保留了代码的重要部分。

标签: c++ linux multithreading sockets


【解决方案1】:

我建议摆脱 SIGPIPE 信号本身。

signal(SIGPIPE, SIG_IGN);

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

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

【讨论】:

  • 我明白了。这可能是一个更好的主意,但是这会修复 Bad file descriptor 错误吗?我认为这是一个完全不同的问题。
  • 相当肯定“错误的文件描述符”是由于代码中的某个错误引起的,并且与实际的套接字操作无关。你永远不应该在正确编写的代码中遇到错误的文件描述符,该代码确切地知道它在任何给定时间打开了哪些文件描述符。
  • 是的,我很确定这也是原因。我知道这通常发生在对方关闭套接字或突然断开连接时。我试图找出一种处理断开连接的方法,例如在每次操作之前检查客户端是否断开连接。我只是不知道有什么办法,所以我正在寻求更有经验的人的答案。
  • @DefinitelyNotMe bad file descriptor 是由于您关闭了套接字,但没有从某些数据结构中删除它,例如选择器的 readFDs
【解决方案2】:

你有 UB。如果读取的字节数为0&amp;(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};
}

【讨论】:

  • 评估任何数据结构末尾的地址不是未定义的行为。第二个recv() 调用也应该测试为零,而不仅仅是第一个。
  • @EJP 表达式buffer[0]实际上引用了不存在的第一个元素和空的vector,那不是UB吗?
  • 非常感谢!调整你给我的东西让它工作了:)
猜你喜欢
  • 1970-01-01
  • 2014-01-22
  • 2010-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-08
相关资源
最近更新 更多