Nodejs 发送 TCP 消息的正确姿势
最近使用 NODE-RED 跟 TCP 打交道。NODE-RED 里内建了一个节点叫“tcp-out”,看文档呢使用这个节点可以很方便的把 payload 用 TCP 协议发送出去,但是事实上事情没有这么简单。其实当我第一次看到这个节点用法的时候我就觉得会有问题,果不其然。既然节点有问题,那么就干脆写代码吧,反正 NODE-RED 支持自定义 javascript function 。于是就花了点时间研究了下用 Nodejs 来发送 TCP 消息。
问题
上面说了使用内建的节点“tcp-out”发送 TCP 消息会有问题。那么到底是什么问题呢?
“tcp-out” 节点只是简单的把 payload 字符串转成了 buffer 然后发送了出去。其实如果自己做测试,发送一个消息然后服务端接受一个消息一点问题都没有的。但是稍微有一些 socket 编程经验的人都知道,这么做在生产环境是有问题的。因为在真实的生产环境下,服务端都是会定义消息的结构的。比如我们这次对接的服务端就要求每个消息头部都需要带4字节的包头,来标识整个消息的长度。所以我们直接发送的消息服务端校验包头不通过会直接丢弃。
那么为什么要这么做呢?
粘包?
服务端这么做的原因是 TCP 服务端接收消息有可能出现“粘包”的问题。这时候肯定有同学会出来说了:TCP 是流式协议,根本没有包的概念怎么可能粘包呢?是的 ,这说的没错。本质上 TCP 作为流式协议根本不可能出现粘包的问题。但是如果从应用层开发者的角度来看,TCP 服务端在接受消息的时候确确实实会出现多个消息同时收到,或者收到1.x个消息的问题。站在应用层开发者的角度看,就是几个包(消息)黏在了一起。所以也没必要去咬文嚼字,毕竟大家多数都是应用层开发玩家。
那么为什么会有以上问题?让我们先回顾一下 OSI 网络模型:
TCP位于传输层(第四层),传输的单位叫 Segment(段);
下面是 IP 协议位于网络层,传输的单位叫 Packet (包);
下面是 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 来发送。
那么如何解决上述问题呢?方法就是上面说的 ,在每个消息的开始的地方放一个固定长度的头部用来表示整个消息的长度。

服务端收到消息后,先截取4个字节的长度,读取里面的值获得整个消息的长度。然后 payload 长度 = 整个长度-4。然后使用这个长度截取对应的长度的数据。这样就得到了一个完整的消息。如果后面的长度不够了就等下一个消息到达后补齐对应长度的数据。如此循环以上操作,服务端就能解决这个问题了。
使用 Nodejs 发送 TCP 报文(消息)
好了上面铺垫了这么多 ,总算要开始写代码了。
如果你打开 Google 搜索 "nodejs 发送 tcp" 你会得到很多代码示例。但是大多数代码都是 demo 级别的。也就是都是简单的把所有的消息当做 payload 发送到服务端,然后服务端打印一下而已。这也是我写这篇文章的初衷,科普一下一个真正的 TCP 报文(消息)该怎么发送。
就以上面的结构为例:头部固定4字节表示整个消息的长度(4 + length(payload))。
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');
console.log('send data done .');
});
其实代码也没几行。简单说一下就是,在发送 payload 之前,需要先分配一个 4 字节长度的 buffer,然后写入整个消息的长度,发送出去,紧接着发送真正的 payload 。这样就完成了一次 TCP 报文消息的发送。
总结
虽然题目叫 Nodejs 发送消息,但是代码却是寥寥几行。本文多数文字都是在描述 TCP 协议相关的东西。TCP是个伟大(复杂)的协议,要理解它不是件容易的事情,光是链接建立,链接关闭的过程都非常复杂。更别说它那些算法了(NODELAY,窗口算法,拥堵避免算法等等)。但是有时间的话还是可以花点时间研究下,这对于我们这些应用层开发者来说也是一件非常有意义的事。当你了解了 TCP 协议后,很多以前似懂非懂的问题都豁然开朗了。比如到底有没有粘包问题,应用层为什么要定义数据结构,同一个连接服务端会有并发问题吗?
Nodejs 发送 TCP 消息的正确姿势的更多相关文章
- 微信小程序如何发送订阅消息,正确姿势来了,建议收藏!
小程序订阅消息公测已经有些日子,今天以世界上最好的语言(PHP)为例,说一下如何发送订阅消息. 1.订阅消息 其实如果用过模板消息的话,改用订阅消息挺简单的,看一下官方文档稍加摸索就能使用. 但是对于 ...
- android发送udp,tcp消息
发送方创建步骤: 1. 创建一个DatagramSocket对象 DatagramSocket socket = new DatagramSocket (4567); 2. 创建一个 InetA ...
- nodejs的TCP相关的一些笔记
TCP协议 基于nodejs创建TCP服务端 TCP服务的事件 TCP报文解析与粘包解决方案 一.TCP协议 1.1TCP协议原理部分参考:无连接运输的UDP.可靠数据传输原理.面向连接运输的TCP ...
- 在Linux(ubuntu server)上面安装NodeJS的正确姿势
上一篇文章,我介绍了 在Windows中安装NodeJS的正确姿势,这一篇,我们继续来看一下在Linux上面安装和配置NodeJS. 为了保持一致,这里也列举三个方法 第一个方法:通过官网下载安装 h ...
- 高效的TCP消息发送组件
目前的.net 架构下缺乏高效的TCP消息发送组件,而这种组件是构建高性能分布式应用所必需的.为此我结合多年的底层开发经验开发了一个.net 下的高效TCP消息发送组件.这个组件在异步发送时可以达到每 ...
- NTCPMSG 开源高性能TCP消息发送组件
https://www.cnblogs.com/eaglet/archive/2013/01/07/2849010.html 目前的.net 架构下缺乏高效的TCP消息发送组件,而这种组件是构建高性能 ...
- TCP/IP三次握手与四次挥手的正确姿势
0.史上最容易理解的:TCP三次握手,四次挥手 https://cloud.tencent.com/developer/news/257281 A 理解TCP/IP三次握手与四次挥手的正确姿势http ...
- 剖析和解决Python中网络粘包的正确姿势
目录 1.粘包及其成因 1.1.粘包产生 1.2.粘包产生的原因 2.尝试解决粘包 2.1.指定数据包的长度 2.2.固定数据包的长度 2.3.用函数实现多次调用发送数据 3.解决粘包问题的正确姿势 ...
- ZeroMQ接口函数之 :zmq_msg_send – 从一个socket发送一个消息帧
ZeroMQ 官方地址 :http://api.zeromq.org/4-0:zmq_msg_send zmq_msg_send(3) ØMQ Manual - ØMQ/3.2.5 Name zmq_ ...
- Mina、Netty、Twisted一起学(二):TCP消息边界问题及按行分割消息
在TCP连接开始到结束连接,之间可能会多次传输数据,也就是服务器和客户端之间可能会在连接过程中互相传输多条消息.理想状况是一方每发送一条消息,另一方就立即接收到一条,也就是一次write对应一次rea ...
随机推荐
- nodejs发布cesium问题,其他电脑访问发布
在电脑上安装nodejs后在选择的cesium文件中,按住shift和鼠标右键,打开powershell,输入命令行hs -p 1212,完成cesium的发布,出现两个网址,127.0.0.1:12 ...
- oracle常用知识随笔
1.创建表空间及用户赋权 create tablespace spaceone datafile '/dev/spaceone'size 80mextent management localsegme ...
- 第六章 mysql日志
第六章 mysql日志 一 错误日志 错误日志的默认存放路径是 mysql 存放数据的地方 hostname.err 1. 修改错误日志存放路径 [mysqld] log-error=/data/m ...
- Pytest Fixture(一)
Fixture 是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们.我们可以用它做一些事情,比如数据库的链接操作之类的 import pytest @pytest.fixture() ...
- Docker学习——Dockerfile 指令详解(五)
我们已经介绍了 FROM (指定基础镜像) , RUN(执行命令) ,还提及了 COPY , ADD ,其实 Dockerfile 功能很强大,它提供了十多个指令.下面我们继续讲解其他的指令. COP ...
- CSS尺寸设置的单位:px、rem、em、vw、vh
px:pixel像素的缩写,绝对长度单位,它的大小取决于屏幕的分辨率,是开发网页中常常使用的单位. em:相对长度单位,在 `font-size` 中使用是相对于父元素的字体大小,在其他属性中使用是相 ...
- SpringBoot怎么管理封装java包的关系
首先SpringBoot直接写注解加依赖就可以了,基本上不用写xml,非常方便,在这里我只写了两个核心包 为什么选择jar类型? SpringBoot基本上是个应用程序了,只要用java命令程序去运行 ...
- 使用DockerCompose部署微服务项目(基于Springboot搭建一个简易计数器)
准备Dockerfile FROM java:8 #基于jdk8的环境 COPY *.jar /app.jar #拷贝所有的jar包到/app.jar目录下 CMD ["--server.p ...
- 狐漠漠养成日记 Cp.00003 第二周
上一周整周都在做Unity Newbies Jam,除了一些必要的比如考试或者课程,其他的都推后了. 为了赶项目进度,这一周我可以说是废寝忘食,基本上每天一顿饭,就睡仨小时那种. 以至于到最后一天,也 ...
- AXI4_LITE总线vivado2019.1官方模板源码(verilog实现)
AXI lite总线读写时序 1. AXI_SLAVE源码 `timescale 1 ns / 1 ps module myip_v1_0_S00_AXI # ( // Users to add pa ...