前几天发了一个朋友圈,发现暗恋已久的女生给我点了个赞,于是我当晚辗转反侧、彻夜未眠!想着妹子是不是对我有感觉呢?不然怎么会突然给我点赞呢?要不趁机表个白?
于是第二天我在心中模拟了多次表白的话语,连呼吸都反复练习。到了晚上,我拨通了妹子的微信语音,还没等对方开口我就按捺不住内心的想法,开始自说自话,一阵狂乱的表达...足足五分钟一气呵成,一切都是那么自然!
可是在我说完之后却半天都没有等到妹子的回应...过了好一会儿才听到对方的声音:“喂!喂!我这边信号不好,你刚刚在说啥我一句都没听到,我在跟我男朋友逛街呢...”。
我挂断了电话,我也对我这次失败的表白进行了深度的总结!原因就是因为我没有学好TCP!
如果我懂TCP,那我在表白之前至少要先问一句“”!先建立可靠的连接,确保连接正常才能开始表白!
如果我懂TCP,那我在我说话的过程中需要对方不断的确认,这样才能保证我说的每一句话对方都能听到!这样我才能表白成功!
所以一切都是因为我没有学好TCP,于是我走进了图书馆...
我们先来看下TCP的定义:
这里面每一个字我们都认识,但是连在一块就不是那么好理解了!那我们就提炼一些关键的词,也就是我上面高亮的那些:面向连接、可靠、基于字节流、传输层、协议、端到端!理解了这些关键字也就理解了TCP的实现原理,那我们就来从这些关键字开始进行分析!
传输层
我们先讲传输层,因为可以从比较高的层面去看TCP,我们先看下经典的OSI七层网络参考模型:
当我们需要在网络上进行数据交换的时候,就需要经过这么几层。每一层都有相关落地的实现,我们今天要讲的TCP就是传输层的一种落地实现。可能我们平时在说到传输层的时候自然而然的就想到的TCP,但是TCP只是传输层的一种实现,其他比较常见的传输层协议还有UDP等!
我知道干巴巴的文字对你来说太抽象,那我就抓个包来看看,让这几层更加具象!本文中所有的包都是通过postman发送请求,然后用wireShark来抓的!如果对这两款软件还不了解的盆友可以先去了解下哈,这里不过多说明。我们在postman中输入www.17coding.info的域名,然后发送请求,wireshark就能抓到数据包了。
图上已经标明每一层与抓到的数据包对应的关系了!咦!我们上面不是说的7层网络参考模型么?为什么数据包只有5层呢?注意参考二字,7层模型是一个理论模型,实际的网络中往往都把应用层、会话层、表示层统为应用层!
什么是协议?
说到协议,就是双方共同遵守的一种约定!比如我写的这篇文章里,你能够看懂我写的每一个字并明白我的意思,那就是因为我们都遵循了汉语的语法,这本身也就是一种协议。还有比如我们写代码就必须按照规定的语法进行编写,这样编译器才能进行正确编译。
在计算机网络中也有很多协议,比如常见的应用层协议http、ftp、dns协议等等。常见的传输层协议有TCP、UDP等等...其实这些协议都是发送方和接收方都在遵循的一种规范。如果我们遵循了其规范,也能成为协议的实现者,比如自己写一个web服务器处理用户请求。甚至我们还能自己规定一套协议,供别人使用!
TCP头部格式
我们前面说了协议的定义,那TCP协议肯定也有一定的规范咯!这样通信双方才能识别对方的数据报文,进行数据交换,我们先看下TCP的报文格式
TCP报文包含数据头和数据体,头部有5行的固定长度以及1行可变长度!图上前面5行就是固定长度!固定长度的每一行占有4个字节(32位)。因此头部固定长度就为5*4=20个字节!
到这里我们可以抓个包来看下加深印象,我们依然向www.17coding.info发送一个请求,然后看看其TCP部分的数据包
接下来那我们就一行一行的来分析TCP的头部:
第一行:
前面我们说到TCP是的,这里就能很好的体现了!每个数据包中都有发送方和接收方的端口。这里每个端口占用2个字节(16位)。
第二行、第三行:
这里的序号和确认号是保证所不可或缺的,我们后面会通过抓包来详细分析!序号和确认号分别都占用了4个字节(32位)!
第四行:
第五行:
第六行:
我们找到一个数据包,看看其详细的头部数据:
面向连接怎么理解
从我表白失败的例子就能看到,我还未确保连接的正常就开始表白,导致我说完了对方却因为信号不好没有听到。如果我事先确保连接正常,就不会出现这样的情况了!我们前面说了TCP是面向连接的,那TCP是怎么面向连接的呢?
三次握手交代了什么?
没错,都是从握手开始!我们都知道,tcp建立连接需要经过三次握手,那每次握手都交代了什么呢?如果只进行两次握手行不行?我们先看一个电话接通的场景:
在正式通话之前,为了确保通话的可靠,往往都需要经过上面的三次对话进行确认。那这三次对话是必须的吗?每一次对话的必要性又是什么呢?
只有经过三次的对话,才能确认自己的声音能被对方听到且能听到对方的声音。这也才能开展后续的对话。这里我们就不得不祭出经典的三次握手图了:
我们分析三次握手过程及每次握手后的状态如下:
这里我们要注意的几点是:
我们依然向www.17coding.info发送请求,下面为三次握手的包:
在info那一栏,我们很明显的能看到发送的数据包头部有我们上面说到的那些标志位,还有Seq、Ack等头部信息,还有Win、MSS等头部选项数据!因此三次握手!
当我鼠标选择某一行时,如果这个数据包包含了对某个数据包的确认(也就是有ACK的标记),就能在对应的数据包的No列上面看到一个小勾勾,比如上面图中我鼠标选择的是第三次握手的数据包,在第二次握手的数据包前面就有个小勾勾。
为什么握手只需要三次而挥手需要四次?
通过三次握手,双方就建立了一个可靠的连接,就能进行数据的传输了!当数据传输完成,就得将连接关闭,因为连接也是一种资源!连接的关闭需要经过四次挥手!
为什么握手可以三次完成,但是挥手却需要四次呢?我偏要三次行不行?其实也没啥不可以的!比如下面的对话场景:
这样三次对话就可以实现挥手了,但是在实际的网络中,当我发出一个请求的时候,可能服务器的响应体比较大,需要较长时间的传输!所以当客户端主动发起断开请求的时候,服务器先回应一个确认,等所有数据传输完毕后再发送服务器断开的请求。
所以大部分情况下都需要进行四次挥手!但是,在我个人的抓包实践中,也会有三次挥手就能完成断开连接的情况。
这里我们又不得不祭出经典的四次挥手图了:
我们分析四次挥手过程及每次挥手后的状态如下:
在图中我们能看到,A的TIME_WAIT状态会持续2MSL再变成CLOSED,MSL(Maximum Segment Lifetime)的中文可以译为“报文最大生存时间”!他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。那TIME_WAIT维持2MSL的作用是什么呢?
我们看上面向www.17coding.info发送请求的挥手数据包:
可能大家在抓包的时候不能立马看到四次挥手的数据包!那是因为在HTTP1.1及之后,默认都开启了长连接!也就是在一次请求之后,建立的连接并不会立马关闭,而是供后续的其他请求继续使用,以减少每次重新建立连接的资源消耗!如果想发出请求后立马能抓到四次挥手的数据包,可以设置Http的头部Connection:close
。这样每次发送请求都能看到完整的三次握手四次挥手的过程啦!
TCP是怎么保证可靠传输的?
保证传输的可靠我们前面已经说到了面向连接,建立连接是保证数据传输的第一步。那在连接建立之后的数据传输怎么保证可靠呢?
我们再次回到我们打电话的场景,一般在对话的过程中,都是得双方都有互动,给与对方回应。而不是一个人一个劲的说而另一方没有任何回应!比如下面场景:
这样的确认和应答就确保了双方的通信能够完整可靠。TCP也采用了这种y应答和确认重传的机制,保证在不可靠的网络上实现可靠的传输。只要我没有收到确认,我就认为没有发送成功,就会重发。
停止等待协议
停止等待协议就是每次给对方发送数据包后,需要等待对方的回应然后再发送下一个数据包!停止等待协议会出现如下几种情况:
我们从上面能看到,停止等待协议每次都是等到收到确认后再发下一个数据包。只要我没收到你给我的确认,我就认为你没有收到我发的数据包,我就会进行重发!这样虽然可靠,但是会导致!
流水线传输
流水线传输就是每次发送多组数据包,不必每次发完一组就停下来等待对方的确认。由于信道上一直有数据不间断的传输,因此可以获得较高的信道利用率!
流水线传输如何保证可靠的呢?需要发送方维持发送窗口,假如发送窗口是5,那5个数据包会同时发送,然后等确认!如果有收到接收方的确认,窗口就会滑动,进行第6个数据包的发送。
如果都是单个确认,可能效率会比较低,所以有了!也就是说假如发送方发送了数据包1、2、3、4,接收方只需要回复对数据包4的确认,那表示1234数据包都已经收到了,就可以进行第五个数据包的发送了!假如发送了数据包1、2、3、4,其中第三个数据包丢失,那该怎么确认呢?TCP只会回复对数据包2的确认,并且对数据包4进行选择性确认(TCP头部选项讲到过的SACK),这样发送方就知道数据包4已经成功发送,只需要重发数据包3。
继续前面抓包的例子,接收方并不是对每个数据包都进行确认,而是对多个数据包进行累计确认:
这里我们能看到服务器发送多个数据包后,客户端才进行了一次确认。
流量控制和拥塞控制
通过前面我们知道了,通过建立可靠的连接和确认机制,保证了TCP的连接的可靠!但是每个人使用的计算机的处理能力都是不一样的,我发送太快了对方处理不过来怎么办呢?通信双方怎么去协调发送和接收数据的频率呢?
以字节为单位的滑动窗口技术
在介绍TCP头部的时候,我们已经提到过滑动窗口,并且介绍了相关的控制参数Win!也说到了接收窗口和发送窗口!那他们的关系是怎么样的呢?
假设现在A需要传输数据给B,B就先要告诉A自己的接收窗口有多大。A根据B的接收窗口设置自己的发送窗口!A的发送窗口时不能大于B的接收窗口的!在开始传输数据之前,初始的窗口设置如下图:
如上图我们能否看到,B的接收窗口设置为10个字节,那A的发送窗口设置不能超过10个字节!如果开始传送数据,A会将数据封装成多个数据包进行传输,如下图
在没有收到B的确认之前,A的窗口不会滑动,也就是说最多能发10个字节的数据。如果B接受到数据且回复确认给了A,那A的窗口则进行滑动,如下图:
这样,A又可以进行第11、12个字节的发送啦!如果B的处理能力变弱了,也可以通知A将发送窗口调小!这样也也就很好的协调了双方的接收和发送能力!这也就很好的实现了TCP的可靠传输和流量控制!
上面的数据包继续发送,如果在发送过程中,3、4、5这三个字节组成的数据包丢了,但是后面的数据却收到了,这时候A的发送窗口会移动么?
如果是这种情况,A的发送窗口是不会移动的。B在接收到后面数据包的时候回复给A的Ack会设置为3,且在选项中设置一个SACK(在TCP头部选项里面有描述),告诉A哪部分数据收到了,而哪部分数据需要进行重发!
拥塞控制
利用滑动窗口技术,可以很好的协调双方的收发能力。但是,网络状况是非常复杂的,且在同一个网络上可能有千千万万个发送方和接收方!如果大家都需要传输数据都需要占用网络,不做好控制措施,就会导致整个网络会堵塞甚至瘫痪。
如果我要从深圳开车去广州,我就会走高速。如果只有我一个人开车,那肯定能畅通无阻!但是高速公路不是我家的,大家都能通行!所以一到了节假日,大家都蜂拥而上,而高速的承运能力不会因为节假日而调整!这时候往往就需要交通管制、限流等措施去舒缓交通!
网络就好比高速公路,传输的数据包就好比要通过的车辆,而TCP则就更像一个交警,维护着数据传输的秩序!那TCP是怎么做的呢?
慢开始与拥塞避免
发送方维持一个cwnd(拥塞窗口,注意这里的拥塞窗口不能大于前面说到的发送窗口!),刚开始拥塞窗口设置为1。如果发现这个包没有丢失,则调整拥塞窗口为2!如果又没有丢包,则调整拥塞窗口为4!这样每次以2倍的速度一直增长到16!然后17、18、19这样一个一个的增加,直到大小与发送窗口一致。这就是所谓的慢开始和拥塞避免,16就是......
有没有得寸进尺的感觉!
如果在发送的过程中发现有丢包现象,则会调整拥塞窗口大小为1,并且设置新的慢开始门限为出现拥塞时的二分之一,也就是说当拥塞窗口为24的时候出现丢包现象,那新的慢开始门限就调整为12!如果理解了上面的文字描述,下面的图就不难理解了!
快重传
前面说过累计确认,还说到了选择性确认。这个就跟快重传有关!接收方如果发现丢包,不会等到累计确认,就通知发送方三个重复的确认通知对方重新发送丢失的包。当接收方收到三个重复的确认,则意识到数据包丢失,进行重传!
通过下图能看到,当出现丢包的情况,接收方的Ack都是等于50,而SACK分别对60~89之间的字节都进行了选择性的确认!这时候发送方也就知道50~59这部分数据丢失而进行重传!
快恢复
如果一旦发生丢包,拥塞窗口就变成1,这种方式也太傻了吧。如果能有个快速恢复的机制就好了!TCP就使用了快恢复机制!当出现丢包时,不会再次进行慢开始,而是直接转入拥塞避免!也就是从新的慢开始门限进行加法增加!
看完全文,我们再回到TCP的定义,你是不是又能有更多的理解了呢?