TCP协议
TCP在网络OSI的七层模型中的第四层-Transport层,IP在第三层--Network层,ARP在第二层--DataLink层,在第二层上的数据我们将Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。
首先,我们需要知道,我们程序的数据首先会打到TCP的Segment中,然后TCP的Segment会打到IP的Packet中,然后再打到以太网的Ethernet的Frame中,传到对端后,各个层解析自己的协议,然后把数据交给更高层的协议处理。
TCP头格式
注意:
- TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。
- 一个TCP连接需要四个元祖来表示是同一个连接(src_ip,src_port,dst_ip,dst_port)准确来说是五元组,还有一个是协议,但因为这里只是说TCP协议,所以只说四元组。
- Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
- Acknowledgement Number就是ACK--用于确认收到,用来解决不丢包的问题。
- Window又叫Advertised-Window,也就是著名的滑动窗口,用于解决流控的。
- TCP Flag,也就是包的类型,主要用于操控TCP的状态机的。
TCP状态机
网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。
下面是:“TCP协议的状态机”和"TCP建连接",“TCP断连接”,“传数据”的对照图:
为什么建连接要3次握手,断连接需要4次挥手?
- 对于建连接的3次握手,主要是要初始化Sequence Number的初始值。通信双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)--所以叫SYN,全称 Synchronize Sequence Numbers。也就是图中的x和y。这个号码要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。
- 对于断开连接的4次挥手,其实仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要FIN和ACK,只不过,有一方是被动的,所以看上去就成了所谓的4次挥手,如果两边同时断开连接,那就会进入到CLOSING状态,然后到达TIME_WAIT状态。下图是双方同时断连接的示意图:
另外,还有几个事情需要注意一下:
- 关于建连接时SYN超时。试想一下,如果server端接到了client发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间没有收到回复,TCP会重发SYN-ACK,在Linux下,默认的重试次数为5次。
- 关于SYN Flood攻击.一些恶意的人就为此制造了SYN Flood攻击--给服务器发一个SYN后,就下线了,于是服务器需要经历重传和等待之后最终才会断开连接,这样攻击者就可以把服务器的SYN连接队列耗尽,让正常的连接请求不能处理,于是Linux下给了一个叫tcp_syncookies的参数来应对这个是--当SYN队列满了后,TCP会通过源地址端口,目标地址端口和时间戳打造一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个SYN Cookie发回来,然后服务端可以通过cookie建立连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为synccookies是妥协版的TCP协议,并不严谨,对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是tcp_synack_retries可以用他来减少重试次数;第二个是tcp_max_syn_backlog,可以增大SYN连接数;第三个是tcp_abort_on_overflow处理不过来干脆就直接拒绝连接了。
- 关于ISN的初始化。ISN是不能hard code的,不然会出问题,RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微妙对ISN做加1操作,直到超过2^32,又从0开始,这样一个ISN的周期大约是4.55个小时。因为我们假设我们的TCP Segment在网络上的存货时间不会超过Maximum Segment Lifetime(缩写为MSL),所以只要MSL的值小于4.55小时,那么,就不会重用到ISN。
- 关于MSL和TIME_WAIT。通过上面的ISN描述,在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是2*MSL(RFC793定义MSL为2分钟,Linux设置成了30s)为什么要设置TIME_WAIT状态,为什么不直接转成CLOSED状态呢?主要有两个原因1):TIME_WAIT确保有足够的时间让对端接收到ACK,如果被动关闭的那方没有收到ACK,就会触发被动端重发FIN,依赖一区刚好是2个MSL,2):有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自作主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟的包就有可能会跟新连接混在一起)
数据传输过程中的Sequence Number
下图展现的是SeqNum是怎么变的:
SeqNum的增加是和传输的字节数相关的。
注意:如果你用Wireshark抓包程序看3次握手,你会发现SeqNum总是为0,不是这样的,Wireshark为了显示更友好,使用了Relative SeqNum--相对序号,你只要再右键惨淡中的protocol preference中取消掉就可以看到"Absolute SeqNum"了。
TCP重传机制
TCP要保证所有的数据包都可以到达,所以必需要有重传机制。
注意,接收端给发送端的ACK确认只会确认最后一个连续的包,比如发送端发送了1,2,3,4,5一共5份数据,接收端收到了1,2,于是回ack 3(表明接收端下一个需要的包是3号数据包),然后收到了4(注意此时3没有收到),此时TCP该怎么办呢,我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然发送端就以为之前的都收到了。也就是说这种情况下,接收端会一直回复ack 3,知道3号包收到位置。
超时重传机制
一种是不回ack,死等3号包,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack回5,意味着3,4号包都收到了,我下一个需要的包是5号包。
但是这种方式会有比较严重的问题,那就是因为要死等3号包,所以导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到3号包的ack,所以发送方可能会悲观的任务,4号包和5号包也丢失了,导致4号包和5号包也重传。
对此,有两种选择:
一种仅重传timeout的包,也就是第3份数据。
一种是重传timeout后所有的数据,也就是3,4,5这3份数据。
这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等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还是没有收到,于是发送端收到了3个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的实际的实现)。可见,这是一把双刃剑。
SACK方法
另一种更好的方式叫 Selective Acknowledgment (SACK),这种方式要在TCP头里加一个叫SACK的从西,ACK还是Fast Retransmit的ACK,SACK是汇报收到的数据碎版。参看下图:
这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没到,于是就优化了Fast Retransmit算法。当然,这个协议需要两边都支持,在Linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)
这里还需要注意一个问题--接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样做是不被鼓励的,因为这个事会吧问题复杂化,但是接收方这么做可能会有些极端的情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out,如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外接收端永远不能把SACK的包标记为Ack。