【发布时间】:2011-03-20 18:26:17
【问题描述】:
我了解端口如何工作的基础知识。但是,我不知道多个客户端如何同时连接到端口 80。我知道每个客户端都有一个唯一的(对于他们的机器)端口。服务器是否从可用端口回复客户端,并简单说明回复来自 80?这是如何工作的?
【问题讨论】:
标签: http tcp connection client-server port
我了解端口如何工作的基础知识。但是,我不知道多个客户端如何同时连接到端口 80。我知道每个客户端都有一个唯一的(对于他们的机器)端口。服务器是否从可用端口回复客户端,并简单说明回复来自 80?这是如何工作的?
【问题讨论】:
标签: http tcp connection client-server port
重要:
很抱歉,“Borealid”的回答不准确且有些不正确——首先,回答这个问题与有状态或无状态无关,最重要的是,套接字的元组定义不正确。
首先记住以下两条规则:
套接字的主键:套接字由{SRC-IP, SRC-PORT, DEST-IP, DEST-PORT, PROTOCOL} 而非{SRC-IP, SRC-PORT, DEST-IP, DEST-PORT} 标识 - 协议是套接字定义的重要组成部分。
OS 进程和套接字映射:一个进程可以与(可以打开/可以侦听)多个套接字相关联,这对许多读者来说可能是显而易见的。
示例 1: 连接到同一服务器端口的两个客户端表示:socket1 {SRC-A, 100, DEST-X,80, TCP} 和 socket2{SRC-B, 100, DEST-X,80, TCP}。这意味着主机 A 连接到服务器 X 的 80 端口,另一台主机 B 也连接到同一服务器 X 到同一端口 80。现在,服务器如何处理这两个套接字取决于服务器是单线程还是多线程(我会稍后解释)。重要的是一台服务器可以同时监听多个套接字。
回答帖子的原始问题:
无论有状态或无状态协议,两个客户端都可以连接到同一个服务器端口,因为我们可以为每个客户端分配不同的套接字(因为客户端 IP 肯定会不同)。同一个客户端也可以有两个套接字连接到同一个服务器端口——因为这些套接字的不同之处在于SRC-PORT。平心而论,“Borealid”基本上提到了相同的正确答案,但提到无状态/完整状态有点不必要/令人困惑。
回答关于服务器如何知道要回答哪个套接字的问题的第二部分。首先要了解,对于侦听同一端口的单个服务器进程,可能有多个套接字(可能来自同一个客户端或来自不同客户端)。现在只要服务器知道哪个请求与哪个套接字相关联,它就可以始终使用同一个套接字响应适当的客户端。因此,除了客户端最初尝试连接的原始端口之外,服务器永远不需要在其自己的节点中打开另一个端口。如果任何服务器在绑定套接字后分配不同的服务器端口,那么在我看来,服务器正在浪费其资源,它必须需要客户端再次连接到分配的新端口。
为了完整起见多一点:
示例 2: 这是一个非常有趣的问题:“服务器上的两个不同进程能否监听同一个端口”。如果您不将协议视为定义套接字的参数之一,那么答案是否定的。这是因为我们可以说在这种情况下,尝试连接到服务器端口的单个客户端将没有任何机制来提及客户端打算连接到的两个侦听进程中的哪一个。这与规则 (2) 所主张的主题相同。然而,这是错误的答案,因为“协议”也是套接字定义的一部分。因此,同一节点中的两个进程只有使用不同的协议才能侦听同一端口。例如,两个不相关的客户端(比如一个使用 TCP,另一个使用 UDP)可以连接到同一个服务器节点和同一个端口并与之通信,但它们必须由两个不同的服务器进程提供服务。
服务器类型 - 单个和多个:
当服务器的进程监听一个端口时,这意味着多个套接字可以同时连接并与同一个服务器进程通信。如果服务器仅使用单个子进程来服务所有套接字,则该服务器称为单进程/线程服务器,如果服务器使用许多子进程通过一个子进程为每个套接字服务,则该服务器称为多线程进程/线程服务器。请注意,无论服务器的类型如何,服务器都可以/应该始终使用相同的初始套接字进行响应(无需分配另一个服务器端口)。
如果可以的话,建议Books 和其余两卷。
关于父/子过程的说明(针对“Ioan Alexandru Cucu”的查询/评论)
无论我在哪里提到与两个进程相关的任何概念,比如 A 和 B,请考虑它们与父子关系无关。操作系统(尤其是 UNIX)在设计上允许子进程从父进程继承所有文件描述符 (FD)。因此,进程 A 侦听的所有套接字(在 UNIX 等操作系统中也是 FD 的一部分)可以被更多进程 A1、A2、.. 侦听,只要它们与 A 有父子关系。但是一个独立的进程 B(即与 A 没有父子关系)不能监听同一个套接字。此外,还要注意,不允许两个独立进程监听同一个套接字的这条规则位于操作系统(或其网络库)上,到目前为止,大多数操作系统都遵守它。但是,可以创建自己的操作系统,这很可能会违反此限制。
【讨论】:
sendmsg() 系统调用和SCM_RIGHTS。这不仅适用于套接字,而且一个进程拥有的任何文件描述符都可以转移到另一个进程,即使它不是子进程。
首先,“端口”只是一个数字。所有“连接到端口”真正代表的是一个数据包,该数据包在其“目标端口”标头字段中指定了该编号。
现在,您的问题有两个答案,一个针对有状态协议,一个针对无状态协议。
对于无状态协议(即UDP),没有问题,因为“连接”不存在——多人可以向同一个端口发送数据包,他们的数据包将按任意顺序到达。没有人处于“连接”状态。
对于有状态协议(如 TCP),连接由 4 元组标识,该 4 元组由源端口和目标端口以及源 IP 地址和目标 IP 地址组成。因此,如果两台不同的机器连接到第三台机器上的同一个端口,就会有两个不同的连接,因为源 IP 不同。如果同一台机器(或两个在 NAT 之后或以其他方式共享相同 IP 地址)连接到一个远程端两次,则连接由源端口(通常是一个随机的高编号端口)来区分。
简单地说,如果我从我的客户端两次连接到同一个 Web 服务器,从我的角度来看,这两个连接将具有不同的源端口和来自 Web 服务器的目标端口。因此没有歧义,即使两个连接具有相同的源 IP 地址和目标 IP 地址。
端口是一种复用 IP 地址的方法,以便不同的应用程序可以侦听同一 IP 地址/协议对。除非应用程序定义了自己的高级协议,否则无法复用端口。如果使用相同协议的两个连接同时具有相同的源 IP 和目标 IP 以及相同的源端口和目标端口,则它们必须是相同的连接。
【讨论】:
那么,当服务器侦听 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,这是“监听所有”的代码。一个容易犯的错误是监听地址 127.0.0.1,它只接受来自当前计算机的连接。所以这不是一个连接,这只是意味着一个进程请求绑定()到端口 IP,并且该进程负责处理与该端口的所有连接。这暗示了每台计算机只能有一个进程监听端口的限制(有一些方法可以使用多路复用来解决这个问题,但这是一个更复杂的话题)。如果网络服务器正在侦听端口 80,则它无法与其他网络服务器共享该端口。
现在,让我们将用户连接到我们的机器:
quicknet -m tcp -t localhost:500 -p Test payload.
这是一个简单的脚本 (https://github.com/grokit/dcore/tree/master/apps/quicknet),它打开一个 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 地址之间永远不会混淆。
【讨论】:
多个客户端可以连接到服务器上的同一个端口(比如 80),因为在服务器端,在创建 socket 和 binding 之后(设置本地 IP 和端口) listen 在告诉操作系统接受传入连接的套接字上调用。
当客户端尝试在端口 80 上连接到服务器时,accept 调用在服务器套接字上调用。这将为尝试连接的客户端创建一个新的套接字,并且类似地,将为使用相同端口 80 的后续客户端创建一个新的套接字。
斜体字是系统调用。
参考
【讨论】:
通常,对于每个正在连接的客户端,服务器都会派生一个与客户端 (TCP) 通信的子进程。父服务器将已建立的套接字传递给子进程,该套接字与客户端进行通信。
当您将数据从子服务器发送到套接字时,操作系统中的 TCP 堆栈会创建一个返回客户端的数据包并将“来自端口”设置为 80。
【讨论】: