TCP协议

一.TCP协议段格式

1.源端口号和目的端口号的最大值都是65535(64KB),超过最大值会发生截断。

2.4位首部长度:指的是TCP报头长度,对于TCP报头而言,它的长度是可以发生改变的。此处的4位,不是4个比特位,而是单位为4个字节,比如说,里面的长度为0xF那么就是15*4=60个字节的长度大小。

3.保留位:用于考虑未来的可扩展性。若在之后使用TCP的时候,其需要新增属性或者某个属性的长度不够用就可以拿出保留位对其进行作用。

4.校验和:此处的校验和与UDP相同。(若不清楚,可以看UDP博客)

5.选项:TCP报头变长的主要原因,选项代表可以有或者可以没有。(可以通过首部长度中推出选项有几个4个字节,有几个就是几个选项。)

二.TCP核心机制

        该机制可以感知接收方是否接收到数据。例如:发送方给接收方发送一条一起打游戏的消息,接受方回应我在写博客,此时接收方回应的我在写博客就是应答报文,也就是acknowledge,简称ack。 

        但是上述的应答报文机制还是存在问题,当接收方发送多个消息的时候,就可能存在歧义。例如:发送方,发送两条消息,第一条消息为你今天要和我一起出去玩吗?第二条消息为你可以做我男朋友吗?接收方也回复了两条消息,第一条是好啊,第二条是拒绝。如果发送方接收到,接收方的消息时,如果是这样接收消息的话则没有问题,但是在网络传输中存在后发先至,也就导致,发送方在接收,接收方返回来的消息时,可能先收到第二条消息的拒绝,再收到第一条消息的好啊,也就导致了歧义。这种情况是无法改变的。于是可以给传输数据加上编号,可以区分出数据的先后顺序。

        由于TCP是面向字节流的,实际上的编号,并不是按照第一条,第二条来编号的,而是按照第几个字节的方式来进行编号的。每个字节都有独立的编号,字节和字节之间的编号是连续的,递增的。按照字节编号的这样的机制,就称为TCP的序号。在应答报文中,针对之前收到的数据进行对应的编号,称为TCP的确认序号。

  

这里的32位序号就表示TCP数据报载荷中的第一个字节的序号,由于序号是连续递增的,知道了第一个字节的序号,后续字节的序号也就知道了。

        这里举个例子:在数据不丢失的情况下,当主机A给主机B发送消息的时候,A发送的这段数据的编码是1~1000,那么B接收之后,就会发送一个应答报文1001,1001的编号是通过A发送的数据的编号的下一个数字。1001表示的是<1001的编号的数据都被成功接收了。

1. 应答报文

        对于应答报文来说的话,确认序号会按照发来的数据的最后一个字节序号后+1的方式来填写。另外在六个标志位中,第二个标志位会设置成1。对于普通报文来说,ack是0,对于应答报文来说,ack是1。如果是普通报文,序号是有效的,但是确认序号是无效的。如果是应答报文的话,序号和确认序号都是有效的。(应答报文默认是不带数据的)

后发先至

        举个例子来再谈后发先至:主机A给主机B发送三条数据,第一条的数据是1~1000,第二条的数据是1001~2000,第三条数据是2001~3000。如果1001~2000的数据先发送给主机B的话,主机B并不会先接收这个数据,因为接收方是调用read,当没有数据的时候就会进行阻塞等待,即使1001~2000的数据先到达了,但是B还是会阻塞等待。直到1~1000的数据到达了才会解除阻塞等待,才会继续读取1001~2000的数据(在B接收方这边的操作系统内核里,会有一段内存空间,作为接收缓存区,收到的数据就会现在接收缓存区进行等待,只有等到真正的开头数据到了,应用程序才会进行数据的读取。),这样做是为了确保发送方write顺序和接收方的read顺序一样。

丢包

丢报的原因有很多,只说主要的两种:

第一种:数据传输的过程中,发生了bit的翻转,收到这个数据的接收方/中间的路由器等,计算校验和,就会发现校验和对不上。(发现发生bit翻转的时候,要及时把这个数据包丢弃,不继续往后转发/不交给应用层使用。)

第二种:数据传输到某个节点(路由器/交换机)这个节点负载太高了。(某个路由器,单位时间只能转发N个包,假设网络高峰期,这个路由器单位时间需要转发的包超过了N个,就会导致后续的传输过来的数据直接被丢弃了。)

        TCP无法避免丢包,只能感知到数据是否丢包,如果丢包,重新再发一次。(数据重发次数越多,数据到达对方的概率越大。)

        丢包是通过应答报文来进行区分的,收到应答报文的时候,说明数据没丢包,没收到应答报文的时候,就说明数据丢包了。但是数据在网络传输中也是需要消耗时间的,如果只是暂时没收到报文的话,需要进行等待,不能被判定为数据被丢包了,所以发送方设置数据的时候,会给出一个时间限制,如果在这个时间限制里面,ack没有收到,就视为是数据丢包了。

        还有一种情况是,主机A给主机B发送数据,当时B发送应答报文的时候,数据丢包了,但是此时B已经接收了是数据A发送来的数据了,此时又因为应答报文没有发送回A,A有传输了相同的数据给B,此时B并不会有两份相同的数据,因为TCP对此进行了处理,也就是接收方会有一个缓存区,收到的数据先放入缓存区中,后续再收到数据的时候,就会根据序号,在缓存区中,找对应的位置(排序),如果发现,当前序号的这个数据,已经在缓存区中了,就直接把新收到的数据丢弃了。确保程序read读出来的数据只有一份。

2.超时重传

        超时重传的设定时间是动态变化的,不是固定的。例如:A给B发送数据,数据丢失了,A进行第一次重传,超时 时间是t1,如果重传之后依旧没有ack,那么进行第二次重传,超时 时间是t2,而t2>t1的。

        每多重传一次,超时时间的间隔会变长,重传的频次会降低。重传多次会提高数据到达对方的概率,但是重传次数过多,还是没有ack,那么就是网络的丢包率,已经达到了一个很高的概率了。网络发生严重事故,大概率没办法继续使用了,于是TCP就不会尝试重传了。(TCP先尝试进行重置/复位的链接,发送一个特殊的数据包,复位报文,如果网络恢复了,复位报文就会重置连接,使通信可以继续进行。如果网络还是有严重的问题,复位报文也没有得到回应,此时TCP就会单方面的放弃连接)

3.连接管理

建立连接:三次握手

        网络中的握手,发送不携带业务数据(没有载荷,只有报头)的数据包,但是能起到“打招呼”的作用。这里通过一个例子和图片来说明三次握手:

        这是最基本的原理图,其中syn也就是synchronized,这里不是加锁的作用,这里就是同步,这里的同步指的是客户端希望服务器和他统一步调,来完成后续的传输。(需要注意的是,在不同场景下syn的意思是不同的)

        但是实际上,中间B传输给A的时候,syn和ack是一起传输回来的,如下图所示:

        所以造就了三次握手的说法,也就使连接完成了(连接是抽象概念,表示通信双方保存对方的信息)。合并是很重要的,因为分两次发送,效率会打折扣,每个数据包都需要进行一系列的封装分用。

 意义

1.投石问路,初步的验证通信的链路是否畅通。(这是进行可靠传输的前提)

        例如地铁在每天开始运营之前都会先跑一次完整的路线,保证路况稳定可以通行,在进行运营。

2.确认通信双方各自的发送能力和接受能力是否正常。

        A和B进行游戏开黑,当A通过麦克风给B说话时,B进行回复,这里就可以知道A的麦克风没问题,B的听筒没问题,当A在进行回复的时候,此时A的听筒也没问题,B的话筒也没问题。

3.让通信双方在通信之前,对通信过程中需要用到的一些关键参数,进行协商。

        TCP通信时,起始数据序号就是通过三次握手决定的。换而言之,TCP每一个数据的起始序号不一定都是从1开始的。每次建立的连接,TCP的起始数据序号差别都很大,是为了避免所谓的前朝的剑,斩后朝的官,如下图所示:

        第一次A与B建立连接之后,A在给B传输数据,其中红色那条数据迷路了,导致A与B断开连接的时候还没有到达终端,当A与B重新连接的时候才到达终端,可是此时已经“改朝换代”了,即使A和B还是原来的主机,但是这次连接的应用程序可能不同,所以对于这种迟到的数据直接丢弃即可。那么如何区分迟到的数据包,在连接的时候,TCP会协商不同的序号,如果发现收到的数据序号和起始序号以及和最近收到的数据的序号差别很大的话就视为前朝的数据包了。

        需要注意的是,TCP的可靠性不是三次握手实现的,只是三次握手对于可靠性传输具有一定的支持,因为三次握手只是在建立连接的时候使用,连接之后就与确认应答和超时重传的关联较大了,就与三次握手无关了,所以其中确认应答和超时重传才是负责传输数据的可靠性。

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

        前面说的超时重传的时候,说到了单方面释放连接。而这里的断开连接是双方各自把对端的信息删除掉。四次挥手的流程图:

        

        四次挥手流程图解释:当主机A给主机B发送FIN的时候,B接收到A传来的FIN,则会返回ack,并且会间隔一段时间发送FIN给A,A收到了B发送的FIN在返回ack给B,则完成四次挥手的流程。那么三次握手拆开看也可以是四次,为什么被说为三次,是因为握手时的ack+syn是可以合并的,并且这两部都是在操作系统内核中,由操作系统来负责进行的,时机都是在syn接收后,同一时机发送两条数据则可以合并,而四次挥手时,ack和FIN大概率是没办法合并的,ack是系统内核控制的,但是FIN的触发使用过应用程序来进行的,是通过调用close或者进程结束来控制的。(代码中的socket.close() => 系统内部发送FIN)

TCP状态(部分)

 

LISTEN:完成服务器的初始化,等待客户端的发送数据的状态。(服务器绑定端口类似于LISTEN状态)

ESTABLISHED:TCP连接建立完成(双方各自保存了对方的信息),可以进行数据业务的通信的状态。

CLOSE_WAIT:被动断开连接或者说是收到FIN的一端。(等待代码执行close的一方)

TIME_WAIT:主动断开连接的一方会进入的一种状态。此处会按照设定的时间来继续等待,达到指定的时间之后释放连接,这是为了防止ack丢包。(画图解释)

  这里的TIME_WAIT存在的时间,称为2MSL(MSL:数据报在网络传输中消耗的最大时间),不同系统的MSL值是不一样的,在正常网络中传输数据的时间不会那么久。

4.滑动窗口

        TCP使用了可靠性传输,导致传输效率降低,为了提高TCP传输数据的效率,引入滑动窗口进行解决问题。(需要注意的是,引入了滑动窗口只能降低因为可靠性导致的效率降低,不会将TCP传输数据的效率快于UDP传输数据的效率)

 没有滑动窗口的TCP传输数据:

有滑动窗口的TCP传输数据:

        改进的方案就是不再是一个数据传输一次再返回一个ack,而是直接缓存一堆数据,再发一堆ack回来,也就是把多次等待ack的时间,合并成等待一段时间,批量发送数据越多,效率也就越高(不是无休止的多),批量发送数据的时候,不需要等待的数据量就是滑动窗口的大小。(批量发送的数据是字节数不是条)

 

        上图为滑动窗口实际的操作流程,当上述流程主机A接收到主机B返回的ack,窗口直接移动向右移动,直接发送下一个数据。上图中A是发送了1001~2000的数据,并且B返回了一个ack,说明下一条是2001,则说明1001~2000的数据B已经接收了,并且A此时会直接发送5001~6000的数据,并且向右移动,此时等待的ack数据范围就是2001~6000。

滑动窗口丢包

1.数据包已经抵达,ACK被丢弃:

      

        这里就需要再次复习一下确认序号的意义了,确认序号表示收到的数据最后一个字节的下一个序号,进一步可以推导出,确认序号之前的数据,已经被收到了,接下来发送的数据只需要从确认序号之后发送就行。

        这里下一个是1001的ack被丢弃了,但是下一个是2001的ack没有被丢弃,那么主机A则会收到2001序号之前的数据被接收。(后一个ack可以覆盖前一个ack)

2.数据报丢了

        在传送1~7000的数据时,1001~2000的数据包丢失了,主机B会不断返回一个下一个是1001的ack,因为是滑动窗口进行一堆数据的发送,那么1~7000的数据都会直接发送给B,但是从1001开始,后面发送的数据,B返回的都是下一个为1001的ack,因为这是在提示主机A发送的数据包1001~2000丢失了,主机A收到多个下一个为1001的ack,也就是相同的ack,主机A则就意识到了1001~2000的数据包丢失,再进行重发,当数据重发之后,由于除了1001~2000的数据包丢失,其他数据包没有丢失,那么当1001~2000的数据包重发并且主机B接收成功,就会直接把剩余的数据也直接接收了,然后返回下一个是8001的数据包。

5.流量控制

        流量控制就是用来控制滑动窗口的窗口过大也就是传输的数据过多时,防止数据被丢弃。因为当主机A给主机B传输数据的时候,主机B中会存在一个接收缓存区(内核中的内存空间,每个socket对象都存在一个这样的缓存区),当A的滑动窗口过大的时候,会导致传输的数据过快,导致B的内存缓存区满载导致后续A传输过来的数据直接被丢弃了。这里类似生产者消费者模型。

        故流量控制是可以通过接收方来反向制约接收方传输数据的速度,由此可以通过接收方内存缓存区剩余空间的大小来控制传输方传输数据速度的快慢。当接收方的内存缓存区剩余空间较多的时候可以让发送方传输数据快一点也就是扩大窗口大小,当接收方的内存缓存区剩余空间较少的时候,可以让发送方传输数据慢一点,也就是缩小窗口大小。

        在TCP中,接收方收到发送方传输过来的数据会发送ack报文传回发送方,此时ack中会将接收方的内存缓存区剩余的空间大小也传给发送方,由此就可以制约发送方传输数据的快慢。但是ack报文是不携带任何数据的,所以TCP是通过窗口16位大小来体现接收方的内存缓存区剩余空间,这个属性只有在ack报文中有效也就是tcp种的ack为1时有效。

        此处窗口大小16位,并不是意味着表示的范围只有64KB,在选项中可以设置一个特殊选项,窗口扩展因子,发送方的窗口大小=窗口大小 <<窗口扩展因子(<<左移几次就是2的几次方倍增长)。

        当主机A给主机B通过滑动窗口发送一堆信息之后,主机B接收信息之后,会返回ack报文,其中ack报文中就会将接收内存缓存区的剩余空间告诉发送方A,这样就能通过剩余空间来进行滑动窗口的窗口大小设置。在下一个是1001 的右边的数字就是剩余空间的大小。当剩余空间为0的时候,主机A就不能继续发送数据包了,就需要等待主机B将数据处理完之后才能传输数据包,这期间主机A就会在等待,但是等待的时候主机A会发送窗口探测包,发送窗口探测包是为了查询接收方的空间剩余量还有多少或者是否更新了,并且接收方也会在窗口不为0的时候主动触发窗口不为0的更新通知,

6.拥塞控制(动态变化)

        流量控制是在接收方的角度控制发送方的传输速度,拥塞控制是在传输链路的角度来控制发送方的传输速度。

        如果B接收方处理数据很快,A的发送速度也很快,就有可能造成数据链路中的某个节点的负载很高,当A还是继续快速的发送数据的话,可能会造成丢包。

如果需要考虑中间节点的问题,大致有4个方向:

1.中间节点非常多。

2.每次传输数据走的路线不一样。

3.无法知道中间哪个节点遇到了瓶颈。

4.中间节点传输的数据不只有A的数据还有其他设备上的数据。

解决方法也是5个方向:

1.先按照一个比较小的速度进行传输数据。

2.数据非常畅通,没有丢包,就可以进行加快数据的传输速度。

3.增大到一定程度的时候,出现了丢包,说明网上存在拥堵现象,需要减缓传输数据的速度。

4.减速之后,发现不在丢包,就进行加速。

5.加速之后,又发现丢包了,就进行减速。

综上所诉,流量控制和阻塞控制都会限制窗口大小,所以窗口大小由这两个决定。

拥塞控制中窗口大小变化动态图:

1.刚开始传输数据的时候,拥塞窗口非常小,用一个很小的速度进行发送数据。(由于不知道当前网络是否拥堵,故在刚启动的时候,发送的数据很慢)

2.不丢包,增大窗口,指数增大。

3.增长到一定程度,达到某个指定的阈值,此时,即使没有丢包,也会停止指数的增删,变成线性增长。(防止太快进入丢包的传输速度)

4.线性增长,也会持续使传输速度越来越快,达到某个情况下就会丢包。(丢包出现后,就会降低发送方的传输速度,减小窗口大小。)

丢包出现后,两种处理情况:

1.经典方案:回归慢开始非常小的初始值,指数增长,线性增长。

2.现在方案:回归到新的阈值上,线性增长(之后不会有指数增长)

7.延时应答

        此处的延时应答可以提高传输效率,尽可能降低可靠传输带来的性能影响,图解:

        如果此时之间返回ack应答报文,那么此时接收方的内存缓存区的剩余空间只有5KB,此时发送方的窗口大小只有5KB,但是应用层也在不断地消耗内存缓存区的数据,这就导致了,ack中返回给发送方的剩余空间大小比实际小了一些。由此,将ack应答报文进行延时应答,应用层序就会有时间读取缓存区的数据,此时ack中返回的剩余空间大小就可以增大了。

8.捎带应答

        在延时应答的基础上,引入的提升效率的机制,把返回的业务数据和ack合二为一,实际网络通信中,大部分都是一问一答的方式。

        ack是内核返回的,是收到请求,就会立即返回ack,响应则是通过应用层序返回的,代码中,根据请求计算得到响应,再把响应写回到客户端。正常情况下,ack和响应不会是同一时机,导致无法合并,但是ack涉及到了延时应答,会导致ack返回的时间变长,可能会赶上响应数据返回的操作了,于是就可以在返回响应的时候带上ack。(其中四次挥手也涉及到这种情况,当ack延时应答时,ack返回时间会变晚,此时就可以和FIN合并)

9.面向字节流

        通过面向字节流的方式传输数据,都会涉及到粘包的问题。(粘的是TCP鞋带的载荷(应用层数据包))

        应用层数据包在TCP接收缓存区中连城一片,黏在一起,就成为粘包问题。应用层序需要读取接收缓存区的数据时,由于TCP是面向字节流的,此处的操作,咋读都可以,可以读出一个aa  abb  bcc  c  还可以  aaabb  bcc  c 总而言之有很多读法,这就导致了无法拿到我们需要的数据。

        如果希望在文件中存储结构化的数据,也是会存在这样的问题。所以文件经常会用到xml/json这样的格式来存储(也就是解决粘包问题)为了解决粘包问题,需要明确包之间的边界。

方案一:指定分隔符(xml/yml/json),适合于文本类的数据,需要确认数据的文本中,不能包含数据分隔符,如果传输数据是纯文本的话,此时使用/n或者;之类的可能都不合适,但是可以使用ASCII中靠前的控制字符:

方案二:指定数据长度(protobuf),比如,约定在每个应用层数据包,开头2/4个字节,表示数据包的长度。

10.异常处理:

1.进程崩溃

        在java中体现在异常抛出时,未进行catch操作,最终异常到了JVM这里,JVM就会直接崩溃。当进程崩溃时,进程中的PCB就会被回收。PCB中的文件描述符表里对应的所有文件,也都会被系统自动关闭。其中针对socket文件,也会触发正常的关闭流程(TCP四次挥手)

2.主机关机(正常关机)

        正常流程点击关机按钮,此时操作系统就会干掉所有的进程,干的过程同样会触发四次挥手。其中会存在两种情况,第一种情况:四次挥手很快,四次挥手完成后,关机动作才算真正的完成了。第二种情况:四次挥手还没结束,关机就完成了。

3.主机掉电(拔电源)

a.接收方掉电:

        当主机A给主机B发送数据之后,B则无法传送ack返回A,A触发超时重传,重传的数据肯定没有响应,因为接收方B已经掉电了,反复多次之后,A尝试重置连接,重置操作也没有ack,A就会单方面释放连接。(A保存的B的信息)

b.发送方掉电:

        当主机A给主机B发送一些数据之后,A掉电了,则不再发送数据,此时B会发送给A一个数据包,探测A是否掉电,这个数据包不带任何业务数据,属于探测报文,如果发送给A探测报文之后,A返回ack,则A存活,只是休息一会儿。如果发送了多个探测报文,A没有返回ack,则A主机挂掉了。(这样的探测包是周期性的,同时这个报文是用来探测对方的生死的,所以把这类包成为心跳包)       

4.网线断开:

        

A视角:收不到ack,超时重传,重置连接,单方面断开连接。

B视角:A不发送数据,心跳包不通,重复多次,判断在,单方面断开连接。

三.TCP报文剩余介绍

        

        URG和紧急指针一起使用,当URG为1时,紧急指针才有效,紧急指针里面保存了一个偏移量,TCP正常情况下都是按照顺序来传输数据的,紧急指针可以让后面的数据通过紧急指针的偏移量进行插队,把指定位置的数据优先发出去。(日常开发很少涉及)

        

        PSH催促标志位,带有这个标志位的数据,就相当于在提醒接收方,要尽快来处理这个数据(特殊场景下的特殊方案)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值