TCP的报文结构
首部长度:
与UDP不同,TCP没有有描述数据的长度的变量(UDP的是UDP长度),但有描述TCP的报头的长度,为什么要和UDP不同呢?其实是UDP的报头长度是固定的,而TCP的报头是“变长”的。其首部长度就是描述报头的长度,首部长度所占4个比特,但这并不是说用4个比特位(范围为0 -> 15)来描述报头长度,这里的单位为4个字节,也就是把这4个比特位所构成的数值再乘以4个字节,才是真正的报头长度,因此TCP的报头,最大长度为60字节,TCP报头的前20个字节是固定,因此TCP报头的最短长度为20字节。为什么说报头是变长的呢?关键在于这个可变选项,选项可以有多个,也可以没有选项,这就使得报头的长度是变化的,也就影响着报头的长度。所以要使用首部长度来确认报头从哪里结束,数据从哪里开始。
保留位:
现在暂时没用,先占个位置,后面有需要时再用(倘若后面首部长度太小,就可以使用保留位)。因此保留位是给未来升级拓展的空间。
校验和
与UDP的校验和一样。
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来查询当前接收缓冲区的情况,一旦发现有空间,就继续发送数据,接收方就可以根据窗口大小来反向限制发送方的传输速度了。
拥塞控制