网络协议 TCP协议
TCP协议,也称“传输控制协议”(Transmission Control Protocol),其作用是要对数据的传输进行一个详细的控制。
1. TCP协议段格式
仔细阅读上图我们能够获得以下内容:
-
源端口号/目的端口号:表示数据是从哪个进程来,到哪个进程去;
-
4位TCP报头长度:表示该TCP头部有多少个32位bit(单位为4字节),则TCP头部最大长度是15 * 4 = 60字节;
-
6位标志位:
- URG:
紧急指针
是否有效; - ACK:
确认号
是否有效,也称应答报文
,接收端返回给发送方表示是否接收到数据; - PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走;
- RST:对方要求重新建立连接,我们把携带RST表示的称为复位报文段;
- SYN:请求建立连接,我们把携带SYN标识的称为同步报文段;
- FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段;
- URG:
-
16位校验和:发送端填充,CRC校验,接收端校验不通过,则认为数据有问题,此处的校验和不光包含TCP首部,也包含TCP数据部分;
-
16位紧急指针:标识哪部分数据是紧急数据;
-
32位序号/32位确认号、16位窗口大小下文将进行详解;
接下来我们将围绕TCP协议的特性来对其中的一些核心机制进行分析!
2. 可靠传输保障机制
对于TCP来说,它被创建的初心就是用来解决“可靠传输”的问题。网络通信过程是复杂的,无法确保发送方发出去的数据100%能够到达接收方。此处可靠性,只能“退而求其次”,只要尽可能的去进行发送了,发送方能够知道对方是否收到,就认为是可靠传输。
那么,又有哪些机制被用来保障可靠传输的进行呢?
2.1 确认应答
TCP将每个字节的数据都进行了编号,即为序列号。
接收端接收到数据后,会给发送端返回一个ACK(应答报文),每一个ACK都带有对应的确认序列号,来告诉发送端我已经收到了哪些数据,下一次你从哪里开始发送。
注:应答报文中的确认序列号是按照发送过去的最后一个字节的序号再加1来进行设定的:
同时,如果当前报文是应答报文,此时报头中ACK这一位就是1:
2.2 超时重传
超时重传是确认应答的补充,如果一切顺利,通过应答报文就能告诉发送端当前其发送的数据是否已成功收到。但是,网络上也存在着“丢包”的情况,如果数据包丢了,没有到达接收端,接收端自然也没有ack报文返回给发送端,这个时候就需要超时重传了。
发送方如果一段时间内没有收到ack应答报文,就会视为是丢包了,从而触发重传,把刚才的包再传一次,
不过,丢包不一定是发的数据丢了,还可能是ack丢了:
以上两种情况都会触发超时重传!
注:超时是会重传,但重传也不是无限的重传,它遵守以下规则:
- 重传次数是有上限的,重传到一定程度,还没有ack,就尝试重置连接,如果重置也失败,就会直接放弃连接;
- 重传的超时时间阈值也不是固定不变的,随着重传次数的增加而增大(重传频率越来越低);
补充: 为什么网络中会出现丢包?
网络中的通信使用到的通信设备(如路由器/交换机)不单单只用于两台主机之间的通信,还需要提供给很多的主机进行使用。
因此,整个网络中,就可能存在某个路由器/交换机在某个时刻突然负载量很高,短时间内有大量的数据包要经过这个设备转发,同时又因为一台设备能够处理的数据量是有限的,很可能瞬间的高负载超出了这台设备能够转发的数据量极限!此时多出来的部分就无了,就被设备丢包了。
3. 连接保障机制
对于TCP协议来说,它是有连接的,其需要连接管理机制来保障其正常进行,即“三次握手,四次挥手”
3.1 三次握手(建立连接)
在之前介绍过的TCP套接字编程(文章链接)代码中,客户端执行这样一段代码:
socket = new Socket(serverIp, serverPort);
- 1
上述操作就是在建立连接,但只是调用socket api,真正连接建立的过程是在操作系统内核中完成的:
那么内核又是怎样完成上述的“建立连接”过程的呢?–通过“三次握手”!
下图是“三次挥手”的简化流程图:
在上述过程中,第一次交互一定是客户端主动发起的。
其中syn是一个特殊的TCP数据报:
-
没有载荷,不会携带应用层数据,但是会带有TCP报头等;
-
六个标志位中的SYN为1
客户端先向服务器发起连接请求syn,服务器在收到syn之后,会返回ack(确认应答),表示我已经收到连接请求,接下来服务器还会返回syn,这个syn意思就是我接受你的连接(我也愿意和你建立连接),之后客户端接收到syn后再向服务器发送ack确认应答,表示我也接收到你的回复了!
所谓的建立连接过程,本质上就是通信双方各种给对方发起一个syn,各种给对方回应一个ack!
注: 虽然第一次握手客户端已经把自己的信息告诉给服务器了,但是需要等所有的握手环节完成服务器才会最终保存客户端的相关信息。
进行三次握手的意义
- 三次握手,可以先针对通讯路径进行投石问路,初步的确认通信链路是否畅通(可靠性的前提条件);
- 三次握手也是在验证通信双方的发送能力和接收能力是否正常(服务器无法判断自身发送能力是否正常,所以需要客户端返回ack进行判断);
- 三次握手的过程中也会协商一些必要的参数,通信是客户端服务器两方的事情,需要两方同时配合,其中的有些内容要保持一致;
3.2 四次挥手(断开连接)
断开连接的本质目的,就是为了把对端信息从数据结构中删除掉或释放掉,对此,内核中通过“四次挥手”来断开连接:
在四次挥手中,第一次中断请求可以由客户端主动发送,也可以由服务器主动发送,通过调用
socket.close()
,触发fin数据报
,一方在接受到fin后向对端发送ack应答报文以及fin数据报表示我同意与你断开连接,对端接受到ack和fin后返回ack表示已经接收到同意断开连接的信息!
如果当前处于结束报文段,六个标志位中FIN为1:
注: 在四次挥手中:
- 调用socket.close() 就会触发FIN(FIN也是内核负责完成);
- 如果进程直接接受,也会触发FIN;
- 关闭socket文件也会触发FIN;
下图是TCP建立连接和断开连接的详细过程图:
三次握手和四次握手之间有相似之处,也有不同之处:
- 相似之处:都是通信双方各自给对方发起一个syn/fin,各自给对方返回一个ack;
- 不同之处:三次握手中间两次一定能合并,四次挥手则不一定;三次握手必须是客户端主动,而四次挥手客户端/服务器都可以主动。
3.3 TCP状态转换过程
下图为TCP状态转换过程的汇总图:
可以与上述TCP建立/断开连接的过程图结合分析:
- LISTEN: 表示服务器这边,已经创建好了serverSocket,并且完成了端口号绑定(手机?开机了,信号良好,随时可以给我打电话);
- ESTABLISHED: 客户端和服务器连接已建立完毕(三次握手握完了);
- CLOSE_WAIT: 表示接下来代码中需要调用close来主动发起fin,并且在收到对方的fin之后进入CLOSE_WAIT状态(谁被动断开连接谁进入CLOSE_WAIT状态);
- TIME_WAIT: 表示本端给对方发起FIN之后,对端给本端也发送了FIN,此时本端进入TIME_WAIT,给最后一个ACK的重传留有一定时间,防止最后一个ACK丢包(谁主动断开连接谁进入TIME_WAIT状态);
4. 传输效率保障机制
4.1 滑动窗口
在上述确认应答的机制下,TCP的可靠性得到来保障,但也因此降低传输效率:
确认应答机制下,每次发送方收到一个ack才会发下一个数据,导致大量的时间都消耗在等待ack上了,此处等待消耗的时间成本是非常多的
TCP希望保证可靠传输的前提下,能够让效率尽量高点,让消耗的尽可能少点。对此就引入了滑动窗口!如下图所示:
滑动窗口的本质就是批量传输数据。之前发了一个数据需要等待ack再发下一条数据,现在是连续发了一定数据后,统一等一波ack,把多次请求的的等待时间使用同一份时间来等待,减少了总的等待时间。
由上图可知,数据是被批量传输出去的,传输出这四份数据之后,就等待ack,暂时不传了,其中白色区域(不需等待ack,能够批量传输的最大数据量)被称为“窗口大小”!
批量发了四个数据,就会对应有四个ack,此时四个ack也不一定是同时到达,而是有先有后,等到返回一个ack后就可以继续往后发送数据了,窗口向右快速移动,直观上呈现一种“滑动”的效果,故上述过程就被称为滑动窗口。
注:
滑动窗口中当然也有确认应答,只不过是把等待策略做了调整,转成批量发送了,批量的前提是短时间需要发很多数据,如果发的数据很少,此时滑动窗口滑不起来,就退化成了确认应答。
4.2 快速重传
在滑动窗口中,出现丢包要如何进行重传,这里分两种情况讨论:
情况一: 数据包已经抵达,ACK被丢了
这种情况下,部分ACK丢了并不要紧,可以通过后续的ACK进行确认(后续的ack能够确认之前的数据都收到了)
情况二: 数据包直接丢了
这种情况就必须需要重传了,对此我们使用快速重传机制来进行重传:
在上述重传的过程中,整体的效率是非常高的,这里的重传做到了“针对性”的重传,哪个丢了就重传哪个。整体的效率没有额外损失的,就把这种重传称为“快速重传”。
5. 控制机制
5.1 流量控制
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等一系列连锁反应。
因此,TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制。
接收端将自己可以接收的缓冲区大小分入TCP首部中的“窗口大小
”字段,通过ACK端通知发送端(窗口大小字段越大,说明网络的吞吐量越高);接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口之后,就会较慢自己的发送速度!如果接收端缓冲区满了。就会将窗口设置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测包触发ack,若查询窗口非0,缓冲区又可以使用了,发送方就可以继续发送了。
5.2 拥塞控制
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,任然可能引发问题,因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据对设备来说很可能就是雪上加霜。
对此,TCP通过拥塞控制机制来解决上述问题:
如果按照某个窗口大小发送数据之后,出现丢包,就视为中间路径存在拥堵,就减小窗口大小;如果没出现丢包,就视为中间路径不存在拥堵,就增大窗口大小
那么,这个拥塞控制具体是怎么把这个窗口大小给试出来的?具体如下图所示:
- 慢启动:刚开始传输的数据,速率是比较小的,采用的窗口大小(拥塞窗口)也就比较小,此时网络拥堵情况未知,不能一上来就传大量数据;
- 指数增长:如果上述传输的数据,没有出现丢包,说明网络还是畅通的,就要增大窗口大小,此时,增大方式是按照指数来增长的 (由于使用慢启动,开始的时候窗口大小非常小,也有可能网络上本身就很通畅,通过指数增长可以让上述的窗口大小快速变大,这样就可以保证传输的效率了);
- 线性增长:指数增长也不会一直持续的保持,可能会增长太快一下就导致网络拥堵。这里引入一个“阈值”,当拥塞窗口达到阈值之后,此时,指数增长就转变为线性增长。线性增长能够使当下的窗口持久的保持在一个比较高的速率,并且也不容易一下就造成丢包;
- 乘法减小:线性增长也是阈值在增长,积累一段时间之后,传输的速度可能太快,此时还是会引起丢包。一旦出现丢包,就把拥塞窗口重置为较小的值,回到最初的慢启动过程(之后又要重新指数增长),并且这里也会根据刚才丢包时的窗口大小,重新设置指数增长到线性增长的阈值。
拥塞控制,归根结底还是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
6. 应答机制
6.1 延时应答
当接收数据的主机立刻就返回了ack应答,接下来返回的窗口可能就比较小,如下:
假设接收端缓冲区为1M,,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K,但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了,这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
对此如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M,这样的话我们的传输效率也能有所提高!
所以,接收方在收到数据之后不会立即返回ack,而是延时一会之后再返回ack,等了这一会相当于给接收方的应用程序这里腾出更多的时间来消费这里的数据,这就是延时应答。
注: 窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标就是在保证网络不拥塞的情况下尽可能提高传输效率。
6.2 捎带应答
在延迟应答的基础上引入了捎带应答机制来提升传输效率!
捎带应答就是尽可能的把能合并的数据包进行合并,从而起到提高效率的效果
就如我们“三次握手”中的ack应答,捎带上了syn一起返回给了发送端(握手是打招呼,而打招呼是一个没有实际意义的数据报,是没有载荷数据的)
7. 面向字节流
7.1 缓冲区
在TCP套接字网络编程中,我们通过创建一个TCP的socket,同时在内核中创建一个发送缓冲区和一个接收缓冲区来实现面向字节流编程,在此基础上:
- 调用write时,数据会先写入发送缓冲区中;
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其它合适的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动持续到达内核的接收缓冲区;
- 应用程序可以调用read从接收缓冲区拿数据;
注:TCP的一个连接,即有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据,这个概念就叫做全双工。
7.2 粘包问题
粘包问题中的包指的是“TCP载荷中的应用数据包”,结合之前讨论过的内容来进行理解,tcp传输的数据到了接收方之后,接收方要根据socket api
来read
出来,read出来的结果就是应用层数据包
,但由于整个read过程非常灵活,可能会使代码中无法区分出当前的数据从哪到哪是一个完整的应用数据包:
粘包问题不是TCP独有的问题,只要是面向字节流的,都有同样的问题,而解决问题的关键,就是要明确包之间的边界:
- 通过特殊符号作为分割符,见到分割符就是视为一个包结束了;
- 指定出包的长度,比如在包开始的位置加入一个特殊的空间来表示整个数据的长度;
对应的,UDP就没有这个问题。UDP传输的基本单位是UDP数据报,在UDP这一层就已经分开了。只要约定好,每个UDP数据报都只承载一个应用层数据包,就不需要额外的手段来进行区分了。
粘包问题是TCP引起的,但是TCP本身不会解决,需要程序员写代码在应用层逻辑的时候自己去进行处理!
8. 异常情况
在出现以下异常情况时,会有对应的处理方式:
-
进程终止/机器重启:进程终止会释放文件描述符,仍然可以发送FIN,进行正常的四次挥手结束,和正常关闭区别不大;
-
机器掉电/网线断开:
-
若断电的是接收方,发送方就会突然发现没有ack了,就要重传,重传几次后发现还是不行,TCP就会尝试“复位”连接,相当于清除原来的TCP中的各种临时数据,重新开始:
此时上述RST为1,若重装了还不行,则单方面放弃连接;
-
若断电的是发送方,则接收方需要区分出发送方是挂了还是好着但暂时没发。对此,接收方一段时间之后,若没有接收到对方的消息,就会触发“心跳包”来询问对方的情况(心跳包是不携带应用层数据的特殊数据包,具有周期性),若判断对方没有心跳,此时本端也就会尝试复位并且单方面释放连接。
-
评论记录:
回复评论: