TCP:两个不同的套接字可以共享一个端口吗?

208

这可能是一个非常基本的问题,但它让我感到困惑。

两个不同的连接套接字是否可以共享一个端口?我正在编写一个应用程序服务器,应该能够处理超过100k并发连接。我们知道系统上可用的端口数量大约为60k(16位)。连接套接字被分配给一个新的(专用)端口,这意味着并发连接数受限于端口数,除非多个套接字可以共享同一个端口。所以问题来了。

8个回答

333

TCP / HTTP监听端口:如何让多个用户共享同一个端口

当服务器监听TCP端口时会发生什么?例如,假设您在80端口上有一个Web服务器。假设您的计算机具有公共IP地址24.14.181.229,试图连接您的人具有IP地址10.1.2.3。这个人可以通过打开到24.14.181.229:80的TCP套接字来连接您。很简单。

直观地(并且错误地),大多数人认为它看起来像这样:

    Local Computer    | Remote Computer
    --------------------------------
    <local_ip>:80     | <foreign_ip>:80

    ^^ not actually what happens, but this is the conceptual model a lot of people have in mind.

这很容易理解,因为从客户端的角度来看,他有一个IP地址,并连接到IP:PORT上的服务器。由于客户端连接到端口80,那么他的端口也必须是80吗?这是一个合理的想法,但实际上并不是发生的事情。如果是正确的,我们只能为每个外部IP地址提供一位用户服务。一旦远程计算机连接,那么他将独占端口80到端口80的连接,其他人就无法连接了。
必须理解以下三点:
1.)在服务器上,进程正在监听一个端口。一旦它获得连接,它会将其交给另一个线程。通信从不独占监听端口。
2.)通过操作系统,连接通过以下5元组唯一标识:(本地IP、本地端口、远程IP、远程端口、协议)。如果元组中的任何元素不同,则这是完全独立的连接。
3.)当客户端连接到服务器时,它选择一个随机的未使用的高阶源端口。这样,单个客户端可以对同一目标端口的服务器拥有多达~64k个连接。
因此,当客户端连接到服务器时,实际上创建的是:
    Local Computer   | Remote Computer           | Role
    -----------------------------------------------------------
    0.0.0.0:80       | <none>                    | LISTENING
    127.0.0.1:80     | 10.1.2.3:<random_port>    | ESTABLISHED

观察实际发生的情况

首先,让我们使用netstat查看此计算机上正在发生的情况。我们将使用端口500而不是80(因为在端口80上会发生很多事情,但从功能上讲并没有区别)。

    netstat -atnp | grep -i ":500 "

如预期所示,输出为空。现在让我们启动一个Web服务器:

    sudo python3 -m http.server 500

现在,运行netstat命令的输出如下:

    Proto Recv-Q Send-Q Local Address           Foreign Address         State  
    tcp        0      0 0.0.0.0:500             0.0.0.0:*               LISTEN      - 

现在,有一个进程正在端口500上主动监听(状态:LISTEN)。本地地址为0.0.0.0,这是“监听所有IP地址”的代码。一个容易犯的错误是只在127.0.0.1端口上进行监听,这将只接受来自当前计算机的连接。所以这不是一个连接,这只意味着一个进程请求绑定到IP端口,该进程负责处理该端口的所有连接。这暗示了每台计算机只能有一个进程监听一个端口的限制(有一些方法可以通过复用来解决这个问题,但这是一个更加复杂的话题)。如果一个Web服务器正在侦听端口80,则不能与其他Web服务器共享该端口。

现在,让我们将用户连接到我们的计算机:

    quicknet -m tcp -t localhost:500 -p Test payload.

这是一个简单的脚本(https://github.com/grokit/quickweb),它打开一个TCP套接字,发送有效负载(在本例中为“测试有效负载。”),等待几秒钟后断开连接。在此过程中再次执行netstat显示如下内容:
    Proto Recv-Q Send-Q Local Address           Foreign Address         State  
    tcp        0      0 0.0.0.0:500             0.0.0.0:*               LISTEN      -
    tcp        0      0 192.168.1.10:500        192.168.1.13:54240      ESTABLISHED -

如果您连接到另一个客户端并再次运行 netstat 命令,您将看到以下内容:
    Proto Recv-Q Send-Q Local Address           Foreign Address         State  
    tcp        0      0 0.0.0.0:500             0.0.0.0:*               LISTEN      -
    tcp        0      0 192.168.1.10:500        192.168.1.13:26813      ESTABLISHED -

也就是说,客户端为连接使用了另一个随机端口。因此,IP地址之间永远不会混淆。


30
这是我在SO上看过的最好的答案。 - Jobs
2
@N0thing “这样,单个客户端可以对同一目标端口与服务器建立多达64k个连接。”因此,在实践中,如果客户端不会同时两次或多次连接到相同的服务器和端口,则客户端甚至可以拥有超过64K个连接。这是真的吗?如果是的话,那就意味着从客户端侧的单个端口可以连接到许多不同的服务器进程(使套接字连接不同)。因此,总体而言,多个客户端套接字可以驻留在客户端机器上的同一端口上吗?请阅读我对“Remey Lebeau”答案的评论。谢谢:D - Prem KTiw
8
是的,如果多个客户端套接字连接到不同的服务器IP/Port对,并且本地和远程对的元组是唯一的,那么它们可以同时绑定到相同的本地IP/Port对。同时,一个客户端可以拥有超过64K个并发连接总数。只要服务器IP/Port对是唯一的,从单个端口可以连接到潜在无限数量的服务器(受可用操作系统资源、可用路由器端口等的限制)。 - Remy Lebeau
2
@bibstha 当所有传入连接都被拒绝时,防火墙如何处理随机端口? - PatrykG
2
@PatrykG 随机端口是为客户端而设的,防火墙不会查看连接客户端端口,而是目标端口。 - Bibek Shrestha
显示剩余7条评论

258
一个服务器套接字监听一个端口。所有在该服务器上建立的客户端连接都与该监听端口在服务器端相关联。建立的连接由客户端和服务器端的IP/端口对组合唯一标识。在同一台服务器上的多个连接可以共享相同的服务器端IP/端口对,只要它们与不同的客户端侧IP/端口对相关联,并且服务器能够处理尽可能多的客户端,取决于系统可用资源的数量。
客户端侧,通常惯例是使用随机的客户端侧端口进行新的出站连接,这种情况下如果在短时间内建立大量连接,就可能用尽可用端口。

4
如果我没记错的话,连接不仅通过源/目标端口/IP 进行区分,还要根据协议 (TCP、UDP 等) 进行区分。 - Ondrej Peterka
2
@RemyLebeau,客户端套接字和服务器端套接字有区别吗?我问这个问题是因为我想使用单个Socket连接到多个服务器。据我所知,在连接建立后它们之间没有区别。但是我找不到API来初始化使用单个Socket连接到多个主机、端口对的连接。 - user2268997
12
您不能使用一个套接字连接多个服务器,您必须为每个连接创建单独的套接字。 - Remy Lebeau
4
只要不连接到相同的远程IP/端口对,单个客户端可以绑定多个TCP套接字到相同的本地IP/端口对上。这不仅限于Windows,而是TCP通信的一般工作原理。 - Remy Lebeau
2
@FernandoGonzalezSanchez:尽管MSDN文档中提到,如果两个TCP监听套接字尝试使用SO_REUSEADDR在相同的IP/端口(通配符或其他方式)上绑定,第二个bind()仍将失败,并显示WSAEADDRINUSE。我刚试过了。在同一IP/端口对上有两个TCP服务器监听是没有意义的,即使文档也警告不要这样做。 - Remy Lebeau
显示剩余19条评论

60

连接的套接字不会被分配到一个新的(专用的)端口

这是一种常见的直觉,但是它是不正确的。连接的套接字并不会被分配到一个新的/专用的端口。TCP协议栈必须满足的唯一实际约束是(local_address, local_port, remote_address, remote_port)四元组对于每个套接字连接必须是唯一的。因此,服务器可以有许多使用同一个本地端口的TCP套接字,只要该端口上的每个套接字都连接到不同的远程位置即可。

请参见W. Richard Stevens、Bill Fenner和Andrew M. Rudoff所著《UNIX网络编程:套接字联网API》中的“Socket Pair”段落,网址:http://books.google.com/books?id=ptSC4LpwGA0C&lpg=PA52&dq=socket%20pair%20tuple&pg=PA52#v=onepage&q=socket%20pair%20tuple&f=false


8
你说的对服务器端来说完全正确。然而,BSD套接字API的结构意味着实际上出站客户端端口必须是唯一的,因为bind()操作在隐式或显式地进行connect()操作之前进行。 - user207421
1
@EJP 你好,我认为bind()只在服务器端在accept()之前使用。那么客户端也会绑定特定的端口吗? - Sam YC
6
bind() 可以在客户端使用,在 connect() 之前调用。 - Remy Lebeau
在得票最高的答案中,您只能在同一端口上有一个套接字侦听。最好说“因此,服务器可以使用相同的本地端口拥有许多TCP连接...” 连接和套接字是不同的东西,对吧? - joe
2
在一个端口上只能有一个 TCP 套接字 监听,但是你可以拥有多个已连接的 TCP 套接字,同时使用该端口发送和接收数据。(这些套接字是由监听套接字上的 accept() 返回的) - Jeremy Friesner

13
理论上是可以的,但实践中并不行。大多数内核(包括linux)不允许您对已分配的端口进行第二次bind()。如果允许这样做,则需要一个相对较小的补丁。

从概念上讲,我们应该区分socketport。Socket是双向通信终点,即可以发送和接收字节的 "things"。这是一个概念上的东西,在数据包头中没有名为 "socket" 的字段。

端口是一种标识符,能够标识一个socket。在TCP的情况下,一个端口是一个16位整数,但也有其他协议(例如,在unix sockets上,一个 "port" 本质上是一个字符串)。

主要问题是:如果一个传入的数据包到达了,内核可以通过其目标端口号来确定其socket。这是最常见的方式,但不是唯一的可能性:

  • 套接字也可以通过传入数据包的目标IP地址来识别。例如,如果我们有一个服务器同时使用两个IP,则可以在相同的端口上运行不同的Web服务器,但在不同的IP上。
  • 套接字也可以通过它们的源端口和IP来识别。这是许多负载均衡配置的情况。

因为您正在使用应用程序服务器,所以它将能够执行此操作。


2
他没有询问如何创建第二个 bind() - user207421
1
@user207421 你有没有见过在操作系统中没有通过 bind() 设置监听套接字的情况?我可以想象得出,这是很有可能的,但事实上,无论是 WinSock 还是 Posix API 都使用 bind() 调用来进行设置,即使它们的参数设置基本相同。 即使一个API没有这个调用,某种方式你也需要指定从哪里读取传入的字节 - peterh
1
当然可以使用相同的端口处理100k或更多TCP连接,listen() / accept() API调用可以以一种区分它们的方法创建套接字,即按其传入端口来区分。 OP的问题可以这样解释,他本质上是在问这个问题。我认为这很现实,但这并不是他的问题字面意思。 - peterh

3
我猜没有一个答案涵盖了整个过程的所有细节,以下是完整过程:
考虑一个HTTP服务器: 1. 它要求操作系统将端口80绑定到一个或多个IP地址上(如果你选择127.0.0.1,则只接受本地连接。你可以选择0.0.0.0来绑定到所有IP地址(本地主机、本地网络、广域网和两种IP版本))。 2. 当客户端连接到该端口时,它将会被锁定一段时间(这就是为什么套接字有一个backlog队列:它排队了一些连接尝试,因为它们不是即时的)。 3. 然后操作系统选择一个随机端口并将该连接转移到该端口(将其视为一个临时端口,从现在开始处理所有的流量)。 4. 端口80随后释放给下一个连接(首先,它将接受backlog中的第一个连接)。 5. 当客户端或服务器断开连接时,该随机端口将保持打开一段时间(在远程端处为CLOSE_WAIT,在本地端为TIME_WAIT)。这允许刷新路径上的一些丢失的数据包。该状态的默认时间为2 * MSL秒(并且在等待时将消耗内存)。 6. 在等待结束后,该随机端口再次空闲以接收其他连接。
因此,TCP甚至不能在两个IP之间共享一个端口!

这完全是错误的。服务器根本不使用任何随机端口(除非它绑定到端口0)。当客户端连接并被放入等待队列时,accept()返回的套接字会被分配本地IP/端口,即服务器正在侦听的IP/端口,以及客户端在连接之前绑定的远程IP/端口。通常,客户端会在连接之前在其一端绑定到一个随机端口(但这不是必需的,它也可以绑定到特定端口),它不会在服务器端使用随机端口。服务器的端口根本不会被锁定。 - Remy Lebeau

0

不,无法在同一时间共享相同的端口。但是您可以使应用程序以这样的方式运行,使其在不同的时间访问端口。


0
不可以,两个不同的连接套接字不能共享相同的端口号。在TCP/IP协议栈中,套接字由一个唯一的组合IP地址端口号来标识。因此,如果它们使用不同的IP地址,则两个套接字可以绑定到同一主机上的相同端口号。

1
你说的一切都是不正确的。只要两个套接字连接到不同的远程IP /端口,它们可以共享相同的本地 IP/端口。 TCP连接(而不是套接字)由协议、本地IP /端口和远程IP /端口的唯一组合来标识。例如,2个客户端可以绑定(使用SO_REUSEADDR/ SO_REUSEPORT如果需要)到相同的本地IP/端口,然后连接到不同的服务器。 - Remy Lebeau

-1
绝对不行,当多个连接可能使用相同的端口时,它们必须具有不同的IP地址。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接