tcp通道建立过程 首先我们来看一下tcp通道的建立表面过程,首先我们看四张图,三次握手解析图和tcp协议头图、tcp状态迁移图,tcp协议头图; 和三次握手相关的有两个队列:syn队列和accept队列,这两个队列的数据装填是内核维护的,调用accept会在accept队列取数据。
其实和三次握手有关的协议头常规字段有四个:Sequence Number、Acknowledgment、SYN位、ACK标记位,syn和ack分别标记Sequence Number、Acknowledgment两个32bits字段字段值有效
第一次握手:主机A发送位码为SYN=1,rand随机产生Sequence Number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;
第二次握手:主机B收到请求后要确认联机信息,向A发送Acknowledgment=(主机A的seq+1),syn=1,ack=1,rand随机产生seq=7654321的包;
第三次握手:主机A收到后检查Acknowledgment是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
以上几张图应该已经能说明tcp的整个通道建立过程细节
TCP通道如何关闭 首先我们看一下两张图: 以上两张图应该很好理解,已经能很好的说明了tcp通道连接断开的过程了,也就是我们常说的四次挥手,其他的我们就不多说了,下面我们重点说一下tcp四次回收过程中讨论的比较多的问题, time_wait过多的情况: 这是因为:虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文,并保证于此。这时客户端关闭,服务端会收到RST,也就是一个报错为了防止当前连接中的重复报文干扰下一个连接。
close_wait(server端)、fin_wait1(client端)过多的情况: close_wait 按照正常操作的话应该很短暂的一个状态,接收到客户端的fin包并且回复客户端ack之后,会继续发送fin包告知客户端关闭关闭连接,之后迁移到Last_ACK状态。但是close_wait过多只能说明没有迁移到Last_ACK,也就是服务端是否发送fin包,只有发送fin包才会发生迁移,所以问题定位在是否发送fin包。fin包的底层实现其实就是调用socket的close方法,这里的问题出在没有执行close方法(其实要验证也很简单,程序里面不调用close能看到了)。说明服务端socket忙于读写,导致关闭socket不及时:例如I/O线程被意外阻塞,或者I/O线程执行的用户自定义Task比例过高,导致I/O操作处理不及时,链路不能被及时释放。 程序Bug,接收到FIN信号后没有及时关闭socket,这可能是Netty的Bug,也可能是业务层Bug; 关闭socket不及时:例如I/O线程被意外阻塞,或者I/O线程执行的用户自定义Task比例过高,导致I/O操作处理不及时,链路不能被及时释放。
解决方案: 代码层面做到第一:使用完socket调用close方法;第二:socket读控制,当读取的长度为0时(读到结尾),立即close;第三:如果read返回-1,出现错误,检查error返回码,有三种情况:INTR(被中断,可以继续读取),WOULDBLOCK(表示当前socket_fd文件描述符是非阻塞的,但是现在被阻塞了),AGAIN(表示现在没有数据稍后重新读取)。如果不是AGAIN,立即close 可以设置TCP的连接时长keep_alive_time还有tcp监控连接的频率以及连接没有活动多长时间被迫断开连接
出现rst的情况 测试关键代码如下:
对方端口未打开,客户端连接未打开的端口(前提是曾经打开过,程序崩溃重启,或者崩溃没启动了,如果之前没建立过链接,很多系统时不会主动回复这个的): 上图为server异常推出后,client主动connect,也会触发server端主动发送rst;
close Socket 时recv buffer 不为空: 上图测试结果显示,关闭一个没接收完数据的socket,会促发本端主动给对方发送rst;
3.在一个已关闭的socket上接收数据 上图测试结果显示,在一个已关闭的套接字上recv数据,会触发内核主动给对端发送rst;
**总结:**当client端总是莫名其妙的收到后台发送的rst,可以往上面三个情况查一下。 ###################################################################### 滑动窗口的基本概念: sender允许在没经过recver确认的情况下,同时往网络发出的package数量。
TCP协议中影响实际业务流量的参数很多,这里主要分析一下窗口的影响。 滑动窗口解决的问题: 为了获得最优的连接速率,使用TCP窗口来控制流速率(flow control),滑动窗口就是一种主要的机制。这个窗口允许源端在给定连接传送数据分段而不用等待目标端返回ACK,一句话描述:窗口的大小决定在不需要对端响应(acknowledgement)情况下传送数据的数量。官方定义:“The amount of octets that can be transmitted without receiving an acknowledgement from the other side”。 滑动窗口原理: 发送端的发送窗口是基于接收端的接收窗口的应答ACK来计算的,TCP header中有一个Window Size字段,它其实是指接收端的窗口,即接收窗口,用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的。其实TCP在整个发送过程中,也在度量当前的网络状态,目的是为了维持一个健康稳定的发送过程,比如拥塞控制。数据传输需要对端确认,发送的数据分为如下四类来看: (1)已经发送并且对端确认(发送窗外 缓冲区外) (2)已经发送但未收到确认数据 (发送窗内 缓冲区内) (3)允许发送但尚未防的数据(发送窗内 缓冲区内) (4)未发送暂不允许(发送窗外 缓冲区内)
TCP窗口就是这样逐渐滑动,发送新的数据,滑动的依据就是发送数据已经收到ACK,确认对端收到,才能继续窗口滑动发送新的数据。可以看到窗口大小对于吞吐量有着重要影响,同时ACK响应与系统延时又密切相关。需要说明的是:如果发送端的窗口过大会引起接收端关闭窗口,处理不过来反之,如果窗口设置较小,结果就是不能充分利用带宽,所以仔细调节窗口对于适应不同延迟和带宽要求的系统很重要。
tcp如何保证应用上层数据顺序性(采取延迟确认ack,并根据seq序列号重组报文): tcp协议规定在接受到数据段时需要向对方发送一个确认,但如果只是单纯的发送一个确认,代价会比较高(20字节的ip首部,20字节的tcp首部),最好能附带响应数据一起发送给对 方.所以tcp在何时发送ack给对方有以下规定:
当有响应数据要发送时,ack会随响数据立即发送给对方.
如果没有响应数据,ack的发 送将会有一个延迟,以等待看是否有响应数据可以一起发送 ,这称是"Delayed Ack".但这个延迟最多不会超过500ms,一般为200ms.如果在200ms内有数据要发送,那么ack会随数据一起立即发送给对方.注意这里的延迟200ms,不是指的从接受到对方数据到发送ack的最长等待时间差.而是指的内核启动的一个定时器,它每隔200ms就查看下是否有ack要发送.例如:假设定时器在0ms时启动,对方的数据段在185ms时到达,那么ack最迟会在200ms时发送,而不是385ms时发送.
如果在等待发送ack期间,对方的第二个数据段又到达了,这时要立即发送ack.但是如果对方的三个数据段相继 到达,那么第二个数据段到达时ack立即发送,但第三个数据段到达时是否立即发送,则取决于上面两条.
TCP窗口尺寸 最早TCP协议涉及用来大范围网络传输时候,其实是没有超过56Kb/s的连接速度的。因此,TCP包头中只保留了16bit用来标识窗口大小,允许的最大缓存大小不超过64KB。为了打破这一限制,RFC1323规定了TCP窗口尺寸选择,是在TCP连接开始的时候三步握手的时候协商的(SYN, SYN-ACK,ACK),会协商一个 Window size scaling factor,之后交互数据中的是Window size value,所以最终的窗口大小是二者的乘积. Window size value: 64 or 0000 0000 0100 0000 (16 bits) Window size scaling factor: 256 or 2 ^ 8 (as advertised by the 1st packet) The actual window size is 16,384 (64 * 256) 这里的窗口大小就意味着,直到发送16384个字节,才会停止等待对方的ACK.随着双方回话继续,窗口的大小可以修改window size value 参数完成变窄或变宽,但是注意:Window size scaling factor乘积因子必须保持不变。在RFC1323中规定的偏移(shift count)是14,也就是说最大的窗口可以达到Gbit,很大,但是这一机制并不总是默认开启的和系统有关,貌似Linux默认开启,Windows默认关闭。 Wireshark抓包实例
TCP窗口的参数设置 TCP窗口起着控制流量的作用,实际使用时这是一个双端协调的过程,还涉及到TCP的慢启动(Rapid Increase/Multiplicative Decrease),拥塞避免,拥塞窗口和拥塞控制。可以记住,发送速率是由min(拥塞窗口,接收窗口),接收窗口在下文有讲。 TCP窗口优化设置 TCP窗口既然那么重要,那要怎么设置,一个简单的原则是2倍的BDP.这里的BDP的意思是bandwidth-delay product,也就是带宽和时延的乘积,带宽对于网络取最差连接的带宽。 buffer size = 2 * bandwidth * delay 还有一种简单的方式,使用ping来计算网络的环回时延(RTT),然后表达为: buffer size = bandwidth * RTT 为什么是2倍?因为可以这么想,如果滑动窗口是bandwidthdelay,当发送一次数据最后一个字节刚到时,对端要回ACK才能继续发送,就需要等待一次单向时延的时间,所以当是2倍时,刚好就能在等ACK的时间继续发送数据,等收到ACK时数据刚好发送完成,这样就提高了效率。 举个例子:带宽是20Mbps,通过ping我们计算单向时延是20ms,那么可以计算:20000000bps8*0.02 = 52,428bytes,因此我们最优窗口用 104,856 bytes = 2 x 52,428,所以说当发送者发送104,856 bytes数据后才需要等待一个ACK响应,当发送了一半的时候,对端已经收到并且返回ACK(理想情况),等到ACK回来,又把剩下的一半发送出去了,所以发送端就无需等待ACK返回。 发现了么?这里的窗口已经明显大于64KB了,所以机制改善了,上一级。 TCP窗口流量控制 现在我们看看到底如何控制流量。TCP在传输数据时和windows size 关系密切,本身窗口用来控制流量,在传输数据时,发送方数据超过接收方就会丢包,流量控制,流量控制要求数据传输双方在每次交互时声明各自的接收窗口「rwnd」大小,用来表示自己最大能保存多少数据,这主要是针对接收方而言的,通俗点儿说就是让发送方知道接收方能吃几碗饭,如果窗口衰减到零,也就是发送方不能再发了,那么就说明吃饱了,必须消化消化,如果硬撑胀漏了,那就是丢包了。
流量控制
慢启动 虽然流量控制可以避免发送方过载接收方,但是却无法避免过载网络,这是因为接收窗口「rwnd」只反映了服务器个体的情况,却无法反映网络整体的情况。 为了避免网络过载,慢启动引入了拥塞窗口「cwnd」的概念,用来表示发送方在得到接收方确认前,最大允许传输的未经确认的数据。「cwnd」同「rwnd」相比不同的是:它只是发送方的一个内部参数,无需通知给接收方,其初始值往往比较小,然后随着数据包被接收方确认,窗口成倍扩大,有点类似于拳击比赛,开始时不了解敌情,往往是次拳试探,慢慢心里有底了,开始逐渐加大重拳进攻的力度。 拥塞窗口扩大 在慢启动的过程中,随着「cwnd」的增加,可能会出现网络过载,其外在表现就是丢包,一旦出现此类问题,「cwnd」的大小会迅速衰减,以便网络能够缓过来。
拥塞窗口与丢包
说明:网络中实际传输的未经确认的数据大小取决于「rwnd」和「cwnd」中的小值。 拥塞避免 从慢启动的介绍中,我们能看到,发送方通过对「cwnd」大小的控制,能够避免网络过载,在此过程中,丢包与其说是一个网络问题,倒不如说是一种反馈机制,通过它我们可以感知到发生了网络拥塞,进而调整数据传输策略,实际上,这里还有一个慢启动阈值「ssthresh」的概念,如果「cwnd」小于「ssthresh」,那么表示在慢启动阶段;如果「cwnd」大于「ssthresh」,那么表示在拥塞避免阶段,此时「cwnd」不再像慢启动阶段那样呈指数级增长,而是趋向于线性增长,以期避免网络拥塞,此阶段有多种算法实现,通常保持缺省即可。 如何调整「rwnd」到一个合理值 很多时候TCP的传输速率异常偏低,很有可能是接收窗口「rwnd」过小导致,尤其对于时延较大的网络,实际上接收窗口「rwnd」的合理值取决于BDP的大小,也就是带宽和延迟的乘积。假设带宽是 100Mbps,延迟是 100ms,那么计算过程如下: BDP = 100Mbps * 100ms = (100 / 8) * (100 / 1000) = 1.25MB
此问题下如果想最大限度提升吞度量,接收窗口「rwnd」的大小不应小于 1.25MB。 如何调整「cwnd」到一个合理值 一般来说「cwnd」的初始值取决于MSS的大小,计算方法如下:
min(4 * MSS, max(2 * MSS, 4380))
以太网标准的MSS大小通常是1460,所以「cwnd」的初始值是3MSS。当我们浏览视频或者下载软件的时候,「cwnd」初始值的影响并不明显,这是因为传输的数据量比较大,时间比较长,相比之下,即便慢启动阶段「cwnd」初始值比较小,也会在相对很短的时间内加速到满窗口,基本上可以忽略不计。下图使用IxChariot完成一次设置 以下图片不是真是网络环境中的慢启动和拥塞控制过程,是自己实现用户态协议栈自己定的策略,但是和真是环境中唯一的区别就是发送端窗口大小变化的策略,下图过程用的是每个应答ack+1,并且小于接收方窗口大小的策略;而真实的应该是cwnd=cwnd*2的策略,只是为了说明滑动窗口和拥塞控制的过程是由发送方根据网络环境和接收方的缓冲区承受能力共同决定的。
在这里插入图片描述
设置cwnd
不过当我们浏览网页的时候,情况就不一样了,这是因为传输的数据量比较小,时间比较短,相比之下,如果慢启动阶段「cwnd」初始值比较小,那么很可能还没来得及加速到满窗口,通讯就结束了。这就好比博尔特参加百米比赛,如果起跑慢的话,即便他的加速很快,也可能拿不到好成绩,因为还没等他完全跑起来,终点线已经到了