【Linux】网络--传输层--深入理解TCP协议
个人主页~
深入理解TCP协议
- 一、TCP数据传输问题
- 1、发送数据丢包问题---重传机制
- (一)客户端数据发送丢包
- (二)服务器确认应答丢包
- (三)时间间隔问题
- 2、三次握手问题---奇数次握手
- (一)验证可靠全双工
- (二)确定连接成本
- 二、TCP三次握手问题
- 三、TCP四次挥手问题
- 四、流量控制
- 五、滑动窗口
- 1、基本概念
- 2、丢包问题
- 3、滑动窗口大小
- 拥塞控制
- 六、延迟应答
- 七、粘包问题
- 八、文件和网络的关系
一、TCP数据传输问题
1、发送数据丢包问题—重传机制
(一)客户端数据发送丢包
在我们客户端对服务器发送数据的时候,可能会出现丢包的问题,即数据没有到达服务器,此时服务器不会向客户端发送应答报文,客户端在等待了特定的时间后,会认为数据丢失,对已经发送的数据进行补发
这里是因为主机对于发出去的报文是否丢失是无法判定的,所以必须要通过一定的规定来决定是否要进行重传
(二)服务器确认应答丢包
还有一种情况就是发送数据没有丢包,但是服务器发给客户端的应答报文丢包了,同样的,对于客户端来说上面的情况和下面是一样的,客户端在等待了特定的时间后,会认为数据丢失,对已经发送的数据进行补发。这叫做超时重传
(三)时间间隔问题
最理想的情况下,找到一个最小的时间,保证确认应答在这个时间内返回,但是这个时间的长短随着网络环境的不同会有差异,如果超时时间设的太长,会影响整体的重传效率,如果超时时间设的太短,有可能会频繁发送重复的报文
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间,一般Linux中超时以0.5
s为一个单位进行控制,每次判定超时重发的超时时间都是0.5
s的整数倍,如果重发一次仍得不到回答,等待2*0.5
s,重发n次仍得不到回答,等待0.5*2^n
s,以指数形式递增,累计到一定重传次数,TCP会认为网络或者对端主机出现异常,强制关闭连接
2、三次握手问题—奇数次握手
(一)验证可靠全双工
注意这里验证的是可靠和全双工两个概念,三次握手是最少次数的验证可靠全双工的方式
- 一次握手:一次握手就不必多说了,只能让服务器知道客户端是就绪的,而客户端对服务器丝毫不知情,此时只能验证客户端可发送报文,服务器可接收报文,并且多个客户端对服务器发起连接会造成SYN洪水问题,即服务器接受大量的各个客户端发来的SYN报文
- 二次握手:二次握手可以验证客户端发送报文,服务器接收和发送报文,不能验证客户端是否可以接收报文,验证不了全双工
- 三次握手以及多次握手:三次握手以及多次握手就可以保证双方全双工的通信模式,其中三次握手是代价最小的验证可靠全双工的方式
(二)确定连接成本
三次握手是由客户端发起,最终由客户端应答的连接模式,通过客户端主动发起连接并两次确认(SYN 和 ACK),迫使客户端承担连接失败的重试成本,而服务器仅在最终确认后才分配资源,避免了无效连接的资源浪费,从而将连接成本转移至客户端
二、TCP三次握手问题
-
半连接队列和连接队列:当客户端向服务器发起连接请求时(通过 TCP 协议的三次握手),服务器会维护两个队列来处理这些请求:半连接队列(SYN 队列)和全连接队列(
accept
队列),全连接队列用于存放已经完成三次握手过程的连接,服务器接收到客户端的 SYN 包后,会向客户端发送 SYN+ACK 包,并将该连接信息放入半连接队列中,等待客户端的 ACK 包,也就是说,当客户端和服务器完成了 TCP 三次握手,建立了一个有效的连接之后,这个连接就会被放入全连接队列中等待服务器调用accept
函数来处理 -
队列特点
- 全连接队列有一个最大长度限制,这个限制就是
listen
函数的第二个参数,它表示全连接队列的最大长度,如果有半连接队列中的节点完成三次握手,当全连接队列未满时,那么该节点脱离半连接队列进入全连接队列,当全连接队列已满时,那么服务器一般会忽略新连接,并且这个参数不能太大也不能太小,因为这个值太大会导致多个客户端连接到服务器上引起的资源减少的问题,太小又会导致服务器同时处理多个任务的能力差 - 半连接队列不会被服务器长时间维护,因为半连接队列中的连接处于 TCP_SYN_RECV 状态,也就是三次握手中的第二步,这个状态设计目标也是快速完成,而非长期持有
- 全连接队列有一个最大长度限制,这个限制就是
-
三次握手是下层协议:连接建立成功和上层有没有
accept
没有任何关系,三次握手是双方操作系统自动完成的,accept
只是处理三次握手完成后的动作
三、TCP四次挥手问题
- TIME_WAIT:不知大家是否会发现一种现象,在我们前面的代码中被绑定的端口号比如8080、8888、9090等,在我们终止进程之后再次绑定会短时间的无法绑定,显示出端口号被占用的错误,实际上,这就是主动断开连接的一方,在四次挥手完成之后要进入TIME_WAIT状态,等待若干时长才会释放,来到CLOSED状态,所以一直占用着端口号,当然也包括ip地址,这里的若干时长指的是两个MSL(TCP报文的最大生存时间),在不同操作系统上的MSL不同
- 等待两个MSL的原因是:能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失,否则若服务器立刻重启,可能会收到上个进程迟到的数据,同时在理论上保证最后一个报文可靠到达,假设最后一个ACK丢失,那么服务器会重发一个FIN,这时虽然客户端的进程不在了,但是TCP连接还在,仍可以重发LAST_ACK
四、流量控制
接收端处理数据的速度是有限的,如果发送端发得太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送就可能造成丢包,因此TCP的流量控制原理就是根据接收端的处理能力,来决定发送端的发送速度
接收端通过报头中的窗口大小字段对自身缓冲区剩余空间向发送端通知,发送端发现接收端的缓冲区的剩余空间变小,它发送数据的速度也慢慢变小,当接收段缓冲区满了,就会将窗口大小设为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
五、滑动窗口
1、基本概念
当我们发送和接收数据时,发送方和接收方一发一收性能极低,一次发送多条数据可以大大的提高性能,因为这样可以把多个段的时间重叠到一起,因为有了滑动窗口区域,我们才实现了一次向对方发送大量的TCP报文,滑动窗口位于发送缓冲区
滑动窗口顾名思义就是一个滑动的窗口,这个窗口的左边是已发送并且对方已确认的数据,窗口中的数据是已发送但对方未确认的数据,窗口的右边是待发送的数据,正常情况下,窗口中的数据被确认后,窗口就会向右移动,如下图所示
滑动窗口的维护可以用两个指针实现,已发送并且对方已确认的数据,即滑动窗口左边的数据就可以被覆盖掉了,滑动窗口中的数据是需要被维护的,这也实现了我们TCP协议中的信息保护,只要没有收到对方的应答就一直维护刚刚已经发出的报文
2、丢包问题
-
数据包成功抵达,ACK丢失:我们打个比方,假如我们一下发送了四个报文,然后前3个报文返回的ACK都丢失了,但是因为第4个报文返回了ACK,所以前面的都收到了
-
数据包在发送的过程中丢失:假设我们一下发送六个报文,其中第2个报文在发送过程中丢失了,然后从第3个开始的后面的报文发送到接收端后,接收端返回的ACK都是2001,连续收到三个同样的ACK就会触发重发系统,对数据进行重发,重发以后,接收端返回的ACK就是7001了,因为之前收到的报文都在接收缓冲区中,这种机制被称为快重传(高速重发控制),我们的快重传和超时重传一个起到能快速重传的效果,一个起到至少将报文重传的效果
3、滑动窗口大小
- 滑动窗口的大小取决于三个因素,取三者中最小的作为滑动窗口的决定因素,一是接收端接收能力,也就是接收缓冲区还有多少空间,二是发送缓冲区中的数据多少,也就是发送缓冲区未发送的数据还有多少,三就是网络环境,这就与我们下面说的拥塞控制息息相关
- 整个对于发送缓冲区的控制,包括滑动窗口左右两边以及滑动窗口中的内容,滑动窗口并不是发了数据之后一直向右的,而是通过取模运算的一种环形结构
拥塞控制
- 网络是大家的网络,在同一局域中,同一时间如果有大量的数据被发送到服务器,会导致网络状态拥塞,此时所有的发送端有一个共同的动作就是感受到网络拥塞的状态后使用慢启动机制,先发少量的数据,探测网络状况,如果状况现在比较好了,就渐渐增多数据
- 引入一个新的概念:拥塞窗口,发送开始的时候,定义拥塞窗口大小为1,每次收到一个ACK应答时,拥塞窗口加1,每次发送数据包时,将拥塞窗口和接收端主机反馈的窗口大小做对比,取较小的值为实际发送的窗口
- 慢启动机制:TCP开始启动时,慢启动阈值等于拥塞窗口最大值,每次超时重传时,慢启动阈值会变成原来的一半,同时拥塞窗口置为1,超过阈值前指数增长,慢启动阈值就是指数增长的最大点,超过阈值后进行一次函数单调递增
- 如果是少量丢包,我们会认为是触发了超时重传,大量丢包,我们就会认为是网络拥塞,当TCP开始通信时,网络吞吐量会逐渐上升,随着网络拥塞,吞吐量会立即下降
六、延迟应答
- 延迟应答的策略也是为了提高性能,假设接收端的缓冲区为100KB,一次接收到50KB的数据,如果立刻应答,窗口大小就是50KB,但如果处理端的处理速度比较快,很快就能把这50KB取走,我们可以稍微等一等,让处理端先把数据取走再往回传窗口大小,所以我们设置一个确定的时间,尽量保证在这一段时间里处理端如果处理速度较快的话能将数据取走又不会很耽误应答,结合我们前面提到的窗口越大,传输速度就会越快,所以能提高传输性能
- 延迟应答的策略有两种,一种是数量限制,每隔N个包应答一次,另一种是时间限制,每超过最大延迟时间就应答一次
七、粘包问题
- TCP传输协议是面向字节流的,即发送的数据是没有边界的,是以字节为单位发送的,所以有可能会引发粘包问题,粘包问题就是在TCP 通信中,发送方连续发送的多个独立数据包,在接收方被合并成一个大数据包接收,导致数据边界丢失
- 解决粘包问题的方法是定长报文、使用特殊字符、使用自描述字段+定长报头、使用自描述字段+特殊字符,即在通过在应用层定制协议,使通信双方可以拿到对应的数据
八、文件和网络的关系
网络本质上也是文件,因为Linux一切皆文件,在网络中,网络套接字与文件描述符相同,读写的接口都是一样的,我们可以用和文件操作一样的操作对网络套接字进行操作
如上图所示,我们的网络通信实际上也就是进程间通信,前面不多说了,在struct file
中有两个指针,其中一个private_data
指向网络套接字socket
,另一个f_op
指向网络相关的方法,其中结构体socket
中还有一个struct file
类型的指针指向这个file
结构体
在结构体socket
中还有一个字段是struct sock* sk
,其中sock
结构体是udp_sock
和tcp_sock
的第一个参数,可以通过强制类型转换的方式将sock
结构体转为udp_sock
或tcp_sock
,sock
结构体中有接受队列struct sk_buff_head sk_receive_queue;
和写队列struct sk_buff_head sk_wirte_queue;
,其中sk_buff_head
是用于管理结构体sk_buff
,是多个sk_buff
组成链表的头部
struct socket
{//...struct file* file;struct sock* sk;//...
}
struct sock
{struct sk_buff_head sk_receive_queue;//接收队列struct sk_buff_head sk_wirte_queue;//写队列
}
struct sk_buff
{sk_buff_data_t tail;sk_buff_data_t end;unsigned char *head, *data;unsigned int truesize;
}
head
和 data
之间的区域主要用于在数据包处理过程中添加协议头部,当数据包在网络协议栈中从上层向下层传递时,每经过一层协议,都需要在数据包的前面添加该层的协议头部,由于 head
位置固定,而 data
指针可以向前移动,因此 head
和 data
之间的空闲空间就可以用来存放这些新添加的头部信息,每经过一层协议 data
指针就向上向下移动,添加或移除报头
今日分享就到这里了~