【问题标题】:How to pick a free port for a subprocess?如何为子进程选择一个空闲端口?
【发布时间】:2017-12-06 02:57:40
【问题描述】:

我正在围绕 Appium 服务器编写 Python 包装器。 Appium 接受要绑定到的本地端口的命令行参数。不幸的是,Appium 不能为自己自动选择一个空闲端口,所以它要么绑定到明确指定的端口,要么失败并显示EADDRINUSE。即使告诉它绑定到端口0,它也会成功启动,但不会显示它绑定到的端口。

如果我自己在 Python 包装器中找到了一个空闲端口,则无法保证在我将它传递给 Appium 的同时,其他一些进程不会绑定到同一个端口。而且如果我自己不先发布,Appium就无法绑定,所以我必须要。

我知道这在实践中不太可能发生,但是在以跨平台方式(Linux、macOS、Windows)将本地端口号传递给另一个进程之前“保留”本地端口号的“正确方法”是什么? ?

【问题讨论】:

  • 你也许可以选择一个随机端口,将它传递给 Appium,然后检查正确的错误消息。
  • 你不能尝试一个任意端口,如果它返回EADDRINUSE,则增加它并循环直到找到一个空闲端口?
  • @AlexHall,这就是我现在正在做的事情。然而,问题是关于“正确的方式”——例如。有没有办法为子进程保留端口号?
  • @rodrigo,如果我是唯一将使用该端口的人,这将起作用。但是我想将它传递给 Appium,所以我必须先释放它,然后,当 Appium 启动时,其他进程可能会占用它(因为我正在运行许多具有动态分配端口的服务器)。
  • @toriningen:啊,一个沉重的服务器......我实际上对Appium一无所知,但无论如何我都会建议。如果0启动成功,我可以想到两个解决方案: 1. 给Appium打补丁报告使用的端口(毕竟是开源的); 2.使用lsof -p <pid> -i4 -P -n | grep LISTEN之类的命令来发现它正在使用的端口。

标签: python linux macos networking network-programming


【解决方案1】:

感谢 cmets 中的@rodrigo 建议,我最终得到了以下代码:

import platform
import re
import subprocess
from typing import Set

if platform.system() == 'Windows':
    def _get_ports(pid):
        sp = subprocess.run(['netstat', '-anop', 'TCP'],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.DEVNULL,
                            check=True)

        rx_socket = re.compile(br'''(?x) ^
                                    \s* TCP
                                    \s+ 127.0.0.1 : (?P<port>\d{1,5})
                                    \s+ .*?
                                    \s+ LISTENING
                                    \s+ (?P<pid>\d+)
                                    \s* $''')

        for line in sp.stdout.splitlines():
            rxm = rx_socket.match(line)
            if rxm is None:
                continue

            sock_port, sock_pid = map(int, rxm.groups())
            if sock_pid == pid:
                yield sock_port
else:
    def _get_ports(pid):
        sp = subprocess.run(['lsof', '-anlPFn', '+w',
                             f'-p{pid}', '-i4TCP@127.0.0.1', '-sTCP:LISTEN'],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.DEVNULL,
                            check=True)

        for line in sp.stdout.splitlines():
            if line.startswith(b'n'):
                host, port = line.rsplit(b':', 1)
                port = int(port)
                yield port


def get_ports(pid: int) -> Set[int]:
    """Get set of local-bound listening TCPv4 ports for given process.

    :param pid: process ID to inspect
    :returns: set of ports
    """

    return set(_get_ports(pid))

print(get_ports(12345))

它适用于 Linux、macOS 和 Windows,并为处于 LISTEN 状态的给定进程找出所有本地绑定的 TCPv4 端口。它还跳过各种主机/端口/用户名反向查找以使其更快,并且不需要提升权限。

所以,最后,我们的想法是让 Appium(或其他任何东西)在 0.0.0.0:0 上启动,它将自己绑定到 OS 提供的第一个可用端口,然后检查它现在正在侦听的端口.没有竞争条件。

【讨论】:

    【解决方案2】:

    Selenium 库使用了这个技巧:

    https://github.com/SeleniumHQ/selenium/blob/master/py/selenium/webdriver/common/utils.py#L31

    import socket
    
    def free_port():
        """
        Determines a free port using sockets.
        """
        free_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        free_socket.bind(('0.0.0.0', 0))
        free_socket.listen(5)
        port = free_socket.getsockname()[1]
        free_socket.close()
        return port
    

    如果您将套接字绑定到端口 0,内核将为它分配一个空闲端口。它适用于 Windows 和 Linux。

    https://msdn.microsoft.com/en-us/library/windows/desktop/ms737550.aspx

    对于 TCP/IP,如果端口指定为零,则服务提供者 从动态客户端端口为应用程序分配一个唯一端口 范围。

    http://man7.org/linux/man-pages/man7/ip.7.html

    在 ip_local_port_range 中,您可以阅读以下内容:

    一个临时端口在下面被分配给一个套接字 情况:

    • 当套接字地址中的端口号被指定为 0 时 调用 bind(2);

    getsockname() 用于知道选择了哪个端口。

    【讨论】:

    • 虽然这可能会提供问题的答案,但需要一些解释。请更新问题,解释此解决方案的工作原理和原因。
    • 您的 sn-p 只是选择第一个可用端口,这与问题无关。诀窍是在不引入竞争条件的情况下将其传递给第 3 方子进程,而不仅仅是选择端口。
    猜你喜欢
    • 2012-01-25
    • 2019-06-25
    • 2013-10-26
    • 2023-03-30
    • 2015-02-17
    • 1970-01-01
    • 1970-01-01
    • 2016-08-01
    • 2011-03-17
    相关资源
    最近更新 更多