`
hongtoushizi
  • 浏览: 376731 次
  • 性别: Icon_minigender_1
  • 来自: 天津
社区版块
存档分类
最新评论

pomelo广播的实现(chat例子分析)

阅读更多

转载自: http://blog.csdn.net/fjslovejhl/article/details/11703651

      其实最开始要读pomelo框架无非是因为自己没有读过什么node.js框架的源码,不过后来就逐渐变成了想要知道pomelo框架是如何实现广播的,貌似这也是游戏服务器比较重要的功能吧。。。。

一开始会觉得这种广播在分布式的环境下实现会比较的复杂。。但是当搞明白了pomelo的实现之后,发现它是采用了一种折中的方法实现广播。。虽然没有刚开始自己想的那么牛逼,不过觉得也算是一种比较好的解决方案吧。。


那么接下来就用pomelo给的chat这个例子来分析吧,来看登录吧,首先会向gate服务器发起连接:

[javascript] view plaincopy
  1. function queryEntry(uid, callback) {  
  2.     var route = 'gate.gateHandler.queryEntry';  
  3.     pomelo.init({  
  4.         host: window.location.hostname,  
  5.         port: 3014,  
  6.         log: true  
  7.     }, function() {  
  8.         pomelo.request(route, {  //发起请求,用于获取用于连接的connector服务器的地址  
  9.             uid: uid  
  10.         }, function(data) {  
  11.             pomelo.disconnect();  
  12.             if(data.code === 500) {  
  13.                 showError(LOGIN_ERROR);  
  14.                 return;  
  15.             }  
  16.             callback(data.host, data.port);  
  17.         });  
  18.     });  
  19. };  
这部分代码主要要完成的目的就是与gate进行通信,gate会返回该客户用于连接的connector服务器的地址,我们来看看gate服务器是怎么生成这个地址的吧:
[javascript] view plaincopy
  1.  //next是一个函数,用于执行一些操作,将返回的数据发送回去  
  2. handler.queryEntry = function(msg, session, next) {  
  3.     var uid = msg.uid;  
  4.     if(!uid) {  
  5.         next(null, {  
  6.             code: 500  
  7.         });  
  8.         return;  
  9.     }  
  10.     // get all connectors  
  11.     var connectors = this.app.getServersByType('connector');  //获取素有connector服务器的配置信息  
  12.     if(!connectors || connectors.length === 0) {  
  13.         next(null, {  //第一个参error,第二个参数wie返回给客户端的信息  
  14.             code: 500  
  15.         });  
  16.         return;  
  17.     }  
  18.     // select connector  
  19.     var res = dispatcher.dispatch(uid, connectors);   //选取一个connector服务器  
  20.     next(null, {  
  21.         code: 200,  
  22.         host: res.host,  
  23.         port: res.clientPort  
  24.     });  
  25. };  
  26. var crc = require('crc');  
  27.   
  28. module.exports.dispatch = function(uid, connectors) {  
  29.     var index = Math.abs(crc.crc32(uid)) % connectors.length;  
  30.     return connectors[index];  
  31. };  

到这里就应该知道gate服务器是怎么挑选connector服务器的了吧。。。那么在获取了用于连接的connector之后,就应该建立与connector服务器的连接,进行登录了。。。代码如下:

[javascript] view plaincopy
  1. //query entry of connection  
  2. queryEntry(username, function(host, port) {  
  3.     pomelo.init({  
  4.         host: host,  //这里是返回的用于连接的connector服务器的host与port  
  5.         port: port,  
  6.         log: true  
  7.     }, function() {  
  8.         var route = "connector.entryHandler.enter";  //这里可以当做是进行登录吧  
  9.         pomelo.request(route, {  
  10.             username: username,  
  11.             rid: rid  
  12.         }, function(data) {  
  13.             if(data.error) {  
  14.                 showError(DUPLICATE_ERROR);  
  15.                 return;  
  16.             }  
  17.             setName();  
  18.             setRoom();  
  19.             showChat();  
  20.             initUserList(data);  
  21.         });  
  22.     });  
  23. });  

可以看到这里调用的是connector服务器的handler的enter方法,然后传过去的参数是username和rid(房间的id),那么我们来看看这个connector服务器的enter方法干了些什么事情吧:

[javascript] view plaincopy
  1. handler.enter = function(msg, session, next) {  
  2.     var self = this;  
  3.     var rid = msg.rid;  
  4.     var uid = msg.username + '*' + rid  //用户名字还要加上组名字  
  5.     var sessionService = self.app.get('sessionService');  
  6.   
  7.     //duplicate log in  
  8.     if( !! sessionService.getByUid(uid)) {  //表示有相同的用户了  
  9.         next(null, {  
  10.             code: 500,  
  11.             error: true  
  12.         });  
  13.         return;  
  14.     }  
  15.   
  16.     session.bind(uid);  //将这个session与uid绑定起来  
  17.     session.set('rid', rid);  
  18.     session.push('rid'function(err) {  
  19.         if(err) {  
  20.             console.error('set rid for session service failed! error is : %j', err.stack);  
  21.         }  
  22.     });  
  23.     session.on('closed', onUserLeave.bind(null, self.app));  //设置closed事件的处理函数  
  24.   
  25.     //put user into channel  
  26.     //这里session适用于挑选后台的chat服务器的,这里还要讲当前frontend服务器的serverID传送过去,因为后台要知道当前channel的用户都在哪些frontend服务器上面连接着  
  27.     //这里挑选后台的chat服务器的时候,用的是rid,所以可以保证同一个房间的人分到同一个chatserver  
  28.     self.app.rpc.chat.chatRemote.add(session, uid, self.app.get('serverId'), rid, truefunction(users){  
  29.         next(null, {  
  30.             users:users  //远程服务器返回的当前channel里面的所有的用户  
  31.         });  
  32.     });  
  33. };  

其实这部分的处理韩式很简单的,无非是处理一下从connector组件中分配的 session,设置一下rid,uid等基本的信息,最后有一个比较重要的操作,那就是进行chat的远程调用,在chat服务器中添加一个user, 这里上面的注释也已经很清楚了,其实到这里就已经知道pomelo是怎么实现广播的了,但是还是来看看究竟是怎么搞的吧。。。

那么我们来看看这个远程调用是怎么进行的,如果看过之前对pomelo框架proxy模块的分析,上面的实际上执行的是下面的方法:

[javascript] view plaincopy
  1.       /* 
  2. { namespace: 'sys', 
  3.     serverType: 'chat', 
  4.     path: '/home/fjs/Desktop/pomelo/game-server/node_modules/pomelo/lib/common/remote/backend/' }, 
  5. */  
  6.       proxyCB.call(null, serviceName, methodName, args, attach, invoke);  //调用proxyCB方法来处理数据  

这里serviceName就是chatRemote,methodName是add,args就就是上面传进来的参数,attach就是这个远程调用的基本西溪,例如上面注释的那种形式,invoke可以忽略,那么我们在来看看proxyCB函数究竟干了些设么事情吧:

[javascript] view plaincopy
  1. var proxyCB = function(client, serviceName, methodName, args, attach, invoke) {  
  2.   if(client.state !== STATE_STARTED) {  
  3.     throw new Error('[pomelo-rpc] fail to invoke rpc proxy for client is not running');  
  4.   }  
  5.   
  6.   if(args.length < 2) {  
  7.     logger.error('[pomelo-rpc] invalid rpc invoke, arguments length less than 2, namespace: %j, serverType, %j, serviceName: %j, methodName: %j',  
  8.       attach.namespace, attach.serverType, serviceName, methodName);  
  9.     return;  
  10.   }  
  11.   
  12.   var routeParam = args.shift(); //用于route的参数,一般情况下是session  
  13.   var cb = args.pop();  //用于处理返回消息的回调函数  
  14.   //其实msg也就是pomelo定义的远程方法调用的消息格式,远程服务器会根据这个消息来解析需要调用的方法名字等信息  
  15.   //namespace可以是sys和user,servicename是当前调用的js源码或者说模块的名字,method就是方法的名字,args为传给方法的参数  
  16.   var msg = {namespace: attach.namespace, serverType: attach.serverType,  
  17.     service: serviceName, method: methodName, args: args};  
  18.   // do rpc message route caculate  
  19.   var route, target;  
  20.   if(typeof client.router === 'function') {  
  21.     route = client.router;  
  22.     target = null;  
  23.   } else if(typeof client.router.route === 'function') {  
  24.     route = client.router.route;  //router函数,是用于在服务器中挑选一个,甚至可以理解为负载均衡吧  
  25.     target = client.router;  
  26.   } else {  
  27.     logger.error('[pomelo-rpc] invalid route function.');  
  28.     return;  
  29.   }  
  30.   
  31. //这里调用route函数获取serverID,想这个server发送消息  
  32.   route.call(target, routeParam, msg, client._routeContext, function(err, serverId) {  
  33.     if(err) {  
  34.       utils.invokeCallback(cb, err, serverId);  
  35.       return;  
  36.     }  
  37.   
  38.     client.rpcInvoke(serverId, msg, cb);  
  39.   });  
  40. };  

其实这里之所以想要再将整个rpc的过程又弄出来,主要就是想要证明一个东西,那就是 同一个房间的用于将会被分配到同一个后台chat服务器,在这里我们可以看到一个route函数,不知道大家是否记得在application的时候的一 段代码:app.route('chat', routeUtil.chat);   //chat是server类型,第二个是route函数

这里就会为挑选后台的chat服务器提供一个route函数,那么将在这里使用,那么我们在这里来看看这个函数是怎么定义的吧:

[javascript] view plaincopy
  1. var exp = module.exports;  
  2. var dispatcher = require('./dispatcher');  
  3.   
  4. exp.chat = function(session, msg, app, cb) {  
  5.     var chatServers = app.getServersByType('chat');  
  6.   
  7.     if(!chatServers || chatServers.length === 0) {  
  8.         cb(new Error('can not find chat servers.'));  
  9.         return;  
  10.     }  
  11.   
  12.     var res = dispatcher.dispatch(session.get('rid'), chatServers);  //这里可以保证相同的rid最后访问的是同一个chat服务器  
  13.   
  14.     cb(null, res.id);  
  15. };  

这里将会会dispatch函数传入rid,也就是房间的id,这也就能够知道为什么同一个房间的用于将会被分配到同一个chat服务器了吧。。。好了那么这里对rpc的说明就到此了吧,那么接下来来看看调用的chat服务器的add方法究竟干了些什么事情吧:

[javascript] view plaincopy
  1. //当有用户进来的时候会调用这个方法  
  2. //这里uid是用户的id,sid是前端的connector服务器id,那么是房间的id,  
  3. //由于这里是远程的rpc调用访问的方法,cb是用于将执行结果返回过rpc客户端  
  4. hatRemote.prototype.add = function(uid, sid, name, flag, cb) {  
  5. var channel = this.channelService.getChannel(name, flag);  //这里的flag表示如果没有这个channel的时候要创建这个channel  
  6. var username = uid.split('*')[0];  
  7. var param = {  
  8.     route: 'onAdd',  
  9.     user: username  
  10. };  
  11. channel.pushMessage(param);  //想这个channel广播消息,表示当前有用户加入了channel  
  12.   
  13. if( !! channel) {  
  14.     channel.add(uid, sid);  // 在这个channel中添加一个人,这里还将前段的connector服务器的serverid也传进去了  
  15. }  
  16.   
  17. cb(this.get(name, flag)); //获取当前channel所有的用户,返回回去  
  18. ;  

这部分其实代码一看就基本上就能明白的差不多吧,无非是根据房间的名字来获取这个房间 的channel,然后再想这个channel广播有用户加进来的消息,接着还要在这个channel中设置新的用户,并且还要讲当前channel中所 有的用户返回,,,。。那么这里涉及到广播消息的就是channel.pushMessage(param);

但是在分析这个广播消息的方法之前,我们先来看看channel中是如何添加用户的吧,也就是channel.add(uid, sid);  // 在这个channel中添加一个人,这里还将前段的connector服务器的serverid也传进去了  

[javascript] view plaincopy
  1.  //在当前channel中添加一个新的user,uid是username*rid  ,sid就是这个user所属的前端connector服务器的id  
  2. Channel.prototype.add = function(uid, sid) {  
  3.   if(this.state > ST_INITED) {  
  4.     return false;  
  5.   } else {  
  6.     //这里add说白了就是为了记录当前的前端connector服务器总所属于当前channel的user  
  7.     var res = add(uid, sid, this.groups);  //用于添加用户,这里groups是用于记录当前前端connector服务器属于当前channel的所有的user  
  8.     if(res) {  
  9.       this.records[uid] = {sid: sid, uid: uid};  //相当于是记录当前user的前端服务器  
  10.     }  
  11.     return res;  
  12.   }  
  13. };  
  14.   
  15. /**  

其实这个函数要执行的工作就两个:

(1)记录当前user的前端connector服务器

(2)记录这个前端connector服务器的user

到这里应该就更能够明白pomelo是怎么进行广播的了吧

那么接下来我们还是来看看这个广播方法究竟是怎么进行的广播的吧

[javascript] view plaincopy
  1. //将数据发送给这个channel的所有用户  
  2. hannel.prototype.pushMessage = function(route, msg, cb) {  
  3.  if(this.state !== ST_INITED) {  
  4.    utils.invokeCallback(new Error('channel is not running now'));  
  5.    return;  
  6.  }  
  7.   
  8.  if(typeof route !== 'string') {  
  9.    cb = msg;  
  10.    msg = route;  
  11.    route = msg.route;  
  12.  }  
  13. /这里group是保存了所有有当前用户的前端connector服务器  
  14.  sendMessageByGroup(this.__channelService__, route, msg, this.groups, cb);  
  15. ;  

好像没什么意思吧,那么继续来看这个sendMessageByGroup方法吧:

[javascript] view plaincopy
  1.  //这个函数用于向group的所有的用户发送消息,这里group保存的数据格式是  
  2.  //key:前端的connector服务器的serverID  
  3.  //value:[],一个数组保存这个服务器中要接受数据的user  
  4. var sendMessageByGroup = function(channelService, route, msg, groups, cb) {  
  5.   var app = channelService.app;  
  6.   var namespace = 'sys';  //这里是进行rpc的参数  
  7.   var service = 'channelRemote';  //服务  
  8.   var method = 'pushMessage';  //方法  
  9.   var count = utils.size(groups);  
  10.   var successFlag = false;  
  11.   var failIds = [];  
  12.   
  13.   if(count === 0) {  
  14.     // group is empty  
  15.     utils.invokeCallback(cb);  
  16.     return;  
  17.   }  
  18.   
  19.   var latch = countDownLatch.createCountDownLatch(count, function(){  
  20.     if(!successFlag) {  
  21.       utils.invokeCallback(cb, new Error('all uids push message fail'));  
  22.       return;  
  23.     }  
  24.     utils.invokeCallback(cb, null, failIds);  
  25.   });  
  26.   
  27.   var rpcCB = function(err, fails) {  
  28.     if(err) {  
  29.       logger.error('[pushMessage] fail to dispatch msg, err:' + err.stack);  
  30.       latch.done();  
  31.       return;  
  32.     }  
  33.     if(fails) {  
  34.       failIds = failIds.concat(fails);  
  35.     }  
  36.     successFlag = true;  
  37.     latch.done();  
  38.   };  
  39.   
  40.   var group;  
  41.   for(var sid in groups) {  
  42.     group = groups[sid]; //当前server要接受数据的用户  
  43.     if(group && group.length > 0) {  
  44.       //向相应的服务器发送rpc消息,将数据发送给对应的用户  
  45.       //挨个向所有的服务器发送消息  
  46.       app.rpcInvoke(sid, {namespace: namespace, service: service,  
  47.         method: method, args: [route, msg, groups[sid]]}, rpcCB);  
  48.     } else {  
  49.       // empty group  
  50.       process.nextTick(rpcCB);  
  51.     }  
  52.   }  
  53. };  

其实上面的方法无非就是调用前端connector服务器的方法,让他们将数据发送给最终的用户,那么到这里后端的chat服务器要做的事情就差不多了,工作又回到了前端的connector服务器,那么又来看看吧:

[javascript] view plaincopy
  1. //uid是应该接受数据的user,msg就是要发送的数据,route就是route  
  2. emote.prototype.pushMessage = function(route, msg, uids, cb) {  
  3.  if(!msg){  
  4.    logger.error('Can not send empty message! route : %j, compressed msg : %j',  
  5.        route, msg);  
  6.    return;  
  7.  }  
  8.   
  9.  var connector = this.app.components.__connector__;  
  10.   
  11.  var sessionService = this.app.get('sessionService');  
  12.  var fails = [], sids = [], sessions, j, k;  
  13.  for(var i=0, l=uids.length; i<l; i++) {   
  14.    sessions = sessionService.getByUid(uids[i]);  //获取这个用户的session  
  15.    if(!sessions) {  //如果么有session,那么发送失败  
  16.      fails.push(uids[i]);     
  17.    } else {  
  18.      for(j=0, k=sessions.length; j<k; j++) {  
  19.        sids.push(sessions[j].id);  //这里session的id其实就是connector组件中socket的id,其实这里session就已经有send方法了,为什么不调用?  
  20.      }  
  21.    }  
  22.  }  
  23.   
  24.  connector.send(null, route, msg, sids, {isPush: true}, function(err) {  //调用connector的send方法将数据发送给刚刚弄出来的socket  
  25.    cb(err, fails);  
  26.  });  
  27. ;  

哈,到这里整个广播的过程我想基本上也就弄的比较的清楚了。。。最后再用一张图来总结一下吧:



好了,到这里就基本上搞清楚了我最开始想要搞清楚的问题。。那么整体上pomelo框架的内容就差不太多了,可能还有一些模块要做的事情我没有细看,不过也无所谓吧。。以后如果真有机会用pomelo框架的时候再去看也不迟,只要搞清楚了pomelo框架总体的脉络。。。

其实这里还有一种高并发网络系统的设计思想,叫做连接的离散化。。。这里将用户的连接 分不到不同的connector服务器上,但是如果他们属于同一个房间,则他们访问的依然是同一个后台chat服务器,那么也就做到了职责的分离,前台的 connector服务器主要负责维护用户的连接,用于尽可能多的连接最终的用户,而后台的chat服务器就专职处理一些业务逻辑,只需要维护较少的与前 端connector服务器的rpc连接就可以了。。。

 
 
分享到:
评论

相关推荐

    pinus-chat-example:pomelo ts版本chat例子,居于pinus为框架编写(pinus是pomelo ts版本,修改一些bug,优化了框架)

    pomelo ts版本聊天例子,居于pinus为框架编写(pinus是pomelo ts版本,修改一些bug,优化了框架) windows平台 npm-install.bat //安装game-server,web-server模块依赖 linux平台 npm-install.sh //安装game-server...

    Pomelo聊天例子代码

    Pomelo支持广播和多播机制,可以高效地将消息推送给所有在线用户或者特定用户群组。你需要理解如何利用这些特性来实现实时的聊天功能。 6. **错误处理和重连机制**:网络通信难免会出现断开连接的情况,因此,良好...

    pomelo-chat-demo:pomelo 分布式聊天例子(c# socket + chatofpomelo-websocket)

    c/s 聊天例子目的:实现了一个简单的c/s分布式聊天例子,演示了c#客户端和pomelo服务器的交互,使用原生socket通信方式。本来是想用unity3d和pomelo通信,但是unity3d里面处理异步比较麻烦,为了使例子简洁,这里用...

    【socket编程】pomelo-chat-unity-socket.zip

    在这个案例中,我们关注的是"pomelo-chat-unity-socket.zip",它是一个专门为Pomelo聊天服务器设计的Unity客户端示例,使用了原生的Socket接口。以下将详细讲解如何在Unity中进行Socket编程,以及如何与Pomelo聊天...

    pomelo-androidchat:聊天客户端应用程序,使用Android客户端进行柚

    pomelo-androidchat 这是一个使用android客户端进行pomelo的聊天应用程序,服务器端是chatofpomelo( )。 ##许可证(MIT许可证) 版权所有(c)2013 NetEase,Inc.和其他贡献者 特此免费授予获得此软件和相关...

    pomelo源码分析.7z

    综上所述,Pomelo源码的深度分析不仅有助于理解其实现原理,也为开发者提供了优化和扩展的思路。通过对启动流程、组件加载、RPC通信、admin和monitor组件的掌握,以及session和channel的理解,开发者可以更高效地...

    Pomelo 集成 Unity 客户端 Demo

    Pomelo官方的Chat Demo有Bug,Unity客户端是无法连击上服务器的。这个是修正了Bug的版本,包含了服务器和客户端全部代码。 如果要部署到远程服务器,注意要把chatofpomelo-websocket\game-server\config\servers....

    pomelo框架下的聊天室开发

    本文将深入探讨如何利用Pomelo框架进行聊天室的开发,为读者揭示其中的关键技术和实现细节。 Pomelo是一款由网易公司开源的高性能、轻量级的Node.js游戏服务器框架。它基于Node.js的EventEmitter模型,提供了强大的...

    基于pomelo框架开发的聊天demo

    这个聊天demo展示了如何利用Pomelo实现服务器与客户端之间的数据交互,以及实现聊天功能和排行榜系统。 1. **Pomelo框架**:Pomelo由网易公司开源,设计目标是提供快速、可扩展和高可用性的网络游戏服务器解决方案...

    pomelo源码分析.docx

    ### Pomelo源码分析 #### 一、概览 Pomelo是一款高性能、可扩展的游戏服务器框架,基于Node.js开发。它提供了丰富的功能模块来帮助开发者构建复杂的多人在线游戏和其他实时应用。本文将深入分析Pomelo的核心启动...

    pomelo服务器管理工具

    - **集群部署**:工具支持一键部署,可以在多台服务器上快速启动和配置Pomelo服务,实现服务的快速扩展。 - **实时监控**:可以实时查看服务器的CPU、内存使用情况,网络流量等关键性能指标,帮助开发者及时发现并...

    linux部署pomelo详细文档

    Pomelo是一款由NetEase(网易)开发的高性能、轻量级的游戏服务器框架,适用于实时交互的应用场景。本文将详细阐述如何在Linux环境下部署Pomelo,以及解决可能出现的问题。 首先,确保你的Linux服务器已经安装了...

    基于Pomelo分布式服务器后台管理的人事工资系统设计与实现.pdf

    标题:“基于Pomelo分布式服务器后台管理的人事工资系统设计与实现.pdf” 描述:未提供具体描述。 标签:分布式、分布式系统、分布式开发、参考文献、专业指导。 详细知识点: 1. Pomelo技术概述: Pomelo是一个由...

    Pomelo 游戏框架 集成 Unity 客户端 Demo - 优化版

    原Pomelo框架的Unity Demo有bug,无法正常运行。并且API回调在通信线程,使用起来极不方便。 该Demo对官方的Unity API进行了重构,主要功能如下: 1. 所有方法的回调均在主线程(原PomeloClient回调在Socket线程,...

    pomelo 2.2.5

    3. Middleware(中间件):Pomelo采用Express风格的中间件,允许开发者插入自定义处理逻辑,实现请求处理链。 4. RPC机制:Pomelo提供基于JSON-RPC的跨服务器通信机制,支持高效的数据传输和服务器间协作。 三、...

    pomelo-admin-web.zip

    - **日志管理**:查看和分析应用的日志,便于定位和解决问题。 - **用户管理**:查看在线用户,包括用户行为、连接状态等信息。 - **服务器管理**:启动、停止、重启服务器,以及进行其他运维操作。 - **性能优化**...

    CentOS 6.3下搭建Pomelo部署环境.docx

    在这个例子中,我们使用的是64位的CentOS 6.3。在开始之前,请确保你的服务器也运行这个版本的操作系统。 接下来,安装必要的软件包。第一步是安装Git,因为Pomelo的源代码托管在GitHub上。如果在运行`yum install ...

    Pomelo 游戏框架 Unity 客户端 亲测可用

    所有方法的回调均在主线程(原PomeloClient回调在Socket线程,用起来很麻烦) 2. 增加了DisconnectEvent和ErrorEvent两个事件通知,方便捕捉网络断开事件和其它异常 3. 所有报文回调时,会收到一个Message对象而...

    Pomelo​的Cocos2d-js客户端pomelo-cocos2d-js.zip

    pomelo-cocos2d-js 是 Pomelo 的 Cocos2d-js 客户端。var pomelo = window.pomelo; var route = 'gate.gateHandler.queryEntry'; var uid = "uid"; var rid = "rid"; var username = "username"; ...

    网易开源项目pomelo VS2010工程项目

    【标题】:“网易开源项目pomelo ...通过以上分析,我们可以看出“网易开源项目pomelo”是一个针对游戏服务端开发的强大框架,而其与VS2010的良好兼容性使得C++开发者能够更加高效地利用这一工具进行游戏服务器的构建。

Global site tag (gtag.js) - Google Analytics