node.js socket 高频率发消息导致的消息合并成一条的解决方案

最近在完成node的一个socket实现过程中,发现了很神奇的一件事情。

过程是这样的,我们用node创建了一个server,然后监听该server的“data”事件,如下:

net.createServer(function(socket) {
    sock.on('data', function(data) {
        //得到data之后进行JSON格式化处理
        JSON.parse(data)
    }
}).listen(port, ip)

经过测试之后,我们发现一切正常,窃喜之后突然得到了这样的一个噩耗,在高并发的情况下该方法会直接报错,然后中断。

经过检查发现原来是data事件的处理上的一个问题,正常的情况下data接到的数据应该如下

{"data":"123"}

但是当我们连续向该socket发送请求的时候,会发现,只要间隔时间足够短,两条消息就会合并在一起

{"data":"123"}{"data":"123"}

这时候进行JSON.parse直然会报错。

问题的原因找到了,但是如何去解决呢?

我们想到了几个方案:

  1. 限制单个data之间的间隔时间。
  2. 对返回的值进行处理。

第一种方法简单粗暴,通过发送的时候吧所有的发送请求设置一定的时间间隔,但是!这种方法绝对是彻彻底底的反人类。

于是通过仔细观察发现了这样的规律,由于socket遵循tcp协议,所以收到的最小单位一定是一条完整的json,这样的情况下我们完全不需要担心收到半条的情况。接下来就可以全力处理多条消息合在一起的情况了。

实验了各种方法之后,我们采取了下面的正则来处理。

.match(/(\{.+?\})(?={|$)/g)

关键点是这个正则 “ /({.+?})(?={|$)/g ”,他能匹配单个的JSON字符串, 专业术语叫 “零宽断言” , 关于 “零宽断言” 后面会发文章单独说明。

这样匹配出来之后,接下来的事情就好办了,你可以用任何方法单独处理取到的每一个JSON了。

Write a response...
Mofei Zhu
publish
。。
2016-09-24 09:51
您这不就一粘包么,写包头和长度,接收的地方做写缓存就OK了。
0
 Replay
@。。  
Replay
匿名
2016-01-23 18:08
@Mofei 你说的这种方法实际上同时使用了上面提到了两种方法(1. header+body, 2. 分隔符),没问题只是更复杂了一点。实际上header+body是指 在每个完整的消息前面加上一个定长,比如2字节,的header. 这2字节是一个二进制的整数,告诉另一端消息本身是多少byte长。接收方读到header之后就知道应当再读多少byte就是一个完整的消息了,读完完整的消息就再读一个2字节,作为header... 如此往复。 另一种分隔符的方法,就是先定义一个不会在消息里面出现的字节或者字节串作为分隔符(比如你们的消息是文本的,随便用个单字节不可见字符就行),客户端发消息本身,接一个分隔符,再发下一个消息,再发分隔符... 如此往复。
0
 Replay
@  
Replay
Mofei
2016-01-23 15:07
@Mofei的好朋友 我可以这样理解么,收到带有header的数据默认为开始,之后收到的累加起来,然后直到收到结束的标识后就认为一条消息接收完毕了?
0
 Replay
@Mofei  
Replay
匿名
2016-01-23 15:00
@Mofei 比较久之前写过一个处理二进制数据(protobuf)的node server, 所以使用的是定长header的办法。 逻辑上比较像这个:https://github.com/freedaxin/head_body_buffers. (这个repo是刚才随手搜到的,我没有仔细看过) 我能想到的一些需要小心的地方是: 1. header别太长,而且里面最好有个magic number在最前面,读到不合法header要及时丢弃并且主动关掉socket(比如body长度字段值明显不对的),防止被搞; 2. 小心处理客户端主动关连接的情况,防止buf泄露; 3. 你的body是json 解析起来比较简单而且没有歧义,而header一般用二进制的,所以要和客户端约定好字节序之类问题
0
 Replay
@  
Replay
Mofei
2016-01-22 19:52
@Mofei的好朋友 Thanks , by the way , 你们在实际项目中使用的是流式协议? 可否给个参考?
0
 Replay
@Mofei  
Replay
匿名
2016-01-22 19:37
@Mofei 赞! 祝你精进! 若想出现data中只有半条的情况,只要客户端每次都发大一点数据, 和/或setopt调整socket的收发buffer大小/tcp_cork,和/或调整OS和网络的MTU,都可以辅助调试。
0
 Replay
@  
Replay
Mofei
2016-01-22 14:41
@Mofei的好朋友 恩,多谢“指点”,可能有些地方我描述的不是很清楚,查证后我会修改。这么几个地方我想解释一下: 1. 这段代码的demo见这个地址 https://github.com/robertlyc/ybmp/blob/191832575ccd9c7e96b7ac3fb47153780c2e4ebf/lib/pubsub/server.js#L18 2. 使用场景是在收到data之后检测是否匹配正则"dataStr.match(/(\{.+?\})(?={|$)/g); ",如果匹配的话继续处理。我同意你说的data数据的byte是不确定的,但是在实际使用中,我发现接收到的只出现了多条消息黏贴在一起的情况,并没有半条的情况。这里当时写的时候确实没有考虑到会有少于一条的byte的情况。 3. 关于“并发”,是我描述的问题,这里我会修改。 谢谢
0
 Replay
@Mofei  
Replay
匿名
2016-01-22 13:29
@Mofei tcp确实处理了丢包的问题,但这和消息粘在一起没有关系,也不能保证“收到的最小单位一定是一条完整的json”. tcp是面向流的协议,它本身不能分辨传输数据的哪一部分是一个“消息”. 实际上每一次'data'事件究竟吐给你多少byte数据,几乎完全属于node和你所使用系统环境网络协议层的实现细节(协议倒是有一些章节规定一些最小值,但实际上有很多优化并不遵守它们),你能做的恐怕只有在tcp上面定义自己的消息分割协议:定长header + 变长body, 预先定义的分隔符(考虑到你们的消息正文是json,这也不失为好的方法), etc. 我不知道'onEnd'是什么,也不知道同一个socket上如何产生“高并发”的问题,可能我们对于“同时”的理解是不同的。比如我认为一个客户端线程不停地,阻塞地,向同一个socket调用write,这里显然没有任何的“并发”,因为任意时刻最多只有1个frame在网络上传输; 即使多个客户端线程同时地,不加同步地,向同一个socket调用write发送"消息", 这里从网络层面(以及服务端)来看,也没有任何并发,毕竟任意时刻,仍然只有1个frame在网络上传输;虽然这里传输的各个"消息"可能完全被打乱,但这只是客户端这种离奇实现的必然问题,只能说明它是一个错误的客户端,而不涉及任何服务端并发处理的问题。 简要总结下: 1. 单线程阻塞write只要比较频繁,可以轻易触发粘包的问题,tcp_nodelay更会"助纣为虐",但这不是tcp的问题,不是“node判断xxx结尾”的问题,只是这个'data'处理函数自己的问题。2. 这个问题不涉及任何“并发”,因为对于生活在第7层的代码来说,同一个socket上没有任何事情是并发的。
0
 Replay
@  
Replay
Mofei
2016-01-21 19:59
@Mofei的好朋友 高并发指一条socket上同时收到了很多条消息
0
 Replay
@Mofei  
Replay
Mofei
2016-01-21 19:57
@Mofei的好朋友 我的理解是,同一条消息在触发tcp的onEnd的时候,应该是一条完整的数据,协议本身已经处理了丢包的问题。 该问题的主要原因是同时发送过来的请求“粘连”在一起,可能是过多的访问量,导致node在判断单条消息的结尾时候出错,把多条消息当成了一条来处理。
0
 Replay
@Mofei  
Replay
匿名
2016-01-21 19:25
另外同一个socket上,“高并发”又是什么意思呢?
0
 Replay
@  
Replay
匿名
2016-01-21 19:23
"由于socket遵循tcp协议,所以收到的最小单位一定是一条完整的json" 这不一定吧.. 既然已经意识到了是tcp协议的问题, 还是老老实实实使用面向流的协议吧
0
 Replay
@  
Replay
匿名
2015-02-12 10:50
很赞
0
 Replay
@  
Replay
剧中人
2014-06-03 21:57
/({.+?})(?={|$)/g 关键是这个正则
0
 Replay
@剧中人  
Replay