浅谈网络中的分层与两个主要的协议:TCP和UDP协议

309 阅读6分钟

最近上了极客时间刘超老师的《趣谈网络协议》,对网络协议有了一些深入的理解,在这里记录一下

第一个问题: 既然MAC层的MAC地址是全局唯一的,那么通过MAC地址来找设备就好了,为什么还需要IP地址呢?

在这里,刘超老师用了十分形象的比喻MAC地址就像是一个人的身份证号,而IP地址是他的现住址,我们是不可能根据一个人的身份证号就可以在全世界范围内找到他的,所以其只是作为一个身份标识,就像ID一样,而IP不一样,一个IP携带了许多信息,可以帮助我们快速地寻址。这两者的用途是完全不同的,这也解释了我们从一个地方到了另一个地方,连的网不同了,IP也就不同了,但MAC标识不会改变,就像你住址变了,但你人还没变是一样的。

接下来,我们来聊一聊UDP协议和TCP协议

UDP协议

UDP的头部中,只有源端口号和目标端口号,除此之外就剩下其他一些校验和之类的辅助信息了,这相对于TCP协议来说是十分十分简单的了。如此简单的头部也造成它具有以下三个特性:

  • 只尽力传输,不确保可靠传输。 也就是说,发出去了,就不知道他有没有成功到达目的地了,一切都是未知。
  • 延迟低。 不像TCP那样需要考虑这么多,发就完事儿,管他网络堵不堵,接收方收不收的过来,所以时延低。
  • 不建立连接,意味着可以多播。 虽然UDP协议不可靠,但它也是有许多的应用的,如一些可以容忍丢包,但需要低时延的场景,有实时游戏竞技啊,网页、视频传输等等很多,Google还为此特意开发了QUIC协议。

TCP协议

TCP协议相比于UDP协议来说可是麻烦太多了,它的头部如下图:

image.png

TCP协议主要保证了数据的可靠性,也就是解决了以下问题:

  • 丢包问题
  • 失序问题
  • 流量控制
  • 拥塞控制

我们分别来讲讲TCP是如何解决这几个问题的:

丢包问题与失序问题

TCP分为接收端和发送端两方,我们先讲发送端:

TCP发送端

发送端维护了一个滑动窗口

image.png

可以看到上图中整个SendBuffer分为发送已确认、发送未确认、允许发送但未发送以及不允许发送四个部分,而其中的发送未确认和允许发送但未发送组成了滑动窗口部分。 我们假设发送方已经发送了包1、2、3、4、5,而1、2、3的确认已经收到,而4的包则丢失了,这个时候接收方若收到包5也不会确认包5,而是先缓存着,若是设置了快速重传则会发送冗余ACK,这后面会提到。当过了一定时间后,4的确认(也即包5的ACK)超时了,触发重传,然后由于接收方之前缓存过5,于是会发送6的ACK,表明他现在期望包6了,所以发送方接收到6的ACK后,所以LastByteAcked(也就是滑动窗口的左端点)会移到包6的位置,右端点也会左移两个包,而每次触发重传都会将下次重传的触发时间加倍,这也于情于理(网络情况差就别占用资源了呗),而每次重传时间TCP也会根据RTT(Round Trip Time)来计算,这样的根据网络情况变化的计算重传时间叫做自适应重传算法 以上便解决了丢包问题。

TCP接收端

接收端也维护了一个滑动窗口

image.png

上图中RcvdBuffer主要分为三个部分:已经ACK过的部分,已经接收但没有ACK的部分,还没有接收的部分。注意TCP协议采用累积ACK机制,所以第一部分的数据包一定是有序的,而如果收到了失序的包,且是在当前期望的包之后的包,会先缓存着,一直到期望的包到达之后,滑动窗口左端点才会移动到最近一个没有被ACK过的包处。而如果设置了快速重传机制的话,收到序号大于目前期望的包会发一个当前期望包的ACK,这称为冗余ACK,当发送端收到三个冗余ACK后,会在当前定时器timeout之前,立刻重传该包。这样就解决了失序问题,因为应用都是从已经ack过的部分来拿数据的,而这一部分必然是有序的。

流量控制

我们注意到在TCP协议头部中携带有“窗口”信息,这便是流量控制用到的窗口大小。 我们想象这样一个场景: 当发送端一下子用力过猛,一下子把还没发送可以发送的部分全发送完了,这也使得AdvertiesdWindow (MaxSendBuffer - LastByteSent) = 0的时候,如下图

image.png 这个时候发送端就不会再发送包了直到包5到达。 而若接收端处的应用一直不处理收到的数据包,如下图

image.png 那么接收方在回复ACk的时候就会附带调整窗口大小的信息,迫使发送方调整其AdvertisedWindow的窗口大小。比如ACK包5的时候,会附带窗口大小8,这个时候发送端接收到包5的Ack之后滑动窗口的右端点不会往右推进一个,以使AdvertisedWindow的大小调整为了8,若接收端一直不接收数据,那么窗口大小会最后调整到0,发送端会停止发送。这个时候,也不能一直就这样僵住呀,所以发送端会定时发送窗口探测的数据包,同时也为了防止一有一个窗口就调整窗口大小,接收端往往会在窗口大小大于一定大小时才会更新窗口大小使发送端得知。

这样便实现了流量控制。

拥塞控制

拥塞控制也是通过窗口大小来控制的,这里使用的是Congestion window(后简称为cwnd),有一个公式:LastByteSent-LastByteAcked <= min {流量控制中的窗口大小,cwnd}。 (简而言之,就是网络好了机器处理不过来还是不行,机器处理的过来网络好了也不行。) 网络环境的好坏对于TCP来说是一个黑盒,所以TCP用它自己一套规则来判断当前网络环境是否拥挤。 首先在一开始,TCP并不知道网络情况怎么样,他就先小心翼翼地放包,这称之为慢启动过程,cwnd会设置一个报文段,一开始一次只能发送一个包;当收到该包的确认时,便加倍,发送两个包;当收到该两个包的确认时,加倍,一次发送四个包...... 长此以往,cwnd以指数形式增加,直到达到预先设定的一个阈值:ssthresh(一般为65535个字节)时,又会放缓速度,呈线性增长:比如发送8个包,每收到一个包cwnd便会加1/cwnd。

拥塞控制中判断网络情况差的依据:丢包和超时重传

当遇到丢包需要超时重传时,TCP会将cwnd报文段重置为1,ssthresh设为cwnd/2, 又一夜回到解放前了。其实仅仅依靠丢包就判断网络情况差是有失偏颇的,这也使本来高速传输的网络一下子卡顿了,慢了下来。而如果之前有设置快速重传,TCP就会认为网络情况不是那么差,会将cwnd设置为cwnd/2, ssthresh设置为cwnd,这样cwnd又变味了线性增长。这样看来,TCP拥塞控制是会发生两个问题的:

  • 一个便是前面提到过的,丢包不绝对意味着网络拥堵,网络状况不好导致丢包而带宽还有空余的情况是很常见的。
  • 另一个便是TCP会等到填满中间设备的缓存再发生丢包,这个时候已经晚了。 针对上述两个问题,后来就有了TCP BBR 拥塞算法,这个算法完美的平衡了上述的两个问题。

至此,对一些近期学习到的关于网络的只是就分享完毕了,关于TCP的三次握手和四次挥手(也即连接的建立和断开)的内容我会另开一个帖子详细记录。