Как я могу передать принятый сокет от родительского процесса его дочернему процессу? - PullRequest
0 голосов
/ 27 апреля 2020

Примечание: Основываясь на ответе ниже, я думаю, что я не правильно изложил этот вопрос. В настоящее время я переписываю его с кодом, чтобы быть более понятным.


Я пишу сервер python, который принимает подключения нескольких клиентов и сохраняет их.

Если я распечатываю правильно подключенный сокет, который используется для связи с одним из подключенных клиенты, я бы получил что-то вроде следующего:

<socket.socket fd=4, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('3.3.3.3', 1234), raddr=('4.4.4.4', 63402)>

, где в целях конфиденциальности я заменил IP моего сервера на 3.3.3.3 и IP клиента на 4.4.4.4. Я действительно надеялся, что сработает, - это сохранить информацию в файл в формате:

4 2049

, а затем, когда загрузится дочерний процесс, он передаст эту информацию конструктор сокета, использующий:

recovered_client = socket(AF_INET, 2049, 0, 4)

Но это не работает. Когда я применяю этот процесс и печатаю восстановленный клиент, я вижу следующее:

<socket.socket fd=4, family=AddressFamily.AF_INET, type=2049, proto=0>

Кажется, что поля laddr и raddr из исходного соединения не удалось восстановить, передав дескриптор файла в конструктор.

Я попытался исправить это вручную, добавив в файл хост и порт из laddr и raddr, а затем подключившись с помощью команды:

recovered_client.connect(('4.4.4.4', 63402))

Но это приводит к ошибке:

OSError: [Errno 88] Socket operation on non-socket

В качестве эксперимента я оставил соединение открытым в родительском процессе, а затем разрешил дочернему процессу принять новое соединение fre sh и распечатать это, и то, что я получил, было:

<socket.socket fd=4, family=AddressFamily.AF_INET, type=2049, proto=0, laddr=('3.3.3.3', 1234), raddr=('75.159.78.189', 49709)>

Другими словами, было установлено новое соединение с тем же значением для fd , с другим клиентским портом. Исходное соединение никогда не закрывалось, а зависало бесконечно, потому что, как и предполагалось, родительский процесс завис, когда вызывал дочерний процесс.

Таким образом, это означает, что у меня есть два разных активных соединения (хотя одно было заморожено), чьи сокеты имеют одинаковые файловые дескрипторы. Означает ли это, что значение, присвоенное полю fd для сокета, относится к процессу, который его создал?

Если это так, мой подход явно безнадежен. Как я могу передать соединение моего клиента, созданное в моем родительском процессе, его дочернему процессу?

1 Ответ

0 голосов
/ 27 апреля 2020

Если так, мой подход явно безнадежен. Как я могу передать соединение моего клиента, созданное в моем родительском процессе, его дочернему процессу?

Дочерний объект наследует все дескрипторы открытого файла от своего родительского процесса. Не нужно ничего «передавать». Рассмотрим следующий код:

#!/usr/bin/python

import os
import socket


s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('localhost', 2049))
s.listen(5)


def child_process(fd, addr):
    while True:
        data = fd.recv(10)
        if len(data) == 0:
            break
        print('read:', data)

    print('client {} has disconnected'.format(addr))


def main():
    while True:
        c_fd, c_addr = s.accept()
        print('new connection from', c_addr)

        pid = os.fork()
        if pid > 0:
            # This is the parent process
            c_fd.close()
        else:
            # This is the child process
            child_process(c_fd, c_addr)
            return


try:
    main()
finally:
    s.close()

Каждое новое соединение обрабатывается дочерним процессом. Файловые дескрипторы, которые были открыты в родительском объекте (например, клиентский сокет, возвращаемый вызовом accept), уже доступны в клиенте. Нам просто нужно убедиться, что мы закрываем клиентский сокет в родительском объекте, поскольку он уже унаследован дочерним элементом.


История в основном такая же, если вы порождаете подпроцессы с использованием subprocess модуль, потому что subprocess просто вызывает fork() и exec() под капотом. Вот почему я сказал, что «подпроцесс» и «дочерний процесс» являются синонимами.

Хотя здесь есть одна загвоздка. Фактически, два из них:

  1. По умолчанию subprocess закроет все дескрипторы открытых файлов перед порождением дочернего процесса. К счастью, есть ключевое слово close_fds, чтобы отключить это поведение.

  2. К сожалению, даже если мы отключим поведение close_fds в subprocess, файловые дескрипторы возвращаются accept установлен флаг CLOSE_ON_EXEC, что означает, что они закрываются ядром, когда процесс вызывает exec.

Но не беспокойтесь, мы можем обойти это, очистив CLOSE_ON_EXEC флаг, подобный следующему:

c_fd, c_addr = s.accept()
flags = fcntl.fcntl(c_fd, fcntl.F_GETFD, 0)
fcntl.fcntl(c_fd, fcntl.F_SETFD, flags & ~fcntl.FD_CLOEXEC)

После этого сокет будет наследоваться процессами, созданными с помощью subprocess.call и друзьями. Например, если мы переписываем наш родительский код следующим образом:

#!/usr/bin/python

import fcntl
import socket
import subprocess


s = socket.socket(socket.AF_INET,
                  socket.SOCK_STREAM|socket.SOCK_CLOEXEC)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('localhost', 2049))
s.listen(5)


def main():
    while True:
        c_fd, c_addr = s.accept()
        flags = fcntl.fcntl(c_fd, fcntl.F_GETFD, 0)
        fcntl.fcntl(c_fd, fcntl.F_SETFD, flags & ~fcntl.FD_CLOEXEC)
        print('new connection from', c_addr)
        # Here we call the child command, passing the
        # integer file descriptor as the first argument.
        subprocess.check_call(['python', 'socketchild.py',
                         '{}'.format(c_fd.fileno()), c_addr[0]],
                        close_fds=False)
        c_fd.close()


try:
    main()
finally:
    s.close()

Затем мы можем написать дочерний код, который использует метод socket.fromfd для преобразования этого целочисленного файлового дескриптора обратно в сокет:

#!/usr/bin/python

import socket
import sys


def child_process(fd, addr):
    while True:
        data = fd.recv(10)
        if len(data) == 0:
            break
        print('read:', data)

    print('client {} has disconnected'.format(addr))


def main():
    fdno = int(sys.argv[1])
    print('got fd:', fdno)
    addr = sys.argv[2]
    fd = socket.fromfd(fdno, socket.AF_INET, socket.SOCK_STREAM)
    child_process(fd, addr)


if __name__ == '__main__':
    main()
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...