Почему сервер сокетов TCP, написанный на C, не должен закрывать дескрипторы файлов клиента в рабочих потоках, но может закрывать их в разветвленных рабочих процессах?

#c #sockets #server #pthreads #file-descriptor

Вопрос:

Я написал простой сервер сокетов TCP на языке C, который создает новый дочерний рабочий поток, когда он принимает новый запрос на подключение от клиента и просто подсчитывает и отправляет номера. Если клиент завершает работу, соответствующий дочерний рабочий поток также должен завершиться, в то время как другие потоки не должны.

Если все клиенты написаны на Python, то при завершении работы клиента на сервере печатается «Соединение сброшено одноранговым узлом», но все остальное в порядке, то есть другие потоки и клиенты все еще работают.

Но если клиент написан на языке Си, то когда какие-либо клиенты и соответствующие им дочерние рабочие потоки завершаются, другие потоки также завершаются, чего не ожидается. Почему это происходит? Я переписал сервер на Python, но этого не произошло, независимо от того, на каком языке написан клиент.

Затем я прокомментировал, close(*client_fd); и проблема решена. Я понятия не имею, так как он отлично работает на сервере с использованием fork() .

Код C для использования сервером pthread выглядит следующим образом:

 #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 amp;amp; i >= 0; i--) {
        printf("%drn", 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, amp;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 *) amp;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, (struct sockaddr *) amp;socket_address, amp;socket_address_size);
        if (client_fds[i] < 0) {
            perror("accept");
            exit(EXIT_FAILURE);
        }
        if (pthread_create(amp;threads[i], NULL, handle_request, amp;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;
}
 

Код C для использования сервером fork() выглядит следующим образом:

 #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, SOCK_STREAM, 0)) == 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, amp;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 *) amp;socket_address, socket_address_size) < 0) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    if (listen(server_fd, 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, (struct sockaddr *) amp;socket_address, amp;socket_address_size)) < 0) {
                perror("accept");
                exit(EXIT_FAILURE);
            }

            for (int i = INT_MAX; send(client_fd, buffer, strlen(buffer), 0) >= 0 amp;amp; i >= 0; i--) {
                printf("%drn", i);
                sprintf(buffer, "%d", i);
            }
            
            close(client_fd);
            return EXIT_SUCCESS;
        }
    }

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

    close(server_fd);
    return EXIT_SUCCESS;
}
 

The C code for client is as follows:

 #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, SOCK_STREAM, 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, (struct sockaddr *) amp;socket_address, socket_address_size) < 0) {
        perror("connect");
        exit(EXIT_FAILURE);
    }
    
    while ((message_len = recv(socket_fd, buffer, MAX_BUFFER_SIZE, 0)) > 0) {
        buffer[message_len] = '';
        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}rn'.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)
 

Комментарии:

1. Возможно, потому, что ресурсы (включая сокеты) в потоках совместно используются одним процессом? Закрытие сокета в одном потоке закрывает его на весь процесс. Если используются рабочие процессы, то сокеты отделены от сокетов в других процессах, даже если процессы были разветвлены от одного и того же родительского процесса.

2. @Someprogrammerdude, но почему это проблема здесь? Таково ожидаемое поведение.

3. В заголовке спрашивалось «почему не закрываются потоки, а закрываются процессы», и в этом, в основном, причина. Да, это ожидаемое поведение, но не все еще знают об этом.

4. accept создает новый сокет. connection_handler должен закрыть этот сокет. Должно быть, в коде есть какая-то другая ошибка.

5. @Someprogrammerdude, Но client_fd в каждом потоке детского рабочего процесса он отличается. Я думаю, что закрытие сокета в одном потоке не должно закрывать его на весь процесс?

Ответ №1:

Если весь ваш процесс внезапно выходит из строя во время записи в сокет, то это происходит из-за SIGPIPE полученного вами сигнала (на который указывает сообщение об ошибке EPIPE). Стандартное действие SIGPIPE состоит в том, чтобы завершить процесс. Реализуйте обработчик сигналов, который будет улавливать SIGPIPE сигналы (и, вероятно, игнорировать их или иметь с ними дело).

Сообщение «Соединение сброшено одноранговым узлом» указывает на перехваченный сигнал SIGPIPE.

Комментарии:

1. Одна из многих проблем с systemd заключается в том, что по умолчанию для параметра IgnoreSIGPIPE установлено значение true.

2. @старк Ну, я не знаю, есть ли в операционной systemd в ее системе, но я все еще думаю, что его завершение процесса связано с этим. Утверждается, что с раздвоенной версией нет никаких «проблем», потому что завершается только этот дочерний процесс, а не, как в многопоточной версии, единственный существующий процесс.

3. @старк, Но, тем не менее, спасибо, я этого не знал.