TCP协议的特性

1.TCP协议的特性

      TCP协议和UDP协议都是传输层的协议,关注的是数据的起点和终点,即源IP地址和源端口号,目标IP和目标端口号。根据IP地址可以在网络上找到对应的主机(前提是主机在网络上是存在的),根据端口号可以确定主机发送/接收数据的应用程序。举个例子:可以参考收发快递的过程,源IP地址就是寄件地址,源端口号是发件人电话,目标IP是收件人地址,目标端口号是收件人电话,通信双方有了对方的IP和端口号,就可以正常通讯。

TCP协议的特性如下:

  1. 有连接:使用TCP协议和网络上的另外一台主机通讯,需要先建立连接,建立连接是一个互相保存对方信息的过程,保存的主要信息为对端的IP地址和端口号;
  2. 可靠传输:有可靠机制保证数据的可靠传输;
  3. 面向字节流:对比UDP协议,应用层可以交给TCP任意多个字节,TCP协议可以自动进行拆包和组包;
  4. 全双工通信:TCP的socket支持同时读取和写入,能够同时收发数据;

       其中可靠传输是TCP最核心的机制。可靠传输的意思并不是保证数据一定能够发送给对方,毕竟如果网络硬件出现问题,还是需要修复硬件问题才能解决,靠TCP协议是无法解决的。可靠传输指的是发送端可以知道对端是否接收到这条信息,如果没有接收到,还可以通过重传的办法进行补救。

2.TCP的报文结构

Source Port和Destination Port:源端口号和目标端口号,分别占用16位,即两个字节,通过端口号可以确定数据是发给哪个应用程序/进程;

Sequence Number和Acknowledge Number:顺序号和确认号,占用32位,即4个字节,后面结合TCP的重要机制再来理解;

Data Offset:TCP报头长度,占用4个位,单位是4个字节,范围是0x5~0xF,表示20~60个字节。TCP的报头长度是可变的,最小20个字节,最大60个字节;

Rsrvd:保留位,占4个位;

控制位:8个控制位,结合后面的重要机制再来理解;

Window:窗口大小,占用16位,即两个字节,节后TCP的重要机制再来理解;

Checksum:校验和,占用16个位,用于判断数据传输是否出现错误;

Urgent Pointer:紧急指针,占用16个位;

Options:可选项,TCP报头长度可变的部分就是Options,Options可以有,也可以没有,可以有1个,也可以有多个,每个可选项都占用4个字节;

3.TCP的重要机制

1.确认应答

场景1:

        发送端给接收端发送一个TCP数据包,接收端接收到TCP数据包后,会给发送端回复一个控制位ACK置1的TCP数据包(后面简称ACK数据包),ACK数据包不会携带业务数据,如下图:

        当发送端接收到ACK数据包,就知道发送的TCP数据包已经被接收端正常接收了;如果发送端没收到这个ACK数据包,就会认为接收端没有接收到TCP数据包。

场景2:

        上面的场景是典型的“一问一答”的场景,实际的数据传输,有可能是“多问多答”。比如发送端给接收端连续发送两个TCP数据包,这里叫它TCP1和TCP2,接收端接收到第一个数据包还没来得及回复ACK,就又接收到第二个数据包,等到接收端回复时,就需要回复两个ACK数据包,就叫它为ACK1和ACK2。实际的网络环境是非常复杂的,如果ACK数据包返回顺序出现颠倒,发送端如何确认ACK1,ACK2和TCP1和TCP2的对应关系?ACK1确认应答的是TCP1还是TCP2?如下图:

        这里就需要用到32位顺序号和32位确认号,顺序号是TCP数据包载荷的第一个字节的编号,确认号是接收端下一次要接收的数据的第一个字节的编号。假设顺序号从1开始,发送端发送TCP1时,比如发送了1000个字节的载荷数据,紧接着又发送了TCP2,也是1000个字节的载荷,顺序号就变为1001,因为前面已经发了1000个字节了,这个包就需要从1001开始编号。接收端接收到TCP1后回复ACK1,确认号为1001,表示收到1000个字节,下一次该收第1001个字节开头的TCP包了。收到TCP2回复ACK2,确认号为2001,表示收到2000个字节,下一次该收第2001字节开头的TCP包了。通过这种方式就可以确认ACK1和ACK2分别对应的是TCP1和TCP2,即使ACK1和ACK2的顺序出现颠倒,也可以确认这一点。如下图:

       通过确认应答的方式,发送端就可以知道接收端有没有收到数据,如果没收到数据,会采取其它措施进行补救,因此确认认应答是TCP可靠传输最核心的机制。

2.超时重传

       实际上的网络环境是复杂的,不仅仅会出现接收顺序错误的问题,还可能会出现丢包问题。丢包有两种情况,第一种是发送的数据丢了,第二种是接收端返回的ACK丢了。

场景1:

       发送端发送的数据出现丢包,此时接收端接收不到数据,自然也没法回复ACK,因此发送端无法收ACK应答。达到超时时间还没有接收到ACK,发送端就会重新发送TCP数据包,如下图:

       TCP通过超时重传的这种方式,尽可能的进行补救。如果多次重传还没有收到ACK,那么基本上可以确定这个网络已经出现了严重的问题。

场景2:

       接收端返回的ACK出现丢包,此时发送端同样接收不到ACK。尽管接收端收到了数据,因为发送端不能区分到底是发送的数据丢包了,还是返回的ACK丢包了,所以发送端还是会认为接收端没有收到数据,依然会进行重传,那么接收端接收到数据后,仍然会返回ACK,如下图:

       同理,如果网络能够正常使用,通过重传的方式,发送端大概率能够接收到接收端重传的ACK。所以超时重传也是TCP可靠传输的重要机制。

注意事项:

  1. 要注意这里的超时时间,达到超时时间会重传数据,如果重传了之后,对端还是没有收到,第二次重传的超时时间就会变成第一次重传的超时时间的2倍,一次类推。但超时时间并不会无限延长下去,一般重传若干次就不会再重传了,若干次对端还没接收到数据,说明网络出现了严重问题,已经不能使用了;
  2. 注意对于场景2中描述的情况,接收端可能会收到2次或者多次的重复数据,本来对于接收端的应用程序可能会造成影响,但是TCP也考虑到了这一点,于是引入了一个接收缓冲区,接收缓冲区不仅仅可以对重复的数据进行去重,同时还能够将顺序颠倒的报文进行重排序。确保应用程序的读取顺序是和TCP数据的发送顺序是一致的。

3.连接管理

       区别于UDP协议,TCP协议是需要建立连接后,才可以通信,通信完毕后,还需要断开连接。这就是TCP协议的“三次握手”和“四次挥手”。

三次握手:

       重点需要知道为什么是三次握手,而不是两次次握手,也不是四次握手。首先了解一下TCP建立连接的过程,如下图:

       首先主机A给主机B,发送一个控制位SYN置1的报文,后面简称SYN报文,此时如果B接收到了,那么B此时就确认A的发送功能是正常的,同时自己的接收功能也是正常的;

       紧接着B回复一个控制位ACK置1的报文,后面简称ACK报文,如果A收到了这个ACK报文,A就可以知道B的发送功能也是正常的,自己的接收功能也是正常的,同时可以推断出自己的发送功能也是正常的;此时B还需要给A发送一个SYN报文,因为B还不知道自己的发送功能是不是正常以及A的接收功能正不正常;

       A收到B发送的SYN报文后,回复一个ACK报文,B接收到ACK报文,就能确定自己的发送功能和A的接收功能是正常的,至此A和B都确定了自己的收发功能是正常的。

       如上图中所画,那TCP应该是四次握手,为什么说是三次握手呢?因为主机B发送的ACK和SYN是可以合并的,握手的这部分逻辑是在操作系统内核中执行,并且这两个报文的发送时机也是相同的,因此可以合并在一起,所以这变成了三次握手,如下图:

       TCP为了实现可靠传输,但是有个前提:网络必须能够正常工作,否则没法可靠传输。如上面所述,三次握手其实就是为了确认网络是否能够正常工作,这里总结一下TCP三次握手的作用:

  1. 确定当前网络是否通常;
  2. 确认双方的收发功能是否正常;
  3. 通信双方在握手时,协商一些重要参数;

       关于上面的第三点,协商重要参数,比如TCP确认应答机制中用到的顺序号,这个号往往不是从1开始,而且连续两次握手通讯的顺序号差异一般都比较大,这就是为了防止两次通讯相互影响。

握手时,还需要了解一下主机A和主机B的TCP的状态变化:

  1. 当主机A发送SYN后,此时A的状态是SYN_SENT,B收到SYN的状态是SYN_RCVD;
  2. A收到B发送SYN ACK,A进入到ESTABLISHED状态;
  3. B收到A发送的ACK,B进入到ESTABLISHED状态;

四次挥手:

四次挥手的流程,如下图:

       FIN也是控制位中的一个,置1表示要断开TCP连接。同“三次握手”一样,为什么不能把主机B的ACK和FIN合并,变成“三次挥手”?原因是当主机B收到FIN后,操作系统内核会立即回复ACK,但是FIN不是操作系统内核直接回复的,而是应用程序处理完数据之后,调用关闭socket的方法,才会回复FIN。因此主机B的ACK和FIN在很多情况下是不能合并的,因为发送的时机不同,所以不能变成“三次挥手”。如果主机B发送ACK和发送SYN的时机相同,那么是可以合并的。

同“三次握手”相同,在断开连接时,也会发生状态变化:

  1. 主机A发送FIN,进入FIN_WAIT_1状态,主机B收到FIN进入CLOSED_WAIT状态;
  2. 主机A收到主机B回复的ACK后,进入FIN_WAIT_2状态;
  3. 主机B发送FIN后,进入LAST_ACK状态,主机A收到主机B发送的FIN,进入TIME_WAIT状态;
  4. 主机A回复ACK,双方进入CLOSED状态;

       这里主要需要了解一下TIME_WAIT状态的意义。主机A先发送FIN,那么主机A后续就会进入TIME_WAIT状态,持续等待。主机A在等什么?等待时间为多久?主机A在等待最后一个ACK送达B,假设两个通信节点通信消耗的最大时间为MSL,那么TIME_WAIT等待的时间就是2*MSL,这个时间足够最后一个ACK达到B。

       为什么要等待最后一个ACK送达B?假设最后一个ACK丢失,那么主机B没有接收到ACK,就会重传FIN,如果A没有等待,那么A就会释放连接,进入CLOSED状态,而B重传的FIN没有节点可以回复ACK,就会一直尝试重传,重传若干次才会放弃连接,进入CLOSED的状态。所以A进入TIME_WAIT状态等待的意义就是,希望ACK能够传给B,让B能够正常释放连接,万一最后一个ACK出现丢包,等待的过程中,收到重传的FIN后,能够继续回复ACK,确保B断开连接。

4.滑动窗口

       “确认应答”,“超时重传”,“连接管理”,这三个机制都是和TCP的可靠传输相关的,目的是保证TCP的可靠传输。但是确认应答和重传会影响TCP的传输效率。为了在保证TCP可靠传输的前提下,尽量降低对TCP效率的影响,因此又引入了“滑动窗口”机制。

如果按照一问一答的方式传输数据,因为每个数据都需要确认应答,主机A收到确认应答之后再发送下一条数据,这样因为等待确认应答用了很多时间,效率就会比较低,如下图:

如果能够批量传输数据,不需要等待确认应答,就能够发送下一条数据,就能很好得提高效率,如下图:

       上面的批量传输,不意味着可以无限传输,批量传输是有上限的。当达到上限后,再统一等待ACK。这个批量传输的上限,就是窗口的大小。前面提到的“统一等待ACK”,以上图为例,指的并不是这5条ACK都回来再立即发送5条数据,而是回来一个ACK,那么就继续发送剩余的数据,让窗口始终都尽量保持在5条数据。直观上看起来像是有一个窗口在向后滑动,所以也叫滑动窗口。窗口越大,传输的数据越多,等待的ACK越多,传输效率就越高。

如果在传输过程中发生了丢包,是否会影响TCP的可靠传输?

场景1:数据包丢失

如果数据包发生丢失,主机B会持续索要丢失的数据,这时候主机A就知道哪个数据丢失了,触发超时重传机制,如下图:

主机B持续索要1001,主机A重传1001后,主机B接收后,会直接索要5001,主机A不需要重传已经发送过的数据。

场景2:ACK丢失

如果主机B的ACK发生丢失,主机A要看接收到的最新的ACK是多少,如下图:

ACK1001,3001和4001都丢失了,但是5001收到了,主机A就知道前面主机已经接收了5001之前的所有数据,就不需要再重传了。

5.流量控制

       有了滑动窗口机制,TCP的效率就提升了。窗口越大,效率就越高,但是窗口不是越大越好,还需要考虑对端的接收能力。一旦传输的数据量超出了对端的接受能力,就会出现丢包,进一步触发超时重传机制,那么传输效率会不升反降。

       设备的接收能力取决于设备应用层处理数据的能力。当主机A传输数据给主机B,数据会存入主机B的TCP缓存区,主机B的应用程序,再从TCP缓存中中读取数据,进行处理,一旦数据被读取,接收缓存区就可以删除这组数据,来接收新的数据。这个数据缓存区就类似一个阻塞队列,当应用层处理数据越快,单位时间内读取的数据就越多,那么主机B的接收能力也就越强。主机B每次接收数据之后,就会给主机A返回ACK,并利用报头中的16位窗口大小字段,来告诉主机A,TCP缓存区中剩余的空间大小。如果窗口大小为0,那么主机A就会停止传输,如下图:

       主机B窗口为0时,主机A就会停止发送数据,并等待,但是不能一直等待下去,因此主机A会发送一个窗口探测报文,询问窗口大小,主机B会实时更新窗口,直到检测到窗口不为0,主机A才会再次传输数据。窗口探测报文不会携带实际的业务数据,只是为了触发ACK,了解主机B TCP缓存区剩余的空间大小。

6.拥塞控制

       实际中的网络环境是复杂的,流量控制只是考虑了接收方接收数据的能力,但是传输数据还需要经过很多中间节点,比如路由器,交换机等,因此也需要考虑中间这些节点的传输能力。接收方TCP缓存区还剩多少空间,接收方很清楚,发送方也能够很清楚的知道,但是这些中间节点的能够传输多少数据,这个难以量化,因此想要尽可能得提高传输效率,就需要通过实验的方式,来测试中间节点的传输能力。

测试方法是这样的:

  1. 先按照较小的窗口发送数据;
  2. 如果没有发生丢包,则使用更大的窗口进行发送;
  3. 随着窗口的增大,开始出现丢包的情况;
  4. 如果继续丢包,发送方就把窗口缩小,继续丢包就再缩小,如果不丢包了,发送方再尝试把窗口扩大;

按照上述过程,发送方反复调整窗口的大小,逐渐达到动态平衡,如下图:

       上图的横轴是TCP得传输轮次,表示TCP第几次发数据;纵轴是拥塞窗口,表示发送方按照多大的发送窗口/多快的传输速度来进行传输。

  1. 刚开始传输数据的时候,发送的非常慢,这个过程也叫做慢开始/慢启动;但是每次传输,拥塞窗口都按照指数增长的方式成倍扩大,目的是想尽快把扩大窗口,提高传输效率;
  2. 等到达到设定好的阈值Threshold(初始值),窗口就不再按照指数增长,而是开始线性增长。因为此时窗口已经变得很大了,再按照指数增长,就会很容易出现网络拥塞/丢包的问题。按照线性增长,窗口也是在不断变大的,增长到一定程度,网络也会出现拥塞,开始丢包;
  3. 出现拥塞之后,就会根据当前窗口的大小,就会重新调整窗口阈值Threshold(更新值),并重复前面窗口增长的过程:慢启动 -> 指数增长 -> 线性增长。

       不断调整发送窗口大小,目的是不断测试中间节点的传输能力,使发送方在不丢包的前提下,尽可能得使用比较大的窗口进行传输,提高传输效率。

       流量控制和拥塞控制都是为了提高TCP传输的效率,流量控制是站在接收方的角度,而拥塞控制是站在中间节点的角度(把所有中间节点认为是一个整体),都是通过限制发送方发送窗口的大小提高传输效率。最终发送窗口的大小,是取流量控制窗口和拥塞控制窗口的最小值。

7.延时应答

       什么是延时应答?通常情况下主机A给主机B发送一个TCP数据包,主机B会立刻返回一个ACK,这个过程我们可以理解为是一个正常的确认应答的过程;如果主机A给主机B发送一个TCP数据包,主机B要延时一会,再给A返回一个ACK,那么延时一会再返回ACK这个动作,就叫做延时应答。

       为什么要延时应答?通过上面,我们也大概了解到,TCP的传输效率取决于发送窗口的大小,发送窗口越大,传输效率也就越高,延时应答的目的是给接收方的应用程序留更多的时间去处理TCP缓存区中的内容,这样接收方返回的ACK中才能携带更大的窗口,那么发送方也能够发送更多的数据,传输效率就提升了。因此延时应答本质上也是为了提高TCP的传输效率。

8.捎带应答

       什么是捎带应答?如果主机A给主机B发送一个TCP数据包,业务数据为应用层协议定义的某种请求,正常情况下,主机B需要立刻给主机A返回一个ACK,但是由于TCP中引入了延时应答的机制,因此会等待一段时间。等待的这段时间,主机B中的应用程序可能已经将这个请求处理完毕,响应数据已经计算完成,那么要发送的ACK和响应,就可以使用一个TCP数据包返回,可以理解为要发送的响应数据包,捎带上了这个ACK,这就是捎带应答。

       如果没有引入捎带应答,那么接收方的TCP协议需要将ACK和响应分别封装成两个包,而发送方需要将两个包分用两次。引入了捎带应答,两个包合并成了一个包,对于接收方来说,只需要封装一次,对于发送方来说,也只需要分用一次,这也就提高了传输效率。

9.面向字节流

       UDP协议是面向数据报的,TCP协议是面向字节流的,面向数据包和面向字节流有什么区别呢?面向数据报是指每次传输的数据就是完整的应用数据包,UDP数据报的最大长度就是64kb,如果一个完整的应用数据超过了64kb,那就意味着UDP协议无法传输,面向数据报就意味着不能拆包和组包。而TCP协议是面向字节流的,每次传输的数据可以是任意个字节,例如一个完整的应用数据包长度是128kb,对于TCP来说可以一次传128kb,也可以分两次,每次传64kb,也可以分4次,每次传32kb。面向字节流意味着可以自由拆包和组包。

       面向字节流传输的方式非常灵活,但是会存在“粘包”问题。TCP会把接收的数据都放在TCP缓存区,TCP缓存区里面是应用数据包的字节,每个字节都是紧挨着的。那我们怎么知道缓存区里面有多少条应用数据?应用层程序在取包的时候如何确定自己取的是一条完整的数据?这就是“粘包问题”。对于UDP协议来说,就不存在这个问题,应用程序每次取一个数据报就可以,在Java中就是取一个DatagramPacket对象。

       那么如何解决“粘包”问题?核心解决思路就是定义好应用层协议,明确应用层数据包之间的边界。通常的解决办法有两种:引入分隔符和引入数据长度。

以引入分隔符为例,假设发送方向接收方发送数据,以“\n”作为分隔符,发送数据如下:

接收方从TCP缓存区中取数据的时候,同样会以“\n”作为分隔符,会分别拿到“ABC”、“DEF”和“GHIJKLMN”这三组数据,与发送方发送的数据一致,不会出现“粘包”问题。

以引入数据长度为例,假设发送方给接收方发送数据,第一个字节作为数据长度,发送数据如下:

接收方从TCP缓存区中读取数据的时候,就会先读一个字符,比如为3,那么就会从前往后读3个字节,得到“ABC”,又读了一个字节为5,那么再读取5个字节,得到“DEFGH”,这样也能成功读取数据,也没有出现“粘包”问题。

10.异常情况的处理

       在使用TCP协议传输数据的时候,也可能会出现一些异常情况,TCP协议会怎么处理?以以下场景为例:

场景1:进程崩溃

       这种情况相当于进程异常关闭了,异常关闭也是关闭,因此会触发FIN,这样对端会返回ACK和FIN,虽然进程崩溃了,但是TCP连接还在,接收到对端的FIN,发送方仍然会返回ACK。这个过程就和正常的断开TCP连接一模一样。

场景2:主机关机

      传输过程中如果发生主机关机的情况,进程就会被强制终止,会触发FIN,对端接收到FIN返回ACK,再返回FIN,如果此时系统还没关闭,就会返回ACK,完成正常的“四次挥手”;如果此时系统已经关闭了,那么就不会返回ACK,对端接收不到ACK就会重传FIN,重传一段时间后会释放连接。

场景3:主机掉电

       传输过程中,主机突然断电了,此时主机还没来得及关闭进程。如果对端是在发送数据,就会等待ACK,进而触发超时重传,重传了也没有响应,就会触发连接重置功能,即发送控制位RST置1的报文,也没有效果就会释放连接。如果对端是在接收数据,接收方会周期性给发送方发送一个心跳包,并期望发送方应答,如果发送多次没有收到应答,就会认为发送方掉线了,也会单方面释放连接。

场景4:网线断开

       传输数据过程中,网线断开,这种情况和场景3的情况是一致的。对于发送方来说,不会得到接收方的应答,会触发超时重传,场景3中的第一种情况一样;对于接收方来说,发送心跳包,得不到响应,和场景3中的第二种情况一样。

4.TCP和UDP的对比

  1. UDP的特点:无连接,不可靠,面向数据报,支持全双工通信;
  2. TCP的特点:有连接,可靠,面向字节流,支持全双工通信;

       TCP和UDP相比,优势在于TCP是可靠传输,因此适用的范围比较广;UDP的优势在于传输效率更高,更适用于对可靠性要求不高,但是对传输效率要求很高的场景,例如,如果带宽充足,设备的负载本就不高,丢包的可能性很小,就更适合使用UDP协议。

       如果要传输的数据大于64kb,那就不能使用UDP了,因为UDP最大支持64kb,超出了就无法传输,这种情况要选用TCP协议。

       如果要进行广播传输,需要优先考虑UDP,因为UDP支持广播,TCP不支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值