前言

前段时间接到了一个支付中转服务的需求,即支付数据通过http接口传到中转服务器,中转服务器将支付数据发送到异构后台(Lua)的指定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就要考虑分布集群了。

总结

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

node.js中TCP Socket多进程间的消息推送示例详解的更多相关文章

  1. html5 http的轮询和Websocket原理

    这篇文章主要介绍了html5 http的轮询和Websocket原理的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. 当iOS应用程序进入后台时,TCP和UDP(与多播)连接会发生什么

    我创建了几个实验:设置1:我创建了一个TCPSender应用程序和一个TCPReceiver应用程序.在本次实验中,我在iOS设备上启动了TCPSender,在另一台iOS设备上启动了TCPReceiver.然后两者都经过验证已建立连接并发送和接收数据.然后我将TCPReceiver应用程序置于后台.TCPSender应用程序指示连接丢失和崩溃(是的,我打算这样).设置2:我创建了一个UDPSen

  3. ios – Swift:使用GCDAsyncUdpSocket接收UDP

    开始:接收:发送:编辑:添加了忘记的代码行.解决方法我终于得到它使用这个套接字设置:

  4. 在iOS中,如何增加主机的HTTP连接限制?

    使用Xcode网络工具,我分析说,我每次只能建立每个主机的4个TCP连接.似乎iOS的每个主机的默认TCP连接限制为4.我们如何增加这个限制?

  5. Swift开发:GCDAsyncSocket通信之TCP服务器

    overridefuncviewDidLoad(){super.viewDidLoad()clientSockets=NSMutableArray()msgTextView.backgroundColor=UIColor.grayColor()msgTextView.text="接收的客户端消息:\n"}//发送消息按钮@IBActionfuncsendBtnClick{ifclientSockets.count==0{return}letmsg=inputTextInput.text!//1.处理请求,

  6. Swift开发:GCDAsyncSocket通信之TCP 客户端

    varmainQueue=dispatch_get_main_queue()overridefuncviewDidLoad(){super.viewDidLoad()}//连接服务器按钮事件@IBActionfuncconBtnClick{do{clientSocket=GCDAsyncSocket()clientSocket.delegate=selfclientSocket.delegateQueue=dispatch_get_global_queue(0,0)tryclientSocket.conn

  7. Swift Socket 实例 两份资料不错

    Swift-使用vaporsocks库进行socket通信(基于TCP、UDP协议)http://www.hangge.com/blog/cache/detail_1588.htmlSwift-使用socket进行通信(附聊天室样例)http://www.hangge.com/blog/cache/detail_756.htmlIBM-Swift/BlueSockethttps://github.

  8. Android从服务器套接字侦听消息

    解决方法您可以在服务中创建一个线程来监听服务器.第二个线程用于发送命令.然后,对于您的服务,您应该创建一个带有处理程序的主线程.此处理程序将处理来自这两个线程的消息.

  9. Android BluetoothSocket.isConnected始终返回false

    解决方法我相信jkane001已经解决了他的问题,所以我希望这个答案可以帮助别人.首先在套接字创建之后你应该通过初始连接之后,您将能够使用socket.isConnected()检查连接状态由于connect()方法没有阻塞,所以socket之后可能还没有连接.我建议使用这样的东西顺便说一句,我发现在一些Android设备上isConnected()总是返回false.在这种情况下,只是尝试写一些东西到socket并检查是否没有异常.

  10. Android:流式摄像机数据并将其写入服务器

    我测试看它是否真的捕获了视频输出:这是以下用于设置mediaRecorder的Android代码解决方法有一些开源项目可以解决这个问题,例如Spydroid和AndroidIPCamera.你的实现类似于Spydroid,所以也许你可以调整它的一些代码.中心问题是MediaRecorder正在将原始视频帧写入套接字.它需要等到视频完成才能写入标题,但它们需要出现在文件的开头.由于套接字不可搜索,因此无法在正确的位置写入标头.上面链接的项目通过将流打包成RTSP或将一系列静态图像“流式传输”到浏览器来解决这

随机推荐

  1. Error: Cannot find module ‘node:util‘问题解决

    控制台 安装 Vue-Cli 最后一步出现 Error: Cannot find module 'node:util' 问题解决方案1.问题C:\Windows\System32>cnpm install -g @vue/cli@4.0.3internal/modules/cjs/loader.js:638 throw err; &nbs

  2. yarn的安装和使用(全网最详细)

    一、yarn的简介:Yarn是facebook发布的一款取代npm的包管理工具。二、yarn的特点:速度超快。Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。超级安全。在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。超级可靠。使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。三、y

  3. 前端环境 本机可切换node多版本 问题源头是node使用的高版本

    前言投降投降 重头再来 重装环境 也就分分钟的事 偏要折腾 这下好了1天了 还没折腾出来问题的源头是node 使用的高版本 方案那就用 本机可切换多版本最终问题是因为nodejs的版本太高,导致的node-sass不兼容问题,我的node是v16.14.0的版本,项目中用了"node-sass": "^4.7.2"版本,无法匹配当前的node版本根据文章的提

  4. nodejs模块学习之connect解析

    这篇文章主要介绍了nodejs模块学习之connect解析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  6. 详解koa2学习中使用 async 、await、promise解决异步的问题

    这篇文章主要介绍了详解koa2学习中使用 async 、await、promise解决异步的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. Node.js编写爬虫的基本思路及抓取百度图片的实例分享

    这篇文章主要介绍了Node.js编写爬虫的基本思路及抓取百度图片的实例分享,其中作者提到了需要特别注意GBK转码的转码问题,需要的朋友可以参考下

  8. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  10. node下使用UglifyJS压缩合并JS文件的方法

    下面小编就为大家分享一篇node下使用UglifyJS压缩合并JS文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

返回
顶部