关于tcp的一些重要特性

        我们在学习和工作中用到最多的传输层协议就是:tcp。那么为什么我们在学习的工程中用到的最多的传输层协议是tcp呢?他有哪些优点?

        首先 tcp是有连接,可靠传输(这里的可靠并不是说一定可以传输到对端,而是说可以知道对端是否可以收到我们的信息),面向字节流,全双工的通信协议。而udp是无连接,不可靠传输,面向数据报,全双工的通信协议。tcp的优缺点:tcp的传输可靠性高,对端更容易收到我们要传输的信息。我们引入了一些其他特性来保障了tcp的可靠性,由于其他的特性的一些机制从而导致了tcp的传输效率下降。总的来说tcp是一个可靠性高但是传输效率低的传输层协议。udp的优缺点:不可靠,但是传输效率高的传输层协议。 

        tcp的报头比udp的报头要复杂很多,这也是为什么tcp的可靠性高的原因。                                

                                                                tcp报头

tcp的报头有20字节。这里面的32位序号和32位确认序号都是tcp可靠性的保障。

六位保留位可以避免tcp因扩容引起的不兼容问题。URG、ACK、PSH、RST、SYN、FIN 这六个是tcp的标志位(学习三次握手,四次挥手所需要的知识就在里面)这是tcp非常核心的部分。这里的选项可以有也可以没有。 载荷就是应用层数据了。

                                                                udp报头

udp的报头有八字节,四个字段,每个字段两个字节

这里的udp校验和用的是crc循环冗余校验。

1.确认应答 (用于确保可靠性的最核心的机制)

       如左图所示,如果我们是等待了对面的回应之后再发送下一条消息的话,那么我们的消息的顺序就一定是正确的。但如果我是多条信息发送不等待对方的回应的话 就会出现一个问题,那就是“先发后至”。                                                                                     那么什么是“先发后至”、“后发先至”?为什么会出现这种情况?

什么是“先发后至”?如下图所示

  很明显,这里我的好兄弟是想和我去吃夜宵但是不想请我的。可能我们这边显示我们发的消息顺序就是如图所示,但是好兄弟这一边收到的消息可能就是你请我好不好啊 再到 肚子好饿,出来吃夜宵。

这就是“后发先至”。

tcp的确认应答是确保tcp可靠性的最核心机制!!!(所以tcp之所以能够保证可靠性并不是因为三次握手四次挥手)

确认应答中,通过应答报文来反馈给发送方,让对方知道他发送的数据我们已经正确的接收到了(应答报文 ACK acknowlege)对应着上述的六个标志位的第二位ACK,在平时 ACK这个标志位为0,如果当前报文是应答报文那么此时报头中的这一位标志位就为1

2.超时重传(对于确认应答的补充)

        如果通讯一切顺利,那么我们就可以知道对方是不是成功的收到了我们所发送的数据。但是网络上存在着“丢包”的情况。如果数据报丢了,对方没有收到我们所发送的数据,自然就没有ack返回给我们。(为什么会存在丢包的情况?在网络中的路由器/交换机 不仅仅是服务于你的这一次数据通信,而是服务于千千万万的主机之间的数据通信。在整个网络中有可能在某时刻就存在着某个 路由器/交换机 的负载量特别高(短时间内有大量的数据需要经过这个 路由器/交换机 去进行转发),每一台设备能够处理的访问量是有限的,而一旦超过这个上限后,路由器/交换机就处理不了这部分多出来的数据,就会被设备丢包。丢包是不可预测的,难以预防)

        这个情况下我们就需要用到超时重传。tcp的可靠性本来就是在对抗网络的“丢包”,在期望存在“丢包”的情况下,我们还可以继续的发送数据成功到达对方。

        当我们数据发送出去并且等待了一段时间后还没有收到对方返回的ack,那么我们就将认为是丢包了,就需要重新再发送一份相同的数据。(注意,这里的等待是有一个时间阈值的,并不是无限的等待,到了这个需要等待的阈值后就是超时,就需要重传)

        但是我们需要考虑是ACK“丢包”还是数据“丢包”,从发送方角度是区分不了的。如果是数据“丢包”了,我们就需要重传等待ACK的返回,如果是ACK“丢包”那就继续发送数据等待ACK的返回,这样我们就多出了一份重复的数据。那么我们是如何处理重复数据呢?在tcp socket在内核中存在着一块接受缓冲区,发送方发过来的数据是先存放到里面的,然后需要程序员调用read/scanner.next才能读到数据。当数据到达接收缓冲区时,接收方会判断接收缓冲区是否有与之相同的数据,如果有那就将其丢弃,这样就不会在调用read/scanner.next出现重复的数据了。

        我们如何判断接收缓冲区是否存在重复的数据?

                (a)数据还没有被read/scanner.next调用,此时就可以拿着新的数据和缓冲区中的数据的序号做比较,如果有一样的那就是重复了,就可以把新的数据丢掉了。

                (b)数据已经被read/scanner.next调用了,此时新来的数据的序号在缓冲区中是查找不到的,但是应用程序在读取数据的时候是要按照序号从小到大的顺序来读的,此时我们就可以在socket api中查看上一次读的数据的最后一个字节的序号。比如上次读的数据最后一个字节的序号为3000,那么就代表着3000以前的数据已经读到了。此时,如果新来的数据的最后一个字节的序号是3000以前的序号,那么就代表着这个数据已经读过了,就可以把这个新来的数据认为是一个重复数据从而丢掉

3.连接管理

        (a)建立连接(三次握手)socket = new socket(serverip,serverport)这个过程就是在建立连接,这个只是在调用socket api来进行连接,但真正的建立双方通信连接是在操作系统内核中进行的。

        那么内核中是如何实现建立连接的过程呢?

                建立连接就是在本端能够保存对端的信息,对端能够保存本端的信息(信息特指ip,port)。

                第一次交互一定是由客户端先发起的,客户端是主动的一方。

 这里的syn其实就是synchroni 但是并不是加锁的意思 而是申请“同步”的意思。syn是tcp协议的一个数据报,它是六位标志位中的第五位,它没有载荷,不携带任何的应用层数据,但是它还会带有ip报头/以太网数据帧帧头以及帧尾,还会有自己的tcp报头。tcp报头携带着端口号,而ip报头携带着IP地址。这个过程等于告诉服务器我是谁?我在哪里?       所谓的建立连接的过程,本质上就是各自发送给对方一个syn和回应一个ack。

        看到这里大家可能会有点疑问。这不是三次握手吗?为什么会出现四次交互呢?                         其实服务器返回的ack和syn这两个交互过程可以连起来一起发送,因为这两个过程就是在操作系统内核中在同一时刻完成的,将其“打包”起来一起发送还可以节省了我们的带宽资源,提升了通信效率。而“打包”起来的这个数据报里面的tcp报头中的六个标志位中的第二个和第五个都为1了,此时这个数据报就起到了两个作用:1.应答上个请求。2.发送同步申请。

        而发出syn后会有两个结果。

                1.服务器同意了,表示可以与你建立连接。

                2.服务器没有同意(一般来说不会这样,大多数情况下是当时负载量极高,没有资源腾出来服务于客户端)没有下文了。

        为什么要握手?握手的意义是什么?(三次握手只是在通信开始的时候,后来传输数据时就于握手无关了)

                1.三次握手,可以针对通信路径进行一个投石问路的一个效果,初步确认一下通信的路径是否通畅(可靠性的前提)                                                                                                                               

                2.三次握手也是在确认双方的发送能力和接收能力是否正常(主要是本端和对端)。

        三次握手的过程中会协商一些必要的参数,因为通信是双方的事情,有一些内容需要双方保持一致,而这些内容往往是通过tcp报头中的”选项“部分来体现的。

        (b)断开连接(四次挥手)

                断开连接的本质是为了把对端的信息从本端中释放掉,不再保存。 四次挥手中,不一定是客户端先发起,也有可能是服务器先发起的。四次挥手用到了tcp六位标志位中的第六位FIN(FINISH)结束报文。

这里我们以客户端发起fin为例。

当客户端给服务器发起了fin(结束报文)请求时,服务器会返回一个ack给客户端,表示同意,再返回一个fin给客户端,客户端也给服务器返回一个ack。 那么这样看来就是四次交互刚好对应着四次挥手,但是为什么不能像三次握手那样吧ack于fin“打包”成一个操作呢?原因是它们的操作时间段是不一样的ack是在内核中操作的,而fin则需要我们通过调用socket.close才可以执行,当ack和第二个fin的时间间隔久了就无法进行合并了,就要分为两次操作来发送。(当时间间隔很小时,是有可能会合并成一个操作的(捎带应答、延时应答))

4滑动窗口

        我们说tcp是一个可靠传输,但是可靠传输本身就会降低效率,所以我们只好引入出一些新的机制来提升我们的传输效率,滑动窗口就能让我们既能保证可靠性的前提下又能尽可能的提升我们的传输效率(这里的提升实际上是降低等待时间成本的消耗)。

        在确认应答机制下,每次发送方都需要等待对方的ack才会发送下一个数据,导致我们花了大量的时间在等待ack的返回上,这里的消耗大时间成本是非常大的。滑动窗口就是解决上述问题的,我们进行批量发送(把多个请求“打包”成一个请求发送),再集体等待ack的返回。

这样我们只需要等待一份ack的时间而获得多份数据,大大的减少了总等待的时间那么我们什么时候才能发送5001这个数据呢?是等到四个ack全部回来吗?还是有一个ack返回就可以进行下一次的数据发送?

        我们不需要等待所有的ack返回,这样子的效率太低了,我们只需要等待一个ack的返回我们就可以进行下一次数据的发送,比如 ack返回已经收到了1001~2000的数据,下一个数据是2001,那么我们的窗口就需要发送5001的数据了,而我们的窗口大小是不会改变的由于这个过程是飞快的,所以就像是滑过去一般,所以就称之为滑动窗口了。

        滑动窗口是在保证可靠性的前提下而产生的机制,无论怎么样,可靠性一定要保障,而不能为了传输效率而失去了可靠性,这就有点忘本了。那么,如果出现了丢包,滑动窗口会怎么样呢?

        (a)ack“丢包”

                                        如果我收到的ack是“下一个是3001”,那么表示我3000以前的数据都已经收到了,此时有没有收到“下一个是1001”或“下一个是2001”已经无关紧要了,对可靠性是没有影响的,我们也不需要进行重传。

        (b)数据“丢包”这里我们的1001~2000的数据是“丢包”了,其他的数据都成功的发送到了对端,而对端在返回了一个确认报文确认了1~1000的数据收到之后,后面的确认报文会一直反复的索要丢失的那一部分数据。而发送方在多次收到了索要1001的确认报文之后,就会认为是1001~2000的数据丢失了,就要进行重传1000~2001的数据。

滑动窗口中判断丢包就是看是否有多条确认报文来索要同一份数据。

5.流量控制

        通过滑动窗口,我们的传输效率就会提高,窗口越大,更多的数据复用同一份等待的时间,传输效率就会变高,那么这个窗口是不是可以无限大小呢?很明显是不可以的,因为在tcp中可靠性是前提,任何提高性能的操作都不可以影响可靠性。发送速度过快,接收方处理不过来就会丢包,那么就没有可靠性可言了。所以我们要控制发送方发送的速度,让其与接受方处理数据的速度保持一个动态平衡。这个其实就像小学的蓄水池问题是一样的,如果你蓄水的速度要大于放水的速度,那么蓄水池经过一段时间后肯定会满的,而这个蓄水池其实就是我们的接受方的缓冲区,如果蓄水的速度远远大于放水的速度,那么这个水就会从池子里溢出来也就是“丢包”了。这个时候你进行重传也是没有效果的,反而会浪费硬件资源。流量控制就是让接受方反过来影响发送方的发送速度。

我们通过tcp报头中的窗口大小来反馈给发送方接下来发送的窗口大小为多少比较合适。在选项中还有一个参数叫做窗口扩展因子,还可以将我们的窗口扩大,不仅限于16位。

6.拥塞控制

        拥塞控制也是要控制发送方的发送速率,流量控制是站在接受方的角度来限制发送方的发送速率。我们按照某个窗口的大小去发送数据,如果丢包了,我们就减小窗口的大小,如果没有丢包我们就增大窗口的大小。总的来说,在拥塞控制和流量控制中的窗口,我们应该是宁小勿大的。那么拥塞控制的窗口我们该如何测试出来呢?

        (1)我们采用慢启动的方式,刚开始时传输的效率会比较低,采用的窗口的大小就比较小。如果此时我们对网络拥堵的状况一无所知就采用较大的窗口,那么就会让我们本就不富裕的带宽资源雪上加霜了。

        (2)如果传输数据没有丢包的情况下,那么说明网络并不拥堵,我们就可以增加窗口的大小,这个增加是以指数来增加的。(这里的指数增加也并不是一直不变的,我们有一个阈值,当增加到了这个阈值时,我们的指数增加就会变成线性增加了。前面的指数增加是为了让我们可以更快的达到一个高传输效率且不会丢包的一个平衡状态,后面的线性增加则是为了使当下的这个窗口能够保持在一个传输速率较高的一个状态,这样也不会丢包)

        (3)线性增加也是在一直扩大窗口的大小,当积累一段时间后,传输效率过高,那么就会出现丢包的情况,一旦出现了丢包的情况我们就又会将拥塞窗口的大小设置为一个较小的值(这里较小的值还是会比一开始传输时的值要大),回到最初的慢启动(指数增长),并且这里也会更具刚才丢包时的窗口大小重新设置一个从指数增长到线性增长的新的阈值。

        拥塞窗口会一直保持一个动态平衡的状态,它是一直在改变拥塞窗口的大小的,主要是因为网络之间的通信路径也是一直在发生变化的。

7.延时应答(基于滑动窗口,让传输的效率尽可能的提高)

        接收方收到数据后并不会马上的返回ack,而是等待一段时间让接受方来处理数据,这样反馈回去的窗口就会大一些,就可以提高了传输的速率正常来说,每一个数据都会有着对应的ack,此时我们每隔几个数据再返回一个ack(起到延时应答的作用)这样还可以减少ack传输的数量,也可以起到节省带宽的效果。

8.捎带应答(基于延时应答引入的机制,能够提升传输的效率)

        捎带应答是尽可能的把能够合并的数据合并起来一起发送,从而提高传输效率很多时候,客户端和服务器之间都是长连接的,需要进行若干次的请求的。 在捎带应答的加持之下,后续为每次传输请求响应都有可能会触发延时应答,所以就都有可能将接下来要传输的业务数据和上一次的ack合并起来。

9.面向字节流

        此处重点讨论“粘包”问题。

                因为tcp是面向字节流的,每一次read的时候都有可能读多数据(因为多个应用层数据包都在缓冲区中),因为它无法区分从哪到哪是一个完整的数据包,所以就出现了“粘包”问题

        如何解决“粘包”问题?

                (a)通过特殊的符号作为分隔符,见到分隔符就意味着这个包结束了。

                (b)用一块特殊的空间来表示这一段数据的长度。

                (c)应用层自定义协议也能很好的解决“粘包”问题,比如:xml、json,protobuffer。

10.异常情况处理

        (1)其中一方出现了进程崩溃

                进程无论是正常结束,还是进程崩溃都会触发回收文件资源,关闭文件这样的效果(系统自动完成,不需要程序员调用)这样就会触发四次挥手。因为tcp连接的生命周期会比进程稍微长一点,虽然进程已经退出了,但是tcp的连接还在,所以还可以继续进行四次挥手操作

        (2)其中一方出现关机(正常流程关机)

                有个主机,触发了关机操作,就会强制终止所有的进程(强杀进程)。终止进程自然就会执行四次挥手操作。如果挥手挥的快,那么本端和对端肯定能正确的删除保存的连接信息,如果挥不快,那么至少也能把第一个fin发送给对端,可以让对方知道我这边要结束了,对端收到了fin之后就会进入释放连接的过程,就要返回一个ack+fin,此时是收不到ack了,fin没有收到ack就会视为“丢包”了,就会进入到重传的状态,传了几次之后还是没有收到ack,那么这时候就会单方面释放连接信息了。

        (3)其中一方出现了断电(非正常关机,突然的)

                如果机器直接断电,那么是来不及发送fin的。

                (a)如果断电的是接受方,那么对端发出fin后就会发现没有ack返回,那么就会进行重传,重传几次后发现还是没有ack,tcp就会尝试“复位”连接(RST tcp报头中的六位标志位中的第四位 复位报文段)此时的RST也不会收到ack,重置了还是不行,那么就会单方面的放弃连接。

                (b)如果断电的是发送方,对端就会阻塞等待发送方发来fin,但是由于断电了,对端迟迟等不到发送方发来的fin请求,但此时对端不知道发送方是还没有发送fin请求还是已经挂了,一段时间后没有收到发送方发来的数据,这时候就会触发“心跳包”(不携带任何应用层数据的特殊数据包)来判断对方是否还存活,如果对端没有了“心跳”那么就会尝试“复位”连接并且单方面的释放连接了

11.tcp的状态

        (a)listen状态,表示着服务器这边已经创建好了serversocket,并且完成了端口号的绑定

        (b)ESTABLISHED(已确立的)表示客户端和服务器的连接已经创立好了(三次握手成功)

        (c)CLOSE_WAIT表示接下来需要用close来主动发起fin给对端(谁被动断开连接谁就是CLOSE_WAIT状态)

        (d)TIME_WAIT本端给对端发起fin后,对端也给我发起fin就会进入TIME_WAIT状态,目的是为了防止最后一个ack丢包,留出时间让其重传(等待时间并不是无休止的)。如果tcp收到fin后直接释放的话就无法返回ack,而对端没有收到ack就会一直重传fin。

题外话:为什么是三次握手呢?两次握手或者四次握手不可以吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值