socket API中的accept()函数是如何工作的?

144

套接字API是TCP/IP和UDP/IP通信(也就是我们所知道的网络编程)的事实标准。然而,它的核心函数之一accept()有点神奇。

借用一个半正式的定义:

accept()用于服务器端。 它接受了一个收到的进入尝试, 从远程客户端创建一个新的TCP连接, 并创建与该连接的套接字地址对关联的新套接字。

换句话说,accept通过一个新的套接字返回给服务器,服务器可以通过该套接字与新连接的客户端进行通信。旧的套接字(在其上调用accept)保持打开状态,在同一端口上监听新的连接。

accept如何工作?如何实现?这个问题存在很多混淆。许多人声称accept会打开一个新的端口并通过它与客户端进行通信。但这显然不是真的,因为没有打开新的端口。你实际上可以通过同一个端口与不同的客户端进行通信,但是怎样做呢?当几个线程在同一个端口上调用recv时,数据如何知道去哪里?

我猜这是客户端的地址与套接字描述符相关联,每当数据通过recv传输时,它就会被路由到正确的套接字,但我不确定。

深入了解这个机制的内部工作原理将是很好的。


3
对于每个客户端请求,服务器端都会打开一个全新的套接字连接。服务器必须始终保持在80端口开放以侦听传入的调用。如果它收到一个调用,它将立即创建一个带有以下四元组的新套接字,这将在客户端和服务器之间建立TCP连接。我的理解正确吗? - brain storm
3
这是一个非常基础的问题,最近在面试中我被问到了:https://dev59.com/U2Af5IYBdhLWcg3wOQq2。如果您对此有任何评论,请发表。 - brain storm
只有当您完全忽略HTTP keep-alive的存在时,才会出现这种情况。 - user207421
4个回答

163
你的困惑在于认为套接字是由服务器IP:服务器端口标识的,实际上,套接字通过四元组信息来唯一标识:客户端IP:客户端端口服务器IP:服务器端口。因此,虽然服务器的IP和端口在所有已接受的连接中都是恒定的,但客户端的信息才是用于跟踪所有数据传输的重要依据。
以下示例可以更好地解释这个问题:
假设我们有一个位于192.168.1.1:80的服务器和两个客户端10.0.0.110.0.0.210.0.0.1打开本地端口1234并连接到服务器,现在服务器有一个套接字,其标识如下:
10.0.0.1:1234 - 192.168.1.1:80  

现在10.0.0.2在本地端口5678上打开一个连接并连接到服务器。现在服务器有两个如下所示的套接字:

10.0.0.1:1234 - 192.168.1.1:80  
10.0.0.2:5678 - 192.168.1.1:80

5
我不知道具体的实现细节(可能因平台而异),我只知道套接字从概念上说是由我描述的四个信息来识别的。 - 17 of 26
4
你有这方面的参考资料吗? - qeek
6
如果使用了NAT,而同一网络中的两个客户端试图在连接服务器时使用相同的本地端口会发生什么呢?例如,如果10.0.0.1和10.0.0.2都连接到具有外部IP地址192.168.0.1的路由器,则位于192.168.1.1的服务器将从192.168.0.1看到两个连接。如果由于随机数生成器的偶然情况,10.0.0.1和10.0.0.2都选择了相同的本地端口,那么会发生什么? - aroth
9
路由器中的NAT支持会处理相关细节。实际上,网络流量将通过两个连接进行传输——客户端到路由器,以及路由器到服务器。路由器会在两个不同的端口192.168.0.1:1234和192.168.0.1:5678上建立出去的连接。然后路由器将传入的流量重定向到正确的客户端。 - 17 of 26
7
如果一个套接字由四元组识别,那么一个监听套接字的四元组信息是什么? - Eric Zheng
显示剩余8条评论

87

补充一下用户“17 of 26”所提供的答案:

Socket 实际上由 5 元组组成 - (源IP、源端口、目标IP、目标端口、协议)。其中协议可以是 TCP 或 UDP 或任何传输层协议。该协议在 IP 数据报的“协议”字段中被识别。

因此,在服务器上有两个不同的应用程序与完全相同的 4 元组上的同一个客户端通信,但协议字段不同,例如:

Apache 在服务器端使用 (server1.com:880-client1:1234,采用TCP),

魔兽世界在服务器端使用 (server1.com:880-client1:1234,采用UDP)

无论哪种情况,客户端和服务器都将处理此问题,因为 IP 数据包中的协议字段不同,即使所有其他 4 个字段都相同。


18

在我学习这个的时候,令我困惑的是术语socketport给人的印象是它们是物理实体,而实际上它们只是内核用来抽象网络细节的数据结构。

因此,这些数据结构被实现为能够区分来自不同客户端的连接。至于它们是如何实现的,答案要么是a.) 它并不重要,套接字API的目的恰恰是让实现不重要; 或者b.)看一下。除了强烈推荐的Stevens系列书籍提供了一个详细的描述,可以查看Linux、Solaris或其中一个BSD的源代码。


是的,大多数网络术语只是为某些位集合分配名称以及基于它们的值所做出的决策(“协议标识符”,“路由”,“绑定”,“套接字”等)。您网络适配器的硬件设计用于接收一系列位。与计算机上的程序相关的是驱动程序和操作系统决定的。如果我们愿意,明天就可以摆脱所有这些术语,但提供一系列位流的原则似乎是根本的... - masterxilo

-1

正如其他人所说,套接字由4元组(客户端IP、客户端端口、服务器IP、服务器端口)唯一标识。

运行在服务器IP上的服务器进程维护一个活动套接字数据库(这意味着我不关心它使用什么样的表/列表/树/数组/魔术数据结构),并监听服务器端口。当它通过服务器的TCP/IP堆栈接收到消息时,它会检查客户端IP和端口是否与数据库匹配。如果在数据库条目中找到了客户端IP和客户端端口,则将消息移交给现有处理程序;否则,创建一个新的数据库条目,并生成一个新的处理程序来处理该套接字。

在ARPAnet的早期,某些协议(例如FTP)会监听指定的端口以获取连接请求,并回复一个移交端口。该连接的进一步通信将通过移交端口进行。这是为了提高每个数据包的性能:在那些日子里,计算机速度慢了几个数量级。


你能详细说明一下“交接口”部分吗? - Eli Bendersky
1
这可能是某些TCP协议的描述,或者过于简化了。客户端尝试连接到一个监听套接字时,会发送一个特殊的数据包来建立连接(设置SYN位)。在创建新套接字的数据包和使用现有套接字的数据包之间有明显的区别。 - John M
...发送一个特殊的数据包来建立连接(设置SYN位)。根据我的理解,这会导致协议栈将其传递给“接收者”(如果有的话),这就是为什么每个地址/端口/协议组合只能有一个监听端口的原因。我不确定这是否在规范中,还是仅仅是实现约定。 - Peter Wone
1
第二段描述TCP层或服务器进程内部发生的事情都不正确。服务器进程不需要维护任何类型的套接字数据结构,也不需要检查传入的IP:端口对与任何东西进行匹配。这就是套接字的作用。FTP使用一个单独的端口用于数据,而不是用于所有“进一步通信”,这是为了简化协议,而不是出于性能原因。在任何方面都不会提高性能的情况下使用新端口。 - user207421
维护一个数据库(意思是我不关心它使用什么样的表/列表/树/数组/神奇的数据结构)。我通常称之为“表”(或者可能是“图”或“决策树”)。 “数据库”对我来说暗示了一些实现。 - masterxilo

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