端到端连接
动机
在本节中,我们将逐步讲解当我们打开电脑、接入以太网并在网页浏览器中输入www.berkeley.edu时会发生什么。在这个过程中,我们会看到网络的各个部分是如何协同工作来处理用户请求的。
我们假设不需要从头启动互联网。例如,路由器已经在积极运行路由协议,并相应地填充了它们的转发表。

步骤 1:DHCP
我们打开电脑并接入以太网。我们还没有任何关于该网络的信息,因此我们广播一个 DHCP 请求。
我们假设家庭路由器就是 DHCP 服务器,这在家庭网络中很常见。路由器 / 服务器向我们单播一个 offer(提供)。这个 offer 包含网络信息:子网掩码、默认网关的 IP 地址以及 DNS 服务器的 IP 地址。该 offer 还会给我们一个可使用的 IP 地址。
为了完成 DHCP 协议,我们发送确认消息,表明我们希望使用所提供的配置,然后路由器 / 服务器会回复一个确认(acknowledgement)。

步骤 2:在第二层找到路由器
从 DHCP 中,我们了解到了路由器的 IP 地址,并且我们的转发表现在表明所有非本地数据包都应转发到该路由器。我们即将向 DNS 服务器(以查找www.berkeley.edu的 IP 地址)和伯克利服务器本身发送一些数据包,这两者都可能是非本地的。
不过,在我们能够将 IP 数据包转发到路由器之前,我们需要知道路由器的第二层 MAC 地址,这样我们才能在本地网络中将数据包发送到路由器。
首先,我们可以确认路由器的 IP 地址 192.168.1.1 属于本地子网 192.168.1.2/24。这告诉我们路由器在本地网络中,通过向路由器的 MAC 地址发送以太网数据包,我们就能到达路由器。
为了找到路由器的 MAC 地址,我们广播一个 ARP 请求,询问 192.168.1.1(路由器的 IP 地址)的 MAC 地址。路由器收到这个请求后会回复:“我是 192.168.1.1,我的 MAC 地址是 01:ab:cd:ef:42:01。”
我们现在可以缓存这个 IP 到 MAC 的映射,并且知道了路由器的 MAC 地址。只要这个条目在缓存中,我们就不必再进行相同的 ARP 请求。所有未来发往外部互联网的请求都可以转发到路由器的 MAC 地址。

步骤 3:DNS 查找
接下来,我们需要查找www.berkeley.edu的 IP 地址。这一切都在操作系统中完成,当浏览器代码调用getaddrinfo之类的函数时,就会触发 DNS 查找。
从 DHCP 中,我们知道了 DNS 服务器的 IP 地址是 8.8.8.8。我们还知道自己处于子网 192.168.1.2/24 中。DNS 服务器不在我们的本地网络中,因此我们需要将 DNS 数据包转发到路由器。
我们现在可以自上而下构建我们的 DNS 请求数据包。
第七层:在问题部分,我们添加一个 DNS 记录,请求www.berkeley.edu的 A 记录(IP 地址)。我们添加带有 ID、记录数量等信息的 DNS 头部。
第四层:DNS 运行在 UDP 之上。我们选择任意一个随机的源端口,因为我们是客户端。我们选择 53 端口作为目的端口,因为解析器和名称服务器在此端口监听 DNS 查询。
第三层:源 IP 是 DHCP 分配给我们自己的 IP 地址。目的 IP 是 8.8.8.8,即我们从 DHCP 中得知的 DNS 服务器的 IP 地址。
第二层:源 MAC 是我们自己的 MAC 地址,它烧录在我们的硬件中。目的 MAC 是路由器(下一跳)的 MAC 地址,我们通过 ARP 得知了该地址。
数据包完全构建好后,我们就可以沿着线路(第一层)发送这些比特了。

当数据包到达路由器时,如果网络使用 NAT(网络地址转换),路由器可能会重写 UDP/IP 头部,将我们的私有 IP 地址转换为公有 IP 地址。不过,作为终端主机,我们不必担心 NAT。路由器会完成所有转换工作,让我们感觉可以使用 DHCP 分配给我们的自己的 IP 地址。
当我们的数据包到达 8.8.8.8 的递归解析器时,如果解析器的缓存中没有我们需要的答案,它可能需要执行一些额外的查找,并向权威名称服务器请求记录。最终,递归解析器会找到答案,并将 A 记录发送回我们。我们现在就有了www.berkeley.edu的 IP 地址。

步骤 4:连接到网站
现在我们有了www.berkeley.edu的 IP 地址,就可以向伯克利服务器发送数据包了。我们使用的是网页浏览器,所以我们的目标是向该服务器发出一个 HTTP 请求。
HTTP 运行在 TCP 之上,因此我们首先必须进行 TCP 握手以与伯克利服务器建立连接。浏览器会在特定的套接字上调用connect之类的函数来建立这个连接,运行 TCP 的操作系统会执行握手,并在浏览器和服务器之间传递数据包。
TCP 握手过程如下:我们发送一个 SYN(同步)报文,伯克利服务器发送一个 SYN-ACK(同步 - 确认)报文,我们再发送一个 ACK(确认)报文。现在我们的计算机和伯克利服务器之间就有了一个字节流。

现在,我们可以自上而下构建我们的 HTTP 数据包。
第七层:HTTP 方法是 GET。我们想要的资源是/(主页)。版本是 HTTP/1.1。
第四层:HTTP 运行在 TCP 之上。浏览器可以选择任意一个源端口,因为它是客户端。通常,这个端口可以由应用程序手动指定,或者应用程序可以指定 “端口 0”,这表示请求操作系统选择一个当前未使用的随机临时端口。(顺便说一下,回想一下 NAT,允许应用程序手动指定端口就是为什么两个用户可能会选择相同的源端口。)目的端口是 80,这是 HTTP 的固定端口号。
第三层:源 IP 是 DHCP 分配给我们自己的 IP 地址。目的 IP 是 141.193.213.21,即我们 earlier 从 DNS 查询中获得的www.berkeley.edu的 IP 地址。
第二层:这与我们 earlier 的 DNS 数据包相同。源 MAC 是我们自己的(烧录在硬件中),目的 MAC 是路由器的(通过 ARP 发现并缓存)。

HTTP 响应返回状态码 200 OK,响应内容包含网站的 HTML 代码。浏览器在套接字上调用read来获取 HTTP 有效载荷的字节(包括状态码和响应内容),并进行相应处理。
在字节流中,HTTP 可以添加诸如换行符之类的分隔符来表示请求或响应的结束。此外,像 Content-Length 这样的 HTTP 头部可以指定有效载荷的长度。这也让浏览器能够分配足够的内存来接收响应。
返回的 HTTP 响应可能会触发进一步的请求。如果响应中的 HTML 包含<img src="/logo.png">这样的语法,这会告诉浏览器发出另一个 HTTP 请求来获取/logo.png资源。或者,用户可能点击网站上的链接,如www.berkeley.edu/about.html,这也会触发对同一服务器的另一个 HTTP 请求。

需要注意的是,对同一服务器的多个 HTTP 请求可以通过同一个 TCP 连接进行流水线传输以提高效率,因此我们可以保持 TCP 连接打开,并继续将其用于后续的 HTTP 请求和响应。
最终,在一些流水线传输之后,客户端或服务器选择关闭连接。会发生正常的断开连接握手,双方各自发送一个 FIN(结束)报文,并且两个 FIN 报文都会被确认。至此,所有操作完成!

需要注意的是,HTTP 请求 / 响应不一定包含在单个数据包中。HTTP 构建在 TCP 字节流之上,因此单个 HTTP 请求或响应可能会被拆分到多个 TCP/IP 数据包中,每个数据包在第 1-3 层具有相同的头部,而第 4 层头部的序列号不同。整个 HTTP 请求 / 响应只有一个头部,即使请求 / 响应被拆分到多个数据包中。对于 HTTP,请求 / 响应与数据包之间不再是一一对应的关系。
套接字
如果你是一个在浏览器中访问网站的用户,你不需要编写任何代码来通过互联网运行应用程序(HTTP)。但是,如果你是一个编写自己应用程序的程序员,你可能需要编写一些代码来与网络交互。
套接字抽象为程序员提供了一种方便的与网络交互的方式。套接字抽象完全存在于软件中,程序员可以执行五个基本操作:
我们可以创建一个新的套接字,对应一个新的连接。在像 Java 这样的面向对象语言中,这可能是一个构造函数调用。
我们可以调用connect,它会初始化与某个远程机器的 TCP 连接。如果我们是客户端 - 服务器连接中的客户端,这会很有用。
我们可以在特定端口上调用listen。这不会启动连接,但允许其他人在指定端口上与我们初始化连接。
连接打开后,我们可以调用write来在连接上发送一些字节。我们也可以调用read(它接受一个参数 N)来从连接中读取 N 个字节。
这种套接字抽象让程序员能够编写应用程序,而无需考虑 TCP、IP 或以太网等较低级别的抽象。
从操作系统的角度来看,每个套接字都与一个第四层端口号相关联。所有进出单个套接字的数据包都具有相同的端口号,操作系统可以使用该端口号进行解复用,并将数据包发送到正确的套接字。
操作系统中的层次
在硬件中,第 1 层和第 2 层在计算机的硬件网络接口卡(NIC)上实现。第 3 层和第 4 层在操作系统的网络栈中实现。第 7 层应用程序在软件中实现。将第 3 层和第 4 层放在操作系统中的好处是,应用程序不必每次都重新实现它们。
通过这种分工,应用程序只需要考虑数据。NIC 只需要考虑数据包。操作系统中的网络栈在连接和数据包之间进行转换。
查看数据包
如果你想查看在网络上发送的数据包,可以使用 tshark 和 wireshark 之类的工具。这些工具在调试代码的网络部分时很有用。
在浏览器中,你也可以使用开发者工具控制台的 “网络” 标签来查看发送和接收的数据。
如果你实际查看在网络上发送的原始数据包,你会发现一些我们在端到端讲解中没有涵盖的现实世界的复杂性。例如,数据包可能会被加密并通过 TLS 发送。此外,如果我们使用 HTTP/3.0,数据包可能会通过 QUIC(为 HTTP 优化的 UDP 变体)而不是 TCP 发送。
再谈分层
完整的端到端图景让我们看到,分层是构建网络的一个有用原则。我们能够在单个层解决特定问题,而无需同时考虑所有层。
事实上,在本课程中我们根本没有讨论过第 1 层。我们没有谈论过通过电线发送信号所需的电气工程或物理学知识。然而,我们仍然能够在第 1 层之上构建其他层,而无需确切知道第 1 层是如何工作的。
在本课程中,我们讨论了 HTTP 作为主要的第 7 层协议,但 HTTP 是一个相对简单的协议。可能有多个应用程序希望在 HTTP 之上构建相同的复杂功能,但它们不希望各自独立编写该功能的代码。为了支持这一点,我们实际上可以在 HTTP 之上构建更多的协议,这样程序员就不必总是从 HTTP 开始从头编写。
第 7 层之上的一个协议示例是远程过程调用(RPC)库。这允许程序员编写一些代码,其中一些函数实际上在网络中其他地方的另一台计算机上执行。如果每个人都必须在 HTTP 之上从头编写 RPC,那会很麻烦,因此像 Apache Thrift 和 gRPC 这样的库存在,以从程序员那里抽象出更多细节。

下面是程序员可能编写的一些网络代码示例。它编写了一个客户端向某个远程服务器打招呼。
请注意,我们讨论的所有网络协议都完全隐藏在两行对网络库的调用之后。程序员不必考虑 HTTP、TCP、IP、以太网、ARP、DHCP 或任何其他较低级别的协议。如果这些协议出现问题,了解它们仍然很有用,并且理解这些协议可以帮助你针对特定协议优化代码,但归根结底,分层是一种非常强大的抽象工具。
(注:文档部分内容可能由 AI 生成)