目录
2. 第二章 用电信号传输TCP/IP数据——探索协议栈和网卡
2. 第二章 用电信号传输TCP/IP数据——探索协议栈和网卡
2.1 创建套接字
2.1.1 协议栈的内部结构
如图1就是从上到下就是一个网络包要在电脑上经过的流程,应用程序 -> Socket库 -> 协议栈(TCP、UDP、IP)-> 网卡驱动 -> 网卡,数据包从网卡出去后就到了交换机或者路由器等设备。接下来的章节就会从上到下一一介绍各个环节。
图1
2.1.2 套接字的实体就是通信控制信息
书中原话:“在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的 IP 地址、端口号、通信 操作的进行状态等。本来套接字就只是一个概念而已,并不存在实体,如果一定要赋予它一个实体,我们可以说这些控制信息就是套接字的实体,或者说存放控制信息的内存空间就是套接字的实体。协议栈在执行操作时需要参阅这些控制信息 5。例如,在发送数据时,需要看一看套接字中的通信对象 IP 地址和端口号,以便向指定的 IP 地址和端口发送数据。在发送数据之后,协议栈需要等待对方返回收到数据的响应信息,但数据也可能在中途丢失,永远也等不到对方的响应。在这样的情况下,我们不能一直等下去,需要在等待一 定时间之后重新发送丢失的数据,这就需要协议栈能够知道执行发送数据操作后过了多长时间。为此,套接字中必须要记录是否已经收到响应,以及发送数据后经过了多长时间,才能根据这些信息按照需要执行重发操作。”
在电脑终端可以使用“netstat”命令查看套接字。
图2
2.1.3 调用Socket时的操作
如图3就是调用Socket时的操作流程,其实和第一章的有个图很像,也是创建、连接、发送、断开这几步。接下来几个小节就是介绍下面的连接、收发、断开等操作。这一小节大概介绍了下第一步的准备阶段。
首先应用程序调用Socket申请创建套接字,协议栈会根据应用程序的申请执行创建套接字的操作,这时协议栈首先会分配一个套接字所需的内存空间。然后,需要将表示这个套接字的描述符告知应用程序。然后,应用程序向协议栈进行收发数据委托时就需要提供这个描述符,因为套接字中记录了通信双方的信息以及通信处于怎样的状态,所以只要通过描述符确定了相应的套接字,协议栈就能够获取所有相关信息,这样应用程序就不用每次都告诉协议栈应该和谁通信了。
图3
2.2 连接服务器
2.2.1 连接是什么意思
这里作者花了一些篇幅来讲“连接”是什么意思,其实说白了就是TCP三次握手。
但是在三次握手之前有一步是,套接字刚创建完,里面并没有任何数据,应用程序不知道该和谁通信,以浏览器为例,它可以通过DNS解析得知通信对象的IP,并且浏览器默认端口是80,这样就把这两个信息写入套接字,客户端就知道了通信对象的地址。但是同时服务器也会创建套接字,它也需要知道通信对象的地址,但是它又没办法主动知道,这个时候就该TCP三次握手了,由客户端去和服务器说它即将发数据包到服务器,三次握手的过程在2.2.3节。
2.2.2 负责保存控制信息的头部
控制信息分为两类:
第一类是客户端和服务器通信时交换的控制信息,其实就是各种头部字段,比如TCP头部(图4)、UDP头部、IP头部等等。各个协议的头部在通信过程中起着非常巨大的作用,没有这些头部就无法正常通信。
第二类就是图2中所示的那些信息,保存在套接字中,用来控制协议栈操作的信息。
图4
2.2.3 连接操作的实际过程
这一节其实就是对三次握手的流程大概介绍。
- 第一步应用程序调用Socket库的connect函数,需要提供描述符和服务器的IP地址、端口号。
- 第二步就是客户端的TCP模块工作,它会与服务器的TCP模块交换控制信息。客户端首先会创建一个包含表示开始数据收发操作的控制信息的头部,主要是发送方和接收方的端口号,然后就找到了服务器的套接字,然后将控制位的SYN设置为1,表示连接。
- 第三步TCP模块将信息传递给IP模块并委托它进行发送。然后服务器的IP模块收到数据后传给服务器的TCP模块,根据TCP头部找到端口号对应的套接字,然后写入对应的数据到缓冲区,并将套接字的状态改为正在连接。然后TCP模块返回响应,将头部中的ACK设为1,表示已接收到相应的网络包。然后传给IP模块,IP模块再传回给客户端。
- 第四步客户端收到数据包后,通过IP模块到达TCP模块,并通过头部SYN是否为1确认服务器是否收到数据包,如果收到了就向套接字写入服务器的IP地址、端口号等,同时将状态改为连接完毕。然后客户端还需要发送一个ACK为1的包发回服务器,告诉服务器刚才的确认包已收到。服务器收到这个包后连接操作才算完成。
- 至此连接操作完毕。
其实第一章中的图9那根管道其实指的就是TCP建立起来的连接,作者画了一根管子来形象表达了一下。TCP三次握手具体的各个字段的传值大家可以网上看,已经有很多文章了,这里主要是想让大家了解下流程。
2.3 收发数据
2.3.1 将Http请求消息交给协议栈
这时三次握手结束,建立起了管道,双方就可以收发数据包了。这时就又是协议栈开始工作了,但是协议栈并不是拿到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区中,并等待应用程序的下一段数据,但是具体逻辑是由应用程序自行决定的,有的可能要马上发出,有的可能要等待数据包到一定大小再发出,具体由两个指标来判断。
第一个是每个网络包能容纳的数据长度。协议栈根据MTU来计算,MTU表示一个网络包的最大长度,在以太网中一般是1500字节,MTU是包含头部的字端长度,需要减去头部得到纯数据长度即就为所能容纳的最大数据长度,这一长度叫MSS。见图5。
图5
第二个是时间。当应用程序发送数据的频率不高时,如果每次等到长度接近MSS时再发送,那么可能会造成发送缓慢,这种情况下,需要在即使没达到MSS长度时也要发出去。为此,协议栈内部有一个计时器,当经过一定时间之后就会把网络包发送出去。
但其实这两者是相互矛盾的,如果长度优先,那么会发送缓慢,如果时间优先,那么每次传输的数据内容又很少,造成效率低效。所以这个取舍不在协议栈里做,一般交由应用程序或者操作系统来判断,像浏览器这种需要及时向服务器发送请求的肯定就是采用的时间优先。
2.3.2 对较大的数据进行拆分
这个就是当一个数据包的数据量太过大时,需要将一个大的数据包拆分成多个小包。当发送缓冲区的数据超过超过MSS的长度时,就不等待后面的数据了,然后就将这些数据放到一个包里,后面的数据放到另一个数据包里,然后都加上同样的TCP头部,这样发往的服务器就是同一个。如图6。
图6
2.3.3 使用ACK号确认网络包已收到
这里就是像三次握手里一样使用序号和ACK号来确保对方收到数据包,比如我现在客户端要传第1-1000字节的数据,那么序号就为1,数据长度为1000,然后发到接收方,然后接收方收到后将ACK设置为1000,表示接收方已经接收到了第1000字节的数据,然后发送方再从序号为1001的数据又开始发,这样周而复始,直到所有数据接收完毕。
接下来这一段就放书中原话了,就是讲TCP数据包双方收发的过程:
首先,客户端在连接时需要计算出与从客户端到服务器方向通信相关的序号初始值,并将这个值发送给服务器(图 2.9 ①)。接下来,服务器会通过这个初始值计算出 ACK 号并返回给客户端(图 2.9 ②)。初始值有可能在通信过程中丢失,因此当服务器收到初始值后需要返回 ACK 号作为确认。同时,服务器也需要计算出与从服务器到客户端方向通信相关的序号初始值,并将这个值发送给客户端(图 2.9 ②)。接下来像刚才一样,客户端也需要根据服务器发来的初始值计算出 ACK 号并返回给服务器(图 2.9 ③)。到这里,序号和 ACK 号都已经准备完成了,接下来就可以进入数据收发阶段了。数据收发操作本身是可以双向同时进行的,但 Web 中是先由客户端向服务器发送请求,序号也会跟随数据一起发送(图 2.9 ④)。然后,服务器收到数据后再返回ACK 号(图 2.9 ⑤)。从服务器向客户端发送数据的过程则正好相反 (图 2.9 ⑥⑦)。
图7
有一个强大的机制是,TCP会在收到ACK号之前发过的包放在发送缓冲区里,如果对方没有返回对应的ACK号,就会重新发送这些包。这样一来,不管因为什么原因在中途包丢了,TCP都会有这个补救措施。有了这一补救措施,就不需要在其他地方对错误进行补救了,所以网卡、集线器、路由器等都没有错误补救机制,一旦检测错误就丢弃对应的包,TCP会进行补救。
ps:以前学计网有个题目就是TCP为什么是三次握手,不是两次?原因就是第三次是服务器发送ACK包到客户端,如果不传这个ACK,客户端就会一直重新进行第二次握手,就会一直发第二个包,持续的发送会导致服务器的带宽被占用,严重点直接给服务器干挂。但是这本书中又说TCP会在重传几次无效之后会强制结束通信并向应用程序报错。哪边才是对的呢,个人倾向于作者这个理解,咋可能会一直重发重发,如果一直重发,隔三差五就丢个ACK包,全世界的网都给干慢了。
2.3.4 根据网络包平均往返时间调整ACK号等待时间
实际上的TCP错误补偿机制是非常复杂的,在何时重传包才更合适呢?一个关键的点是返回ACK号的等待时间,叫做超时时间。当网络繁忙时,包传输可能很慢,如果TCP已经重传了一个包,这时上一个ACK包才姗姗来迟,就会造成重传,让本来就繁忙的网络雪上加霜。那是不是超时时间越长越好?也不是,等待时间长的话,也会造成网络延迟。
因为网络快慢的原因,所以讲等待时间设置为一个固定值并不是一个好方法。TCP采用的方法是动态调整等待时间。这个等待时间是根据ACK包返回所需的时间来判断的。具体来说,TCP会在发送数据的过程中持续测量ACK包的返回时间,如果ACK包返回变慢,就将超时时间设置长一点;相反如果ACK包返回很快,就将超时时间设置短一点。
2.3.5 使用窗口有效管理ACK号
其实TCP在数据收发过程中并不是老老实实的等待上一个数据包返回ACK后再发下一个数据包,这中一来一回的方式太浪费性能了,因为始终只有一方在同时工作。所以TCP采用了滑动窗口的方式。
当接收方接收到包后,会先将数据存到接收缓冲区中,然后,接收方需要计算ACK号,将数据组装起来还原成原本的数据并传递给应用程序,如果这些操作还没完成下一个包就来了,下一个包也会被暂存到接收缓冲区中。但是如果到达的速率并操作速度快很多,那么接收缓冲区迟早会溢出,后面的包就无法被接收了。所以TCP用这个方法来处理:接收方需要告诉发送方自己最多能存放多少数据,然后发送方根据这个值对数据发送操作进行控制,这就是滑动窗口的基本思路。见下面两张图。
图8
图9
2.3.6 ACK与窗口的合并
这一小节讲的就是发送ACK包和发送窗口大小更新的包有没有可能放在一起发送,因为如果拆成两个包去发送,这两个也是在网络中传输,也会占用网络,所以如果能放在一起发送就尽量一起发送。那么事实上是有可能的。
在接收方发送ACK包或窗口更新时,并不会马上把包发送出去,而是等待一段时间,在这个过程中很可能会出现其他的通知操作,这样就可以把两种通知合并在一个包里一起发送。举个例子,在等待发送ACK包时正好需要发送更新窗口的包,那么就合并一起发送。再有当需要连续发送多个ACK号时,也可以减少包的数量,因为ACK号表示已收到的数据了,它是告诉发送方最后的已接收的数据在哪里,所以这几个ACK包只需要返回最后一个ACK号就可以了,就只用发送一个包。
2.3.7 接收Http响应消息
这一小节的操作其实应该是第六章的内容,作者在这里简单介绍了一下。
2.4 从服务器断开并删除套接字
2.4.1 数据发送完毕后断开连接
完成数据发送的一方会发起断开操作。以服务器发器断开为例。首先,服务器的应用程序调用Socket库中的close函数。然后,服务器的协议栈会生成包含断开信息的TCP头部,具体来说就是将FIN字端设置为1,然后由IP协议发送出去。
然后客户端收到这个断开包后,客户端的协议栈会将自己的套接字标记为进入断开1操作状态,然后向服务器发送一个ACK包,表示收到了服务器的断开包。然后,等应用程序使用Socket中的read函数读完所有数据后,应用程序会调用close函数来结束数据收发操作。这时客户端就会发送一个FIN为1的TCP包给服务器。然后服务器收到包后又返回ACK包,至此,数据收发就全部结束。
其实,看到这里,上面执行的操作很熟悉把,就是TCP四次挥手。
2.4.2 删除套接字
收发操作结束套接字也就不需要了,但是并不会立即删除套接字,而是会等待几分钟才会删除。作者举了一个例子:
如果最后客户端返回的 ACK 号丢失了,结果会如何呢?这时,服务器没有接收到 ACK 号,可能会重发一次 FIN。如果这时客户端的套接字已经删除了,会发生什么事呢?套接字被删除,那么套接字中保存的控制信息也就跟着消失了,套接字对应的端口号就会被释放出来。这时,如果别的应用程序要创建套接字,新套接字碰巧又被分配了同一个端口号 48,而服务器重发的 FIN 正好到达,会怎么样呢?本来这个 FIN 是要发给刚刚删除的那个套接字的,但新套接字具有相同的端口号,于是这个 FIN 就会错误地跑到新套接字里面,新套接字就开始执行断开操作了。之所以不马上删除套接字,就是为了防止这样的误操作。
但是具体等待多久再删,TCP没有明确规定,一般是等待几分钟后再删除,应该是由操作系统来决定的。
2.4.3 数据收发操作小结
这一小节就是对从2.1 创建一直到2.4.2 删除套接字的小总结。其实具体看下图就可以。
图10
剩余第二章的内容就放到下一篇来说,这一篇刚好其实就可以看作学习整个TCP的流程,包括其中的一些优化操作。下一篇就是讲IP协议的知识以及UDP协议的知识。