最近使用 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。然后使用这个长度截取对应的长度的数据。这样就得到了一个完整的消息。如果后面的长度不够了就等下一个消息到达后补齐对应长度的数据。如此循环以上操作,服务端就能解决这个问题了。
好了上面铺垫了这么多 ,总算要开始写代码了。
如果你打开 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 协议后,很多以前似懂非懂的问题都豁然开朗了。比如到底有没有粘包问题,应用层为什么要定义数据结构,同一个连接服务端会有并发问题吗?
我正在查看instance_variable_set的文档并看到给出的示例代码是这样做的:obj.instance_variable_set(:@instnc_var,"valuefortheinstancevariable")然后允许您在类的任何实例方法中以@instnc_var的形式访问该变量。我想知道为什么在@instnc_var之前需要一个冒号:。冒号有什么作用? 最佳答案 我的第一直觉是告诉你不要使用instance_variable_set除非你真的知道你用它做什么。它本质上是一种元编程工具或绕过实例变量可见性的黑客攻击
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c
question的一些答案关于redirect_to让我想到了其他一些问题。基本上,我正在使用Rails2.1编写博客应用程序。我一直在尝试自己完成大部分工作(因为我对Rails有所了解),但在需要时会引用Internet上的教程和引用资料。我设法让一个简单的博客正常运行,然后我尝试添加评论。靠我自己,我设法让它进入了可以从script/console添加评论的阶段,但我无法让表单正常工作。我遵循的其中一个教程建议在帖子Controller中创建一个“评论”操作,以添加评论。我的问题是:这是“标准”方式吗?我的另一个问题的答案之一似乎暗示应该有一个CommentsController参
我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:
rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送
我一直致力于让我们的Rails2.3.8应用程序在JRuby下正确运行。一切正常,直到我启用config.threadsafe!以实现JRuby提供的并发性。这导致lib/中的模块和类不再自动加载。使用config.threadsafe!启用:$rubyscript/runner-eproduction'pSim::Sim200Provisioner'/Users/amchale/.rvm/gems/jruby-1.5.1@web-services/gems/activesupport-2.3.8/lib/active_support/dependencies.rb:105:in`co
我需要一些关于TDD概念的帮助。假设我有以下代码defexecute(command)casecommandwhen"c"create_new_characterwhen"i"display_inventoryendenddefcreate_new_character#dostufftocreatenewcharacterenddefdisplay_inventory#dostufftodisplayinventoryend现在我不确定要为什么编写单元测试。如果我为execute方法编写单元测试,那不是几乎涵盖了我对create_new_character和display_invent
我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=
我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi
如果我在模型中设置验证消息validates:name,:presence=>{:message=>'Thenamecantbeblank.'}我如何让该消息显示在闪光警报中,这是我迄今为止尝试过的方法defcreate@message=Message.new(params[:message])if@message.valid?ContactMailer.send_mail(@message).deliverredirect_to(root_path,:notice=>"Thanksforyourmessage,Iwillbeintouchsoon")elseflash[:error]