【问题标题】:How do multiple clients connect simultaneously to one port, say 80, on a server? [duplicate]多个客户端如何同时连接到服务器上的一个端口,比如 80? [复制]
【发布时间】:2011-03-20 18:26:17
【问题描述】:

我了解端口如何工作的基础知识。但是,我不知道多个客户端如何同时连接到端口 80。我知道每个客户端都有一个唯一的(对于他们的机器)端口。服务器是否从可用端口回复客户端,并简单说明回复来自 80?这是如何工作的?

【问题讨论】:

标签: http tcp connection client-server port


【解决方案1】:

重要:

很抱歉,“Borealid”的回答不准确且有些不正确——首先,回答这个问题与有状态或无状态无关,最重要的是,套接字的元组定义不正确。

首先记住以下两条规则:

  1. 套接字的主键:套接字由{SRC-IP, SRC-PORT, DEST-IP, DEST-PORT, PROTOCOL} 而非{SRC-IP, SRC-PORT, DEST-IP, DEST-PORT} 标识 - 协议是套接字定义的重要组成部分。

  2. 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 没有父子关系)不能监听同一个套接字。此外,还要注意,不允许两个独立进程监听同一个套接字的这条规则位于操作系统(或其网络库)上,到目前为止,大多数操作系统都遵守它。但是,可以创建自己的操作系统,这很可能会违反此限制。

【讨论】:

  • 很好的解释。还有一件事,使用“SO_REUSEADDR”两个进程可以共享同一个套接字,但那是多播。如果我有新的 ServerSocket(80) 并且我为每个 accept() 跨越新线程,那么我一次服务一个客户端(即使使用非阻塞队列,我也无法同时发送数据包)。因此,单线程/多线程 Web 服务器之间唯一真正的区别在于,在第一个 HTTP 请求完成之前,单个进程不能为第二个客户端提供服务。
  • 不确定“因此,同一节点中的两个进程只有在使用不同协议时才能侦听同一端口”实际上是否正确...您可以让进程侦听端口然后分叉自己.然后你会得到两个进程在同一个端口上监听。当新连接到达时,操作系统负责决定两个进程中的哪一个将处理请求。
  • @Ioan Alexandru Cucu - 你说得对,为了顾及你的担忧,我在回复中添加了注释。感谢您提出这个问题。但是,请注意,操作系统不会从已经在侦听套接字的进程分叉(至少我不知道),而是可能分叉的应用程序。在这种情况下,程序在监听和处理父进程和/或子进程传入的数据时必须小心。
  • 值得补充的是,如果 processA 通过本地 unix 域套接字将套接字的文件描述符作为辅助消息传输给 processB,则独立进程 B 仍然具有从进程 A 接管套接字的机制(又名控制消息)使用sendmsg() 系统调用和SCM_RIGHTS。这不仅适用于套接字,而且一个进程拥有的任何文件描述符都可以转移到另一个进程,即使它不是子进程。
  • 很棒的解释。谢谢。
【解决方案2】:

首先,“端口”只是一个数字。所有“连接到端口”真正代表的是一个数据包,该数据包在其“目标端口”标头字段中指定了该编号。

现在,您的问题有两个答案,一个针对有状态协议,一个针对无状态协议。

对于无状态协议(即UDP),没有问题,因为“连接”不存在——多人可以向同一个端口发送数据包,他们的数据包将按任意顺序到达。没有人处于“连接”状态。

对于有状态协议(如 TCP),连接由 4 元组标识,该 4 元组由源端口和目标端口以及源 IP 地址和目标 IP 地址组成。因此,如果两台不同的机器连接到第三台机器上的同一个端口,就会有两个不同的连接,因为源 IP 不同。如果同一台机器(或两个在 NAT 之后或以其他方式共享相同 IP 地址)连接到一个远程端两次,则连接由源端口(通常是一个随机的高编号端口)来区分。

简单地说,如果我从我的客户端两次连接到同一个 Web 服务器,从我的角度来看,这两个连接将具有不同的源端口和来自 Web 服务器的目标端口。因此没有歧义,即使两个连接具有相同的源 IP 地址和目标 IP 地址。

端口是一种复用 IP 地址的方法,以便不同的应用程序可以侦听同一 IP 地址/协议对。除非应用程序定义了自己的高级协议,否则无法复用端口。如果使用相同协议的两个连接同时具有相同的源 IP 和目标 IP 以及相同的源端口和目标端口,则它们必须是相同的连接。

【讨论】:

  • 如果您从客户端两次连接到同一个 Web 服务器,这两个连接也将具有相同的目标端口。只有源端口不同。
  • @notacat: "和目标端口在远端"。从服务器的角度来看,连接具有不同的源端口。澄清。
  • "如果使用相同协议的两个连接具有相同的源 IP 和目标 IP 以及相同的源端口和目标端口,则它们必须是相同的连接。" - 这应该在维基百科!
  • @HelloWorld 在 NAT 场景中有两个“源端口”在起作用。原电脑设置的源端口,路由器上的外部源端口。后者是由路由器而不是主机选择的。由于在内部,每个主机都有不同的 IP 地址,因此不会发生冲突。
  • "如果使用相同协议的两个 并发 连接具有相同的源 IP 和目标 IP 以及相同的源端口和目标端口,则它们必须是相同的连接。"我相信只有当它说 concurrent 时,这句话才是正确的。客户端选择的临时端口稍后可能会被重用于与由相同 ip:port 标识的相同服务器的后续连接,从而到达相同的 4 元组,但这些将是两个不同时间点的两个不同连接.我实际上正面临这个问题,因为我正在尝试从数据包跟踪中重建 TCP 连接。
【解决方案3】:

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,这是“监听所有”的代码。一个容易犯的错误是监听地址 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 地址之间永远不会混淆。

【讨论】:

【解决方案4】:

多个客户端可以连接到服务器上的同一个端口(比如 80),因为在服务器端,在创建 socketbinding 之后(设置本地 IP 和端口) listen 在告诉操作系统接受传入连接的套接字上调用。

当客户端尝试在端口 80 上连接到服务器时,accept 调用在服务器套接字上调用。这将为尝试连接的客户端创建一个新的套接字,并且类似地,将为使用相同端口 80 的后续客户端创建一个新的套接字。

斜体字是系统调用。

参考

http://www.scs.stanford.edu/07wi-cs244b/refs/net2.pdf

【讨论】:

    【解决方案5】:

    通常,对于每个正在连接的客户端,服务器都会派生一个与客户端 (TCP) 通信的子进程。父服务器将已建立的套接字传递给子进程,该套接字与客户端进行通信。

    当您将数据从子服务器发送到套接字时,操作系统中的 TCP 堆栈会创建一个返回客户端的数据包并将“来自端口”设置为 80。

    【讨论】:

    • 所以如果一个服务器有 1,000 个同时连接(我知道这很高),它必须与 1,000 个线程抗衡!?这似乎失去了控制。或者是否使用了纤维(螺纹桶)。
    • @IanC 并非所有的网络服务器都是多线程的(Apache 带有工作模块)或多进程(Apache 带有 pre-fork 模块)。为一些非常强大的非线程 Web 服务器寻找 Lighty(以前称为 Lighttpd)和 NginX。即使在多线程环境中,您也不必必须一次处理所有传入连接。您可以使用预设最大大小的队列。
    • 既然说发回客户端的数据包是从端口 80 发出的,这是否意味着当数据通过主服务器时,它可以再次被定向到正确的子进程?
    • 所以既然说返回给客户端的数据包的头部是从80端口来的,那是不是意味着客户端程序会不断地se
    • @m1tk4,所以响应实际上来自端口 80。 ?更重要的是客户端使用 HTTP/1.1 管道,即多个“GET”在同一个套接字上。因此,即使 HTTP 是无状态的,客户端-服务器套接字/TCP 也不是,响应必须来自同一个子进程。
    猜你喜欢
    • 2011-04-13
    • 1970-01-01
    • 1970-01-01
    • 2016-12-23
    • 2019-03-15
    • 2013-05-27
    • 1970-01-01
    相关资源
    最近更新 更多