TCP的报文结构
首部长度:
与UDP不同,TCP没有有描述数据的长度的变量(UDP的是UDP长度),但有描述TCP的报头的长度,为什么要和UDP不同呢?其实是UDP的报头长度是固定的,而TCP的报头是"变长"的。其首部长度 就是描述报头的长度,首部长度 所占4个比特,但这并不是说用4个比特位(范围为0 -> 15)来描述报头长度,这里的单位为4个字节,也就是把这4个比特位所构成的数值再乘以4个字节,才是真正的报头长度,因此TCP的报头,最大长度为60字节,TCP报头的前20个字节是固定,因此TCP报头的最短长度为20字节。为什么说报头是变长的呢?关键在于这个可变选项,选项可以有多个,也可以没有选项,这就使得报头的长度是变化的,也就影响着报头的长度。所以要使用首部长度来确认报头从哪里结束,数据从哪里开始。
保留位:
现在暂时没用,先占个位置,后面有需要时再用(倘若后面首部长度太小,就可以使用保留位)。因此保留位是给未来升级拓展的空间。
校验和
与UDP的校验和一样。
16 位紧急指针
标识哪部分数据是紧急数据;
URG :
紧急指针是否有效
PSH:
提示接收端应用程序立刻从TCP 缓冲区把数据读走
32位序列号和32位确认序列号:
TCP可靠传输的最核心的实现机制为确认应答 ,即当接收方收到数据后,就返回一个确认应答,来确认收到数据, 这就保证了可靠传输。但是即使如此,网络传输中也有可能出现后发先至的问题,这就可能导致得到的语意不同,也就导致结果不同,那么应该如何解决呢?TCP的解决方法为给每个字节的数据进行了编号,即位序列号,只要前面的序列号没有到来,即使后面的序列号都堆满了它也不发送,当前面的序列号到了后,就将数据发出去,接收方收到后,就返回一个确认应答,应答中就包含了下一个数据的序列号从什么时候开始(如收到的数据的序列号最大为1000,即下次的数据序列号就从1001开始),这个序列号就为32位确认序列号(为收到的最后一个序列号加一), 因此只要知道这一串字节的开始编号和数据的长度,那么每个字节的编号自然也就知道了,所以只需要在TCP报头中,把这串字节第一个字节的编号表示出来,再结合报文长度,此时每个字节的编号就确定了,而这个编号就保存在报头中的32位序列号中,同样的,确认报文也有序列号,这就解决了数据传输过程的后发先至问题。
ACK
既然有确认序列号和序列号,那么就得有办法区分出来这个报文为普通报文还是应答报文。
而这就得ACK 来解决,当ACK为0时,则表示这是一个普通报文,此时只有32位序列号是有效的,当ACK为1时,则表示这是一个确认报文,此时32位序列号和确认序列号都是有效的。
RST
这些都建立在数据传输到对方,但是如果发出数据后,接收方并没有收到数据,也就是丢包,那么应该怎么办呢?那么就得重传,当传输方等待一定时间后,没有收到确认应答,那么就要进行重传,也称为超时重传 。超时重传是对确认应答的重要补充,也是保证可靠性的重要机制。 但是丢包问题有两种情况,一种是传的数据丢了,一种是应答报文丢了。这两种情况都将导致收不到确认应答,而传输方无法分辨是那种情况造成的,就都进行了超时重传。如果是数据丢了,在重传一份就好,如果是应答报文丢了,那么接收方将收到两个一模一样的数据,这就又出现问题,因此接收方在收到数据后,会对接收到的数据进行去重,保证应用程序读到的数据不是重复的。那么如何高效的判定当前收到的数据是否是重复的呢?其实是使用TCP的序列号来进行判定的依据,当出现两个相同的序列号,那么就可以确定是相同的,TCP会在内核中,会给每个Socket对象都安排一个内存空间,相当于队列,也称为接收缓冲区,收到的数据都会放到接收缓冲区里,并且按照序列号来排序,此时就能很容易的判定当前收到的数据是否是重复数据,当读完数据后,数据就可以从队列中删除掉了。若是该数据删除后,又来一个重复的数据,这样接收缓存区里就没有该数据,那么是否会进入呢?其实不会,因为该数据被删除后,那么就说明当前最小的序列号都比删除的序列号大,当又进来一个比它小的序列号,那么就可以判定为重复数据。倘若重传后,传输方还是收不到确认应答,那么又该如何?将继续重传,不过等待超时时间将随着重传次数会越来越长,当重传次数到达一定次数后,将不再重传,此时就会尝试重置TCP连接(RST为1时,表示该报文为复位报文 ),如果复位操作重置操作也无法成功,最终就只能放连接了。RST就是要求重新建立连接
SYN
上述传输的前提是,传输方与接收方建立了连接,因此传输时得先建立连接,那么应该如何建立连接呢?这就是著名的三次握手,传输方先向接收方发送一个打招呼的数据(使用打招呼来触发特定场景),这个数据不会携带业务信息,只是告诉我要与你建立连接,它们建立连接的过程,就需要三次这样的打招呼的数据交互,这个打招呼就是握手,三次打招呼也就是三次握手。如A和B说,我想与你建立连接,B对A说好啊,B对A说,我想和你建立连接,A对B说好啊,这样看其实是四次,不过中间两次可以合并,B对A说好啊,我也想和你建立连接,这样就是三次了。合并之后,减少了封装和分用的过程,降低了成本,提升了效率。SYN就是请求建立连接的请求,也就是握手,我们把携带SYN标识的称为 **同步报文段。因此三次握手就是客户端向服务器发出SYN,服务器向客户端发出SYN和ACK,客户端在向服务器发出SYN。**三次握手也是一种保证可靠性的机制,TCP要想保证可靠传输,前提是网络路径得畅通,TCP的三次握手,就是要验证网络通信是否通畅,以及验证每个主机发送能力和接收能力是否正常,恰好三次就可以验证完成。三次握手还起到"消息协商"这样的效果,比如双方的序列号从哪开始,当一个数据很久才到,但此时已经断开联系了,因此就可以判定这个数据为之前连接的,就可以丢弃了。
FIN
既然有连接,那么就得有断开连接,断开连接被称为四次挥手,四次挥手与三次握手非常相似,不过三次握手中的第一次握手必须由客户端发起,而四次挥手则两者皆可。**FIN则是通知对方,本端要关闭了,我们称携带****FIN标识的为结束报文段。它的过程为客户端向服务器发起FIN,服务器收到后返回ACK,在返回一个FIN(并没有合并),客户端收到FIN后,在返回一个ACK。**那么为什么中间的两次不能向三次握手一样合并呢?其实是FIN的触发由应用程序代码来控制,当调用了socket.close或者进程结束就会触发FIN,而ACK由系统内核控制的,只要收到FIN就返回ACK,因为close的执行时间不确定,所以就不能和ACK合并。如果服务器始终不进行close,客户端的连接就始终不断开吗?当服务器收到FIN后,此时服务器的TCP状态,就处于CLOSE_WAIT状态,此时虽然这里的连接没有关闭,但是这个连接其实已经不能用了,当前Socket进行读操作,如果数据还没读完(缓冲区还有数据),能正常读到的,如果数据已经读完,此时就会读到EOF。如果进行写操作后,则会抛出异常。因此只能进行close,如果服务器迟迟不进行close,那么客户端就会进行单方面的断开连接(客户端把自己保存的对端的信息就删除了)。如果通信过程中,出现丢包,又咋处理?当最后一个ACK丢了后,此时发出者就视为四次挥手已经结束了,那么它能立刻进行断开连接吗?不能,因为它可能出现丢包问题,因此发出者在发出ACK后会等待一段时间,看看是否有重传来的FIN,有的话在重传一次ACK,没得话就认为没有丢包,就可以断开连接,释放对端信息。
滑动窗口
由于TCP保证可靠传输的前提下,效率较为低下,为了提升它的传输效率,滑动窗口出现了,使用滑动窗口并不能使TCP变得比UDP快,但可以缩小差距。 前面的传输虽然可以保证可靠性,但是要花费大量的时间花费在等待ACK上,使用滑动窗口就是为了减少等待时间 。为了减少等待时间,可以一次性发出一组数据,发这一组数据的过程中,不需要等待ACK,就直接往前发,此时,就相当于使用"一份等待时间"等待多个ACK。而这个把一次发多个数据,不用等待ACK这样的大小,称为窗口。窗口越大,此时批量发送的数据越多,效率也就越高。 但是窗口并不能无限大,如果是无限大,那岂不是不需要等待ACK了,那就是不可靠传输了,并且如果无限大的话,中间的设备是否能处理这么大的数据量也是一个问题。那么什么是滑动呢?其实是当批量发送数据后,当收到一个ACK后就立即在发送一个数据,不用等待所有的ACK到达后在发送下一个数据。这样就能保证每次要等待的ACK的次数都是固定的,直观上来看,就像窗口滑动了一个格子。这就是滑动窗口。**但是如果按照这种方式传输后,丢包怎么办?这种情况有两种,一种是ACK丢了,一种是数据丢了。首先是第一种情况,如果ACK丢了,不需要进行任何处理,因为当收到后面的序列号的ACK后,就说明前面的数据已经接收到了,因此并不需要进行处理。除非是所有的ACK全丢了。其次是第二种情况,当传输的数据丢失后,那么就必须进行重传,那么什么时候进行重传呢?当一个数据丢失后,后面的数据到达后,接收方就将返回一个ACK,里面的确认序号则会包含缺失数据的序列号,因为确认序号就是下一个要获得的数据编号,当多个ACK报文的确认序号都返回同一个序号,那么就可以判定该序号的数据丢失了,需要重传,当重传的数据到达后,返回ACK的确认序号则为后面要传的数据序号,而不是从这个序号开始重新传输。(接收方有个缓冲区在接收数据,将根据序号排序这些数据,如果发现缺失,那么就会索要这个数据,当得到这个数据,如果还有缺失,则会继续索要,如果没有,则直接索要最后一个数据的下一个。)**此时,就相当于使用最小的成本来完成这个重传数据的操作,(只是把丢的数据重传)这种方式也称快速重传,是超时重传结合滑动窗口产生的变形操作。但是并不是所有的TCP都要使用滑动窗口,当数据量小的情况下,就不需要使用滑动窗口。
16位窗口大小
上面说到滑动窗口的大小不是越大越好,为了避免窗口过大,导致接收方处理不过来和丢包问题等使得传输效率降低。因此就需要流量控制根据接收方的处理能力,来限制发送方的发送速度 (窗口大小)。那么如何衡量接收方的处理速度呢?这里就使用接收缓冲区剩余空间的大小来作为衡量指标 ,**就会直接把接收缓冲区的剩余空间大小,通过ACK报文反馈给发送方,作为发送方下一次数据的窗口大小依据,因此16位窗口大小只对ACK报文有意义。**而这个接收缓冲区的剩余空间大小就是16位窗口大小。这里的16位并不意味着窗口大小最大的就是64kb,选项中其中有一个选项就是窗口大小扩展因子,实际的窗口大小是16位扩展因子 <<(向左移动) 扩展因子,此时就可以是能够表示的窗口大小,因此窗口的大小还是非常大的。当接收缓冲区满了之后,发送方会停止发送,虽然发送方停止发送数据了,但也不知道接收方什么时候可以腾出空间来接收数据,就会周期性的发送"窗口探测包"(不会携带具体数据),只是为了触发ACK来查询当前接收缓冲区的情况,一旦发现有空间,就继续发送数据,接收方就可以根据窗口大小来反向限制发送方的传输速度了。
拥塞控制
虽然TCP 有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。TCP引入 慢启动 **机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传 输数据;如果通畅,就增大数据量,如果不通畅,就减小数据量。**而这个数据量就是拥塞窗口。 发送开始的时候,定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口; 像这样的拥塞窗口增长速度,是指数级别的。 " 慢启动" 只是指初使时慢,但是增长速度非常快。当指数增长到一个阈值后,就会从指数增长变成线性增长。当TCP 开始启动的时候,慢启动阈值等于窗口最大值;在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1。所以,拥塞控制为先慢增长,然后指数增长,在线性增长,然后回归小窗口。因此拥塞控制和流量控制,共同的限制了滑动窗口在保证可靠性的前提下高效快速的传输数据。
延迟应答
如果接收数据的主机立刻返回 ACK 应答,这时候返回的窗口可能比较小。那么是否有办法在条件允许的基础上尽可能的提高窗口的大小呢?延迟应答就是为了解决这种情况的,只需要在返回ACK的时候,拖延一点时间,利用这点时间就可以给应用程序腾出更多的消费数据的时间,接收缓冲区的剩余空间就会更大了,这样下次发的数据就可以更多了。因此,此处到底能提升多少性能取决于应用程序处理数据的能力,不过并不是所有的包都可以延长应答。
捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 " 一发一收 " 的。意味着客户 端给服务器说了 "How are you" ,服务器也会给客户端回一个 "Fine, thank you" ;
那么这个时候 ACK 就可以搭顺风车,和服务器回应的 "Fine , thank you" 一起回给客户端。
粘包问题
首先要明确,粘包问题中的 " 包" ,是指的应用层的数据包。 在TCP的协议头中,没有如同UDP一样的 "报文长度" 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。站在应用层的角度,看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。 那么如何避免粘包问题呢?归根结底就是一句话, 明确两个包之间的边界 。
1)对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小
的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可
2)对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位
置
3)对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定
的,只要保证分隔符不和正文冲突即可)
TCP 异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送 FIN 。和正常关闭没有什么区别。机器重启:和进程终止的情况相同。机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset 。即使没有写入操作, TCP 自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。 另外,应用层的某些协议,也有一些这样的检测机制。
可靠性: 校验和 序列号(按序到达) 确认应答 超时重发 连接管理 流量控制 拥塞控制
提高性能: 滑动窗口 快速重传 延迟应答 捎带应答