1.TCP协议的可靠传输
我们知道TCP协议的特点有:有连接,可靠传输,面向字节流,全双工
其中连接,面向字节流和全双工在《网络编程》该文章中详细说明了,而要深入了解TCP协议,了解可靠传输也是重中之重(可靠传输也是TCP协议的一大特点)
那么TCP协议是如何做到可靠传输的呢?
1.1确认应答
可靠传输不是说100%的传输率就是可靠传输,因为即使协议做的再好,也挡不住别拔网线,TCP协议的可靠传输是指可以让发送方清楚该条数据是否真的送达了.
那么如果要确认是否送达,那么就可以让接收方回复一下就好了,这也是TCP中确认应答机制的原理.
举个例子:
如上这张图,A给B发"可以请你吃饭吗?" B回应"好啊好啊",此处B的回应就称之为应答报文,也叫做ack, 此时A收到了B的回应,A就知道这条消息发送成功了,如果没收到回应,就说明丢包了,没发送过去.
但是网络上可能会出现"后发先至"的情况.这种情况下,收到消息的顺序可能会错乱.
举个例子:
在正常情况下:
如果发生了"后发先至" :
原本B的期望回复顺序是:先回复"吃饭"的"好啊好啊",然后回复"处对象"的"gun",但是由于后发先至,导致先回复"吃饭"的是"gun",后回复"处对象"的是"好啊好啊".
此时就发生了应答错乱的现象,而这种情况又是客观存在,无法避免的,所以TCP中对发送的数据和应答都加上了序号,来确定应答的数据对应的是哪一条数据.
如下图(描述了TCP报文的结构),其中被红圈圈住的序号和确认序号就是TCP中确认应答的机制所在.
TCP的序号方式也很特别,它是按照字节来进行编号的,它给每一个字节都"加"上了序号,但是报文中只记录第一个字节的序号,而应答报文中则要传输的序号是 传输数据的最后一个字节的序号+1
举个例子:当主机A给主机B发送了一条1000字节的数据,那么此时该数据的序号就是1(假设从1开始),而应答就是1001,当下一次进行传输的时候就会从1001开始序号.
如下图:
小结:TCP的可靠传输能力,最主要是通过确认应答机制来保证的,通过应答报文,可以让发送方清楚的知道传输是否成功,又通过序号的方式,将多组数据进行区分.
1.2 超时重传
上面的确认应答中描述的是没有丢包的情况,那么如果在传输过程中丢包了又改如何处理呢?
此处,丢包大致可以分为两种情况:
发送过去的数据丢了
返回的ack丢了(但实际上传输的数据没有丢)
但是无论以上那种情况,在发送方看来就是没有收到ack,无法区分是那种情况,此时TCP就一视同仁的认为:就是丢包了
因此TCP又引入了重传机制,就是把丢包的数据在发送一份.
也就是超时重传:当接收方等待了一段时间没有收到响应,就会进行重传(发送方在发送了一个数据之后,就会等待ack,此时开始计时)
但是超时重传也有一个问题,因为第二种丢包的情况,数据以及发送过去了,此时重传就会造成同一份数据发送了两遍.
如下:
我们想象一下,如果一个支付数据被发送了两遍,那么此时我付了两份钱,但是只得到一份收益,这显然不是我们想要的,TCP肯定不会让这种情况发生.
所以,TCP对于这种重复的数据就有处理办法的,也就是去重.
TCP中存在一个"接收缓冲区"(接收方系统内核里面的一段内存),每个TCP的socket对象都会有一个"接收缓存区"
当主机B收到主机A的一条数据,会先将它放到接收缓存区中,然后,后序应用在进行getInputStream等操作,在接收缓存区中读取数据.
这个接收缓存区我们可以把它理解为一个 优先级阻塞队列 ,根据数据就序号,可以很容易的找出重复的数据,如果有重复的数据,那么新来的这份数据就会被直接丢弃,也就保证了,接收方读到的数据都是不重复的.(至于为什么要使用优先级而不用普通的阻塞队列,是因为在传输中可能会出现"后发先至"的情况,所以要根据序号的大小来排序)
当然重传的数据也有可能会再次丢包,此时TCP会进行N次传输,每次重传的时间间隔都不同,会越来越大,当丢的次数比较多的时候就不会在重传了.
小结:可靠传输是TCP中最核心的部分,TCP的可靠传输就是通过 确认应答 和 超时重传 来进行实现的.其中,确认应答描述的是传输顺利的情况;超时重传描述的是传输出现问题的情况.
2.连接管理
2.1 建立连接(TCP三次握手)
TCP中的连接,不是我们真正意义上的连接,而是通讯双方各自有一块空间存储对方的ip和端口.
如下图:
当A和B的这块空间被维护好了,此时A和B就建立了连接,同时存储这些信息空间的数据结构也被成为连接
那么这个连接是如何建立的呢?
此时就需要A和B之间进行信息的交互了.
举个例子:
假如有一天我向女神进行表白,此时我们的表白流程是这样的,如下图:
首先我向女神进行表白,女神同意, 又向我进行了表白,我也同意了.此时我和女神之间的连接就建立完成了.
而其中的每一次交互就相当于一次"握手",其中一共进行了4次交互,理论上应该要进行4次握手.
但是实际上,TCP建立连接虽然需要进行4次交互,但是实际上只需要进行三次握手,是为什么呢?
因为其中女神给我的两句话完全可以合并成一句话,如下图:
此时交互次数就由4次变成了3次了.
而TCP需要进行4次交互,但是只进行3次握手的原因也很简单.
因为每一次交互都是发送应该数据报,每一个数据报都需要进行各各层级的 封装和分层 ,这其中也是有不小的开销的,所以TCP之间建立连接只进行3次握手.
小结:TCP的3次握手,本质上是4次交互,通信各方要各自想对方发起一个"建立连接"的请求,同时也需要回复对方一个ack.
那么此时问题来了,既然减少握手有消耗,那如果只进行2次握手是否可行呢?
如下:
我们发现上面的对话很怪,但是也可以说得通.
我发出了"做我女朋友"的请求,同时还告诉她"如果她同意,那我也做他的男朋友"
在TCP中这样的交互是可行的,但是这样的前提是:3次握手除了各自保存对方的信息之外,没有其他的用途
这个前提显然是不存在的,因为TCP的3次握手还有其他功能:确认交互双方的发送能力和接收能力是否正常.
举个例子:
在现实生活中,我们和别人打电话时,总是要确认对方的设备情况是否正常.
此时A给B打电话:
第一次对话:
A先给B说了一句"能听到吗?",假如此时B听到了
此时B知道:A的麦克风正常,B的听筒正常
此时A知道:(空)
第二次对话:
此时B给A回复:"能听到,你呢?"
此时B知道:A的麦克风正常,B的听筒正常
此时A知道:A的听筒正常,A的麦克风正常,B的麦克风正常,B的听筒正常.
第三次对话:
此时A给B回复:"我也能听到"
此时A和B都知道:彼此双方的听筒和麦克风都是正常的.
那么如果是只有两次握手,不管里面的内容那么丰富,B始终都不知道A的听筒和B的麦克风是否正常,也就无法保证通信双方的发送能力和接收能力是正常的.
小结:3次握手在一定程度上保证了TCP传输的可靠性.
除此之外,TCP3次握手还有一个作用:在握手的过程中,双方来协商一些重要参数.
如下图:
其中syn代表的意思是同步报文段,ack代表的意思是回应报文
在TCP通信过程中,有些数据需要通信双方进行同步,此时恰好可以通过3次握手来完成数据的同步,而不需要额外的交互过程.
总结TCP三次握手的意义:
让通信双方各自保存好对方的信息.
验证通信双方的发送能力和接收能力是否正常.
在握手的过程中,双方协商一些重要的参数.
2.2 断开连接(TCP四次挥手)
四次挥手和三次握手的本事十分相似,都是通信双方发出请求,然后对方进行回应,而挥手代表的加锁断开连接的请求,如下图:
注意:断开连接既可以是客户端想服务器发起,也可以是服务器想客户端发起,此处使用客户端发起为例.
首先客户端先服务器发起FIN,也就是断开连接的请求
然后服务器给出了客户端FIN的回应
在然后,服务器向客户端发起FIN
最后,客户端想服务器给出回应
到此连接断开.
那么此处服务器的ACK和FIN是否可以打包到一起变成三次挥手呢?
答案是不可以!
在三次握手中,之所以可以将ACK和SYN打包到一起发送,是因为ACK和SYN的发送是处于同一时期的.会处于同一时期的原因是,TCP建立连接的过程是完全由操作系统内核来操控的,应用程序无法感知和操控,当服务器的内核收到SYN之后,就会立即发送ACK,也会发送SYN.
但是断开连接不一样,FIN的发起是由应用程序来进行控制的,当应用程序调用socket的close方法或者程序退出,就会触发FIN.
而ACK是由系统内核控制的,当服务器收到FIN时,服务器主机的系统内核就会立即发起ACK,而服务器的FIN什么时候发送是由服务器的程序来决定的(close或退出),如果ACK和FIN之间的发送间隔比较短,那么ACK和FIN是可以合并到一起发送过去的,但是如果FIN的发起间隔比较久,那么就无法合并成一个进行发送了,所以无法将四次挥手全部看成三次.
在四次挥手中,当接收方收到FIN的请求后会进入CLOSE_WAIT状态,此时接收方会等待应用程序执行close或者退出程序.
当发送方收到接收方发来的FIN数据报时,会发送ACK并进入TIME_WAIT状态,此时可以认为四次挥手已经完成,但是TIME_WAIT状态还要保持TCP连接不要释放一段时间.
等待的原因是:传过去的ACK数据报可能会丢包,此时接收方要重新传过去一份FIN,如果此时连接已经断开了,那么接收方就无法收到ACK,TIME_WAIT保持一段时间的原因就是为了能处理最后一个ACK出现丢包的情况,可以在收到重传的FIN之后发送ACK.
TIME_WAIT的等待时间为2MSL(通常情况下MSL为60s),当等待时间达到这个时间还没有收到重传的FIN,就会认为ACK已经被送达,连接就会真正被释放,此时即使ACK还是没有发送过去也没办法了.
3.滑动窗口
TCP的滑动窗口机制,本质上是为了让TCP的效率尽可能快一点,因为引入了可靠性,导致TCP在传输效率上面相比于UDP要慢了很多,而传输效率的降低主要体现在等待ack上面.
滑动窗口机制就是在一定程度上缩短了等待ack的时间.
对于确认应答机制来说,每发送一条数据,都要等待对方的ack,然后在进行下一条的传输.
如下图:
滑动窗口机制,就是让一组数据不等待的批量发送,然后使用一份时间等待着一组数据的ack,其中,将不需要等待就可以发送的数据最大的量,成为窗口大小.
如下图:
上图中的窗口大小就为4000.
当批量发送了一次数据之后,并不会等待这一批数据的ack全部到达之后在发送数据,而是收到1条ack之后,就会接着发送下一条数据.
那么在滑动窗口这样的机制下,如果发生丢包该怎么办呢?
此处可以将丢包分为2种情况:
ack丢了
数据丢了
第一种:ack丢了
此时其实不用进行过多的处理,虽然ack没有接收到,但是数据是确实的发送过去了,那么当下一个ack传过来的时候就可以将丢失的ack信息给补全.
如下图:
虽然ack1001和ack2001丢包了,但是ack3001传输成功,这个3001可以包含前面的1001和2001(因为确认序号的含义就是告诉发送方:3001之前的数据已经接收到了),此时主机A就清楚1~1000和1001~2000以及2001~3000的数据已经传输过去了,不用等ack了,
第二种:数据丢了
如下图:
此时1001~2000的这条数据丢了,主机B没有收到,那么接下来主机B再收到其他(不是1001~2000)的数据时,它仍然会发送1001的ack,此时主机A发现,主机B多次发送1001的ack,就意识到1001~2000的这条数据可能丢包了,会再次进行发送,当主机B接收到了1001~2000这条数据,此时主机B已经接收到了1~4000这些数据,那么此时会发送4001的ack,当A收到了4001之后,就会发送4001~5000这条数据了,以此类推....
4.流量控制
流量控制是一种干预窗口大小的机制.
在滑动窗口机制下,理论上,窗口越大,一份时间里等待的ack就越多,传输效率越高.
但是如果窗口变得无限大,不等ack,此时可靠性就无法得到保障.
同时,窗口太大也会消耗大量的系统资源,因为同一时间的等待数据过多需要的缓冲区空间就越大.
如果一次发送的量太多,太快,接收方处理不过来,丢包了,效率就更慢了.
所以,我们需要对窗口的大小进行一个约束,此时就引入了流量控制的机制.
流量控制机制,会根据接收方的接收能力来判断当前的窗口大小为多少.
接收方的接收能力的判断,是根据接收方当前缓冲区的剩余空间来决定的
假设主机A是发送方,主机B是接收方.
每当A给B发送了一条数据,B就需要算一下当前缓冲区中的剩余空间,然后将这个值通过ack反馈给A,A再根据这个值来决定接下来的窗口大小时多少.
所以,窗口的大小也不是一成不变的,而是随着接收方的缓冲区剩余空间大小,一直进行动态变化.
在ack数据报中,有一个模块表示了ack返回的窗口大小数值.
如下图:
这个16位窗口大小,并不代表窗口大小最大只能是64KB,在选项中有一个窗口扩展因子.
假如窗口大小里面已经是64KB,扩展因子里面写了一个2,此时就可以表示64<<2,也就是256KB.
而当窗口大小为0时,发送方就要暂停发送,在暂停发送的过程中,发送方会给接收方定期发送窗口探测报文,这个报文不携带具体的业务逻辑,只是为了触发ack查询窗口大小,当窗口大小不为0了,就会继续发送数据.
5.拥塞控制
上面说到的流量控制是针对接收方的处理能力来判断当前窗口大小.
但是,接收方有能力接收这个频率的数据,不代表其中间节点的设备(交换机、路由器等)可以承受得住.
但是,这些设备接收方是无法感知的,它无法感知中间究竟有多少个中间节点,而且每次数据的传输路径可能都不相同,因此,中间节点的阈值是无法通过信息的传递来获取的.
所以,设计TCP的大佬们选择使用"实验"的方式来测试出一个合适的值.
如下图:
这张图的纵轴(拥塞窗口)代表当前尝试以多大的窗口大小进行发送;
横轴(传输轮次)代表是第几次传输.
首先,第0轮传输是以1个窗口开始的,当传输顺利,没有丢包时,就会进行扩大窗口的操作.
前几次会不断以指数增长的状态快速增长,当增长到阈值之后,会进行转变成线性增长.
而在其中的传输过程一旦发生丢包的情况,说明此时的发送速率已经接近当前网络的极限了.此时会将窗口大小一下缩短成一个很小的值,同时,指数增长的阈值也会随之下降,下降的大小为:发生丢包时的大小除以2,然后再重复刚才的操作.
所以,拥塞窗口也不是一个固定数值,而是一直动态变化的,在这个变化的过程中,拥塞窗口的大小会逐渐趋于动态平衡.
这样做,既可以将问题解决,又可以随着网络的动态变化而变化.
而实际的窗口大小,是取 拥塞窗口和流量控制窗口中的较小值.
6.延时应答
延时应答机制是基于滑动窗口的基础上,尽可能的增大窗口的大小(在允许的范围内)
其原理也很简单,就是接收方在返回ack的时候,稍微等待一会,然后在返回ack.
这样做的好处就是:接收方晚发送了一会,在这段时间里面,接收方就可以多处理一些数据,此时接收方的缓存区的剩余空间就更大了,就可能让下一次的窗口大小增加.
如下图:
比如在这张图里面的策略是,每隔一条数据,返回一次ack.
7.捎带应答
捎带应答是建立在延时应答的基础上的机制,也是提高TCP的效率的机制.
假设现在有一个场景,如下图:
因为ack是由系统内核进行发送处理的,所以当收到A发来的请求时,B就会立即发送一个ack过去,业务响应随后到达.
但是如果在上面的场景中出现了延时应答,那么此时B就不会发送ack,但是紧接着B又给A发送了业务上的响应,此时ack就可以随着这个响应一起被发送过去.
如下图:
和四次挥手比较相似,都是有一定的几率会合并,将本来不同时机的数据,在延时应答的影响下,成为了同一时机的数据,进行了合并的操作,而延时应答就是提高了这样的概率.
8.面向字节流--粘包问题
TCP是面向字节流的,但是面向字节流,会导致一个问题--粘包问题
什么是粘包问题呢?
我们知道,服务器收到的数据会存放到缓存区里,当服务器进行read的时候,就会从缓存区读取数据,但是,读取的单位是字节,我们不清楚每一个请求里面有多少字节,那么就可能会出现一次读半个数据报、一次读一个半数据报.....这样的情况.
如下图:
假设一次读4个字节,那么第一次读的就是aaaa,第二次是abbb.... 这就属于是一次读半个数据报
假设一次读6个字节,那么第一次读的就是aaaaab,第二次是bbbbcc... 这就属于是一次读一个半数据报
但是我们要处理请求,是需要一次读一个数据报,无论是多了还是少了都无法正确的传达用户的请求,这样的问题成为粘包问题.
解决方法也很简单:约定好应用层协议就好了.
也就是在应用层协议中规定好 应用层数据报 和 应用层数据报 之间的边界.
比如:约定好分割符.(比如:以"\n"作为分隔符......)
再比如:约定好每个包的长度.(比如:每个包都是5个字节......)
等等...
这样就可以很好的解决粘包问题了.
9.异常情况
在TCP传输过程中可能会出现一些不可抗力的因素导致TCP无法整除正常连接
大致有以下四种情况:
进程崩溃了
电脑关机了(正常进行手动关机)
电脑掉电了(死机、拔电源等不正常关机)
网络断开了
进程崩溃和电脑关机
进程崩溃其实就是进程关闭了,进程关闭就会进行资源的关闭,也就会执行socket的close,此时会进行四次挥手,此时属于是一个正常的断开.
电脑关机和进程崩溃类似,因为电脑要关机需要先关闭所有的进程,在关闭进程的时候也会进行四次挥手(虽然可能没有挥完就关机了,但是无大碍),也属于是一个正常的断开.
电脑掉电和网络断开
电脑掉电需要分析两种情况
第一种:服务器掉电了
服务器掉电了,就意味着客户端的请求无法得到ack回应,此时就会进行超时重传,但是也不会得到回应,接下来会尝试进行TCP重连,同样也会失败,此时客户端就会单方面放弃连接.
第二种:客户端掉电了
客户端掉电后,服务器长时间没有收到请求,此时服务器会给客户端周期性的发送一个消息(心跳包)却对方工作是否正常,如果没有回应,就会放弃连接.
(心跳包属于保活机制,会周期性的发送消息,如果没有回应认为对方挂了)
网络断开就相当于客户端和服务器各自执行上面的操作.