老生常谈(一)tcp与udp TCP UDP的区别 TCP 三次握手、四次挥手 TCP协议如何保证可靠传输 TCP 粘包、拆包

为了更进一步了解网络层面的知识,先晒出一张网络体系结构图,加深理解

TCP UDP的区别

TCP UDP
面向连接的协议。基于这种连接方式, 通信设备应在传输数据前建立连接,并应在传输数据后关闭连接 面向数据报的协议。意味着打开、维护、终止连接不会有开销。UDP对于广播和多播类型的网络传输是有效的。
点对点通信,连接两端的socket
面向字节流。TCP把传输的各种数据当做无结构的字节流来用
可靠性。它能够保证向目标路由器的数据传输 不可靠性。不能保证向目的地传送数据
错误检测机制。TCP提供了广泛的错误检查机制,这是因为它提供流量控制和数据确认。 UDP只有使用校验和的基本错误检查机制。
数据排序。数据包能够按照顺序到达接收器 没有数据排序。若有需求,则需要再应用程序层进行管理。
速度较慢。相对UDP而言速度较慢。 快、简单、高效
重传机制。支持重传丢失的数据包 无重传机制
标头大小为20个字节 标头大小为8个字节。
重量级 轻量级
用于HTTP,HTTP,FTP,SMTP和Telnet 用于DNS,DHCP,TFTP,SNMP,RIP和VoIP

注:本文所指的Client 均为发送方,Server为接收方

TCP 三次握手、四次挥手

三次握手

建立一个TCP连接时,需要Client和Server总共发送3个包。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。

  • Step 1(SYN).

    Client 端要和Server端 建立连接,所以要发一个SYN(即同步序列号)的包,初始序号x,保存在包头的序列号(Sequence Number)字段里,指明打算连接的Service port,。

    用于告知Server:我(Client)可能要与你开始通讯了,现在发给你一个我(Client)启动段的序列号。

    此时Client进入SYN_SEND状态

  • Step 2(SYN+ACK).
    Server 端 接收到数据包(通知)后

    使用一个SYN-ACK信号位设置,来响应Client 端的请求。

    即发送了自己的序列号(SVN),初始序号为y,和确认号(ACK,即Client发来的序列号递增1, 即x + 1)。

    此时Server进入 SYN_RCVD 状态

  • Step 3(ACK).

    Client接收到Server端的响应后

    发送确认包(ACK,即Server 发来的序列号递增1, 即y + 1)来确认收到响应,此时Client 进入 ESTABLISHED 状态,当Server 接收到该ACK包后,也进入 ESTABLISHED 状态。

四次挥手

TCP 的连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake)。

需要四个包的原因是是因为TCP的半关闭引起的

客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

下面假设Client主动发起挥手动作

  • Step 1(FIN).

    Client 端(发起方)要关闭TCP连接,所以要发一个FIN包,序号为x

    发送完毕后,此时Client进入 FIN_WAIT_1 状态(此时表明无数据可发送,但仍可接受数据)

  • Step 2(ACK).

    当Server 端 接收到FIN包后,立即向Client发送确认包(即Client发来的FIN包的序号递增1,x + 1)。

    发送完毕后,此时Server 进入 CLOSE_WAIT 状态(此时表明接收到了Client的关闭,但还没做好“思想准备“关闭连接)

    当Client 端 接收到ACK包后,进入 FIN_WAIT_2 状态

  • Step 3(FIN).

    Server 端 发送ACK包一段时间(这段时间它有一些关闭过程)后,开始发送FIN包,序号为y

    发送完毕后,此时Server 进入 LAST_ACK 状态

  • Step 4(ACK).

    当Client 端 接收到FIN包,即关闭请求后,发送一个确认包(即Server发来的FIN包的序号递增1,y + 1

    发送完毕后,此时Client 进入 TIME_WAIT 状态,目的在于在时间周期n内,允许Client 在发送的ACK包丢失的情况下重新发。

    当Server 端接收到ACK包后,连接正式关闭,此时Server进入 CLOSED 状态。Client资源(包括端口号、缓冲区数据)都被释放

    当Client在时间周期n结束后,仍没收到Server 发的ACK包,则认为已正常关闭连接,此时Client 也进入 CLOSE 状态

TCP协议如何保证可靠传输

从上面的体系图可以看到,TCP(即运输层)的报文信息最终会交付到网际层。而网际层不会提供可靠的服务。所以还是要TCP来保证可靠的传输,才能最终保证数据服务的可靠

原理

​ 宏观上看,从TCP的特性可以得知,它有自己的错误检测机制、数据按序传输、确认应答+序列号、支持重传的功能。然后具体的内部处理是怎样的呢?主要有以下两点

  • 停止等待协议

    这是最简单的保证可靠传输的协议

    以下会发生两种情况

    • 无差错

      可以看到Client 在发送分组M1(即数据单元)后,暂停,等到Server发回确认后,继续发送下一个分组…

      这是理想条件下的无差错情况

    • 有差错

      当Client 发送 分组M1(会先设置一个计时器,在此计时器内M1仍存在,以便重传)时,可能会遇到数据无法到达Server,或者Server 检测出问题并丢弃了它,在指定时间内Client 如果未收到来自Server 的确认,则会重传M1,即人们常说的超时重传

      超时重传会有以下情况

      • 确认丢失(发回延迟)

        Client发送分组M1,Server收到M1并发送确认分组,而在指定的时间内Client没有收到确认,后会重传M1.

        而由于Server 已经收到过M1了, 所以此时它需要 丢弃M1分组, 发送确认分组

      • 确认迟到(发送延迟)

        由于网络延迟等原因,Client发送的分组M1,在指定时间后才到Server, 此时Client 还没来得及收到确认,再次发送分组M1

        而由于Server 刚好收到了M1,所以此时它需要 丢弃M1分组, 发送确认分组

        Client 收到>=2个以上的确认,会执行丢弃操作,并且停止发送

  • 连续ARQ协议

    由于停止等待协议对信道的利用率太低,故可以采用流水线的方式来传输,即连续ARQ协议。

    这里需要提到一个发送窗口的概念。发送窗口支持滑动,所以也有滑动窗口这么一个概念

    ​ Client 会维护一个发送窗口,一个窗口内可以有多个连续分组进行发送,而不必等待对方的确认一条条分组发。

    ​ Server 亦不会对每个分组进行回传确认,而是在按需发送到达的最后一个分组到达之后,发送确认,代表这个窗口的分组已经发送成功

具体实现

1. 使用滑动窗口

​ 窗口主要分为接收窗口和发送窗口

  • 接收窗口

    “接收窗口”大小取决于应用(比如说tomcat:8080端口的监听进程)、系统、硬件的限制。图中,接收窗口是31~50,大小为20。

  在接收窗口中,黑色的表示已收到的数据,白色的表示未收到的数据。

  当收到窗口左边的数据,如27,则丢弃,因为这部分已经交付给主机;

  当收到窗口右边的数据,如52,则丢弃,因为还没轮到它;

  当收到已收到的窗口中的数据,如32,丢弃;

  当收到未收到的窗口中的数据,如35,缓存在窗口中。

  • 发送窗口

​ 发送窗口的大小swnd=min(rwnd,cwnd)。rwnd是接收窗口,cwnd用于拥塞控制,暂时可以理解swnd= rwnd =20。

  图中分为四个区段,其中P1到P3是发送窗口。

  tips:发送窗口以字节为单位。为了方便画图,图中展示得像以报文为单位一样。但这不影响理解。

2. 重传与确认

  • 确认

    这里主要是通过累计确认的方式

  • 重传

    这里主要是上面说的超时重传,每一个报文都会有超时计数器,当超过指定时间后,Client(发送方)会触发重传报文

3. 流量控制(基于滑动窗口)

​ 流量即发送方发送的报文流量。当接收方来不及处理数据时,通过滑动窗口,告诉发送方能够接受的单位字节是多少,以降低发送的频率,防止包丢失

  • 在建立连接时,接收方(B),告诉了发送方(A):我的接收窗口是400(单位字节).
  • 图中的ACK为TCP首部的ACK字段,ack为首部的确认号字段.
  • 流量控制体现在:rwnd=300, rwnd=100, rwnd=0.在确认报文的窗口字段设定了发送方能够发出的数据多少,从而控制流量.注意只有到首部的ACK字段值为1,窗口字段的值才有效.
  • 假设在B发送了rwnd=0之后,过段时间由于自己又希望接收到数据,于是发出rwnd=400的报文,但是该报文丢失了,这样A依然无法发送数据,B希望接收但接收不到数据.

​ 为解决该问题,TCP为每个链接都设有一个持续计时器.只要接收到对方窗口为0的通知,就启动持续计时器.在计时器到期后,就发送探测报文,对方可以在该报文的确认中告知当前的窗口值.若窗口任然为0,那么就重新设定计时器,若不为0,那么上述的问题就解决了.

4. 拥塞控制

​ 拥塞是指对网络某一资源(带宽,缓存等)的需求超过了可提供的部分,从而使网络中传送的数据不能按时到达,网络性能变差的情况.

拥塞控制就是防止过多的数据注入到网络中,这样网络中的资源压力就小了.

流量控制和拥塞控制似乎很相似,但是他们不同.前者立足于接收和发送者双方的情况;而后者注重的是数据量对网络环境的影响

TCP 粘包、拆包

由于TCP 是一个面向字节流的协议,这也决定了它的数据是无结构的。所以TCP无法得知应用层对于这快数据的定义,而是基于自身缓冲区的实际情况进行数据包的拆分,或者将多个数据包进行合并来发送。

参考下图,在不同的条件下,会发生多种现象

  • Server 分别接收P1,P2,没有发生粘包、拆包
  • Server 一次接收P1+P2两个报文,发生了粘包
  • Server 先接收P2, 再分别接收了P1_1, P1_2,发生了拆包
  • Server 先接收了P2+P1_2,再接收了P1_1,发生了粘包、拆包
  • 另一种极端情况,当窗口非常小,恰逢P1又很大时,可能会发生多次对P1进行拆包

首先我们要知道,发送的数据会先传入发送缓冲区,再通过网络传输发送到接收端的缓冲区

以上现象发生的原因主要是

  • 发送的字节 大于 TCP发送缓冲区的大小,会发生拆包
  • 发送的报文 大于 MSS(最大报文长度),会发生拆包
  • 发送的字节 小于 TCP发送缓冲区的大小,会将多次写入缓冲区的报文一并发送,即发生粘包

解决方案,需要上层应用程序做对应的处理

  • 规定报文长度。例如设定每条报文固定长度为200字节,当不够时,用空格填充
  • 报文末尾添加回车换行符。例如FTP协议
  • 将报文分为header and body,在头部中声明报文长度,然后根据这个长度来获取报文

我们常用的Netty 已经帮我们处理好这些问题,我们仅需调用特定的方法即可。这个在后续的Netty挖掘机系列文章会提到栗子。

比如有:

  • LineBasedFrameDecoder 基于换行符解决
  • DelimiterBasedFrameDecoder 基于分隔符解决
  • FixedLengthFrameDecoder 指定长度解决

参考链接:【读】这一次,让我们再深入一点 - TCP协议

参考链接:什么是 TCP 拆、粘包?如何解决?