协议栈

Socket库会去委托协议栈来发出请求,协议栈中包含TCP\UDP模块和IP模块。TCP模块会
接受委托包装数据添加TCP控制信息等头部信息
。之后交给IP模块发送,添加好IP头部后发出消息。接着服务器端IP模块收到交给TCP模块再解析出数据。
TCP模块
TCP头部:

TCP头部信息中端口和控制位很重要
TCP在发送请求时会做
- 创建一个包含表示开始数据收发操作的控制信息的头部
- 将头部中的控制位的 SYN 比特设置为 1,表示连接
- 设置适当的序号和窗口大小
TCP在接收请求时会做
- 根据 TCP 头部中的信息找到端口号对应的套接字
- 找到后套接字中会写入相应的信息,并将状态改为正在连接
- TCP 模块会返回响应,这个过程和客户端一样,需要在 TCP 头部中设置发送方和接收方端口号以及 SYN 比特,时还需要将 ACK 控制位设为1
- 客户端收到响应后一样,同样会给套接字中写入信息,并且改状态为连接完毕。
- 再次发送请求给服务端。将 ACK 比特设置为 1 并发回服务器,告知已经收到之前的包了,服务端再次收到,连接过程才算完成。

以上其实就是tcp三次握手的过程
数据收发
TCP连接建立后,数据并不会立即发送出去,而是先放在缓冲区中等待一段时间后发送,这样是因为应用程序有的是一起发送的,有的是分段发送的。
MTU: 一个网络包的最大长度,以太网中一般为 1500 字节
MSS: 除去头部之后,一个网络包所能容纳的 TCP 数据的最大长度
TCP粘包
如果到达缓冲区的数据大部分都是小包(远小于MSS),这是为了提高网络效率,就需要将多个包粘在一起,合并成一个接近MSS但不超过的容量发送出去,这样虽然提高了效率但是却增加了延迟时间,所以可以通过设置不经过缓冲直接发送,浏览器便设置了此选项。
tip: 如果一直都在等待小包合并却仍然远小于MSS,这时延迟就很高了。所以协议栈内部有一个定时器,如果到达时间还没有发送则忽略容量直接发送
TCP拆包
如果到达缓冲区的数据超过了MSS,这时就需要对数据包按照MSS大小进行拆分。并在每一块数据前面加上 TCP 头部,并根据套接字中记录的控制信息标记发送方和接收方的端口号。然后交给IP模块发送。

根据序号、ACK确认是否需要重发
当客户端进行拆包时,会先算好每一块数据相当于从头开始的第几个字节,接下来在发送这一块数据时,将算好的字节数写在 TCP 头部中(seq:序号),发送的数据长度服务端可以用整包的大小减去头部的长度就可以算出,所以不需要提供。
通过这些信息,就可以检查收到的包是否有遗漏,假设上次接收到第 1460 字节,那么接下来如果收到序号为 1461 的包,说明中间没有遗漏;但如果收到的包序号为 2921,那就说明中间有包遗漏了。
如果没有遗漏,服务端会将到目前为止接收到的数据长度加起来,计算出一共已经收到了多少个字节,然后将这个数值写入 TCP头部的 ACK 号中发送给发送方,告诉客户端现在一共收到多少字节的包。

包的起始序号不一定是从1开始的。一般是随机的,同时通信时双向的,有可能客户端给服务端发送包时,服务端也在给客户端发送包,所以双方在三次握手时都需要互相告知自己的起始序号

所有的包在得到对方确认之前都在缓冲区中,如果对方没有返回某些包中的ACK号。那么就需要重发。之后的网卡、集线器、路由器都没有错误补偿机制。都是利用TCP来进行错误补偿。但是如果尝试多次仍然无效则会结束通信并报错。
image.png
ACK返回时间
如果网络繁忙时,ACK返回会变慢,这样就必须设置等待时间长一点。不让会重发包。TCP 会在发送数据的过程中持续测量 ACK 号的返回时间,如果 ACK 号返回变慢,则相应延长等待时间;相对地,如果 ACK 号马上就能返回,则相应缩短等待时间
窗口机制
如果在等待服务端返回ACK的期间什么都不做,那么就比较浪费了。所以TCP采用滑动窗口机制来管理发送和ACK号的操作,就是不需要等待ACK返回,直接发送下一个包,这也是提高TCP效率的方式
。
这样带来的问题是:发送方有可能发送的太快超过了接收方处理的速度,接收方缓冲区有可能溢出
解决办法:就是接收方会在收到包后,会通过 TCP 头部中的窗口字段将自己能接收的数据量告知发送方。这样一来,发送方就不会发送过多的数据,导致超出接收方的处理能力了。接收方处理程序每次从缓冲区取走包时都需要发送一个剩下的窗口大小,而且这个请求不会立即发送,会稍等一下看看有没有需要返回的ACK信号一起合并发送,提高效率。所以提高窗口大小是TCP调优的方式之一

只有确认了前面的窗口,窗口才会后移

如果前面的响应没有收到,窗口不后移,所有都会重传,也可以设置选择重传

下图中只展示发送方到接收方,但是接收方也可以当发送方,过程刚好相反,所以没有画出

避免拥塞控制
上面的窗口是为了控制流量的,但是为了防止过多的数据注入到网络中,让网络中的路由器或者链路不至于过载。会有拥塞控制机制
有慢启动和拥塞避免两种算法
慢启动:不是一次全部发送,而是根据网络状况动态的缓慢增加。慢启动只是起点低,增长的并不是很慢,1-2-4-8-16。。。
拥塞避免:刚开始发送使用慢启动算法,但是会设定一个阈值,等发送超出阈值后,呈加法增加而不是倍数,1-2-4-8
-9-10-11。。。
接受响应消息
浏览器在委托协议栈发送请求消息之后,会调用 read 程序来获取响应消息。协议栈会将数据暂存到接收缓冲区中,但这个时候请求消息刚刚发送出去,响应消息可能还没返回,协议栈会将应用程序的委托的工作暂时挂起 ,等服务器返回的响应消息到达之后再继续执行接收操作
接着协议栈会检查收到的数据块和 TCP 头部的内容,判断是否有数据丢失,如果没有问题则返回 ACK 号。然后,协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序。具体来说,协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序。将数据交给应用程序之后,协议栈还需要找到合适的时机向发送方发送窗口更新。
数据发送完毕 -- 断开
等到数据全部接受完毕,会发起断开的过程,这个行为有可能是客户端或者服务端任何一方发起
,http1.0中客户端发出请求接受到响应,这个过程就结束了,服务端会主动发起断开请求,但是在http1.1中有可能一个tcp连接会发送多个http请求,一个请求收到响应后会等待是否还有请求发出,如果没有了客户端会主动发起断开。
以服务端发起为例,服务端先调用Socket中的close程序,服务器的协议栈会生成包含断开信息的 TCP 头部,具体来说就是将控制位中的 FIN 比特设为 1。接下来,协议栈会委托 IP 模块向客户端发送数据,同时,服务器的套接字中也会记录下断开操作的相关信息。
接下来轮到客户端了。当收到服务器发来的 FIN 为 1 的 TCP 头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态。然后,为了告知服务器已收到 FIN 为 1 的包,客户端会向服务器返回一个 ACK 号。之后协议栈就可以等待应用程序来取数据了。
应用程序就会调用 read 来读取数据后,协议栈会告知知应用程序(浏览器)来自服务器的数据已经全部收到了,这是客户端应用程序就会调用close来断开连接了。
客户端的协议栈也会和服务器一样,生成一个 FIN 比特为 1 的 TCP 包,然后委托 IP 模块发送给服务器,一段时间之后,服务器就会返回ACK 号
这就是TCP的四次挥手

为什么TCP连接需要三次握手,而断开需要四次?
答:因为断开不是实时的,服务端发起断开,客户端有可能还需要等到数据处理完毕。而握手期间,SYN连接和ACK确认信号可以一起返回给客户端。所以只需要三次
删除套接字
断开连接之后要做的就是删除套接字了。但是这个删除操作不是立即的。而是等待一段时间。避免误操作。比如最后断开信号的ACK信号服务端没有收到,但是这是客户端套接字已经删除了,那么服务端会重复FIN断开消息。这时就会有误操作,因为有可能删除的端口对应的套接字已经被新请求占用了。
创建套接字-连接-请求-响应-断开的整体流程如下

网友评论