Mki's Blog

网络:可靠传输协议TCP

前言

  前一篇文章讲了关于Socket通信的一些东西,而Socket的下端就是传输层了。传输层最重要的就是两个协议:TCP与UDP。事实上,这两种传输方式各有优缺点,本篇主要讲一下可靠传输方式TCP。

正文

传输层的作用

​ 网络模型这么多层,传输层是干什么的呢。它的作用一般来说就是为应用进程提供端对端的服务。是应用进程中的一种"直接连接"。它的作用包括线路分用/复用,控制流量,检验错误等服务。网络模型这么多层,传输层是干什么的呢。它的作用一般来说就是为应用进程提供端对端的服务。是应用进程的一种"直接连接"。它的作用包括线路分用/复用控制流量检验错误等服务。

分用和复用技术

​ 为什么要有分用和复用这个功能呢?

​ 首先,注意,分用和复用是协议的功能。

​ 而分用是对于接收端来说的,比如传输层收到了多个TCP的IP数据报,那么就通过TCP分用技术,将这些数据报传到对应的端口去。

​ 复用是对发送端来说的,比如传输层收到了多个数据,那么给每个数据封装上头部信息,然后生成报文,交给网络层。

可以说分用和复用是同一个过程的两个方向。

UDP

​ UDP是一种并不太可靠的传输协议,为什么说不可靠呢,因为它的传输是不建立连接的。也就是说,这是一种“尽力而为”的传输方式:能传输成功就传输成功,不行就算了

​ 但是,既然如此,你这个“垃圾”传输方式连个能不能成功传输都不能保障,我要您做什么呢?

​ 事实上,并不是所有的传输都需要百分之白传输到的,有些服务偶尔丢失一点数据问题不大,比如某些网站提供的在线看小电影服务,像流媒体,DNS和SNMOP应用大多依靠UDP。看番的时候偶尔丢失一两帧人眼根本看不出来。而且因为不建立连接的原因,UDP要比TCP省一些时间,多好呀。

​ 况且,UDP也不是就不回来了,它也可以设置错误校验的机制,一个比较典型的就是设置校验和,从而验证在传输里是否发生位反转(0变成1,1变成0)。

可靠传输TCP

​ 但是,假如是像支付宝付款这种操作,要是传输不可靠那可能会被打死。因此,一种可靠的传输方式是很重要的。

​ 何谓可靠的传输方式呢?可靠,即传输:不错,不乱,不丢。接收到的数据包的内容,顺序要和发出来的时候的一毛一样。

​ 而TCP就是这么一种可靠的传输方式。在探究可靠性之前,我们先要知道会什么会发生不可靠传输。

理想的可靠传输模型

第一种Rdt模型-Rdt1.0

tcp1.jpg ​ 这是一个理想的Rdt(Reliable Data Transfer)模型,不会有丢包,一切正常。

第二种Rdt模型-Rdt2.0

tcp1.jpg ​ 这里我们发现,底层的信道变得不可靠了,可能会发生位翻转的错误,这时,为了尽可能的弥补这种缺陷,不难想到,我们可以利用校验和去判断收到的数据正不正确,假如正确,那就给发送方发一个正确收到的信息,让它发下一个(确认机制ACK);假如不正确的话,给发送方发一个不正确的消息,让它重发一个就好了(报错机制NAK)。这种基于报错重传的rdt协议叫做APQ(Automatic Repeat reQuest)协议

​ 用两只自动状态鸡(FSM)来表现这个过程。

​ 这是发送方的状态鸡,有两个状态,左边的等待上层传数据,收到上层的数据(rdt_send(data))之后,将数据打包(snkpkt=make_pkt(data,checknum) udt_send(sndpkt)),然后发送方的状态变成了等待ACK或者NAk的状态。此时,收到了信息,假如这个信息是“老哥你给我发了个错误的消息啊!--来自气呼呼的接收方”(rdt_rcv(reckpt)&&isNAk(rcvpkt)),那就重新发送一个原来的消息过去(udt_send(sndpkt)),接着依旧转换成等待ACK或者NAK的状态。这时,终于传来了好消息,“你的消息我成功收到啦!”(rdt_rcv(rcvpkt)&&isACK(rcvpkt)),那么,状态鸡又进入等待上层消息的状态,假如上层发消息了,那就回到这段开头无限循环~~~~~ TCP2.jpg

​ 那么,作为接收方的状态鸡又是什么样的呢?

​ 首先,这只状态鸡处于等待下层消息的状态,好!来消息了,校验和一看,我去!竟然是错的(rdt_rcv(rcvpkt)&&corrupt(rcvpkt)),这不行啊,于是发送报错信息“不行啊老哥你发给我的东西出错了”(udt_send(NAK)),于是气乎乎地还是保持等待下层发来消息的状态。接着又来一个消息,一看校验和,欸,没问题!(rdt_rcv(rcvpkt)&&notcorruopt(rcvpkt)),ok,那就把这个数据包解开,提取数据(extract(rcvpkt,data) deliever_data(data)),然后给发送方再发个消息“辛苦啦老哥,消息我收到了,你发下一个吧”(udt_send(ACK)),如此往复循环。妙啊! TCP3.jpg

第三种Rdt模型-Rdt2.1

​ 然而生活不幸啊,事情变得越来越离谱了,连接收端发送的ACK和NAK消息都有可能会出错,该怎么办呢?

​ 此处我们引入两个新的机智:对于ACK和NAK都发生错误的情况,我们用一个检验ACK/NAK包的方法;同时引入一个1/0的序列号,来标志是哪个的包,防止混乱。 TCP4.jpg ​ 如这个图所示,发送方先是等待上层调用序列号0,收到上层的请求,那么把数据和序列号打包(sndpkt(0,data,checksum) udt_send(sndpkt)),然后进入等待ACK或者0的NAK的状态,倘若接收到消息,发现是损坏了的数据报(corrupt(rcvpkt))或者是正确的报错信息(isNAK(rcvpkt)),那么重发刚才的包(udt_send(sndpkt)),然后还是进入等待ACK或者0的NAK的状态。此时接收到了一个完好无损的信息,而且是ACK(rdt_rcv(rcvpkt)&&notcorrupt(rcvpkt)&&isACK(rcvpkt)),ok,说明序列号为0的包已经发送完毕了。此时进入等待上层发出要发送序列号1的指示,状态变化同序列号0。

TCP5.jpg

​ 而我们的接收方可以用这么一只状态鸡来表示。 ​ 接收方等待来自网络底层的序列号为0的数据包,此时,假如收到一个数据包,发现是损坏的(rdt_rcv(rcvpkt)&&corrupt(rcvpkt)),那么就把NAK信息和期望收到的数据包序列号打包,发给对方(sndpkt=make_pkt(NAK,shksum) udt_send(sndpkt));或者此时收到的数据包是完好无损的(rdt_rcv(rcvpkt)&&notcorrupt(rcvpkt)),但是却和我们想要的数据包不一样(has_seq1(rcvpkt)------这里的意思是发现是序列号为1的数据包)(这种情况可能会出现在发送方发了一个序列1的包,接收方成功接收,但是返回的ACK却坏掉了,这时接收方的期待序列已经变成0,但是发送方又重发了序列1的包),于是接收方将ACK和期待的序列号打包(sndpkt=make_pkt(ACK,chksum)),然后发送这个包,重新进入等待序列0的状态;假如成功收到了期待的序列号包,那就解包,提取数据,打包ACK和下一个期待收到的序列,发送。然后状态鸡进入期待序列号1的状态,和状态0类似。

第四种Rdt模型-Rdt2.2

​ 这是上一种模型的优化方案优化的点在于我们不一定需要ACK和NAK两种标记,只需要把ACK和确认收到的最后一个序列号发过去就可以了(序列号还是0和1)。TCP6.jpg

​ 上半部分是发送方,等上层传来序列0的要求,发包,然后等待0的ACK包。假如收了包,发现发现坏了或者不是期待的,那么重发刚才的包;假如没坏而且还的确是期待的,那就转换到等待序列1的状态。

​ 下半部分是接收方,原本他是处于等待序号1的状态。假如收到了包并且不是坏的,并且还是期待的包,那么就解包,提取数据,然后把原期待包的ACK和期待的下一个包的序号打包,发送。假如不是期待的包,那就重发之前的sndpkt。

​ 注意,上下两个图都是按虚线对称的,0/1循环。

​ 这个模型就是省去NAK,然后用最后一个期待数值chksum来判断是不是发错了。

第五种Rdt模型-Rdt3.0

​ 添加一个计时器机制。这个计时器是为了防止(重新把 ACK 期待值 打包的重发包) 也丢了 这种情况发生。 TIM截图20181015205341.jpg

​ 如图,跟之前的大同小异。 ​ 但是这个由于设置计时器实际上还是要等,相比之前的,就是把失去响应变成了需要很长一段时间响应,从不能工作到勉强能正确工作,其实所以时间上还是不划算的。

流水线机制和滑动窗口协议

流水线机制

​ 由于之前的所有模型,发送接收的过程都是基于一个"停-等 协议",每次发送一个包,等待一个包,这样的过程是很低效的。自然而然,为什么不能一次发多个包呢。

​ 这个时候我们就可以使用流水线机制()来弥补这个不足。

滑动窗口协议

​ 滑动窗口协议包括两部分,一部分是GBN协议(Go-Back-N),另一部分是SR协议(Selective Repeat)。

GBN协议(Go-Back-N)

GBN

​ 滑动窗口有一个Size N,在这里面的是可以一次性发的包的最大数量,如图,前面绿色的是已经完成了发送/接受过程的数据包,黄色和蓝色的在滑动窗口内,黄色表示已经发送,但是并没有收到ACK信息,蓝色表示可以继续发送的数量;空的表示不可用的数量。

​ 简单来说,可以这么看,上面的每一个长方形就是一个序列号,绿色表示使用完成,黄色表示已使用,但未完成,一旦完成也会变成绿色,而所有绿色的里面最先完成的一定是最左边的(不是最左边的会拒收,详细看下面的状态机),蓝色的表示还没用过的序列号,等待上层传来使用的信息。而白色的表示超出了可用范围之外的序列。

发送方

​ 这是发送方的状态鸡,首先,我们会设置一个base和一个nextseqnum,都是1,前者表示滑动窗口的第一个序列,nextseqnum表示下一个期待收到的序列号。

​ 首先,发送方接收到上层发来的数据包,假如这个数据包的期待序列号超出了我们的滑动窗口(nextseqnum<base+N),那么,我们将拒绝发送这个数据包;假如没超过,那么我们就可以把这个包的序列号,数据,校验和打包然后发送(sndpkt[nextseqnum]=make_pkt(nextseqnum,data,chksum) udt_send(sndpkt[nextseqnum]) );假如我们的期待序列是滑动窗口的开头(base=nextseqnum),那么计时器启动;把期待序列号自增1位(此时上一个序列号就是“黄色的”,这个新的序列号是“蓝色的”。);

​ 然后,假如计时器到了我们设定的最大时间,砰!GG!那我们就重新初始化计时器,把活动窗口内所有的已经打包好的包全部重新发送一遍。

​ 当然,在计时器砰一声挂掉的过程中,我们还是很有可能受到ACK包的。在成功收到ACK包之后,这个包所对应的序列号相当于变成了“绿色的”,即滑动窗口向右移动了一位。假如此时滑动窗口里面都是未使用的序列号(全是“蓝色的”),那么把计时器关了;否则,重启计时器。(因为计时器总是相对于滑动窗口的第一个序列,当第一个序列到了,假如不重启,继续把后面包的发送时间往里面算,是不对的。)

​ 假如收到坏包那就啥也不干。

而接收方的状态鸡是这样的

接收方

​ 首先设置期待序列号1,并且把期待序列号,ACK,校验和打包成一个默认sndpkt。

​ 在收到期待的包之前,无论是收到没有损坏的错误序列包,还是怀抱,都发送前面的这个默认sndpkt,这个包就是期待的序列号包。

​ 假如收到了没坏的期待包,那就解压,提取数据,再把默认sndpkt变成此时的exprcvseqnum的ACK,把期待值自增一位。

简单点来说就是,接收方要一个苹果,无论发送方发的是桃子,香蕉,还是水蜜桃,接收方统统回复一句“我要苹果”,直到发送方正确的发送了苹果,接收方才会提出下一个要求:“再来一个火龙果”,如此往复。

SR协议(Selective Repeat)

​ SR协议相当于在GBN的情况下,在接收方也设置了一个滑动窗口,而发送方给每一个包单独设定了一个计时器(原来是一个序列一个计时器),同时接收方也有了缓存的功能。这样子效率提高了不少。

后记

​ 最近忙着招新,本来写了一半的TCP没想到拖了这么久才写完,好气啊。不过一切也都还顺利。前天,趁着打折首发价入手了MagicBook,轻薄本真舒服┭┮﹏┭┮,Amd,yes!