tcp协议详解

来源:互联网 发布:协同服务器端口 编辑:程序博客网 时间:2024/06/04 23:27

为什么ip协议不实现为可靠传输

       第一:tcp/ip协议中,每层协议都有各自的功能,不能越权.这样才能实现内部结构的隐藏,降低耦合性.这就是分层或者划分模块的意义.

       第二:ip(网络层)的任务是尽可能快的将数据传到目的ip,ip协议来说,我需要做的仅仅是路由选择和寻址,然后尽快将报文发送对方手中.

       反过来讲,加入在网络层实现了可靠传输,那么他的代价太高昂,这样会使传输过程中花费大代价来保证数据传输的可靠性,牺牲了效率来保证可靠性.而当我们需要即时操作的时候,例如交互操作,用什么协议来保证传输的速度呢?我们是不是还要在网络层另外实现一个高效的传输协议,这样我们还不如让网络层的协议实现的目标就是追效率,而其他可靠或者不可靠的需求由上层协议实现.所以在追求可靠性保障的时候,运输层有tcp协议,而追求即时性的时候有udp协议.

       所以从这里我们可以看出,协议的实现是为了帮助我们实现各种目的,他们只是一种手段或者工具,根据我们所要追求的目标,就要有选择的使用tcp或者udp甚至是自己实现一中新的协议.适合自己的才是最好的.

 

tcp的特点

tcp有四个特点:

1.面向连接的.

2.可靠地

3.传输数据按序到达.

4.可以进行流量控制

下面我们会逐条展开这些特点,看看他们具体是怎么实现的.

 

tcp的连接建立和关闭:tcp三次握手和四次挥手

tcp是面向连接的.

       其意义就是端到端之间建立一条虚拟传输通道,我们之间传输的数据可以通过这条通道直达对方,而且注意这条道是双向通车的.

       既然是面向连接的,就要建立连接和关闭连接.下面我们来仔细看一看tcp是怎么建立连接和关闭连接的.

tcp建立连接:


从这里我们可以看对于三次握手:

1.客户端主动打开连接,然后发送一个随机选择的syn序号.客户端消耗一个序号(用于syn)

2.服务器端被动打开,接受一个syn序号,选择一个syn序号,连带确认一同发送给客户端.服务器端消耗一个序号,用于syn.

3.客户端收到服务器的确认,并发送一个确认给服务器.此时不消耗序号.

 

三次握手的过程清楚了,但是为什么要三次握手呢,两次不行吗?

答案:不行,我们可以想象这种情况,当客户端发送给服务器端一个请求连接,但是由于网络原因长时间没有到达,然后客户端又发送一个连接请求,此时经过两次握手建立连接,当连接关闭后鬼使神差,第一次发送的由于网络原因未到达的请求到达了,所以服务器端发送一个确认并且建立请求,而客户端没法送请求,所以丢弃服务器发来的确认,此时连接实际上没建立,但是服务器端却误以为连接建立,一直等待客户端数据到来,这样造成了资源浪费.

那三次握手就可以完美解决连接建立的问题吗?

答案:当然不是,每次客户端发送的确认都不能保证可以按时到达服务器端,但是服务器超时未接收到确认,服务器会认为自己发送的确认丢失,进而发送重新发送确认.而客户端已经收到了确认,此时会丢弃新到来的确认,直接发送数据.而服务器端因为没有收到确认而直接收到数据,会发送复位,这样就得重新建立连接了.

 

我们再来看一看连接的关闭:


1.我们假设客户端主动关闭,则服客户端会主动发送一个fin包给服务器告诉他,我没有数据要发送了,你不用接收了.客户端消耗一个序号(用在fin包上了).

2.服务器端收到fin,发送一个ack包给客户端.这样客户端到服务器端的数据传输就终止了.此时双向通车变成单向.服务器没有消耗序号.

3.服务器端没有数据要发送了,就会给客户端发送一个fin,告诉他我没有数据发送了.此时服务器端消耗一个序号,用于fin.

4.客户端收到服务器发来的fin,关闭连接,并发给服务器端一个ack.不消耗序号.

5.服务器接收到确认,关闭连接.

 

2MSL等待时间:

这里要注意一下,客户端发送完确认后,会有一个2MSL的等待时间,即平静等待时间.msl是数据在网络中存在的最长时间.等待2msl的原因有两个:

第一:2msl时间可以让网络上的数据包(刚才自己发送的)都消失掉.使得下一个新的连接中不会出现这种旧的数据包.

第二:2msl等待时间使得最后一个ack到达服务器端,如果最后一个ack丢失,服务器端会重新发送一个fin,并等待确认,而客户端收到fin后也是重新设置2msl,再次发送ack.

 

2msl时间内,端口不可重用:

在上面的2msl等待时间之中,端口是不可重用的,原因很简单,因为这个端口还在使用,,虽然数据传完了,tcp连接还没有完全结束.

 

我们还要了解一下tcp连接在计算机中的实现.


 

socket函数:创建了套接口描述字.

connect函数:发起三次握手,仅当握手成功或者握手失败才返回.

bind函数:绑定套接口,ip地址和端口号.

listen函数:由主动发起连接转为被动接受连接,调用listen函数后处在等待状态,等待连接.

listen函数有一个参数叫做backlog,他是指定未完成连接和已完成连接的个数之和不大于backlog.


 

 

accept函数:从已完成连接队列头返回一个已完成的连接,如果完成队列为空,则进入睡眠.

然后就是readwrite对数据进行读和写.

close函数:将套接口做上已关闭的标记,并且这个套接口不能再被进程使用,但是tcp会在背后做如下工作,将一排队待发送的数据发送完成,然后按照tcp流程关闭套接口.

注意:从这里我们可以看出,三次握手和四次挥手不在这几个函数之中,这些函数仅仅完成套接口初始化和数据传送的任务,三次握手发生在listen函数和accept函数之间,而四次挥手发生在close函数之后.

 

在三次握手过程中还会产生一个问题:tcpsyn攻击,这个稍后会讲到.

 

 

tcp协议特性二:可靠性传输.

其实tcp可靠性传输的根本来源就是一个对数据包的确认.仅仅这一个就能保证tcp数据的可靠传输.但是事实并不是这么简单地,要保证确认不仅仅只是收到数据包后发送一个确认那么简单,他还要实现一系列机制来保证确认.于是引出了超时计时器以防止包在中途丢失,滑动窗口可以高效的利用网络,以及流量控制来应对对滑动窗口产生的网络拥塞(也可能不是由于滑动窗口产生的网络拥塞)等等.

这就是一个"ack"引发的"血案".

 

首先我们来讲一讲ack,ack是对什么确认的呢?

ack是对到来的数据包的序号的确认,tcp三次握手和四次挥手的过程中,我提及了他们会消耗一个序号,ack就是对这个序号的确认.

tcp数据传输过程中,首先会随机产生一个序号(32),然后每个字节会使得这个序号加1,这样,当数据包到达目的地的时候酒可以根据序号来判断是否有包丢失,如果有丢失,丢失的是什么包,如果没有丢失,就可以对包进行排序了,这就引出了tcp协议的第三个特性:"按序"到达,这里的"按序"到达就是可以将乱序到达的数据包进行排序,然后交个上层协议.后面还会提到tcp按序这个特性.

我们先来讲一讲滑动窗口协议:

在阐述滑动窗口协议之前我们先来了解一下停等协议:

停等协议:我们假设ab发送数据,没法送一次a就停止发送,然后等待确认,如果超时未收到确认就重发这个包,然后重复上述操作.

这样有什么好处呢?好处就是简单,协议的实现非常简单.但是有什么缺点呢?显而易见,闲置了大量资源,我们使用计算机一般都是尽可能压榨硬件资源,直到极限.我们这样闲置资源就是犯罪,不可饶恕的.我们如果有这样的一个程序,他仅仅占用资源不干活,而且干活非常低效,那么就把这个程序早早的丢弃吧.

其实这个协议意义分常重大,他给人们一个方向.于是乎我们想到可以不以让a一次发送若干个分组,然后等待确认,接受方可以接受一个范围内的分组,然后进行确认,这就是滑动窗口协议的雏形,也是思想.

滑动窗口协议:

发送方维持一个发送窗口,仅仅发送窗口内的分组,接收方维持一个接受窗口,仅仅接受窗口内的分组,然后对分组进行确认.

他们会在tcp建立连接的时候,通告可接受的最大分组长度mss,不接受超过这个长度的分组.

我们来看一下滑动窗口协议:


可以清楚地看到滑动窗口协议,我在用语言描述一遍:

1.发送窗口如果有数据可以发送,就会发送数据.

2.发送窗口没接收到一个确认就会将窗口向前滑动,滑动的距离视接受到的分组而定.

3.接受端仅仅接受接受窗口以内的分组.

4.接受端发送已按序到达的最后一个分组的确认,一般这个确认是在向发送端发送数据时候捎带发送的.

5,接收端每发送一个确认就向前滑动接受窗口.

这里特别注意一点:接收端和发送端的窗口大小不是一成不变的,视上层应用程序处理的速度而定,有可能由于上层处理的速度过慢而把窗口大小是定为0,这就会造成一种情况,这种情况的解决方法为设定坚持定时器,后文会详细介绍.

 

要想保证ack的到达,就不能缺少定时器,下面我们详细讲解一下定时器:
如果ack丢失,或者发送端发送的分组丢失,那么就无法接收到ack,这样就需要一个定时器,当发送一个分组超时未收到确认的时候,就重新发送.而接受端重复收到一个分组时候会发送一个重复确认.或者不发送,这是可选择地,因为一般而言,接收端只发送最后一个按序到达的分组的确认.

好了看到使用定时器的原因了,那么我们来讲一讲定时器的设定和超时时间的评估.

没法送一个分组就对该分组设定超时定时器,每当超时未接到确认就会重新发送该分组.

超时时间的评估算法:

RTT:分组的往返时间.

RTTs:分组平均往返时间.

RTO:超时重传时间.

RTTd:超时偏差的加权平均值.

    首先我们要计算报文的往返时间:RTT,然后设置重传时间:RTO

     RTTs = (1-a)(旧的RTTs)+a*(RTT样本)   a一般取值1/4

    其中a为权重,反应RTTs受新来的样本影响的大小.a取较大的值时候,说明样本对     RTTs影响较大.

     RTO = RTTs +4*RTTd    //RTO应该略大于RTTs

    在第一次计算时候RTTdRTTs的一半,以后用以下公式计算:

     RTTd = (1-B)*(旧的RTTd) + B*|RTTs-新的RTT样本| //B 一般取值1/4

     RTTd反映了RTTs和新样本的偏差

由于重传会给超时的设定带来相当大的误差,如果错误的将重传报文的往返时间设定为原来的报文往返时间,那么超时设定值将增大,将早成重传时间的不断累积.相反重传时间将偏小,将导致不断的重传.

       解决之道:不将超时重传分组的往返时间计入往返时间,仅仅人为的增大往返时间,新的重传时间为旧的重传时间的两倍,当不发生重传时候就恢复原来的往返时间.

 

tcp第三个特性:按序到达.

前文提到了,tcp可以根据序号对包进行排序,那是什么原因导致tcp包的不按序到达呢?因为数据包在由运输层传给网络层的时候可能会发生分片,而且上层来的数据如果较大的话也会在运输层发生分片,即运输层和网络层都可能会发生分片.而数据包在传输过程中可能由于路径不同而到达的时间也不同,这样就造成了可能不按序到达的情况.而由于tcp对数据进行了标号,那么就可以让乱序变成有序,重新组合后交给上层.

 

tcp第四个特性就是流量和拥塞控制:

发送窗口的大小可变是解决端到端流量的控制,但是不能解决网络中的流量问题,于是乎有了拥塞控制,防止造成网络负载过大.

在控制拥塞过程中,首先设定拥塞窗口(cwnd)大小,后面会频繁的用到,也会自动根据网路情况来调节.一般cwnd会设定为mss两倍的数值.

拥塞控制算法主要有四种,慢启动,拥塞避免,快重传和快恢复.我们接下来将依次讲解他们的原理以及怎么配合使用.

轮次:一个分组的往返时间叫做一个轮次.

慢开始门限:ssthresh

慢启动:发送窗口从1开始每次经过一个轮次就翻倍,直到慢开始门限.

拥塞避免:每次增加一直到发生重传.

快重传:每次收到一个失序的报文就立即发送重复确认,而不是等到自己有数据才捎带发送ack.接收方如果连续收到三个重复确认,就重传对方未收到的分组.例如发送方发送分组1,2,3.而接收方只接受了1,2分组3丢失了.当接收方接收到4时候,即收到了失序的分组,就会发送一个对分组2的重复确认.如果接收方连续收到三个重复确认就发送3.

快恢复:当连续收到三个重复确认后,ssthresh减半,然后将cwnd设置为ssthresh,执行拥塞避免算法.

一般在开始的时候使用慢开始算法+拥塞避免算法.然后执行快重传和快恢复算法.

过程如下:

(1)cwnd设置为mss.

(2)执行慢开始算法,直到cwnd等于ssthresh

(3)执行拥塞避免算法,直到发生重传

(4)执行快重传+快恢复算法.当收到三个重复确认立刻重传分组,然后将ssthresh设置为cwnd一半,然后设置cwndssthresh,执行拥塞避免算法.

 

下面我们来讲解一下tcp产生的各种坑,以及填坑的方法.

第一个坑:tcp发送分组时候一般是等待数据达到一定量的时候一起发送,这样的操作不适合交互性强的服务.

nagle算法,每次收到一个确认就发送一个分组,然后收集上层到来的数据,等待ack到来时候一并发出去.

这也有一个问题,就是当交互数据量较少时候可以达到要求,而交互数据量较大的时候也会造成延时,而且延时比较严重,还不如滑动窗口协议.这是后就需要udp协议了.

 

第二个坑:由于tcp交互时候会通知窗口大小,一遍进行流量控制,当窗口大小减小为0的时候,就暂时不进行传输,而后窗口不变为0的时候会进行通告.这就产生了一个问题,当窗口变为0后程序崩溃了(或者发送的窗口变大通告报文丢失),无法通知了,这样发送端永远等待,造成资源浪费.

解决方法:tcp坚持定时器:每当窗口变为0,我们会设定保活定时器,然后每隔一段时间就会进行查询.如果对方还活着,可以接受数据但是窗口依旧是0,那么我们继续等.如果对方死了(对方不可达,或者受到一个rst报文),我们就不用等了,结束连接.

 

第三个坑:我们仔细观察发现,tcp可以建立连接,消耗资源,但是就是不发送数据,这种情况是可能发生的.那么我们怎么能确保双方还存在呢(即还保持着连接).

tcp保活定时器.如果没有发送数据就会设定保活定时器,然后超时进行查询.

 

糊涂窗口综合征:有小窗口就通告或者发送小型数据.

解决之道:发送端仅在待发送数据达到mss一半后或者有不需要确认的数据发送时才发送数据.接收端仅在可用窗口达到mss一半时候才进行通告.

 

 

 

0 0