什么是粘包

首先请执行以下以下nodejs代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const net = require( 'net' );

const app = net.createServer( connect => {

connect.on( 'data', function ( data ) {
console.log( data.toString() );
} );

} );

app.on( 'error', error => {} );

app.listen( 8080, '0.0.0.0', function () {

let client = net.connect( { port: 8080, host: '127.0.0.1' } );

client.write( 'Hi, server!' );

client.write( 'Hi, server!' );

} );

你看到的输出一定是 Hi, server!Hi, server!,并且data事件的回调函数只执行了一次,WTF.

将代码稍作修改, 客户端write的时候加一个间隔时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const net = require( 'net' );

const app = net.createServer( connect => {

connect.on( 'data', function ( data ) {
console.log( data.toString() );
} );

} );

app.on( 'error', error => {} );

app.listen( 8080, '0.0.0.0', function () {

let client = net.connect( { port: 8080, host: '127.0.0.1' } );

client.write( 'Hi, server!' );

setTimeout( () => {
client.write( 'Hi, server!' );
}, 1000 );

} );

你会看到输出分了两次执行,回调函数也执行了两次,符合了我们的预期,但是,总不能我们的所有代码都加一个延时吧?

为什么会出现粘包?

网上的解释已经很明确,这里引用一段,出处已经无法考证了

1
2
3
4
5
6
7
8
9
10
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

如何处理粘包或者半包的情况?

这里采用了一个类似HTTP的方法,定义一个HTTP头:Content-Length: 100,然后这个数据包的大小就是100,我只需要在读到头数据之后,再读100,就是我这个数据包的带下了,如果数据还有剩下,那么就是下一个包的内容,如果数据不足100,那就说明这里只传了半个包,等待下次data事件触发,后半部分的数据就是这个包里面。

举个例子

假设我要传送一个数据: hello world!,首先我需要进行封包,一个数据包由头部和身体组成,其中,头部组成如下:

1
||||00000012

||||这个是头部开始的标示,由服务端和客户端互相约定,00000012,这个是数据体的大小,前面的0为了补全大小位,这里采用了8位,因为默认情况下,TCP的缓冲区大小是8K,8K换成byte就是: 8388608。

所以我们的头部其实固定为12位,那么接下来的就都是数据体,hello world!组成一个数据包之后的样子就是: ||||00000012hello world!,服务端的解包就不说了,就是自己维护一个缓冲区,从头开始解就行了。

better-packet

better-packet,提供了一个封包的解包的方法,专门为了用来解决粘包问题,看看使用了better-package之后会怎么样.

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const net = require( 'net' );

const betterPacket = require( './index' );

const app = net.createServer( ( connect ) => {

let unpackager = new betterPacket.UnPackager();

connect.on( 'data', unpackager.addBuffer.bind( unpackager ) );

connect.on( 'error', () => {} );

unpackager.on( 'package', function ( data ) {

// Hi, server!
// Hi, server!
console.log( data.toString() );

} );

} );

app.on( 'error', error => {} );

app.listen( '8080', '0.0.0.0' );

client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const net = require( 'net' );

const betterPacket = require( './index' );

const client = net.connect( { port: 8080, host: '127.0.0.1' } );

const packager = new betterPacket.Packager();

let sendData = packager.packageData( 'Hi, server!' );

client.write( sendData );

client.write( sendData );

这里服务端的数据就变得正常了,而且data事件的回调函数也被调了两次。

id2