【TCP协议】---协议详解

TCP协议

本文内容如下:
      1)TCP协议概念
      2)TCP头部结构和字段介绍
      3)TCP流量控制
            滑动窗口
      4)TCP拥塞控制
           慢启动、拥塞避免、快重传、快恢复

有关TCP的三次握手四次挥手单独写了一篇博客:【TCP协议】---TCP三次握手和四次挥手

有关TCP粘包和黏包,也单独写一篇博客,下一篇博客就写有关粘包黏包问题。

一、TCP概念

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接(连接导向)的、可靠的、 基于IP的传输层协议。

首先来看看OSI的七层模型

 

      我们需要知道TCP工作在网络OSI的七层模型中的第四层——传输层,IP在第三层——网络层,ARP 在第二层——数据链路层;同时,我们需要简单的知道,数据从

应用层发下来,会在每一层都会加上头部信息,进行 封装,然后再发送到数据接收端。这个基本的流程你需要知道,就是每个数据都会经过数据的封装和解封 装的过程。

在OSI七层模型中,每一层的作用和对应的协议如下:

 

 

二、TCP头部结构和字段介绍

     从上面图片可以看出,TCP协议是封装在IP数据包中。

下图是TCP报文数据格式。TCP首部如果不计选项和填充字段,它通常是20个字节。

 

 下面分别对其中的字段进行介绍:

源端口和目的端口

     各占2个字节,这两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。有时一个IP地址和一个端口号也称为socket(插口)。

序号(seq)

     占4个字节,是本报文段所发送的数据项目组第一个字节的序号。在TCP传送的数据流中,每一个字节都有一个序号。例如,一报文段的序号为300,而且数据共100字节,

则下一个报文段的序号就是400;序号是32bit的无符号数,序号到达2^32-1后从0开始。

确认序号(ack)

     占4字节,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个报文段的首部中的序号;确认序号应该是上次已成功收到数据字节序号+1。

只有ACK标志为1时,确认序号才有效。

数据偏移

      占4比特,表示数据开始的地方离TCP段的起始处有多远。实际上就是TCP段首部的长度。由于首部长度不固定,因此数据偏移字段是必要的。数据偏移以32位为长度单位,

也就是4个字节,因此TCP首部的最大长度是60个字节。即偏移最大为15个长度单位=1532位=154字节。

保留

     6比特,供以后应用,现在置为0。

6个标志位比特

① URG:当URG=1时,注解此报文应尽快传送,而不要按本来的列队次序来传送。与“紧急指针”字段共同应用,紧急指针指出在本报文段中的紧急数据的最后一个字节的序号,

              使接管方可以知道紧急数据共有多长。

② ACK:只有当ACK=1时,确认序号字段才有效;

③ PSH:当PSH=1时,接收方应该尽快将本报文段立即传送给其应用层。

④ RST:当RST=1时,表示出现连接错误,必须释放连接,然后再重建传输连接。复位比特还用来拒绝一个不法的报文段或拒绝打开一个连接;

⑤ SYN:SYN=1,ACK=0时表示请求建立一个连接,携带SYN标志的TCP报文段为同步报文段;

⑥ FIN:发端完成发送任务。

窗口

      TCP通过滑动窗口的概念来进行流量控制。设想在发送端发送数据的速度很快而接收端接收速度却很慢的情况下,为了保证数据不丢失,显然需要进行流量控制, 协调好

通信双方的工作节奏。所谓滑动窗口,可以理解成接收端所能提供的缓冲区大小。TCP利用一个滑动的窗口来告诉发送端对它所发送的数据能提供多大的缓 冲区。窗口大小为

字节数起始于确认序号字段指明的值(这个值是接收端正期望接收的字节)。窗口大小是一个16bit字段,因而窗口大小最大为65535字节。

检验和

       检验和覆盖了整个TCP报文段:TCP首部和数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。

紧急指针

      只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。

 

三、TCP流量控制(滑动窗口协议)

      在看这里,为了更好的理解,建议先了解TCP三次握手,四次挥手。博客:【TCP协议】---TCP三次握手和四次挥手 

      TCP流量控制主要是针对接收端的处理速度不如发送端发送速度快的问题,消除发送方使接收方缓存溢出的可能性。

      TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量

控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;

同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

我们可以通过下图来分析:

1、发送方接收到了对方发来的报文 ack = 33, win = 10,知道对方收到了 33 号前的数据,现在期望接收 [33, 43) 号数据。那我们开始发送[33, 43) 号的数据。

2、[33, 43) 号的数据你是已经发送了,但接受方并没有接受到[36,37]数据。所以接收方发送回对报文段 A 的确认:ack = 35, win = 10。

3、发送方收到了 ack = 35, win = 10,对方期望接收 [35, 45) 号数据。那么发送方在发送[35, 45) 。

    这里面需要思考一个问题?

    第一步发送了[33, 43),如果这次发送[35, 45),那中间重叠部分不是发送了两次,所以这里要思考: 是全部重新发送还是只发送接收端没有收到的数据,如果全部发送那么重复

发送的数据接收端怎么处理。这个下面快速重传会讲。

4、接收方接收到了报文段 [35, 41),接收方发送:ack = 41, win = 10. (这是一个累积确认)

5、发送方收到了 ack = 41, win = 10,对方期望接收 [41, 51) 号数据。

6、.......

 这样一直传输数据,直到数据发送完成。这么一来就保证数据数据的可靠性,因为如果某数据没有获取到,那么ack永远不会跳过它。

 这里也要思考一个问题,如果某一数据一只没有获取到,总不能一直这样堵塞在这里吧,这里就要讲接下来有关堵塞的解决方法。

 

四、TCP拥塞控制

      流量控制是通过接收方来控制流量的一种方式;而拥塞控制则是通过发送方来控制流量的一种方式。

      TCP发送方可能因为IP网络的拥塞而被遏制,TCP拥塞控制就是为了解决这个问题(注意和TCP流量控制的区别)。

TCP拥塞控制的几种方法:慢启动,拥塞避免,快重传和快恢复。

这里先理解一个概念: 拥塞窗口

      拥塞窗口:发送方维持一个叫做拥塞窗口 cwnd的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态变化。

      发送方的让自己的发送窗口=min(cwnd,接受端接收窗口大小)。说明: 发送方取拥塞窗口与滑动窗口的最小值作为发送的上限。

      发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少

注入到网络中的分组数。

下面将讨论拥塞窗口cwnd的大小是怎么变化的。

1、慢启动

     TCP在连接过程的三次握手完成后,开始传数据,并不是一开始向网络通道中发送大量的数据包。因为假如网络出现问题,很多这样的大包会积攒在路由器上,很容易导致网

络中路由器缓存空间耗尽,从而发生拥塞。因此现在的TCP协议规定了,新建立的连接不能够一开始就发送大尺寸的数据包,而只能从一个小尺寸的包开始发送,在发送和数据被

对方确认的过程中去计算对方的接收速度,来逐步增加每次发送的数据量(最后到达一个稳定的值,进入高速传输阶段。相应的,慢启动过程中,TCP通道处在低速传输阶段),

以避免上述现象的发生。这个策略就是慢启动。

画个简单的图从原理上粗略描述一下

 

我们思考一个慢启动引起的性能问题?

       在海量用户高并发访问的大型网站后台,有一些基本的系统维护需求。比如迁移海量小文件,就是从一些机器拷贝海量小碎文件到另一些机器,来完成一些系统维护的基本需求。

慢启动为什么会对拷贝海量小文件的需求造成重大性能损失?

      举个简单的例子,我们对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝就是这个例子的实际场景,很常见的用法)。那么工作过程应该是,每传输一个文件建立一个

连接,然后连接处于慢启动阶段,传输小文件,每个小文件几乎都处于独立连接的慢启动阶段被传输,这样传输过程所用的TCP包的总量就会增多。更细致的说一说这个事,如果在

慢启动过程中传输一个小文件,我们可能需要2至3个小包,而在一个已经完成慢启动的TCP通道中(TCP通道已进入在高速传输阶段),我们传输这个文件可能只需要1个大包。

网络拷贝文件的时间基本上全部消耗都在网络传输的过程中(发数据过去等对端ACK,ACK确认归来继续再发,这样的数据来回交互相比较本机的文件读写非常耗时间),撇开三次

握手和四次握手那些包,如果文件的数量足够大,这个总时间就会被放大到需求难以忍受的地步。

因此,在迁移海量小文件的需求下,我们不能使用“对每个文件都采用独立的TCP连接来传输(循环使用scp拷贝)“这样的策略,它会使每个文件的传输都处于在一个独立TCP的慢启

动阶段。

如何避免慢启动,进而提升性能?

       很简单,尽量把大量小文件放在一个TCP连接中排队传输。起初的一两个文件处于慢启动过程传输,后续的文件传输全部处于高速通道中传输,用这样的方式来减少发包的数

目,进而降低时间消耗。同样,实际上这种传输策略带来的性能提升的功劳不仅仅归于避免慢启动,事实上也避免了大量的3次握手和四次握手,这个对海量小文件传输的性能消耗

也非常致命。

2、拥塞避免

先补充下: 慢启动中拥塞窗口的cwnd值,开始是1,接下开是指数型增涨的。1、2、4、8、16.....这样涨太快了吧。那么就有了堵塞避免。

cwnd不能一直这样无限增长下去,一定需要某个限制。TCP使用了一个叫慢启动门限(ssthresh)的变量,一旦cwnd>=ssthresh(大多数TCP的实现,通常大小都是65536),慢

启动过程结束,拥塞避免阶段开始;

拥塞避免:cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段都被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过

快导致网络拥塞,慢慢的增加调整到网络的最佳值。(它逻辑很简单就是到一定值后,cwnd不在是指数增长,而是+1增长。这样显然慢多了)。

非ECN环境下的拥塞判断,发送方RTO超时,重传了一个报文段,它的逻辑如下:

      1)把ssthresh降低为cwnd值的一半。

      2)把cwnd重新设置为1。

      3)重新进入慢启动过程。

 上面的图还是蛮好理解的。

3、快速重传

    TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

注意:  接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到)

此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端

就以为之前的都收到了。

3.1)超时重传机制

         一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。

但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会

悲观地认为也丢了,所以有可能也会导致4和5的重传。

对此有两种选择:

① 一种是仅重传timeout的包。也就是第3份数据。

② 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。

这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长。

3.2)快速重传机制

于是,TCP引入了一种叫Fast Retransmit的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到

3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传,而是只是三次相同的ack就重传。

比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2

因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下

 

 Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重转之前的一个还是重装所有的问题。对于上面的示例来说,是重传#2呢还是重传

#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从#2到

#20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

 总结: 不管是超时重传还是快速重传确实能保证数据的可靠性,但它无法解决的问题就是:比如发送端发了1、2、3、4、5,而接收端收到了1、3、4、5,那么这个时候它发送的

ack是2。那么发送端发送的是重传#2呢还是重传#2,#3,#4,#5的问题。如果在发送#2,#3,#4,#5,本身资源是一种浪费,因为接受方#3,#4,#5已经缓存下来,只需

#2,所以在发一遍是无意义的。

TCP三次握手和四次挥手

   

有关TCP协议详解,请看博客:【TCP协议】(1)---TCP协议详解

TCP有6种标示:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急) 

一、TCP三次握手                         

  第一次握手

      客户端向服务器发出连接请求报文,这时报文首部中的同部位SYN=1,同时随机生成初始序列号 seq=x,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状

态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。这个三次握手中的开始。表示客户端想要和服务端建立连接。

  第二次握手

      TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己随机初始化一个序列号 seq=y,此

时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。这个报文带有SYN(建立连接)和ACK(确认)标志,询问客户端

是否准备好。

  第三次握手

      TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。

TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。这里客户端表示我已经准备好。

思考:为什么要三次握手呢,有人说两次握手就好了

举例:已失效的连接请求报文段。

   client发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,直到某个时间才到达server,本来这已经是一个失效

的报文,但是server端接收到这个请求报文后,还是会想client发出确认的报文,表示同意连接。假如不采用三次握手,那么只要server发出确认,新的建立就连接了,但其实这个

请求是失效的请求,client是不会理睬server的确认信息,也不会向服务端发送确认的请求,但是server认为新的连接已经建立起来了,并一直等待client发来数据,这样,server的

很多资源就没白白浪费掉了,采用三次握手就是为了防止这种情况的发生,server会因为收不到确认的报文,就知道client并没有建立连接。这就是三次握手的作用。

  

二、TCP数据的传输过程

  建立连接后,两台主机就可以相互传输数据了。如下图所示(本篇博客图片都是引用它人图片):

  1)主机A初始seq为1200,滑动窗体为100,向主机B传递数据的过程。

  2)假设主机B在完全成功接收数据的基础上,那么主机B为了确认这一点,向主机A发送 ACK 包,并将 Ack 号设置为 1301。因此按如下的公式确认 Ack 号:

       Ack号 = Seq号 + 传递的字节数 + 1 (这是在完全接受成功的情况下)

  3)主机A获得B传来的ack(1301)后,开始发送seq为1301,滑动窗体为100的数据。
       ......

与三次握手协议相同,最后加 1 是为了告诉对方要传递的 Seq 号。上面说了,主机B完全成功接收A发来的数据才是这样的,如果存在丢包该如何。

 下面分析传输过程中数据包丢失的情况,如下图所示:

 

上图表示通过 Seq 1301 数据包向主机B传递100字节的数据,但中间发生了错误,主机B未收到。经过一段时间后,主机A仍未收到对于 Seq 1301 的ACK确认,因此尝试

重传数据。为了完成数据包的重传,TCP套接字每次发送数据包时都会启动定时器,如果在一定时间内没有收到目标机器传回的 ACK 包,那么定时器超时,数据包会重传。

 上面也只是一种可能,比如数据1250丢失,那么Ack返回的就是1250,具体的可以详细看下博客:【TCP协议】(1)---TCP协议详解,这里面滑动窗口有说明。

 

 三、TCP的四次挥手                   

 

第一次挥手  

    TCP发送一个FIN(结束),用来关闭客户到服务端的连接。

    客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),

此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

挥手

    服务端收到这个FIN,他发回一个ACK(确认),确认收到序号为收到序号+1,和SYN一样,一个FIN将占用一个序号。

    服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器

通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个

状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

 

客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

挥手

      服务端发送一个FIN(结束)到客户端,服务端关闭客户端的连接。

      服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,

此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

挥手

     客户端发送ACK(确认)报文确认,并将确认的序号+1,这样关闭完成。

     客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时

TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

 

服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

思考:那么为什么是4次挥手呢?

为了确保数据能够完成传输。

      关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也

即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

可能有人会有疑问,tcp我握手的时候为何ACK(确认)和SYN(建立连接)是一起发送。挥手的时候为什么是分开的时候发送呢.

因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到

FIN报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能

发送FIN报文,因此不能一起发送。故需要四步挥手

思考:客户端突然挂掉了怎么办?

    正常连接时,客户端突然挂掉了,如果没有措施处理这种情况,那么就会出现客户端和服务器端出现长时期的空闲。解决办法是在服务器端设置保活计时器,每当服务器收到

客户端的消息,就将计时器复位。超时时间通常设置为2小时。若服务器超过2小时没收到客户的信息,他就发送探测报文段。若发送了10个探测报文段,每一个相隔75秒,

还没有响应就认为客户端出了故障,因而终止该连接。

 

四、SYN(洪水)攻击

背景

      初始化连接的 SYN 超时问题Client发送SYN包给Server后挂了,Server回给Client的SYN-ACK一直没收到Client的ACK确认,这个时候这个连接既没建立起来,也不能算

失败。这就需要一个超时时间让Server将这个连接断开,否则这个连接就会一直占用Server的SYN连接队列中的一个位置,大量这样的连接就会将Server的SYN连接队列耗尽,

让正常的连接无法得到处理。

      目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第

5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会把断开这个连接。由于,SYN超时需要63秒,那么就给攻击者一

个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server(俗称SYN flood攻击),用于耗尽Server的SYN队列。

什么是 SYN 攻击

       SYN 攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器

需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。SYN 攻击是一

种典型的 DoS攻击。

如何检测 SYN 攻击?

      检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的

netstats 命令来检测 SYN 攻击。

如何防御 SYN 攻击?

      SYN攻击不能完全被阻止,除非将TCP协议重新设计。我们所做的是尽可能的减轻SYN攻击的危害,常见的防御 SYN 攻击的方法有如下几种:

      缩短超时(SYN Timeout)

      时间增加最大半连接数

      过滤网关防护SYN

      cookies技术

 

 四、TCP和UDP的区别      

  我这里简单列举几个,因为我还没有研究UDP这个协议。

  1、基于连接与无连接;UDP是无连接的,即发送数据之前不需要建立连接

  2、TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付

       ,即不保证可靠交付Tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

  3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。

  4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。

  5、TCP对系统资源要求较多,UDP对系统资源要求较少。

【TCP协议】(3)---TCP粘包黏包

有关TCP协议之前写过两篇博客:

      1、【TCP协议】(1)---TCP协议详解

      2、【TCP协议】(2)---TCP三次握手和四次挥手

一、TCP粘包、拆包图解

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:

      1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包

     2)服务端一次接受到了两个数据包,D1和D2粘合在一起,称之为TCP粘包

     3)服务端分两次读取到了数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这称之为TCP拆包

     4)服务端分两次读取到了数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余部分内容D1_2和完整的D2包。

特别要注意的是,如果TCP的接受滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种情况,即服务端分多次才能将D1和D2包完全接受,期间发生多次拆包。

 

二、 粘包、拆包发生原因

      产生原因主要有这3种:滑动窗口、MSS/MTU限制、Nagle算法

1、滑动窗口

      TCP流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量

控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;

同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

现在来看一下滑动窗口是如何造成粘包、拆包的?

粘包:假设发送方的每256 bytes表示一个完整的报文,接收方由于数据处理不及时,这256个字节的数据都会被缓存到SO_RCVBUF(接收缓存区)中。如果接收方的SO_RCVBUF

         中缓存了多个报文,那么对于接收方而言,这就是粘包。

拆包:考虑另外一种情况,假设接收方的窗口只剩了128,意味着发送方最多还可以发送128字节,而由于发送方的数据大小是256字节,因此只能发送前128字节,等到接收方ack

         后,才能发送剩余字节。这就造成了拆包。

2、MSS和MTU分片

MSS: 是Maximum Segement Size缩写,表示TCP报文中data部分的最大长度,是TCP协议在OSI五层网络模型中传输层对一次可以发送的最大数据的限制。

MTU: 最大传输单元是Maxitum Transmission Unit的简写,是OSI五层网络模型中链路层(datalink layer)对一次可以发送的最大数据的限制。

当需要传输的数据大于MSS或者MTU时,数据会被拆分成多个包进行传输。由于MSS是根据MTU计算出来的,因此当发送的数据满足MSS时,必然满足MTU。

为了更好的理解,我们先介绍一下在5层网络模型中应用通过TCP发送数据的流程:

对于应用层来说,只关心发送的数据DATA,将数据写入socket在内核中的发送缓冲区SO_SNDBUF即返回,操作系统会将SO_SNDBUF中的数据取出来进行发送。

    传输层会在DATA前面加上TCP Header,构成一个完整的TCP报文。

    当数据到达网络层(network layer)时,网络层会在TCP报文的基础上再添加一个IP Header,也就是将自己的网络地址加入到报文中。

    到数据链路层时,还会加上Datalink Header和CRC。

     当到达物理层时,会将SMAC(Source Machine,数据发送方的MAC地址),DMAC(Destination Machine,数据接受方的MAC地址 )和Type域加入。

可以发现数据在发送前,每一层都会在上一层的基础上增加一些内容,下图演示了MSS、MTU在这个过程中的作用。

     MTU是以太网传输数据方面的限制,每个以太网帧都有最小的大小64bytes最大不能超过1518bytes。刨去以太网帧的帧头 (DMAC目的MAC地址48bit=6Bytes

+SMAC源MAC地址48bit=6Bytes+Type域2bytes)14Bytes和帧尾 CRC校验部分4Bytes(这个部分有时候大家也把它叫做FCS),那么剩下承载上层协议的地方也

就是Data域最大就只能有1500Bytes这个值 我们就把它称之为MTU。

由于MTU限制了一次最多可以发送1500个字节,而TCP协议在发送DATA时,还会加上额外的TCP Header和Ip Header,因此刨去这两个部分,就是TCP协议一次可以

发送的实际应用数据的最大大小,也就是MSS。

      MSS长度=MTU长度-IP Header-TCP Header

TCP Header的长度是20字节,IPv4中IP Header长度是20字节,IPV6中IP Header长度是40字节,因此:在IPV4中,以太网MSS可以达到1460byte;在IPV6中,以太网

MSS可以达到1440byte。

需要注意的是MSS表示的一次可以发送的DATA的最大长度,而不是DATA的真实长度。发送方发送数据时,当SO_SNDBUF中的数据量大于MSS时,操作系统会将数据进

行拆分,使得每一部分都小于MSS,这就是拆包,然后每一部分都加上TCP Header,构成多个完整的TCP报文进行发送,当然经过网络层和数据链路层的时候,还会分别

加上相应的内容。

需要注意:  默认情况下,与外部通信的网卡的MTU大小是1500个字节。而本地回环地址的MTU大小为65535,这是因为本地测试时数据不需要走网卡,所以不受到1500

               的限制。

3、 Nagle算法

       TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送ACK表示确认。

即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的

情况对于重负载的网络来是无法接受的。

为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。

Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有

收到对方发送的ACK确认该数据已收到。

Nagle算法的规则:

      1)如果SO_SNDBUF(发送缓冲区)中的数据长度达到MSS,则允许发送;

      2)如果该SO_SNDBUF中含有FIN,表示请求关闭连接,则先将SO_SNDBUF中的剩余数据发送,再关闭;

      3)设置了TCP_NODELAY=true选项,则允许发送。TCP_NODELAY是取消TCP的确认延迟机制,相当于禁用了Nagle 算法。

      4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

      5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。


有关TCP粘包黏包的解决办法,将在下一篇通过Netty代码演示。

 

OAuth 2.0详解

概念:OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如基本消息,照片,联系人列表),

而无需将 用户名 和 密码 提供给第三方应用。

一、应用场景

为了理解OAuth的适用场合,这里举一个使用第三方账户进行登录的例子。

现在一般登陆都会采用 第三方授权 登陆,比较常见就是微信、qq、微博授权登陆。这里以微信授权登陆为例:

现在我在未注册的情况下去访问A网站,A网站 为了提高用户体验,可以省去你在这次网站申请注册的步骤,让你通过微信授权登陆去拿去你在微信上的基本信息。

问题就在这里,如果拿到微信用户基本信息给到A网站,直接给A网站我登陆微信的账号密码,那么问题可想而知。

1、这个也太不安全了,我只想给A网站我的在微信上的基本信息,而不是所有信息,通过用户密码可以获取我的所有信息。

2、用户只有修改密码,才能收回赋予"A网站"的权力。但是这样做,会使得其他所有获得用户授权的第三方应用程序全部失效。

3、只要有一个第三方应用程序被破解,就会导致用户密码泄漏,以及所有被密码保护的数据泄漏。

OAuth就是为了解决上面这些问题而诞生的。

从上面可以看出主要有三个身份

用户

使用第三方账户登录一个新的网站,对于用户来说就不需要走复杂的注册流程。

第三方平台(微信)

上面来讲 微信 就是第三方平台,那么对于第三方如何做才能保证用户的安全呢?

就在A网站在通过微信授权登陆之前,需要提供资质到微信,微信审核,审核通过后给要求接入的服务商一个唯一凭证,标明服务商身份。

服务商(A网站)

我们要做的就是将这两者进行连接起来,先到第三方平台资质审核,审核通过后,用户去第三方平台授权登录后,就可以获取用户基本信息,完成登陆。

 

二、OAuth的思路

这里还是以 服务商(A网站),和 第三方平台(微信)授权登录来缕这个思路。

OAuth在 服务商(A网站) 与 第三方平台(微信) 之间,设置了一个授权层(authorization layer)。服务商 不能直接登录 第三方平台,只能登录授权层,

以此将用户与服务商(A网站)区分开来。服务商(A网站) 登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。

服务商(A网站) 登录授权层以后,第三方平台 根据令牌的权限范围和有效期,向 服务商(A网站) 开放用户储存的资料。

这里缕下大致流程

1、接入前准备(资质审核)

如果一个服务商需要使用第三方平台的服务,那么首先是需要向第三方平台提供资料,第三方平台审核通过后,会给服务商一个唯一标识的ID,这样通过第三方平台授权的时候,

第三方平台就知道是哪个商户了。

一般来说你会得到如下的两个参数:

appid 代表你的应用唯一ID
appsecret 对应的密钥

这个部分每家平台都不一样,具体如何获取你的APPID请参考对应平台的指南.

注意 第三方平台给你的不一定是APPID,我的意思不是连名字都完全一样,有的平台给的参数多有的给的少,总之都是用于验明身份的.

2、用户要使用第三方登陆

这里我们以登录为例.

在这个流程中服务器(A网站)接受到了用户想要第三方登录的请求,我们使用之前获取的APPID(不同平台叫法和参数可能不同),然后拼接为成第三方平台指定的url

然后直接重定向到这个url.

例如在这个例子中我们的地址可能长这个样子:

www.xxx.com/oauth2.0/authorize?appid=123456&redirect=www.sss.com/login

参数:

appid 我们的应用对于第三方平台的唯一id
redirect 用户同意授权后被重定向的地址,一般来说都是本应用的首页或者登录页面,在本例中就是www.sss.com/login这个地址.
其他参数 根据第三方平会有不同的额外参数.

然后将用户重定向到这个url中,此时用户会跳转到www.xxx.com(因为如果用户授权成功,你总要回调服务商接口,来告诉它,已经授权成功).

3、用户授权成功

用户授权成功后,微信就会请求上面redirect参数中的接口地址,带上授权成功的参数code

在这个例子中这个url看起来是这个样子的

www.sss.com/login?code=xxxxx

4、获取用户token(令牌)

此时我们的www.sss.com/login接受到了一个含有code的请求,我们知道这个是一个第三方登录授权后的请求.

我们再次拼接一个url(不同平台地址规则不同),但是一般来说这个请求会有如下的参数:

code 用户授权后重定向带回来的code
appid 应用唯一id
appsecret 应用对应的密钥

在这个例子中我们请求服务器的url可能是这个样子的:

www.xxx.com/oauth2/access_token?appid=xxxx&secert=xxxx&code=xxxx

如果一切顺利在这个阶段我们就可以获取第三方平台响应的一个accesstoken,这个accesstoken代表着用户对于这个应用的授权.

除此以外你还会获取到用户的基本信息例如用户的唯一id之类的,后续的请求用户的信息需要使用accesstoken进行请求。

5、获取用户基本信息

利用accesstoken我们向服务器获取了用户的名字,显示在了我们的应用中, 后续的资源获取就是这个模式(不同平台资源获取地址以及方式有可能稍有不同).

6、补充

1、微信认证成功后,我会会把accesstoken存放在cookie中,这样不用每次都需要用户去授权认证,而是我们后台去请问微信,这个时候用户是不会感知的。

2、accesstoken不是一直有效的,它会有过期时间的,就好比微信扫码登陆中accesstoken有效时间是2小时。

3、那么accesstoken时效,是不是就要用户重新授权登陆了,当然也不是,如果没2小时都要重新授权登陆那体验也太差了。这里会有个叫refresh_token

它是在你第一次获取accesstoken一起给你的,也就是说如果你的accesstoken时效了,你还可以通过refresh_token去获取用户信息。这么说refresh_token的

时效时间肯定要比accesstoken,微信扫码登陆refresh_token有效时间是30天

4、也就是当refresh_token也时效的时候,才会需要用户重新授权登陆。

具体的可以看看微信扫码登陆的官方文档:网站应用微信登录开发指南

 

posted @ 2022-02-25 21:06  hanease  阅读(929)  评论(0编辑  收藏  举报