node.js中TCP Socket多进程间的消息推送示例详解


Posted in Javascript onJuly 10, 2018

前言

前段时间接到了一个支付中转服务的需求,即支付数据通过http接口传到中转服务器,中转服务器将支付数据发送到异构后台(Lua)的指定tcp socket。

node.js中TCP Socket多进程间的消息推送示例详解

一开始评估的时候感觉蛮简单的,就是http server和tcp server间的通信,不是一个Event实例就能解决的状态管理问题吗?注册一个事件A用于消息传递,在socket连接时注册唯一的ID,然后在http接收到数据时,emit事件A;在监听到事件A时,在tcp server中寻找指定ID对应的socket处理该数据即可。

尽管node.js在高并发方面有不错的性能,但是单个tcp server实例的承载能力有限,为避免服务器过载,node.js 单进程的内存有上限(默认2G),能容纳的长连接客户端数不多。但随着业务的扩大,我们需要考虑多机集群部署,客户端可以连接到任一节点,并发送消息。如何做到多节点的同时推送,我们需要建立一套多节点之间的消息分发/订阅架构。常用的第三方消息管理库有 RabbitMQ和Redis等。在这里,我用的是Redis的订阅发布服务。

redis.io有一个比较成熟的redis消息中转库socket.io-redis (本地下载)。但我们项目中异构后台用到的并非websocket,而是原生的TCP原生的Socket。用原生redis的sub/pubs实现并不难,就手写了。

redis在该项目中主要起到一个消息分发中心(publish/subscribe)的作用。当http请求的支付数据发送过来时,则通过redis的publish功能往所有的channel推送消息,这样所有订阅该channel的socket server就能收到回调,然后推送到指定客户端。在应用层看跟Event事件消息的处理差不多。

const redis = require("redis"),
 redisClient = redis.createClient,
 REDIS_CFG = {
  host: '127.0.0.1',
  port: 6379
 },
 sub = redisClient(REDIS_CFG),
 pub = redisClient(REDIS_CFG),
 PAY_MQ_CHANNEL = 'pay_mq_channel';

// 监听频道的消息回调
sub.on('message', function(channel, message) {
 switch (channle){
  case PAY_MQ_CHANNEL:
   console.log('notification received:', message);

   // 广播消息到指定socket

   break;
 }
});
// 订阅频道
sub.subscribe(PAY_MQ_CHANNEL);

// 当接收到支付数据时,推送频道消息
pub.publish(PAY_MQ_CHANNEL, {id: '01', msg: `hello ${PAY_MQ_CHANNEL}!`});

由于redis的sub/pub的channel订阅数有上限,所以建议一类消息使用一个channel,一个channel下使用map、set或数组来存储订阅时的回调函数,在接收到订阅消息时遍历执行回调函数。

下面是我封装好的Redis组件(RedisMQProxy.js):

/*
 * redis 订阅/发布
 */
const _ = require('lodash'),
 redis = require("redis"),
 REDIS_CFG = {
  host: '127.0.0.1',
  port: 6379
 },
 sub = redisClient(REDIS_CFG),
 pub = redisClient(REDIS_CFG);

let SubListenerFuns = {}; // channel的回调函数列表

let RedisMQProxy = {

 // 订阅channel
 on(channel, cb, errorCb, once = false) {
  sub.subscribe(channel); // 订阅channel消息

  // 将回调函数存放数组中
  SubListenerFuns[channel] = _.isEmpty( SubListenerFuns[channel] ) ? [] : SubListenerFuns[channel];
  SubListenerFuns[channel].push({
   once, cb, errorCb
  });
 },

 // 监听一次性的channel回调函数
 once(channel, cb, errorCb) {
  this.on(channel, cb, errorCb, true);
 },

 // 发送channel消息
 emit(channel, message) {
  if(!_.isString(message)) {
   message = JSON.stringify(message);
  }
  pub.publish(channel, message);
 },

 // 移除channel上的监听函数
 removeListener(channel, func) {
  let channelHandlers = _.isEmpty( SubListenerFuns[channel] ) ? [] : SubListenerFuns[channel];
  for(let i = 0, l = channelHandlers.length; i < l; i++) {
   let handler = channelHandlers[i] || {};
   let cb = handler.cb;
   if(func && func == cb) {
    channelHandlers.splice(i, 1);
    return false;
   }
  }
 }
};

RedisMQProxy.SubListeners = SubListenerFuns;

pub.on('error', onError);
sub.on('error', onError);

// 监听redis的订阅消息
sub.on("message", function(channel, message) {
 // 遍历执行channel的回调函数
 try {
  message = JSON.parse(message);
 } catch(e) {}
 broadcastToChannel(channel, message);
});

// 广播消息到指定频道
function broadcastToChannel(channel, message, isError) {
 let channelHandlers = _.isEmpty( SubListenerFuns[channel] ) ? [] : SubListenerFuns[channel];
 for(let i = 0, l = channelHandlers.length; i < l; i++) {
  let handler = channelHandlers[i] || {};
  let isOnce = handler.once || false;
  let func = handler.cb;
  let errorFunc = handler.errorCb;

  _.isFunction(func) && func(message);
  isError && _.isFunction(errorFunc) && errorFunc(message);

  isOnce && channelHandlers.splice(i, 1); // 移除一次性监听的函数
 }
}

function broadcastToAllChannels(message, isError) {
 for(let channel in SubListenerFuns) {
  broadcastToChannel(channel, message, isError);
 }
}

function onError(err) {
 err = err || {};
 err.msg = err.msg || 'redis sub/pub fail';

 // 通知所有channel执行错误回调函数
 broadcastToAllChannels(err, true);
}

module.exports = RedisMQProxy;

在使用时就可以比较方便地调用了:

const RedisMQProxy = require('./RedisMQProxy'),
 PAY_MQ_CHANNEL = 'pay_mq_channel';

// 订阅channel
RedisMQ.on(PAY_MQ_CHANNEL, function(message) {
 console.log('notification received:', message);
 // 广播消息到指定socket
 // ...
});

// 订阅一次性的channel
RedisMQ.once(PAY_MQ_CHANNEL, function(message) {
 // ...
});

// 当接收到支付数据时,推送频道消息
RedisMQ.emit(PAY_MQ_CHANNEL, {id: '01', msg: `hello ${PAY_MQ_CHANNEL}!`});

目前该项目已经健康运行了一个多月。由于socket server的多进程间消息推送依赖于redis的消息中转,而Redis使用的是单进程,未能充分利用CPU。当业务膨胀的时候,redis就要考虑分布集群了。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
filemanage功能中用到的lib.js
Apr 08 Javascript
JQuery 确定css方框模型(盒模型Box Model)
Jan 22 Javascript
JQuery 文本框回车跳到下一个文本框示例代码
Aug 30 Javascript
jQuery选择id属性带有点符号元素的方法
Mar 17 Javascript
jQuery模拟360浏览器切屏效果幻灯片(附demo源码下载)
Jan 29 Javascript
AngularJS 过滤器(自带和自建)详解
Sep 19 Javascript
JavaScript html5利用FileReader实现上传功能
Mar 27 Javascript
vue iview实现动态新增和删除
Jun 17 Javascript
详解如何在Javascript中使用Object.freeze()
Oct 18 Javascript
Vue 实现一个简单的鼠标拖拽滚动效果插件
Dec 10 Vue.js
vuex的使用步骤
Jan 06 Vue.js
k8s node节点重新加入master集群的实现
Feb 22 Javascript
vue中$set的使用(结合在实际应用中遇到的坑)
Jul 10 #Javascript
JavaScript中 ES6变量的结构赋值
Jul 10 #Javascript
vue超时计算的组件实例代码
Jul 09 #Javascript
微信小程序自定义底部弹出框
Nov 16 #Javascript
详解vue中组件参数
Jul 09 #Javascript
微信小程序实现手指触摸画板
Jul 09 #Javascript
微信小程序canvas实现刮刮乐效果
Jul 09 #Javascript
You might like
用php实现百度网盘图片直链的代码分享
2012/11/01 PHP
PHP把空格、换行符、中文逗号等替换成英文逗号的正则表达式
2014/05/04 PHP
PHP如何将XML转成数组
2016/04/04 PHP
PHP生成静态HTML文档实现代码
2016/06/23 PHP
CI框架常用经典操作类总结(路由,伪静态,分页,session,验证码等)
2016/11/21 PHP
jQuery 处理表单元素的代码
2010/02/15 Javascript
跟着JQuery API学Jquery 之二 属性
2010/04/09 Javascript
Eclipse下jQuery文件报错出现错误提示红叉
2014/01/13 Javascript
JavaScript实现单击下拉框选择直接跳转页面的方法
2015/07/02 Javascript
在web中js实现类似excel的表格控件
2016/09/01 Javascript
js仿支付宝多方框输入支付密码效果
2016/09/27 Javascript
Angular ng-repeat遍历渲染完页面后执行其他操作详细介绍
2016/12/13 Javascript
解决jQuery ajax动态新增节点无法触发点击事件的问题
2017/05/24 jQuery
bootstrap table sum总数量统计实现方法
2017/10/29 Javascript
Vue中添加手机验证码组件功能操作方法
2017/12/07 Javascript
React Native 真机断点调试+跨域资源加载出错问题的解决方法
2018/01/18 Javascript
关于node-bindings无法在Electron中使用的解决办法
2018/12/18 Javascript
js实现延迟加载的几种方法详解
2019/01/19 Javascript
WebGL three.js学习笔记之阴影与实现物体的动画效果
2019/04/25 Javascript
微信小程序用户拒绝授权的处理方法详解
2019/09/20 Javascript
Vue.js组件通信之自定义事件详解
2019/10/19 Javascript
[02:01]大师之路——DOTA2完美大师赛11月论剑上海
2017/11/06 DOTA
浅析python的优势和不足之处
2018/11/20 Python
Python脚本完成post接口测试的实例
2018/12/17 Python
python中的逆序遍历实例
2019/12/25 Python
倩碧美国官网:Clinique美国
2016/07/20 全球购物
KIKO比利时官网:意大利彩妆品牌
2017/07/23 全球购物
澳大利亚冲浪和时尚服装网上购物:SurfStitch
2017/07/29 全球购物
Opodo英国旅游网站:预订廉价航班、酒店和汽车租赁
2018/07/14 全球购物
英文简历自荐信范文
2013/12/11 职场文书
网络信息管理员岗位职责
2014/01/05 职场文书
建筑工程专业大学生求职信
2014/04/23 职场文书
实习生辞职信范文
2015/03/02 职场文书
肖申克的救赎观后感
2015/06/02 职场文书
MySQL pt-slave-restart工具的使用简介
2021/04/07 MySQL
彻底弄懂Python中的回调函数(callback)
2022/06/25 Python