这可能是一个非常基本的问题,但它让我感到困惑。
两个不同的连接套接字可以共享一个端口吗?我正在编写一个应该能够处理超过 100k 并发连接的应用程序服务器,并且我们知道系统上可用的端口数约为 60k(16 位)。一个已连接的套接字被分配给一个新的(专用)端口,因此这意味着并发连接数受端口数的限制,除非多个套接字可以共享同一个端口。所以这个问题。
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 地址之间永远不会混淆。
服务器套接字侦听单个端口。该服务器上所有已建立的客户端连接都与连接的服务器端的同一侦听端口相关联。已建立的连接由客户端和服务器端 IP/端口对的组合唯一标识。同一服务器上的多个连接可以共享同一服务器端 IP/端口对,只要它们与不同的客户端 IP/端口对相关联,并且服务器将能够处理可用系统资源允许的尽可能多的客户端至。
在客户端,新的出站连接通常使用随机客户端端口,在这种情况下,如果您在短时间内建立大量连接,则可能会用完可用端口。
已连接的套接字分配给新的(专用)端口
这是一种普遍的直觉,但它是不正确的。连接的套接字未分配给新的/专用端口。 TCP 堆栈必须满足的唯一实际约束是 (local_address, local_port, remote_address, remote_port) 的元组对于每个套接字连接必须是唯一的。因此,服务器可以有许多 TCP 套接字使用相同的本地端口,只要端口上的每个套接字都连接到不同的远程位置。
bind()
操作在 connect()
操作之前,甚至是隐式的。
bind()
仅在 accept()?
之前在服务器端使用,所以客户端也会绑定特定端口?
bind()
可以在 connect()
之前在客户端使用。
accept()
在侦听套接字上返回的套接字)
理论上,是的。练习,不是。大多数内核(包括 linux)不允许您第二个 bind()
到已分配的端口。允许这样做并不是一个很大的补丁。
从概念上讲,我们应该区分套接字和端口。套接字是双向通信端点,即我们可以发送和接收字节的“事物”。这是一个概念性的东西,在名为“socket”的数据包头中没有这样的字段。
端口是能够识别套接字的标识符。在 TCP 的情况下,端口是一个 16 位整数,但也有其他协议(例如,在 unix 套接字上,“端口”本质上是一个字符串)。
主要问题如下:如果传入的数据包到达,内核可以通过其目标端口号识别其套接字。这是一种最常见的方式,但不是唯一的可能性:
套接字可以通过传入数据包的目标 IP 来识别。例如,如果我们有一台服务器同时使用两个 IP,就是这种情况。然后,我们可以在相同的端口上运行不同的网络服务器,但在不同的 IP 上。
套接字也可以通过它们的源端口和 ip 来识别。在许多负载平衡配置中就是这种情况。
因为您正在使用应用程序服务器,所以它将能够做到这一点。
bind()
。
bind()
未设置侦听套接字的操作系统?我可以想象,是的,这很有可能,但事实是 WinSock 和 Posix API 都为此使用了 bind()
调用,即使它们的参数化实际上是相同的。即使 API 没有此调用,您需要以某种方式说出来,您想从哪里读取传入的字节。
listen()
/accept()
API 调用可以创建套接字,内核将通过传入端口来区分它们。 OP的问题可以按照他本质上要求的方式来解释。我认为,这是很现实的,但这不是他的问题的字面意思。
不,不可能在特定时刻共享同一个端口。但是你可以让你的应用程序在不同的时刻进行端口访问。
我想没有一个答案能说明这个过程的每一个细节,所以这里是:
考虑一个 HTTP 服务器:
它要求操作系统将端口 80 绑定到一个或多个 IP 地址(如果选择 127.0.0.1,则只接受本地连接。您可以选择 0.0.0.0 绑定到所有 IP 地址(本地主机、本地网络、广域网) ,两个 IP 版本))。当客户端连接到该端口时,它将锁定一段时间(这就是套接字有积压的原因:它将许多连接尝试排队,因为它们不是即时的)。操作系统然后选择一个随机端口并将该连接传输到该端口(将其视为从现在开始处理所有流量的临时端口)。然后释放端口 80 用于下一个连接(首先,它将接受积压中的第一个连接)。当客户端或服务器断开连接时,随机端口会保持打开一段时间(远程端为 CLOSE_WAIT,本地端为 TIME_WAIT)。这允许沿路径刷新一些丢失的数据包。该状态的默认时间是 2 * MSL 秒(它会在等待时消耗内存)。等待之后,该随机端口再次空闲以接收其他连接。
所以,TCP 甚至不能在两个 IP 之间共享一个端口!