首页 > 解决方案 > 如何将接受的套接字从父进程传递到其子进程?

问题描述

注意: 根据下面的答案,我认为我没有正确传达这个问题。我目前正在用代码重写它以更清晰。


我正在编写一个 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>

似乎无法通过将文件描述符传递给构造函数来恢复原始连接中的字段laddrraddr 。

我尝试通过将 laddr 和 raddr 中的主机和端口也添加到文件中来手动修复此问题,然后使用以下命令进行连接:

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

但这会产生错误:

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

作为一个实验,我在父进程中保持连接打开,然后让子进程接受一个新的连接并打印它,我得到的是:

<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的值与创建它的进程相关?

如果是这样,我的方法显然是没有希望的。如何将在父进程中创建的客户端连接传递给其子进程?

标签: pythonsocketssubprocess

解决方案


如果是这样,我的方法显然是没有希望的。如何将在父进程中创建的客户端连接传递给其子进程?

子代从其父代继承所有打开的文件描述符。没有必要“通过”任何东西。考虑以下代码:

#!/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文件描述符也会设置标志,这意味着当进程调用.acceptCLOSE_ON_EXECexec

但不用担心,我们可以通过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()

推荐阅读