如何将接受的套接字从父进程传递到其子进程?

How can I pass an accepted socket from a parent process to its child process?

提问人:doggo 提问时间:4/27/2020 最后编辑:doggo 更新时间:3/29/2021 访问量:1365

问:

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


我正在编写一个 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 的值与创建它的进程有关?

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

Python 套接字 子进程

评论

0赞 user207421 4/27/2020
定义“连接的客户端”。你的意思是“接受的插座”吗?如果是这样,是什么让您认为可以从文本中重建连接的套接字?你不能。
0赞 doggo 4/27/2020
是的,我的意思是一个可接受的套接字。我将编辑我的问题以反映这一点。至于你的第二个问题,我认为 fd 字段是操作系统分配给连接的唯一编号,类似于 C 中套接字映射到整数的方式。那么,如果我不能通过文本来做到这一点,我该怎么做呢?我尝试酸洗可接受的插座,但插座不能酸洗。
0赞 doggo 4/27/2020
@user207421根据您的评论,您会发现接受的答案很有趣。

答:

2赞 larsks 4/27/2020 #1

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

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

#!/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


如果你使用模块生成子进程,情况大致相同,因为只是在后台调用。这就是为什么我说“子进程”和“子进程”是同义词。subprocesssubprocessfork()exec()

不过,有一个问题。事实上,其中两个:

  1. 默认情况下,将在生成子进程之前关闭所有打开的文件描述符。幸运的是,有一个关键字参数可以禁用该行为。subprocessclose_fds

  2. 不幸的是,即使我们禁用了 中的行为,返回的文件描述符也设置了标志,这意味着当进程调用 时,它们会被内核关闭。close_fdssubprocessacceptCLOSE_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)

除此之外,套接字将由 using 和 friends 生成的进程继承。例如,如果我们像这样重写我们的父代码: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()

评论

0赞 doggo 4/27/2020
我必须真诚地道歉,因为我似乎误用了“父进程和子进程”这个术语。我指的是子进程,而不是子进程。我的“主”python脚本正在使用以下语法调用另一个脚本:subprocess.call([“python3”, “second_process.py”]) 对于我的错误术语造成的误解,再次深表歉意。我将不得不解决我的问题。
0赞 larsks 4/27/2020
我不确定你如何区分“子进程”和“子进程”,我认为它们只是同一事物的不同词。也许如果你能展示你的代码,这种差异就会很明显。
1赞 doggo 4/27/2020
我编辑了我之前的评论,因为我太早点击了<Enter>。我绝对可以发布一些代码,但我可能需要几个小时才能获得 MWE。可以说,我首先应该这样做。
0赞 larsks 4/27/2020
我在答案中添加了一些可能会有所帮助的信息。
0赞 doggo 4/27/2020
前 3 行立即解决了我的问题。谢谢!
0赞 Srikanteswararao talluri 3/29/2021 #2

Python 代码片段

sock = socket.socket(socket.AF_INET,
                  socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

套接字重用地址是这里要注意的关键点

然后,您可以使用

sock.file_no将其传递给客户端。

客户端可以使用

s_client = socket.socket(<filehandle from parent>, socket.AF_INET,
                  socket.SOCK_STREAM))