为什么用 C 编写的 TCP 套接字服务器不应该关闭工作线程中的客户端文件描述符,但可以在分叉的工作进程中关闭它们?

问题描述

我用 C 编写了一个简单的 TCP 套接字服务器,它在接受来自客户端的新连接请求时创建一个新的子工作线程,并简单地计算和发送数字。如果客户端终止,相应的子工作线程也应该终止,而其他线程不应该终止。

如果所有客户端都是用 Python 编写的,当一个客户端终止时,服务器上会打印“Connection is reset by peer”,但其他一切都很好,即其他线程和客户端仍在工作。

但是如果客户端是用 C 编写的,当任何客户端和它们对应的子工作线程终止时,其他线程也会终止,这是意料之中的。为什么会发生?我用 Python 重写了服务器,但是无论客户端是用什么语言编写的,都没有发生这种情况。

然后我注释掉了 close(*client_fd);,问题就解决了。我没有任何线索,因为它在使用 fork() 的服务器中运行良好。

服务器使用pthread的C代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>

#define PORT 9000
#define CONNECTIONS 2
#define MAX_BUFFER_SIZE 1024

struct sockaddr_in socket_address;
socklen_t socket_address_size = sizeof(socket_address);
int server_fd;

void *handle_request(void *fd) {
    int *client_fd = (int *) fd;
    char buffer[MAX_BUFFER_SIZE] = {0};

    for (int i = INT_MAX; send(*client_fd,buffer,strlen(buffer),0) >= 0 && i >= 0; i--) {
        printf("%d\r\n",i);
        sprintf(buffer,"%d",i);
    }

    if (close(*client_fd) < 0) {
        perror("close client_fd");
    }
    return NULL;
}

int main(int argc,char *argv[]) {
    int option = 1;
    char buffer[MAX_BUFFER_SIZE] = {0};

    if ((server_fd = socket(AF_INET,SOCK_STREAM,0)) == 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&option,sizeof(option))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }

    socket_address.sin_family = AF_INET;
    socket_address.sin_addr.s_addr = htonl(INADDR_ANY);
    socket_address.sin_port = htons(PORT);

    if (bind(server_fd,(struct sockaddr *) &socket_address,socket_address_size) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd,CONNECTIONS) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    pthread_t threads[CONNECTIONS];
    int client_fds[CONNECTIONS];
    for (int i = 0; i < CONNECTIONS; i++) {
        client_fds[i] = accept(server_fd,&socket_address_size);
        if (client_fds[i] < 0) {
            perror("accept");
            exit(EXIT_FAILURE);
        }
        if (pthread_create(&threads[i],NULL,handle_request,&client_fds[i]) < 0) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    for (int i = 0; i < CONNECTIONS; i++) {
        if (pthread_join(threads[i],NULL) < 0) {
            perror("pthread_join");
        }
    }

    close(server_fd);
    return EXIT_SUCCESS;
}

服务器使用fork()的C代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#define PORT 9000
#define CONNECTIONS 2
#define MAX_BUFFER_SIZE 1024

int main(int argc,char *argv[]) {
    struct sockaddr_in socket_address;
    socklen_t socket_address_size = sizeof(socket_address);
    int server_fd,client_fd,option = 1;
    char buffer[MAX_BUFFER_SIZE] = {0};

    if ((server_fd = socket(AF_INET,CONNECTIONS) < 0) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    pid_t pids[CONNECTIONS];
    for (int i = 0; i < CONNECTIONS; i++) {
        pids[i] = fork();
        if (pids[i] < 0) {
            perror("fork");
            exit(EXIT_FAILURE);
        } else if (pids[i] == 0) {
            if ((client_fd = accept(server_fd,&socket_address_size)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }

            for (int i = INT_MAX; send(client_fd,0) >= 0 && i >= 0; i--) {
                printf("%d\r\n",i);
                sprintf(buffer,i);
            }
            
            close(client_fd);
            return EXIT_SUCCESS;
        }
    }

    for (int i = 0; i < CONNECTIONS; i++) {
        int wstatus;
        if (waitpid(0,&wstatus,WUNTRACED) < 0) {
            perror("waitpid");
        }
    }

    close(server_fd);
    return EXIT_SUCCESS;
}

客户端的C代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define IP_ADDRESS "127.0.0.1"
#define PORT 9000
#define MAX_BUFFER_SIZE 1024

int main(int argc,char *argv[]) {
    int socket_fd;
    struct sockaddr_in socket_address;
    socklen_t socket_address_size = sizeof(socket_address);
    ssize_t message_len;
    char buffer[MAX_BUFFER_SIZE] = {0};

    if ((socket_fd = socket(AF_INET,0)) == 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }
    
    socket_address.sin_family = AF_INET;
    socket_address.sin_addr.s_addr = inet_addr(IP_ADDRESS);
    socket_address.sin_port = htons(PORT);

    if (connect(socket_fd,socket_address_size) < 0) {
        perror("connect");
        exit(EXIT_FAILURE);
    }
    
    while ((message_len = recv(socket_fd,MAX_BUFFER_SIZE,0)) > 0) {
        buffer[message_len] = '\0';
        puts(buffer);
    }

    close(socket_fd);
    return EXIT_SUCCESS;
}

服务端的Python代码如下:

import socket
import threading

HOST = ''
PORT = 9000
CONNECTIONS = 2
TRUNK_SIZE = 1024

def handle_request(connection):
    with connection:
        count = 0
        while True:
            state = connection.send(f'{count}\r\n'.encode('utf-8'))
            if not state:
                print(f"Connection closed from {address}.")
                break
            print(count)
            count += 1

with socket.socket(socket.AF_INET,socket.soCK_STREAM) as s:
    s.setsockopt(socket.soL_SOCKET,socket.so_REUSEADDR | socket.so_REUSEPORT,1)
    s.bind((HOST,PORT))
    s.listen(CONNECTIONS)
    threads = []
    for c in range(CONNECTIONS):
        connection,address = s.accept()
        print(f'Connected by {address}.')
        thread = threading.Thread(target=handle_request,args=(connection,),daemon=True)
        thread.start()
        threads.append(thread)
    for thread in threads:
        thread.join()

客户端的Python代码如下:

import socket

HOST = '127.0.0.1'
PORT = 9000
TRUNK_SIZE = 1024
with socket.socket(socket.AF_INET,socket.soCK_STREAM) as s:
    s.connect((HOST,PORT))
    while True:
        data = s.recv(TRUNK_SIZE).decode('utf-8')
        if not data:
            print("Connection closed.")
            break
        print(data)

解决方法

如果您的整个进程在写入套接字时突然停止,则是由于您收到了 SIGPIPE 信号(由错误 EPIPE 指示)。 SIGPIPE 的标准操作是终止进程。实现一个信号处理程序,它将捕获 SIGPIPE 信号(并且可能会忽略或处理它)。

“Connection is reset by peer”消息表示捕获到 SIGPIPE 信号。