TCP协议是传输层协议的重点协议,对数据的传输进行一个详细的控制,对数据传输提供的管控机制,主要体现在:安全和效率。在保证安全的前提下,尽可能提高传输效率。
TCP协议结构
说明:
(1)源/目的端口号:表示数据时从哪个进程来,到哪个进程去;
(2)32位序号/32位确认号:序号表示给发送的每一个数据,进行编号;确认号是如果当前的报文时一个普通报文,确认序号不生效;如果是应答报文,确认序号表示应答的哪个普通报文;
(3)4位TCP报头长度:TCP头部还有多少个4个字节,最大位15*4;
(4)6位标志位:确定状态。URG,紧急指针是否有效;ACK,确认号是否有效;PSH,读取在TCP缓冲区的数据;RST,对方要求重新建立连接,称为复位报文段;SYN,请求建立连接,称为同步报文段;FIN,通知将要关闭,称为结束报文段;
(5)16位窗口大小:一次最多发送数据量大小;
(6)16位校验和:校验数据是否正确;
(7)16位紧急指针:标记哪些部分是紧急数据。
TCP协议的可靠性机制
TCP协议的几个特点:有连接、可靠传输、面向字节流、全双工。其中,可靠传输是TCP最核心的部分,TCP内的很多机制,都是在保证可靠传输。可靠传输指的是发送方发送过去后,接收方收没收到,自己是清楚的,而不是发送之后,对方一定能收到。UDP协议就是不可靠传输。那么,如何保证可靠性?使用确认应答的模式,发送方发送过去后,接收方如果接收到,就需要回应。
确认应答
当我给女神发消息的时候,如果我收到女神的回信,那么说明对方是一定收到了的;如果我没收到,估计是对方没看到,或者是对方看到了,但是没理我。这就叫确认应答的方式。发送的消息比较少的时候,还好点,但是消息比较多了,可能出现以下的情况。
很明显,女神回复的“可以”是针对“今晚能一起吃个饭吗”的回答,而“滚”是针对能不能做女票的回答。 还有一种情况,就是网络中常说的后发先至,后发的可能先到,先发的可能后到。
这样的情况下,不能通过顺序,来判断当前的应答报文在应答哪个数据。不过,整个过程可以做一个调整,在报文前加一个标记。对于做女票,回复的什么,对于吃饭,回复的什么。更简单的,可以做编号。
可靠性不等于安全性,和安全性没有任何的关系。安全性指的是数据被截获之后,不容易理解内部的意思或者被篡改,是通过加密来做到的。
确认应答,要针对数据进行编号,然后才能明确,应答报文是在应答哪个数据。TCP通过引入“序号”来减少后发先至带来的问题。TCP是面向字节流的协议,在编号的时候,是按照字节来进行编号。
我发送1000个字节的数据,TCP报头中的序号就是1~1000,应答报文可以看成是只有TCP报头,没有载荷。确认序号是1001,说明小于1000的数据,女神已经收到了。接下来我需要从1001开始发送。至于如何确定是普通报文还是应答报文,在6位标志位处ACK就能知道。在确认应答的情况下,如果收到ACK就很好办了,但是没有收到的话,需要通过其他的途径来处理。
超时重传
当接受方没有收到ACK(确认序号)的时候,不是说就会立即放弃,而是隔一段时间后发送方尝试重新发送。网络环境是比较复杂的,有的时候,会出现网卡的情况,可能是网络拥堵,导致数据报从客户端到服务器之间,消耗的时间比较长;也可能是丢包严重。丢包指的是一个或多个数据数据包 (packet)的数据无法透过网上到达目的地。丢包率达到5%,就能够感到卡顿。丢包是无差别的,任何一个数据报都有可能丢包。具体需要分两种情况。
(1)业务数据丢包。发送方发送数据报后,如果等待一定时间后没有收到ACK,就会重传数据。
(2)ACK丢包。业务数据已经到了接收方了,但是反馈的ACK没有回过去,但是发送方不知道,发送方等待了一会后,就触发了重传。
以上两种情况,对于发送方来说,无法区分是业务数据丢了还是ACK丢了,只能做的事就是重传,这种保证可靠性的方式就叫超时重传。这两中情况还是有区别的,下面的情况接收方接收到两份数据。如果这个时候,在应用层读取数据,是读出一份数据的。此时,接收方就会根据序号来进行数据去重,当发现收到重复的数据,就会自动丢弃,保证当前读到的数据是不重复的。
丢包有没有可能丢了一半,比如说发送的数据是1~1000,结果收到的是1~500。这个概率是很小的,但也是有可能的,只需要经过校验和的校验就可以知道了,如果有问题就会主动丢包来丢弃。
超时重传也是有概率丢包的,但是站在概率的角度来看,是很小部分的情况了。超时重传的超时时间具体是多少,是操作系统来配置的。连续两次都没发送过去,意味着当时发送的丢包概率是相当大了。多次都没有发送过去,可能是网络遇到了很严重的问题。超时重传不会无限制的重传下去。多次无法传输,就会放弃尝试,并且断开连接。
确认应答和超时重传是保证TCP可靠性的核心机制。
TCP协议的连接方式
TCP是有连接的协议。
三次握手
建立连接的过程,称为“三次握手”。主动发送的一方是客户端。三次握手,一定是客户端先发起。
整个过程,就是两队操作:客户端和服务器,互相给对方发送了一个syn,再互相给对方发送了一个ack。一共是四次交互,完成整个过程。中间的两次,可以合并成一次,就变成了三次握手。
三次握手的时候,服务器返回的ACK和SYN都是操作系统内核收到发送方的SYN后,立即返回的,时间间隔很短,因此可以合并。下面要讨论的四次挥手中间两次则不能合并。
每一次数据报文传输,都要经过一系列的封装和分用。分成两个包来发,就比一个包的代价很大。可以进行合并,合并成一个包,成本就比较低。
TCP报文格式的6位标志位中,第二位表示ack,当值为1的时候表示确认报文。第五位表示syn,当值为1的时候表示同步报文。第二位和第五位都是1的时候,就可以表示合并的ack+syn。
三次握手,认为是一种保证可靠性的机制。相当于投石问路。在正式通信之前,先确定好通信的链路是不是畅通。如果通信链路不畅通,后序大概率是要丢包的。 握手次数最好的是三次。四次是可以的,但是效率比较低;两次是一定不行的,三次握手是在验证双方的发送能力和接收能力是不是正常的,而两次握手验证不了这个问题。
三次握手,还能够让通信双方协商一些重要的参数。
四次挥手
三次握手是断开连接的流程。三次握手,是客户端主动发起的第一次,而四次挥手,客户端和服务器都可以主动发起。
四次挥手的过程看起来和三次握手差不多,也是双方各自给对方发送FIN,各自给对方发送ACK,但是四次挥手的中间两次,是不一定能够合并的。服务器发送的ACK是内核的行为,操作系统收到FIN之后,就会立即返回ACK,而接下来服务器的FIN是用户代码的行为,需要用户在代码中调用相关的关闭方法,才会触发FIN。所以,服务器发送FIN和发送ACK之间有着不可忽视的时间间隔,不一定能合并。
TCP建立连接,是没有历史包袱的,操作能够立马完成,而断开连接,很可能发送方发送的FIN,接收方还没有读取完成,接收方是不会立即断开的,要把没有处理的数据都要进行处理。接收方啥时候发送FIN,就是代码层次的了。创建一个Socket对象,其实在实例化的时候,就在建立连接。
过程中的状态转换
状态标识当前这个线程在干什么。TCP的状态比较多且比较复杂,只需要认识其中一些重要的状态。
三次握手中的几个状态:
(1)Closed状态。服务器和客户端之间还没有建立连接的时候的状态,不是一个真实的状态。
(2)Listen状态。服务器状态,表示服务器已经启动完成,已经绑定端口号成功。类比于手机已经开机了,信号良好,随时可以打电话。
(3)Established状态。连接建立好了,可以进行后序的通信。类比于打电话的时候已经拨通电话,可以随时交流。
四次挥手中的几个状态:
(1)Close_Wait状态。谁是被动接受FIN的一方,进入该状态。表示自己收到FIN,也返回ACK,在自己调用close方法发送FIN之前的中间状态。该状态就是wait close,等待代码中调用close方法来发送FIN。如果在服务器中发现大量的close_wait,说明程序有可能bug了。
(2)Time_Wait状态。主动发起FIN的一方,就会进入Time_Wait状态。主动的一方,收到对方发送来的FIN,并且返回ACK之后,就会进入该状态,而不是进入Close_Wait状态,会在该状态下停留一定的时间后才进入Close状态彻底释放连接。
Time_Wait状态的意义,就是防止最后一个ACK丢失。万一最后一个ACK丢失,在该状态下,就可以重传,而不是直接连接释放。
Time_Wait等待的时间是操作系统中的一个配置的参数。比如等待2MSL,其中MSL表示两个主机之间,数据从一边到另一边所花费的最大时间。
提高TCP的传输效率
可靠性和效率是冲突的。保证了可靠性,肯定会影响到效率。TCP在保证可靠性的前提下,尽可能提高效率。
滑动窗口
提高效率的机制,本质就是把等待ACK的时间重叠起来,减少了等待时间,相当于提高效率。
在滑动窗口的优化下,不在是发送一条等待一条了,而是发送一批,等待一批ACK。在不等待的前提下,最多可以一次发送N条数据,这里的N条数据,就是窗口的大小。虽然一次发送N条,会出现后发先至的问题,但是TCP数据上是带有序号的,数据就会在TCP接收缓冲区中整队,接收方根据序号,来对数据进行重新的排序。N越大,同时批量发送的数据就越多,传输的效率就越高,但是N也不是越大越好。
上图中,对于1001~5000,针对这4个数据报,等待ACK。当2001的ACK回到A的时候,1001~2000这个数据已经被接收到了,就继续发送5001~6000这个数据报。每一次收到一个ACK,窗口都会对应的往后移动,继续发送后续的数据。整个过程就好像是在滑动一样。
在滑动窗口下,确认应答还是正常的应答,但是存在丢包的问题。还是区分ACK丢包和
(1)ACK丢包。ACK丢包只是个别的丢包,不会出现全部丢包的情况。
如图,如果2001丢了,但是3001到了,对于客户端来说,是知道2000 ~ 3000这个数据是接收方接收到的。后一个会涵盖前一个。
(2)数据报丢包,需要重新传输。
这个时候,主机A发现1001连续多次返回,说明1~1000丢包了,需要进行重传。这里的原则是哪条丢了就重传哪条,已经传输到的数据,不必重复传输,没有冗余的动作(快速重传)。
滑动窗口能提高效率,指的是相比没有滑动窗口,普通的确认应答。如果和无可靠传输(UDP)相比,效率还是微微差点的,不会比UDP更快。
流量控制
滑动窗口。窗口大小越大,发送的速率就越快。流量控制,就是在针对发送速率进行制约。前面我们说过,整体的传输速率 = 发送速率 & 接收速率。如果发送速率 > 接收速率,在这个时候继续提高发送速率,就不能够提高整体的效率了,反而因为接收方丢包,触发更多的重传。流量控制,就是让发送速率和接收速率的步调一致,既能保证可靠性,又能保证效率不受影响。
滑动窗口的窗口大小用来衡量发送的速率,接收的速率该如何衡量?使用TCP的接收缓冲区。
从接收缓冲区中读取数据,取决于应用程序的代码时什么写的,一次读取多少,多少时间读取一次。流量控制,就是通过控制接收缓冲区剩余空间的大小,来作为下一次发送时候的窗口。
流量控制时限制滑动窗口的机制。接收方通过ACK报文中带上接收缓冲区的剩余空间,来告诉发送方,这些信息就在“16位窗口大小”中。
16位窗口大小是当当前的报文是ACK报文的时候,会生效。这个窗口大小,表示了接收缓冲区中剩余空间的大小,根据这个大小,进一步的影响到发送的速率。
拥塞控制
流量控制是站在接收方的角度来控制发送的速率,但是整体的传输,其实不光是有发送方和接收方的,还有中间的一系列的用来转发的设备。要控制发送方发送的快慢,不能光考虑接收方的接收能力,还要考虑中间设备的转发能力。
要想衡量中间设备的转发能力,面临着几个问题:中间的设备有几个、中间的设备的各个参数是啥、两次传输后经历的中间设备是不是相同的等。
解决的办法就是做实验。通过实验的方式,找到一个合适的窗口大小。刚开始的时候,按照小的窗口发送,如果不丢包,说明网络中间环境比较畅通,就可以继续放大发送窗口的能力。但是当放大到一定的时间,速率比较快,网络上很容易出现拥堵。当发送方发现丢包后,就减少发送的窗口大小。后面继续反复,一直到找到合适的窗口大小。
整个过程,就达到了一个动态平衡的状态,发送的速率不慢,接近了能承载的极限,同时还尽可能的减少丢包,还能够适应环境的变化。
流量控制和拥塞控制,都是在通过控制窗口的大小,来制约发送方的发送速率,在保证可靠性的前提下,尽可能提高发送的速度。最终的窗口大小,取决于二者的较小值。如果拥塞控制的窗口比较大,流量控制的窗口比较小,说明中间节点的转发能力强,接收端的代码,处理的比较慢;如果是拥塞控制的窗口小,流量控制的窗口大,中间节点转发能力弱,接收端的代码处理的比较快。
延迟应答
延迟应答也是用来提高效率的机制。流量控制和拥塞控制用来限制窗口大小,而延时应答则是让窗口大一些。延迟应答,让接收方不是立即返回ACK,而是稍微等一会,发送方以滑动窗口的方式发送,这种发送是批量的发送。延迟应答下,ACK不一定要和发送的数据报一一对应,是可以涵盖的。
捎带应答
基于延时应答的策略,为了提高传输效率。延时应答,延时一会,就可能和返回响应数据,导致多条ACK在时间上就可能重合。响应在收到请求后,多长时间内返回是不确定的,如果是快的时候,就可以搭个顺风车,减少报传输的次数
面向字节流
面向字节流指的是在读写载荷数据的时候,是按照“字节流”的方式来读取的,而TCP数据报,本身仍然是一个一个的“数据报”这样的方式来传输的。前面我们说的数据到了接收缓冲区,然后的读取,就是字节流的方式来读取的。
面向字节流最核心的问题是粘包问题。
粘包问题
如果一个TCP连接,里面只传一个应用层数据包,这个时候,不会粘包(短连接)。如果一个TCP连接里,传输多个应用层数据报,这个时候就不容易区分从哪里到哪里是一个完整的应用层数据(长连接),这种问题,就叫粘包问题。
粘包问题的解决办法:在应用程序代码种,明确包之间的边界,比如使用分割符号,或者约定长度。这些就需要用到自定义的应用层协议了。
UDP和TCP协议的应用场景
(1)如果关注可靠性传输,优先考虑TCP;
(2)传输的单个数据报比较大的时候,优先考虑TCP;
(3)UDP协议,对于可靠性不高,但是对于性能要求高。比如网络环境简单,带宽充裕,并且希望主机之间的通信足够快,这个时候,就可以使用UDP协议;
(4)如果需要进行广播,可以优先考虑UDP。