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 相关文章推荐
jquery imgareaselect 使用利用js与程序结合实现图片剪切
Jul 30 Javascript
屏蔽IE弹出&quot;您查看的网页正在试图关闭窗口,是否关闭此窗口&quot;的方法
Dec 31 Javascript
js实现获取焦点后光标在字符串后
Sep 17 Javascript
Linux下编译安装php libevent扩展实例
Feb 14 Javascript
JavaScript事件委托实例分析
May 26 Javascript
js焦点文字滚动效果代码分享
Aug 25 Javascript
ES6中如何使用Set和WeakSet
Mar 10 Javascript
基于JS+Canves实现点击按钮水波纹效果
Sep 15 Javascript
JS实现DIV高度自适应窗口示例
Feb 16 Javascript
基于Vue2.0+ElementUI实现表格翻页功能
Oct 23 Javascript
Javascript作用域和作用域链原理解析
Mar 03 Javascript
解决Mint-ui 框架Popup和Datetime Picker组件滚动穿透的问题
Nov 04 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
java EJB 加密与解密原理的一个例子
2008/01/11 PHP
Laravel框架中实现使用阿里云ACE缓存服务
2015/02/10 PHP
javascript实现上传图片前的预览(TX的面试题)
2007/08/20 Javascript
javascript学习笔记(四) Number 数字类型
2012/06/19 Javascript
jQuery异步加载数据并添加事件示例
2014/08/24 Javascript
浅谈javascript 函数表达式和函数声明的区别
2016/01/05 Javascript
限制文本框只能输入数字||只能是数字和小数点||只能是整数和浮点数
2016/05/27 Javascript
用原生JS对AJAX做简单封装的实例代码
2016/07/13 Javascript
js倒计时小实例(多次定时)
2016/12/08 Javascript
jQuery实现的鼠标响应缓冲动画效果示例
2018/02/13 jQuery
JavaScript定时器设置、使用与倒计时案例详解
2019/07/08 Javascript
详解利用eventemitter2实现Vue组件通信
2019/11/04 Javascript
vue 使用localstorage实现面包屑的操作
2020/11/16 Javascript
[07:12]2014DOTA2西雅图国际邀请赛 黑马Liquid专题采访
2014/07/12 DOTA
[01:39:42]Fnatic vs Mineski 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
python实现自动登录人人网并访问最近来访者实例
2014/09/26 Python
Python备份目录及目录下的全部内容的实现方法
2016/06/12 Python
Win10下Python环境搭建与配置教程
2016/11/18 Python
Jupyter中直接显示Matplotlib的图形方法
2018/05/24 Python
使用coverage统计python web项目代码覆盖率的方法详解
2019/08/05 Python
python3.6环境下安装freetype库和基本使用方法(推荐)
2020/05/10 Python
工程师岗位职责
2013/11/08 职场文书
应聘收银员个人的求职信
2013/11/30 职场文书
技校生自我鉴定
2013/12/08 职场文书
复核员上岗演讲稿
2014/01/05 职场文书
企业管理毕业生求职信范文
2014/03/07 职场文书
计算机系本科生求职信
2014/05/31 职场文书
最美家庭活动方案
2014/08/31 职场文书
个人党性分析总结
2015/03/05 职场文书
2015年度班主任自我评价
2015/03/11 职场文书
2015年试用期工作总结范文
2015/05/28 职场文书
战友聚会致辞
2015/07/28 职场文书
公司晚宴祝酒词
2015/08/11 职场文书
2019个人工作态度自我评价
2019/04/24 职场文书
Python可视化学习之seaborn调色盘
2022/02/24 Python
sql查询语句之平均分、最高最低分及排序语句
2022/05/30 MySQL