概要
- OSI模型和网络术语
- TCP报文段结构
- 重连机制
- RTT算法
- 滑动窗口
- 拥塞处理
- Socket
网络术语和OSI模型
网络术语
1、MSL(Maximum Segment Lifetime) 报文最大生存时间 他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃,因为tcp报文 (segment)是ip数据报(datagram)的数据部分
2、ip头中有一个TTL域,TTL是 time to live 生存时间 而是存储了一个ip数据报可以经过的最大路由数,每经 过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。 MSL要大于等于TTL
3、RTT是客户到服务器往返所花时间(round-trip time,简称RTT),TCP含有动态估算RTT的算法。TCP还持续估算一个给定连接的RTT,这是因为RTT受网络传输拥塞程序的变化而变化
4、2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态。在TIME_WAIT状态 时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置 SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口
5、URG:Urget pointer is valid (紧急指针字段值有效) 6、SYN: 表示建立连接 7、FIN: 表示关闭连接 8、ACK: 表示响应 9、PSH: 表示有 DATA数据传输 10、RST: 表示连接重置。
OSI模型
第一层:物理层 涉及到0 1 0 1二进制数据在媒介(电缆、网线)上传输
第二层:链路层 对应的数据称为数据帧 Frame 网桥、交换机
第三层:网路层 数据包 Packet 路由的选择、ARP地址转换 ip到ip传输
第四层:运输层 报文段 Segment 提供了端(端口)到端的传输 流量的控制
第五层:会话层
第六层:表示层
第七层:应用层
TCP报文段数据结构
前两个字节为:source tcp port 源端口号 2-4个字节是dest port目的端口号 ,sequence number、acknowledge number、sliding windows 、 tcp flag
1、sequence number:报文段的需要,为了重新给报文段排序
2、acknowledge number: 确认收到,解决不丢包的问题
3、sliding windows 滑动窗口,流量拥塞控制
4、tcp flag 用于操控tcp状态机
为什么是三次握手?四次挥手?
三次握手: 主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。 如果client端最后不发送ack Server就开始建立连接,如果一开始client的连接包因网络延迟又到达了服务端,此时服务端又会建立连接,而客户端已经建立了连接,而此时服务端不知道一直等到客户端发送数据,浪费了服务端资源。如果使用三次握手的话,服务端必须等到最后一次ack才会建立连接,而此时客户端知道此包已经被重传过所以并不会发送ack给服务端,因此就不会建立连接
四次挥手:
由于tcp是全双工的(两端都可以在发送数据的同时接受数据) 断开连接时 每端必须发送 ACK 和 FIN
下图是双方同时断连接的示意图
相关的问题
1、SYN连接超时: 当客户端发起SYN连接时,server已经回复了 ACK+SYN,此时客户端掉线了,那么服务端超时就继续向客户端端发送 ACK+SYN 会每隔一定时间发送五次, 时间 1+2+4+8+16+32=63s,63s后仍然没有连上则断开连接
2、SYN Flood 攻击 黑客模拟client 不断的向server发送SYN后就不操作了,这是server会不断的重试,请求不断增加,最后会导致SYN-REVD 未完成队列满了,正常的请求不能处理。tcp_syncookies就可以解决此事,server端会将source port dest port 和时间错发给客户端,如果正常连接会响应server,那么就通过cookie建立了连接(不在SYN-RECVD队列中的连接),但是一般不建议处理正常的大负载的连接,syncookies是妥协版的tcp协议,不严谨。 可以通过调节 tcp_syn_retries 重试次数 tcp_syn_backlog:增大SYN-RECVD对队列长度,tcp_abort_overflow直接丢弃处理不过来的连接
3、关于ISN(initial sequence number)的初始化。 如果ISN始终是1 当客户发送了30个Segment后由于网络原因重连了,此时向server发送数据序号从1开始,但是当之前的30个Sement到了server被认为是新连接发来的包,server认为client后面的sequence是30以后的,但是实际上client的sequence可能是3,顺序全乱了。 解决:客户端会和一个假的时钟进行绑定,每隔4微妙对ISN加1,直到超过2的32次方后又从0开始,一个周期大概有4.55小时。而一个报文在网络上存活时间 MSL远远小于4.55个小时,这样就保重不会重用ISN
4、关于 MSL 和 TIME_WAIT
等待的时间为 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)再关闭连接
原因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)
TCP重传机制
TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。 注意,接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
超时重传 一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。 但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。 对此有两种选择: 一种是仅重传timeout的包。也就是第3份数据。 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。 这两种方式有好也有不好。第一种会节省带宽,但是慢,如果后面的报文没被接收端接收 ,依然需要超时重传后面的数据。第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长(Timeout是动态地计算出来的)。
快速重传
于是,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。
比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下
Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重转之前的一个还是重装所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。
SCK(Selective Acknowledgment)
另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:
为了改进快速重传,需要有接收端维护一个已经收到的报文段范围表,每次和ack一起返回给发送端。发送端根据此选择性的重发。发送端不能全依赖SCK,因为接收端不能保证一定回传SCK(如果有更重要的数据需要内存空间,就会清掉这个报文范围表),因此重传机制需结合超时重传和快速重传来使用。
这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。
这里还需要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out,如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack。
注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。
D-SCK:Duplicate-SCK 表示重叠的报文段
1、ack包含了SCK 那么此时就是D-SCK
2、SCK报文段范围之间有重叠
网络延时:当发送端连续3次收到同个ack=1000,那么就开始重传,但是此时接收端发送的ack已经到了3000了,而SCK=1000-1500说名序号为1000 len为500的报文已经到了,ack包含了SCK说明是网络延迟导致的
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)
500-999 500-999 1000
1000-1499 (delayed)
1500-1999 1500-1999 1000, SACK=1500-2000
2000-2499 2000-2499 1000, SACK=1500-2500
2500-2999 2500-2999 1000, SACK=1500-3000
1000-1499 1000-1499 3000
1000-1499 3000, SACK=1000-1500
RTT算法
RTO 超时重传的时间需要设置影响tcp传输效率,若设置长了影响传输性能,设置短了重发的快了,又会造成网络阻塞,导致超时,从而导致更多的重发。
RTO 超时重传的时间不是根据采集几个RTT(Round Trip Time)数据样本计算的,而是需要根据当时网络情况动态计算的
Jacobson / Karels 算法
滑动窗口
窗口结构
tcp内部缓冲区的数据结构
接受端:
LastByteRead表示应用程序读取缓存区数据的最后的位置 NextByteExpected 表示接受到连续包的最后一个位置 lastRecvd 表示最后一个到达数据的位置
其中中间空白的表示中间那些数据还没有到达
发送端:
LastByteAcked 被接受的ack过的位置 LastByteSent最后一个发送出去的数据但未收到ack LastByteWritten应用程序正在写入的数据
接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
接收端可以根据处理的速度,通过 advertise windows size来控制发送端发送数据的长度,每次ack时可以动态的改变 windows size
下面是发送端的窗口接口
上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口) 1已收到ack确认的数据。 2发送还没收到ack的。 3在窗口中还没有发出的(接收方还有空间)。 4窗口以外的数据(接收方没空间)
下面是个滑动后的示意图(收到36的ack,并发出了46-51的字节):
下面我们来看一个接受端控制和发送端的图示
ZWP(Zero Windows Probe)
当接收端发送 windows size=0时 发生不再发送数据了,但是发送方连续三次发送ZWP数据包询问接受方的 windows size,如果连续三次依然都是0则有的tcp会发RST请求把链接断开。 注意:这里有可能被DDos攻击,当攻击者建立http连接发送GET请求后马上将windows size设置成0 此时服务端会不断向客户端发送ZWP请求,如果攻击多次发送这样的请求,导致服务器资源耗尽。 注:有等待的地法就会有可能收到DDos攻击。
DDos(Distribute Denial of Service):攻击者模拟客户端形式与客户端进行不正常的通讯,影响其他正常客户通讯 方式: 使网络过载干扰正常通讯 提交大量请求导致服务器超负荷 阻断某一用户与服务通信 阻断服务器与某系统或个人通信
Silly Window Syndrome 问题
傻窗口综合症 当接受者处理速度变慢时,窗口不断减小,导致减小到几个字节时,发送方也要发送—>浪费了网络资源) MTU:对于以太网来说 1500字节-20字节IP头部-20字节TCP头部 MSS(Max Segment Size) RFC定默认定义为536个字节的数据 大于MTU的包有两种结局,一种是直接被丢了,另一种是会被重新分块打包发送
分别从接收端和发送端解决:
接收端:当windows size < 某个值时向发送端发送 ack(0) windows size=0 告诉发送暂时不要发数据了,当windows size大于等于 MSS或 有一半可用(接受缓冲区有一半可用)通知发送端
发送端:延迟处理,有个Nagle’s algorithm 算法 1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)等待时间或是超时200ms,这两个条件有一个满足,他才会发数据,否则就是在攒数据 另外,Nagle算法默认是打开的,所以,对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序,你需要关闭这个算法。你可以在Socket设置TCP_NODELAY选项来关闭这个算法。
拥塞处理-Congestion Handling
慢启动算法:cwnd全称Congestion Window
1)连接建好的开始先初始化cwnd = 1,表明可以传一个MSS大小的数据。
2)每当收到一个ACK,cwnd++; 呈线性上升
3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
4)还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”
所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。 所以,我们可以看到,如果网速很快的话,ACK也会返回得快,RTT也会短,那么,这个慢启动就一点也不慢。下图说明了这个过程。
如图:
拥塞避免:
拥塞避免算法 -Congestion Avoidance
前面说过,还有一个ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”。一般来说ssthresh的值是65535,单位是字节,当cwnd达到这个值时后,算法如下:
1)、收到一个ACK时,cwnd = cwnd + 1/cwnd
2)、当每过一个RTT时,cwnd = cwnd + 1
这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。
拥塞状态算法:前面我们说过,当丢包的时候,会有两种情况:
1)等到RTO超时,重传数据包。TCP认为这种情况太糟糕,反应也很强烈。 sshthresh = cwnd /2 cwnd 重置为 1 进入慢启动过程
2)Fast Retransmit算法,也就是在收到3个duplicate ACK时就开启重传,而不用等到RTO超时。 TCP Tahoe的实现和RTO超时一样。 TCP Reno的实现是: cwnd = cwnd /2 sshthresh = cwnd
进入快速恢复算法——Fast Recovery
上面我们可以看到RTO超时后,sshthresh会变成cwnd的一半,这意味着,如果cwnd<=sshthresh时出现的丢包,那么TCP的sshthresh就会减了一半,然后等cwnd又很快地以指数级增涨爬到这个地方时,就会成慢慢的线性增涨。我们可以看到,TCP是怎么通过这种强烈地震荡快速而小心得找到网站流量的平衡点的。
拥塞发生和快速恢复
快速恢复算法 – Fast Recovery TCP Reno这个算法定义在RFC5681。快速重传和快速恢复算法一般同时使用。快速恢复算法是认为,你还有3个Duplicated Acks说明网络也不那么糟糕,所以没有必要像RTO超时那么强烈。注意,正如前面所说,进入Fast Recovery之前,cwnd 和 sshthresh已被更新: cwnd = cwnd /2 sshthresh = cwnd 然后,真正的Fast Recovery算法如下: cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了) 重传Duplicated ACKs指定的数据包 如果再收到 duplicated Acks,那么cwnd = cwnd +1 如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。
如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks。注意,3个重复的Acks并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO超时,于是,进入了恶梦模式——超时一个就减半一下,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发Fast Recovery算法了。 通常来说,正如我们前面所说的,SACK或D-SACK的方法可以让Fast Recovery或Sender在做决定时更聪明一些,但是并不是所有的TCP的实现都支持SACK(SACK需要两端都支持),所以,需要一个没有SACK的解决方案。而通过SACK进行拥塞控制的算法是FACK(后面会讲)TCP New Reno于是,1995年,TCP New Reno(参见RFC 6582)算法提出来,主要就是在没有SACK的支持下改进Fast Recovery算法的—— 当sender这边收到了3个Duplicated Acks,进入Fast Retransimit模式,开发重传重复Acks指示的那个包。如果只有这一个包丢了,那么,重传这个包后回来的Ack会把整个已经被sender传输出去的数据ack回来。如果没有的话,说明有多个包丢了。我们叫这个ACK为Partial ACK。 一旦Sender这边发现了Partial ACK出现,那么,sender就可以推理出来有多个包被丢了,于是乎继续重传sliding window里未被ack的第一个包。直到再也收不到了Partial Ack,才真正结束Fast Recovery这个过程 我们可以看到,这个“Fast Recovery的变更”是一个非常激进的玩法,他同时延长了Fast Retransmit和Fast Recovery的过程。
Socket
Socket:是一种建立在tcp基础之上,供给开发人调用的完成网上计算机之间的通讯的接口。
server: socket + bind + listen + accept + ------- + close
client: socket + connect + --------- + close
队列:SYN+RVD和ESTABLISHED队列,当Server端接收到Client的SYN请求就进入 SYN+RCVD状态,那么就会将连接放入SYN+RCVD 未完成队列中,当Server收到Client ACK后进入已完成 ESTABLISEHED队列中,等待上层应用程序accept掉。 默认ESTABLISHED队列长度为128 SYN+RCVD队列长度为2048。
accept和FD:上层应用会通过accept方法从ESTABLISHED取出一个连接,其实就是获取到ConnFD(连接文件描述符文件)然后通过此文件描述符号进行读写,然而在Client在connect时也会有个ConnFD,此时Client-Server之间的通讯就是通过ConnFD进行。 此时稍微说下异步非阻塞和同步阻塞不同,而Netty没有做到真正的异步非阻塞,它使用的是多路复用的模式,这里会用个select线程不断询问操作系统内核中的读缓冲区是否有数据,如果有,此时会有用户态切换到内核态,从网卡读缓冲区取出数据放入到用户进程中,然后再由线程读区,此时过程是阻塞的。那么真正异步非阻塞是,当操作监听网卡读缓冲区,一有数据就拷贝到进程空间中,然后通知进程进行读取。
ConnFD和listenFD: 如果上层应用不accept连接,并不影响Client和Server之间进行通讯,Client依然可以通过ConnFD发送数据给Server,Server端有listenFD是为了保证可以和多个客户端建立连接,并发处理各个用户请求。
RST异常场景
connection reset by peer 和EOF 是经常在日志里看到的错误。前者是因为收到了对方发来的RST,后者则是正常的FIN。关闭连接时发RST通常是因为close时发现自己缓冲区RevQ还有数据没有读。下面总结几个会发RST的场景。
-
connect不存在的端口,对方会回复RST。
-
异常终止时接收缓冲还有数据将发RST。
-
往对方已经close的连接上写数据,对方回复RST。
既然可以支持半关闭,为什么往关闭的连接上写数据会失败?
close实际上是关闭conn的两个流向
下面例子就是client发了5个字节给server,但是server应用层没有read,一直存在于server的RecvQ中,这时server调用close关闭连接,底层发给client的是RST包,表示连接异常关闭。
c>s: F[S] seq 1761306812, win 43690, options [...]
s>c: F[S] seq 2690519277, ack 1761306813, win 43690, options [...]
c>s: [.] ack 1, win 342, options [...]
c>s: F[P], seq 1:6, ack 1, win 342, length 5
s>c: [.] ack 6, win 342, options [...]
s>c: [R] seq 1, ack 6, win 342, options [...]
保活定时器
先总结下tcp的四个定时器及其功能
-
重传定时器 当超时没有收到某个Seq的ACK后,将以指数退避的方式重传
-
TIME_WAIT定时器 主动关闭连接的一方在收到对方的FIN发送ACK后将等待2MSL时间才关闭连接
-
坚持定时器 为零窗口设计,当接收方告知发送方窗口大小为0后,启动坚持定时器周期性的询问窗口更新通知。以免窗口通知丢失,导致双方无法通信。
-
保活定时器 TCP-keepalive 探查连接双方是否还活着。
如果server就是不close,client一直收不到FIN,是否CLOSE_WAIT和FIN_WAIT_2这对状态会无限制保持下去?
FIN_WAIT_2的生命周期由tcp_fin_timeout设置,如果收不到FIN包连接终将CLOSED。但是CLOSE_WAIT的状态可能持续保持下去。如果server配置了keepAlive时间则每个t就发TCP Keep-Alive包看conn是否被关闭。当client的FIN_WAIT_2 状态超时变为CLOSED之后,server的KeepAlive包将引发client的RST,最后连接终止。与之对比的是,当client半连接时,FIN_WAIT_2状态不会超时,那么KeepAlive包将一直有Keep AliveACK回应,连接不会RST终止。
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(5 * time.Second)
下面例子中keepalive时间是5s,连接建立后没有收发数据,因此KeepAlive包每5秒发一次。10s时client端关闭连接发了FIN包进入FIN_WAIT_2状态,该状态60s后超时变为CLOSED状态。在这之后KeepAlive才失败导致RST重置连接。