> For the complete documentation index, see [llms.txt](https://mqjyl2012.gitbook.io/backend-development/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mqjyl2012.gitbook.io/backend-development/network-communication-and-programming/network-programming/tcp-stick-packages.md).

# TCP的粘包问题

## :pencil2: 1、`TCP`选项之`MSS`

`MSS`（Maximum Segment Size，最大报文段大小）的概念是指`TCP`层所能够接收的最大段大小，该值只包括`TCP`段的数据部分，不包括选项部分。在`TCP`首部有一个`MSS`选项，在三次握手过程中，`TCP`发送端使用该选项告诉对方自己所能接受的最大段大小。 **`MSS` 选项只能出现在 SYN 段中。**

`MSS`选项虽然作为一个选项存在，原则上讲是可有可无的，但是目前绝大多数的`TCP`通信过程中都会携带该选项，可以说它是`TCP`中最重要的一个选项了。关于`MSS`的内容，主要包括两个方面：

1. SYN段和`SYN+ACK`段中携带的`MSS`选项是如何确定的（有些资料中会提到的`RMSS`，即接收`MSS`）？
2. 连接建立后的发包过程中，`MSS`是如何发挥作用的（有些资料中会提到的`SMSS`，即发送`MSS`）？

从客户端和服务器端的三次握手过程中可以看到`RMSS`是如何确定的。从连接态的数据发送过程可以看到`SMSS`是如何发乎作用的。

`MSS` 是软件层的概念，它是由软件控制的，`MTU` 是硬件（比如网卡出口）的属性，是指二层链路层帧携带的数据最大大小。

## :pencil2: 2、`TCP`之`Nagle`算法

`Nagle`算法是为了减少广域网的小分组数目，从而减小网络拥塞的出现；

该算法要求一个`tcp`连接上最多只能有一个未被确认的未完成的小分组，在该分组`ack`到达之前不能发送其他的小分组，`tcp`需要收集这些少量的分组，并在`ack`到来时以一个分组的方式发送出去；其中小分组的定义是小于`MSS`的任何分组；

该算法的优越之处在于它是自适应的，确认到达的越快，数据也就发送的越快；而在希望减少微小分组数目的低速广域网上，则会发送更少的分组；

### :pen\_fountain: 2.1、延迟`ACK`

如果`tcp`对每个数据包都发送一个`ack`确认，那么只是一个单独的数据包为了发送一个`ack`代价比较高，所以`tcp`会延迟一段时间，如果这段时间内有数据发送到对端，则捎带发送`ack`，如果在延迟`ack`定时器触发时候，发现`ack`尚未发送，则立即单独发送；

延迟`ACK`好处：

1. 避免糊涂窗口综合症；
2. 发送数据的时候将`ack`捎带发送，不必单独发送`ack`；
3. 如果延迟时间内有多个数据段到达，那么允许协议栈发送一个`ack`确认多个报文段；

### :pen\_fountain: 2.2、当`Nagle`遇上延迟`ACK`

试想如下典型操作，写-写-读，即通过多个写小片数据向对端发送单个逻辑的操作，两次写数据长度小于`MSS`，当第一次写数据到达对端后，对端延迟`ack`，不发送`ack`，而本端因为要发送的数据长度小于`MSS`，所以`nagle`算法起作用，数据并不会立即发送，而是等待对端发送的第一次数据确认`ack`；这样的情况下，需要等待对端超时发送`ack`，然后本段才能发送第二次写的数据，从而造成延迟；

### :pen\_fountain: 2.3、关闭`Nagle`算法

使用`TCP`套接字选项`TCP_NODELAY`可以关闭套接字选项；

如下场景考虑关闭`Nagle`算法：对端不向本端发送数据，并且对延时比较敏感的操作，这种操作没法捎带`ack`；

对于如上写-写-读操作，优先使用其他方式，而不是关闭`Nagle`算法：

* 使用`writev`，而不是两次调用`write`，单个`writev`调用会使`tcp`输出一次而不是两次，只产生一个`tcp`分节，这是首选方法；
* 把两次写操作的数据复制到单个缓冲区，然后对缓冲区调用一次write；
* 关闭`Nagle`算法，调用write两次；有损于网络，通常不考虑；

## :pencil2: 3、长连接和短连接

长连接：Client方与Server方先建立通讯连接，连接建立后 不断开， 然后再进行报文发送和接收。

短连接：Client方与Server每进行一次报文收发时进行通讯连接，交易完毕后立即断开连接。此种方式常用于一点对多点通讯，比如多个Client连接一个Server。

## :pencil2: 4、`TCP`的粘包问题

`TCP`的socket编程，收发两端（客户端和服务器端）都要有成对的socket，因此，发送端为了将多个发往接收端的包更有效的发到对方，使用了优化方法（`Nagle`算法），将多次间隔较小、数据量小的数据，合并成一个大的数据块，然后进行封包。这样，接收端，就难于分辨出来了，必须提供科学的拆包机制。

对于`UDP`，不会使用块的合并优化算法，实际上是由于`UDP`支持的是一对多的模式，所以接收端的`skbuff`(套接字缓冲区）采用了链式结构来记录每一个到达的`UDP`包，在每个`UDP`包中就有了消息头（消息来源地址，端口等信息），这样，对于接收端来说，就容易进行区分处理了。所以`UDP`不会出现粘包问题。

### :pen\_fountain: 4.1、保护消息边界

保护消息边界，就是指传输协议把数据当作一条独立的消息在网上传输，接收端只能接收独立的消息。也就是说存在保护消息边界，接收端一次只能接收发送端发出的一个数据包。而面向流则是指无**消息保护边界**的，如果发送端连续发送数据，接收端有可能在一次接收动作中，会接收两个或者更多的数据包。

例如，我们连续发送三个数据包，大小分别是2k，4k ，8k,这三个数据包，都已经到达了接收端的网络堆栈中，如果使&#x7528;**`UDP`**&#x534F;议，不管我们使用多大的接收缓冲区去接收数据，我们必须有三次接收动作，才能够把所有的数据包接收完，而使&#x7528;**`TCP`协议**，我们只要把接收的**缓冲区大小设置在14k以上**，我们就能够一次把所有的数据包接收下来，只需要有一次接收动作（**指应用层的接受动作，缓冲区也是应用层的**）。

> 误认为`TCP`丢包的问题：很多人在使用`tcp`协议通讯的时候，并不清&#x695A;**`tcp`是基于流的传输**，当连续发送数据的时候，如果他们使用的缓冲区足够大，他们有可能会一次接收到两个甚至更多的数据包，而很多人往往会忽视这一点，只解析检查了第一个数据包，而已经接收的其他数据包却被忽略了。

### :pen\_fountain: 4.2、 **什么时候需要考虑粘包问题**

（1）、如果利用`tcp`每次发送数据，就与对方建立连接，然后双方发送完一段数据后，就关闭连接，这样就不会出现粘包问题（因为只有一种包结构，类似于`http`协议）。

关闭连接主要是要双方都发送close连接（参考`tcp`关闭协议）。如：A需要发送一段字符串给B，那么A与B建立连接，然后发送双方都默认好的协议字符如`"hello give me sth abour yourself"`，然后B收到报文后，就将缓冲区数据接收，然后关闭连接，这样粘包问题不用考虑到，因为大家都知道是发送一段字符。

（2）、如果发送数据无结构，如文件传输，这样发送方只管发送，接收方只管接收存储就`ok`，也不用考虑粘包。

（3）、如果双方建立连接，需要在连接后一段时间内发送不同结构数据，如连接后，有好几种结构：

1. `"hellogive me sth abour yourself"`
2. `"Don'tgive me sth abour yourself"`

如果发送方连续发送这个两个包出去，接收方一次接收可能会是`"hellogive me sth abour yourselfDon't give me sth abour yourself"`这样接收方就傻了，到底是要干嘛？不知道，因为协议没有规定这么诡异的字符串，所以要处理把它分包，怎么分也需要双方组织一个比较好的包结构，所以一般可能会在包头加一个数据长度之类的，以确保接收。

### :pen\_fountain: 4.3、 **粘包出现原因**

`TCP`粘包是指发送方发送的若干包数据到接收方接收时粘成一包，从接收缓冲区看，后一包数据的头紧接着前一包数据的尾。出现粘包现象的原因是多方面的，它既可能由发送方造成，也可能由接收方造成。

1. 发送端需要等缓冲区满才发送出去，造成粘包
2. 接收方不及时接收缓冲区的包，造成多个包接收

发送方引起的粘包是由`TCP`协议本身造成的，`TCP`为提高传输效率，发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少，通常`TCP`会根据优化算法把这些数据合成一包后一次发送出去，这样接收方就收到了粘包数据。

接收方引起的粘包是由于接收方用户进程不及时接收数据，从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区，用户进程从该缓冲区取数据，若下一包数据到达时前一包数据尚未被用户进程取走，则下一包数据放到系统接收缓冲区时就接到前一包数据之后，而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据，这样就一次取到了多包数据。

**粘包情况有两种，一种是粘在一起的包都是完整的数据包，另一种情况是粘在一起的包有不完整的包。**

不是所有的粘包现象都需要处理，若传输的数据为不带结构的连续流数据（如文件传输），则不必把粘连的包分开（简称分包）。但在实际工程应用中，传输的数据一般为带结构的数据，这时就需要做分包处理。

在处理定长结构数据的粘包问题时，分包算法比较简单；在处理不定长结构数据的粘包问题时，分包算法就比较复杂。特别是粘在一起的包有不完整的包的粘包情况，**由于一包数据内容被分在了两个连续的接收包中，处理起来难度较大**。实际工程应用中应尽量避免出现粘包现象。

#### **包分片的原因：**

* 应用层调用write方法，将应用层的缓冲区中的数据拷贝到套接字的发送缓冲区。而发送缓冲区有一个`SO_SNDBUF`的限制，如果应用层的缓冲区数据大小大于套接字发送缓冲区的大小，则数据需要进行多次的发送。
* `TCP`所传输的报文段有`MSS`的限制，如果套接字缓冲区的大小大于`MSS`，也会导致消息的分割发送。
* 由于链路层最大发送单元`MTU`，在IP层会进行数据的分片。

这些情况都会导致一个完整的应用层数据被分割成多次发送，导致接收对等方不是按完整数据包的方式来接收数据。

### :pen\_fountain: **4.4、解决方案**

粘包问题本质上是因为无法区分包界限，解决包界限的问题主要有以下几种方式：

1. 消息数据的定长，比如定长100字节，不足补空格，接收方收到后解析100字节数据即为完整数据。但这样的做的缺点是浪费了部分存储空间和带宽。
2. 消息数据使用特定分割符区分界限，比如包尾加上`\r\n`标记。FTP协议正是这么做的。但问题在于如果数据正文中也含有`\r\n`，则会误判为消息的边界。
3. 把消息数据分成消息头和消息体，消息头带消息的长度，接收方收到后根据消息头中的长度解析数据。

在实际开发中很多网络框架对`TCP`拆包粘包问题的解决做了很多支持，比如`netty`中`LineBasedFrameDecoder`解析器就是利用换号符号做分割。

### :pen\_fountain: 4.5、总结

（1）`TCP`为了保证可靠传输，尽量减少额外开销（每次发包都要验证），因此采用了流式传输，面向流的传输，相对于面向消息的传输，可以减少发送包的数量，从而减少了额外开销。但是，对于数据传输频繁的程序来讲，使用`TCP`可能会容易粘包。当然，对接收端的程序来讲，如果机器负荷很重，也会在接收缓冲里粘包。这样，就需要接收端额外拆包，增加了工作量。因此，这个特别适合的是数据要求可靠传输，但是不需要太频繁传输的场合（两次操作间隔`100ms`，具体是由`TCP`等待发送间隔决定的，取决于内核中的socket的写法）。

（2）`UDP`，由于面向的是消息传输，它把所有接收到的消息都挂接到缓冲区的接受队列中，因此，它对于数据的提取分离就更加方便，但是，它没有粘包机制，因此，当发送数据量较小的时候，就会发生数据包有效载荷较小的情况，也会增加多次发送的系统发送开销（系统调用，写硬件等）和接收开销。因此，应该**最好设置一个比较合适的数据包的包长，来进行`UDP`数据的发送**。（**`UDP`最大载荷为1472，因此最好能每次传输接近这个数的数据量，这特别适合于视频，音频等大块数据的发送，同时，通过减少握手来保证流媒体的实时性**）
