服务端这么做的原因是 TCP 服务端接收消息有可能出现“粘包”的问题。这时候肯定有同学会出来说了:TCP 是流式协议,根本没有包的概念怎么可能粘包呢?是的 ,这说的没错。本质上 TCP 作为流式协议根本不可能出现粘包的问题。但是如果从应用层开发者的角度来看,TCP 服务端在接受消息的时候确确实实会出现多个消息同时收到,或者收到1.x个消息的问题。站在应用层开发者的角度看,就是几个包(消息)黏在了一起。所以也没必要去咬文嚼字,毕竟大家多数都是应用层开发玩家。
下面是 Datalink 数据链路层,单位是 frame (帧);
好了知道了以上知识,我们可以知道 TCP 是已 segment 单位来传输的。但是 segment 是有最大值限制的。在 TCP 协议中有个叫 MSS(Max Segment Size) 的东西。一般来说 MSS = MTU - 40 = 1460 字节。为什么是一般来说,因为 TCP 协议太复杂了。看上面又引入了一个 MTU 的概念,这里就不展开来说了,有兴趣大家可以自己研究一下 TCP,会大开眼界的。
好了,既然 segment 有最大值限制,那么很显然当我们一次发送的消息长度超过 MSS ,那么消息就会被拆分成多个 segment 来发送。既然有拆分那么显然就有合并。TCP 协议有个 TCP_NODELAY 算法,当传输大量长度短的数据的时候有可能会触发 TCP_NODELAY 算法。TCP_NODELAY 算法就会尝试把多个短消息合并成一个 segment 来发送。
好了上面铺垫了这么多 ,总算要开始写代码了。如果你打开 Google 搜索 "nodejs 发送 tcp" 你会得到很多代码示例。但是大多数代码都是 demo 级别的。也就是都是简单的把所有的消息当做 payload 发送到服务端,然后服务端打印一下而已。这也是我写这篇文章的初衷,科普一下一个真正的 TCP 报文(消息)该怎么发送。
const payloadString = 'hello , world .'; const headerLength = 4; let socket = net.createConnection({ port: 8888, host: '127.0.0.1' }); socket.on('connect', () => { console.log('start send data .'); let messageBuff = Buffer.from(payloadString); let messageLength = messageBuff.length; let contentLength = Buffer.allocUnsafe(4); contentLength.writeUInt32BE(headerLength + messageLength); socket.write(contentLength); console.log('send header done'); socket.write(messageBuff); console.log('send payload done'); // 堆代码 duidaima.com console.log('send data done .'); });其实代码也没几行。简单说一下就是,在发送 payload 之前,需要先分配一个 4 字节长度的 buffer,然后写入整个消息的长度,发送出去,紧接着发送真正的 payload 。这样就完成了一次 TCP 报文消息的发送。
虽然题目叫 Nodejs 发送消息,但是代码却是寥寥几行。本文多数文字都是在描述 TCP 协议相关的东西。TCP是个伟大(复杂)的协议,要理解它不是件容易的事情,光是链接建立,链接关闭的过程都非常复杂。更别说它那些算法了(NODELAY,窗口算法,拥堵避免算法等等)。
但是有时间的话还是可以花点时间研究下,这对于我们这些应用层开发者来说也是一件非常有意义的事。当你了解了 TCP 协议后,很多以前似懂非懂的问题都豁然开朗了。比如到底有没有粘包问题,应用层为什么要定义数据结构,同一个连接服务端会有并发问题吗?