网址输入到页面完整显示,对于此问题,粗略的解释可以分为以下几个步骤:
- 客户端通过 HTTP 协议对数据进行一次包装
- 通过 DNS 服务器(本地无缓存)解析网址的 ip 地址
- 通过 TCP 协议与目标 IP 建立连接,二次包装的数据包在网线和路由器完成传递
- 返回结果经过浏览器的绘制和渲染,完成页面的构建
对于第三点 TCP/IP 协议是怎么来控制数据在网络中的传递一知半解,最近查阅相关网络方面的书籍才彻底搞懂其原理。
数据收发概览
应用程序发起网络请求后,数据的收发实际是委托给 Socket 库来完成,其操作大致可以分为以下4个阶段:
- 创建套接字(创建套接字阶段)
- 将管道连接到服务器端的套接字上(连接阶段)
- 收发数据(通信阶段)
- 断开管道并删除套接字(断开阶段)
下图是这四个阶段的图例:
Socket
库是一组编程接口(API),用于在不同计算机之间通过网络进行双向通信。它是由操作系统层面提供的,允许应用程序创建网络连接并收发数据。Socket
库抽象了底层的网络细节,使得程序员可以通过一组标准的函数调用来实现网络通信,而无需直接处理复杂的网络协议细节。
Socket
库提供了创建、配置和管理socket的一系列函数,其中包括 socket()
,connect()
,write()
,read()
和 close()
等方法
Socket 完成数据的收发过程是通过协议栈来完成,协议栈的内部及所处位置如下图所示
图中最上层是应用程序,它们会将收发数据等工作委派给 Socket 库来完成,Socket库包括解析器,解析器用来向DNS服务器发出查询。
第二层就是操作系统内部,协议栈位于该层。协议栈有两部分,上半部分是 TCP 和 UDP 协议,主要用来控制数据连接。下半部分是 IP 协议,控制网络包收发的操作。在互联网上传送数据时,数据会被切分成一个一个的网络包A,而将网络包发送给通信对象的操作就是由IP来负责的。此外,IP中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据IP地址查询相应的以太网MAC地址。
创建套接字
协议栈是根据套接字中记录的控制信息来工作的
调用 <描述符> = socket(<使用的ip>,<流模式>,...)
方法(伪代码)后,在协议栈内部会创建一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的IP地址、端口号、通信操作的进行状态等,可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体。在Windows中可以用netstat命令显示套接字内容:
连接服务器
创建套接字之后,应用程序(浏览器)就会调用 connect(<描述符>,<服务器的ip和端口号>)
方法(伪代码),随后协议栈会将本地的套接字与服务器的套接字进行连接。数据包在连接的过程变化如下:
- 客户端 TCP 模块:先创建一个包含表示开始数据收发操作的控制信息的 TCP 头部,同时将头部标志位的 SYN 设置为1,此外还需要设置适当的序号和窗口大小。当 TCP 头部创建好之后,TCP 模块会将数据传递给 IP 模块并委托它进行发送。
- 客户端 IP 模块:IP 模块会在数据头部再添加以太网和 IP 的头部信息,处理后的数据包会通过网线和路由器传递给服务端。数据如果丢失会重新发送。
- 服务端 IP 模块:IP 模块会将接收到的数据传递给 TCP 模块
- 服务器的 TCP 模块:根据 TCP 头部中的端口号找到该端口对应的套接字。当找到对应的套接字之后,套接字中会写入相应的信息,并将状态改为正在连接。上述操作完成后,会对将要返回的数据的 TCP 头部中设置发送方和接收方端口号,SYN 和 ACK控制位置1。接下来,服务器TCP模块会将TCP头部传递给IP模块,并委托IP模块向客户端返回响应。
下图是传输过程中数据包的结构
为了创建稳定的连接,TCP 模块会通过三次握手来完成连接,过程如下图,其中 ACK 号改为 x+1 是为了数据的分包,原理将在收发数据时进行讲解。
【扩展知识】IP和TCP协议头部格式
TCP 协议头部格式
-
源端口(Source Port)和目标端口(Destination Port):分别占用16位,表示源端口号和目的端口号;用于区别主机中的不同进程, 而IP地址是用来区分不同的主机的,源端口号和目的端口号配合上IP首部中的源IP地址和目的IP地址就能唯一的确定一个TCP连接;
-
序号(Sequence Number):占用32位,表示发送数据的顺序编号,发送方告知接收方该网络包发送的数据相当于所有发送数据的第几个字节。主要用来解决网络报乱序的问题;
-
确认号(Acknowledgment Number):占用32位,表示接收数据的顺序编号,接收方告知发送方接收方已经收到了所有数据的第几个字节,因此,确认序号应当是上次已成功收到数据字节序号加1。不过,只有当标志位中的ACK标志(下面介绍)为1时该确认序列号的字段才有效。主要用来解决不丢包的问题;
-
数据偏移(Offset):占用4位,表示数据部分的起始位置,也可以认为表示头部的长度
-
标志位(TCP Flags):占用6位,该字段中的每个比特分别表示以下通信控制含义
- URG:此标志表示TCP包的紧急指针有效,用来保证TCP连接不被中断,并且督促中间层设备要尽快处理这些数据;
- ACK:表示接收数据序号字段有效,一般表示数据已被接收方收到;
- PSH:表示通过 flush 操作发送的数据。所谓 flush 操作就是指在数据包到达接收端以后,立即传送给应用程序, 而不是在缓冲区中排队;
- RST:强制断开连接,用于异常中断的情况。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包;
- SYN:发送方和接收方相互确认序号,表示连接操作。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1, ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送 一个只有SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这 种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全 的主机将会强制要求一个连接严格的进行TCP的三次握手;
- FIN:表示断开连接
-
窗口(Window):占用16位,接收方告知发送方窗口大小(即无需等待确认可一起发送的数据量),用来进行流量控制。
-
检验和:占用16位,检验TCP数据报首部是否出现错误。
-
紧急指针:占用16位,表示应紧急处理的数据位置
IP 协议头部格式
- 版本号:占用4位,用于指示IP数据报使用的IP协议版本
- 首部长度:占用4位,IP 头部的长度。可选字段可导致头部长度变化,因此这里需要指定头部的长度
- 服务类型:占用8位,在最初这个字段有一部分用于定义数据包的优先级,剩下的一部分定义了服务类型。IETF已经改变了这个8位字段的解释,现在定义了一组区分服务。在这种解释中,前6位构成了码点(codepoint),最后两位未使用。当码点字段最右边的3位不全为0时,这6位定义了54种服务(低延时,高吞吐量等等)。
- 总长度:占用16位,定义了数据报总长度,其以字节为单位。故IPv4数据报总长度上限值位65536字节。注:为什么需要这个字段?在许多情况下,我们确实不需要这个字段值。但是有些情况下,封装在一个帧里的并不仅仅是数据报,还可能附加了一些填充。比如,以太网协议对帧的数据有最大值(1500字节)和最小值(46字节)的限制,当数据小于46字节时,数据将含有填充数据。
- 标识(identification):占用16位,用于识别包的编号,一般为包的序列号。如果一个包被 IP 分片,则所有分片都拥有相同的 ID。
- 标志(flag):占用3位,第一位保留(未用),剩下两位分别代表是否允许分片,以及当前包是否为分片包
- 分片偏移:占用13位,表示当前包的内容为整个 IP 消息的第几个字节开始的内容
- 生存时间:占用8位,表示包的生存时间,这是为了避免网络出现回环时一个包永远在网络中打转。每经过一个路由器,这个值就会减 1,减到 0 时这个包就会被丢弃
- 协议号:占用8位,定义了使用IPv4服务的高层协议,如TCP,UDP,ICMP,IGMP,OSPF等的数据都将被封装到IP数据报中。这个字段指明数据报必须交付给哪个最终目的协议。
- 检验和:占用16位,用于检查错误,现在已不使用。
- 源地址:占用32位,网络包发送方的 IP 地址
- 目的地址:占用32位,网络包接收方的 IP 地址
收发数据
当控制流程从 connect
回到应用程序之后,接下来就进入数据收发阶段了。数据收发操作是从应用程序调用 write(<描述符>,<发送数据>,<数据长度>)
(伪代码)开始的,该方法执行后应用程序将数据交给协议栈,协议栈收到数据后执行发送操作。
协议栈并不是一收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,等到数据积累到一定量时再发送出去。至于要积累多少数据才能发送,则需要根据以下两个因素来判断:
- 一个网络包能容纳的数据长度(MTU),以太网中一般为 1500 字节。(补充:除去头部之后,一个网络包所能容纳的 TCP 数据的最大长度,称为MSS)
- 等待时间,当等待时间到达后,即便缓冲区中的数据长度没有达到MSS,也应该果断发送出去
当需要发送的数据(博客或论坛的长文)超过了 MSS 时,缓冲区中的数据会被以 MSS 长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络包中。当判断需要发送这些数据时,就在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号,然后交给IP模块来执行发送数据的操作。
在数据发送过程中传输的可靠性和流量控制是两个需要重点理解的点,以上两点都是通过 TCP 协议来控制的。
TCP 可靠传输的实现
使用 ACK 号确认网络包已收到
单向数据传输
当发送方数据超过 MSS 时,会将数据拆分成多个网络包。发送方将包的序号和长度发送给接收方,接收方拿到包后将值+1作为 ACK 号(如果序号为2,长度为1460,则 ACK 号为 1462,公式为:序号+长度 -1 + 1)发送给发送方。在实际的通信中,为了防止信号被截取破译,序号并不是从1开始的,而是需要用随机数计算出一个初始值。
通过序号,长度,ACK 号的机制既能知道网络包对方是否有接受到,也能知道数据包是否有遗漏
双向数据传输
了解了单向数据传输后,我们来总结下双向数据传递完整的工作原理。
连接操作(三次握手):
- 第一次握手:客户端在连接时需要计算出与从客户端到服务器方向通信相关的序号初始值,并将这个值发送给服务器
- 第二次握手:服务器会通过这个初始值计算出ACK号并返回给客户端。客户端传递过来的初始值有可能在通信过程中丢失,因此当服务器收到初始值后需要返回ACK号作为确认。同时,服务器也需要计算出与从服务器到客户端方向通信相关的序号初始值,并将这个值发送给客户端
- 第三次握手:客户端也需要根据服务器发来的初始值计算出ACK号并返回给服务器
收发操作:数据先由客户端向服务器发送请求,序号也会跟随数据一起发送(右图④)。然后,服务器收到数据后再返回ACK号(右图⑤)。从服务器向客户端发送数据的过程则正好相反(右图⑥⑦)
TCP 的流量控制
TCP 的流量控制就是控制网络包发送的等待时间,根据接收方返回的 ACK 号动态调整发送速率,保证接收方来得及接收或者更快的接收。
利用滑动窗口实现 TCP 流量控制
TCP 通过 ACK 号能实现可靠的数据传递,但在等待 ACK 号的这段时间中,如果什么都不做那实在太浪费了。为了减少这样的浪费,TCP 采用滑动窗口方式来管理数据发送和 ACK 号的操作。
所谓滑动窗口,就是在发送一个包之后,不等待ACK号返回,而是直接发送后续的一系列包。这样一来,等待ACK号的这段时间就被有效利用起来了。
当使用滑动窗口方式后,接收端会连续收到数据包。接收方的 TCP 收到包后,会先数据包存放到接收缓冲区中,但当包的发送速度 > 服务器的处理速度,缓冲区会溢出导致后续到达的包无法接受而丢失。为了避免这种情况发生,接收方需要告诉发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口方式的基本思路。
关于滑动窗口的具体工作方式,通过下图更容易理解。
在这张图中,接收方将数据暂存到接收缓冲区中并执行接收操作。当接收操作完成后,接收缓冲区中的空间会被释放出来,也就可以接收更多的数据了,这时接收方会通过TCP头部中的窗口字段将自己能接收的数据量告知发送方。这样一来,发送方就不会发送过多的数据,导致超出接收方的处理能力了。
对于数据的收取,应用程序会调用 read(<描述符>,<接收缓冲区>,...)
(伪代码)来获取响应消息。协议栈会尝试从缓存中取出数据并传递给应用程序,如果此时缓存中没有数据,协议栈会将应用程序的委托挂起。当服务端返回数据后,协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序。
断开连接
在断开连接阶段,主要完成两件事:断开 TCP 连接和删除套接字。
TCP 连接的断开可以是服务端也可以是客户端,分为以下四个步骤(四次挥手):
- 第一次挥手:服务器(可以使客户端,也可以是服务器端)将 TCP 头部控制位中的FIN比特设为1,协议栈会委托IP 模块向客户端发送数据 。同时,服务器的套接字中也会记录下断开操作的相关信息。
- 第二次挥手:当收到服务器发来的 FIN 为1的 TCP 头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态。然后,为了告知服务器已收到 FIN 为1的包,客户端会向服务器返回一个 ACK 号。上述操作完成后,协议栈就可以等待应用程序来取数据了。客户端应用程序收取数据完成后会调用
close(<描述符>)
来结束数据收发操作。 - 第三次挥手:客户端的协议栈也会和服务器一样,生成一个FIN比特为1的 TCP 包,然后委托IP模块发送给服务器。
- 第四次挥手:服务器接到数据包后返回ACK号
和服务器的通信结束之后,用来通信的套接字也就不会再使用了,这时我们就可以删除这个套接字了。不过,套接字并不会立即被删除,而是会等待一段时间之后再被删除。
参考:《网络是怎么连接的》第二章 用电信号传输TCP/IP数据