pomelo 中的chat例子聊天的bearychat客户端下载可服务端怎么连接起来

pomelo(14)
chatOfPomelo是一个聊天室程序,笔者将对chat的服务端game-server进行分析
首先来看它的文件结构
大的结构与hellopomelo一致,都有app,config,logs,node_modules,app.js.
有了hello的经验,我们直奔主题config/servers.json
这里配置着我们自定义的服务器配置
"development":{
"connector":[
{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true}
{"id":"chat-server-1", "host":"127.0.0.1", "port":6050}
{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}
"production":{
"connector":[
{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true}
{"id":"chat-server-1", "host":"127.0.0.1", "port":6050}
{"id": "gate-server-1", "host": "127.0.0.1", "clientPort": 3014, "frontend": true}
这里看出来对于development和production环境都作了同样的配置
这次的聊天室是由三个自定义的服务组成
gate,chat,connector
gate– 前端网关服务,用于最初与服务器的连接,并分配connector给客户端
connector– 前端连接服务器,用于与客户端保持websocket长连接,并转发后端服务器处理的协议
chat– 后端聊天服务器,客户端不能与它进行连接,它处理好的逻辑通过connector传递给客户端
看看3个服务的目录结构
注意到gate,connector作为前端服务器,他们都有handler文件夹
而chat后端服务器,不仅有handler还有remote文件夹
在pomelo中,用户自定义的handler文件夹表示要处理与客户端连接相关的事件
而remote文件则表示处理服务器的rpc调用
handler文件夹里的文件会被作为组件加载.且放入到一个route中,以便于客户端向pomelo请求gate.gateHandler.queryEntry这样的请求时,会被精确定位到
gate/handler/gateHandler.js中的queryEntry接口
看看gateHandler.js的代码
module.exports = function(app) {
return new Handler(app);
var Handler = function(app) {
this.app =
var handler = Handler.
handler.queryEntry = function(msg, session, next) {
var uid = msg.
if(!uid) {
next(null, {
var connectors = this.app.getServersByType('connector');
if(!connectors || connectors.length === 0) {
next(null, {
var res = connectors[0];
next(null, {
code: 200,
host: res.host,
port: res.clientPort
entryHandler.js(这个文件在connector目录底下,不管它叫什么名字,都是connector的模块)
module.exports = function(app) {
return new Handler(app);
var Handler = function(app) {
this.app =
var handler = Handler.
handler.enter = function(msg, session, next) {
var self = this;
var rid = msg.
var uid = msg.username + '*' + rid
var sessionService = self.app.get('sessionService');
if( !! sessionService.getByUid(uid)) {
next(null, {
code: 500,
error: true
session.bind(uid);
session.set('rid', rid);
session.push('rid', function(err) {
console.error('set rid for session service failed! error is : %j', err.stack);
session.on('closed', onUserLeave.bind(null, self.app));
self.app.rpc.chat.chatRemote.add(session, uid, self.app.get('serverId'), rid, true, function(users){
next(null, {
users:users
var onUserLeave = function(app, session) {
if(!session || !session.uid) {
app.rpc.chat.chatRemote.kick(session, session.uid, app.get('serverId'), session.get('rid'), null);
chat分2部分,一部分是handler,另一部分是remote
handler说明它要处理客户端的信息
remote说明它要处理来自后端服务器的信息
由于我们的connecotr用到了chat的add以及kick,因此我们先看remote的内容
chatremote.js
module.exports = function(app) {
return new ChatRemote(app);
var ChatRemote = function(app) {
this.app =
this.channelService = app.get('channelService');
ChatRemote.prototype.add = function(uid, sid, name, flag, cb) {
var channel = this.channelService.getChannel(name, flag);
var username = uid.split('*')[0];
var param = {
route: 'onAdd',
user: username
channel.pushMessage(param);
if( !! channel) {
channel.add(uid, sid);
cb(this.get(name, flag));
ChatRemote.prototype.get = function(name, flag) {
var users = [];
var channel = this.channelService.getChannel(name, flag);
if( !! channel) {
users = channel.getMembers();
for(var i = 0; i & users. i++) {
users[i] = users[i].split('*')[0];
ChatRemote.prototype.kick = function(uid, sid, name) {
var channel = this.channelService.getChannel(name, false);
if( !! channel) {
channel.leave(uid, sid);
var username = uid.split('*')[0];
var param = {
route: 'onLeave',
user: username
channel.pushMessage(param);
从代码上看出都是与pomelo的内部组件打交道的方法,代码简单,不过这里的所有发往客户端的message都是通过channel来完成的.channel又是通过connector来完成,所以此服务器与客户端并不直接相联,都通过connector来中转
最后是 chat.Handler,在pomelo中,handler都是处理客户端消息的地方,不过由于chat不是前端服务器,这里的send事件是由connector捕捉到以后route过来的,而不是客户端直接与chat服务器进行的连接.
var chatRemote = require('../remote/chatRemote');
module.exports = function(app) {
return new Handler(app);
var Handler = function(app) {
this.app =
var handler = Handler.
handler.send = function(msg, session, next) {
var rid = session.get('rid');
var username = session.uid.split('*')[0];
var channelService = this.app.get('channelService');
var param = {
msg: msg.content,
from: username,
target: msg.target
channel = channelService.getChannel(rid, false);
if(msg.target == '*') {
channel.pushMessage('onChat', param);
var tuid = msg.target + '*' +
var tsid = channel.getMember(tuid)['sid'];
channelService.pushMessageByUids('onChat', param, [{
uid: tuid,
next(null, {
route: msg.route
最后我们来看一下整个网络结构图
gate是唯一的,起到大门的作用.客户端首先发起连接是和gate产生的,由gate决定客户端与哪个connecotr连接,gate可以起到负载均衡的作用
这里可以有多个connector群.负责承载来自客户端的连接,并与后端的chatroom进行交互.
这里也可以有多个chatroom,每个chat可以对应到不同的connector.
后端的chatroom不与客户端直接连接
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:96836次
积分:2454
积分:2454
排名:第10948名
原创:134篇
转载:10篇
评论:22条
文章:18篇
阅读:11043
阅读:10181
(4)(7)(4)(4)(4)(4)(4)(6)(6)(6)(6)(8)(5)(4)(2)(4)(5)(5)(5)(5)(5)(14)(1)(1)(8)(4)(3)(6)(5)(4)(15)pomelo(14)
web-server 是客户端通过浏览器的形式方式的远程服务器站点,它其实是一个静态页面
它的文件结构如下:
这个文件结构比较庞大
bin 这个文件夹不用过多介绍,它是工具,用来安装组件的
node_modules 这是nodejs的库文件夹,它里面包括了express这个nodejs的库,作用是用来方便的创建web服务,供大家访问,若不用express,纯的nodejs创建web服务就会吃力得多
app.js web-server的入口,程序从它启动,它非常的简单,用express框架创建了一个web服务,然后加载了public/index.html这个页面展示出来.
var express = require('express');
var app = express();
app.configure(function(){
app.use(express.methodOverride());
app.use(express.bodyParser());
app.use(app.router);
app.set('view engine', 'jade');
app.set('views', __dirname + '/public');
app.set('view options', {layout: false});
app.set('basepath',__dirname + '/public');
app.configure('development', function(){
app.use(express.static(__dirname + '/public'));
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
app.configure('production', function(){
var oneYear = ;
app.use(express.static(__dirname + '/public', { maxAge: oneYear }));
app.use(express.errorHandler());
console.log("Web server has started.\nPlease log on http://127.0.0.1:3001/index.html");
app.listen(3001);
接下来所有的工作都在public这个文件夹下面了,单独上图
index.html,需要加载的展示给客户端的页面
可以使用网页编辑工具来对其进行编辑,它的静态格式为
在其中指定加载以下几段js脚本
src="js/lib/jquery-1.8.0.min.js" type="text/javascript"&
src="js/lib/build/build.js" type="text/javascript"&
type="text/javascript"&
require('boot');
src="js/client.js"& &
src="js/pop.js" type="text/javascript"&
注意它的加载顺序
lib/build/build.js 这里加载了pomelo客户端所需要的组件
boot 把pomelo组件放到window对象中
jsclient.js 客户端使用pomelo来与服务器通讯的代码
pop.js 弹出式的窗口的小控件
注意 components文件夹里面是客户端pomelo的几个核心组件
发射器,pomelo消息驱动机制的基石
协议。网络通讯的基石
websocket 网络通讯协议
protobuf 协议压缩编码
在 jsclient.js中处理了jion的响应以及后面的事情
我们看下主要和几个核心函数是如何轻松工作的
function queryEntry(uid, callback) {
var route = 'gate.gateHandler.queryEntry';
pomelo.init({
host: window.location.hostname,
port: 3014,
}, function() {
pomelo.request(route, {
}, function(data) {
pomelo.disconnect();
if(data.code === 500) {
showError(LOGIN_ERROR);
callback(data.host, data.port);
$(document).ready(function(){
pomelo.on('onChat', function(data) {
addMessage(data.from, data.target, data.msg);
$("#chatHistory").show();
if(data.from !== username)
tip('message', data.from);
pomelo.on('onAdd', function(data) {
var user = data.
tip('online', user);
addUser(user);
pomelo.on('onLeave', function(data) {
var user = data.
tip('offline', user);
removeUser(user);
pomelo.on('disconnect', function(reason) {
showLogin();
$("#login").click(function() {
username = $("#loginUser").attr("value");
rid = $('#channelList').val();
if(username.length & 20 || username.length == 0 || rid.length & 20 || rid.length == 0) {
showError(LENGTH_ERROR);
return false;
if(!reg.test(username) || !reg.test(rid)) {
showError(NAME_ERROR);
return false;
queryEntry(username, function(host, port) {
pomelo.init({
host: host,
port: port,
}, function() {
var route = "connector.entryHandler.enter";
pomelo.request(route, {
username: username,
}, function(data) {
if(data.error) {
showError(DUPLICATE_ERROR);
setName();
setRoom();
showChat();
initUserList(data);
$("#entry").keypress(function(e) {
var route = "chat.chatHandler.send";
var target = $("#usersList").val();
if(e.keyCode != 13
var msg = $("#entry").attr("value").replace("\n", "");
if(!util.isBlank(msg)) {
pomelo.request(route, {
content: msg,
from: username,
target: target
}, function(data) {
$("#entry").attr("value", "");
if(target != '*' && target != username) {
addMessage(username, target, msg);
$("#chatHistory").show();
整个逻辑非常简洁
并且由此看出客户端向服务器发送网络消息的api
pomelo.request(route, {content}, callback)
这个接口非常像curl,其实是个tcp连接。
route:要把消息发送给哪个服务
用户自定义的消息体
callback: 服务器返回时被调用的函数,注意这是异步响应的
客户端响应服务器网络消息的api
pomelo.on(event, callback)
event 网络消息的关键字
callback 网络消息的处理函数
public/js文件夹 里面都是javascript文件以及json文件。
package.json commonJS的标准文件,里面有整个包的说明
相信童鞋们在看了这几篇文章以后,对pomelo会有个大概的认识
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:96839次
积分:2454
积分:2454
排名:第10948名
原创:134篇
转载:10篇
评论:22条
文章:18篇
阅读:11043
阅读:10181
(4)(7)(4)(4)(4)(4)(4)(6)(6)(6)(6)(8)(5)(4)(2)(4)(5)(5)(5)(5)(5)(14)(1)(1)(8)(4)(3)(6)(5)(4)(15)(pomelo系列入门教程)深入浅出Node.js游戏服务器开发--分布式聊天服务器搭建
(pomelo系列入门教程)深入浅出Node.js游戏服务器开发--分布式聊天服务器搭建
[摘要:正在上一篇文章中, 我们先容了游戏办事器的根基架构、相干框架战Node.js开辟游戏办事器的上风。本文我们将经过谈天办事器的计划取开辟,去更深刻地舆解pomelo开辟运用的根基流程、]
在上一篇文章中, 我们介绍了游戏服务器的基本架构、相关框架和Node.js开发游戏服务器的优势。本文我们将通过聊天服务器的设计与开发,来更深入地理解pomelo开发应用的基本流程、开发思路与相关的概念。本文并不是开发聊天服务器的tutorial,如果需要tutorial和源码可以看文章最后的参考资料。
为什么是聊天服务器?
我们目标是搭建游戏服务器,为什么从聊天开始呢?
聊天可认为是简化的实时游戏,它与游戏服务器有着很多共通之处,如实时性、频道、广播等。由于游戏在场景管理、客户端动画等方面有一定的复杂性,并不适合作为pomelo的入门应用。聊天应用通常是Node.js入门接触的第一个应用,因此更适合做入门教程。
Pomelo是游戏服务器框架,本质上也是高实时、可扩展、多进程的应用框架。除了在library中有一部分游戏专用的库,其余部分框架完全可用于开发高实时web应用。而且与现在有的Node.js高实时应用框架如derby、socketstream、meteor等比起来有更好的可伸缩性。
对于大多数开发者而言,Node.js的入门应用都是一个基于socket.io开发的普通聊天室, 由于它是基于单进程的Node.js开发的, 在可扩展性上打了一定折扣。例如要扩展到类似irc那样的多频道聊天室, 频道数量的增多必然会导致单进程的Node.js支撑不住。
而基于pomelo框架开发的聊天应用天生就是多进程的,可以非常容易地扩展服务器类型和数量。
从单进程到多进程,从socket.io到pomelo
一个基于socket.io的原生聊天室应用架构, 以uberchat为例。
它的应用架构如下图所示:
服务端由单个Node.js进程组成的chat server来接收websocket请求。
它有以下缺点:
可扩展性差:只支持单进程的Node.js, 无法根据room/channel分区, 也无法将广播的压力与处理逻辑的压力分开。
代码量大:基于socket.io做了简单封装,服务端就写了约430行代码。
用pomelo来写这个框架可完全克服以上缺点,并且代码量只要区区100多行。
我们要搭建的pomelo聊天室具有如下的运行架构:
在这个架构里, 前端服务器也就是connector专门负责承载连接, 后端的聊天服务器则是处理具体逻辑的地方。 这样扩展的运行架构具有如下优势: * 负载分离:这种架构将承载连接的逻辑与后端的业务处理逻辑完全分离,这样做是非常必要的, 尤其是广播密集型应用(例如游戏和聊天)。密集的广播与网络通讯会占掉大量的资源,经过分离后业务逻辑的处理能力就不再受广播的影响。
切换简便:因为有了前、后端两层的架构,用户可以任意切换频道或房间都不需要重连前端的websocket。
扩展性好:用户数的扩展可以通过增加connector进程的数量来支撑。频道的扩展可以通过哈希等算法负载均衡到多台聊天服务器上。理论上这个架构可以实现频道和用户的无限扩展。
聊天服务器开发架构
game server与web server
聊天服务器项目中分生成了game-server目录、web-server目录与shared目录,如下图所示:
这样也将应用天然地隔离成了两个,game server与web server。
Game server, 即游戏服务器,所有的游戏服务器逻辑都在里实现。客户端通过websocket(0.3版会支持tcp的socket)连到game server。game-server/app.js是游戏服务器的运行入口。
Web server,即web服务器, 也可以认为是游戏服务器的一个web客户端, 所有客户端的js代码,web端的html、css资源都存放在这里,web服务端的用户登录、认证等功能也在这里实现。pomelo也提供了其它客户端,包括ios、android、unity3D等。
Shared目录,假如客户端是web,由于服务端和客户端都是javascript写的,这时Node.js的代码重用优势就体现出来了。shared目录下可以存放客户端、服务端共用的常量、算法。真正做到一遍代码, 前后端共用。
服务器定义与应用目录
Game server才是游戏服务器的真正入口,游戏逻辑都在里, 我们简单看一下game-server的目录结构,如下图所示:
servers目录下所有子目录定义了各种类型的服务器,而每个servers目录下基本都包含了handler和remote两个目录。 这是pomelo的创新之处,用极简的配置实现游戏服务器的定义,后文会解释handler和remote。
通过pomelo,游戏开发者可以自由地定义自己的服务器类型,分配和管理进程资源。在pomelo中,根据服务器的职责不同,服务器主要分为前端服务器(frontend)和后端服务器(backend)两大类。其中,前端服务器负责承载客户端的连接和维护session信息,所有服务器与客户端的消息都会经过前端服务器;后端服务器负责接收前端服务器分发过来的请求,实现具体的游戏逻辑,并把消息回推给前端服务器,最后发送给客户端。如下图所示:
动态语言的面向对象有个基本概念叫鸭子类型。在pomelo中,服务器的抽象也同样可以比喻为鸭子,服务器的对外接口只有两类, 一类是接收客户端的请求, 叫做handler, 一类是接收RPC请求, 叫做remote, handler和remote的行为决定了服务器长什么样子。 因此开发者只需要定义好handler和remote两类的行为, 就可以确定这个服务器的类型。 例如chat服务器目前的行为只有两类,分别是定义在handler目录中的chatHandler.js,和定义在remote目录中的chatRemote.js。只要定义好这两个类的方法,聊天服务器的对外接口就确定了。
搭建聊天服务器
pomelo的客户端服务器通讯
pomelo的客户端和服务器之间的通讯可以分为三种:
request-response
pomelo中最常用的就是request-response模式,客户端发送请求,服务器异步响应。客户端的请求发送形式类似ajax类似:
``` pomelo.request(url, msg, function(data){}); ```
第一个参数为请求地址,完整的请求地址主要包括三个部分:服务器类型、服务端相应的文件名及对应的方法名。第二个参数是消息体,消息体为json格式,第三个参数是回调函数,请求的响应将会把结果置入这个回调函数中返回给客户端。
notify与request—response类似,唯一区别是客户端只负责发送消息到服务器,客户端不接收服务器的消息响应。
``` pomelo.notify(url, msg); ```
push则是服务器主动向客户端进行消息推送,客户端根据路由信息进行消息区分,转发到后。通常游戏服务器都会发送大量的这类广播。
``` pomelo.on(route, function(data){}); ```
以上是javascript的api, 其它客户端的API基本与这个类型。由于API与ajax极其类似,所有web应用的开发者对此都不陌生。
session介绍
与web服务器类似,session是游戏服务器存放用户会话的抽象。但与web不同,游戏服务器的session是基于长连接的, 一旦建立就一直保持。这反而比web中的session更直接,也更简单。 由于长连接的session不会web应用一样由于连接断开重连带来session复制之类的问题,简单地将session保存在前端服务器的内存中是明智的选择。
在pomelo中session也是key/value对象,其主要作用是维护当前用户信息,例如:用户的id,所连接的前端服务器id等。session由前端服务器维护,前端服务器在分发请求给后端服务器时,会复制session并连同请求一起发送。任何直接在session上的修改,只对本服务器进程生效,并不会影响到用户的全局状态信息。如需修改全局session里的状态信息,需要调用前端服务器提供的RPC服务。
channel与广播
广播在游戏中是极其重要的,几乎大部分的消息都需要通过广播推送到客户端,再由客户端播放接收的消息。而channel则是服务器端向客户端进行消息广播的通道。 可以把channel看成一个用户id的容器.把用户id加入到channel中成为当中的一个成员,之后向channel推送消息,则该channel中所有的成员都会收到消息。channel只适用于服务器进程本地,即在服务器进程A创建的channel和在服务器进程B创建的channel是两个不同的channel,相互不影响。
服务器之间RPC通讯
从之前的文章可以了解到,在pomelo中,游戏服务器其实是一个多进程相互协作的环境。各个进程之间通信,主要是通过底层统一的RPC框架来实现,服务器间的RPC调用也实现了零配置。具体RPC调用的代码如下:
```javascript app.rpc.chat.chatRemote.add(session, uid, app.get ('serverId'), function(data){}); ```
其中app是pomelo的应用对象,app.rpc表明了是前后台服务器的Remote rpc调用,后面的参数分别代表服务器的名称、对应的文件名称及方法名。为了实现这个rpc调用,则只需要在对应的chat/remote/中新建文件chatRemote.js,并实现add方法。
聊天室流程概述
下图列出了聊天室进行聊天的完整流程:
通过以上流程, 我们可以看到pomelo的基本请求流程和用法。本文不是聊天室的tutorial,因此下面列出的代码不是完整的,而是用极简的代码来说明pomelo的使用流程和api。
进入聊天室
客户端向前端服务器发起登录请求:
```javascript
pomelo.request('connector.entryHandler.enter',
{user:userInfo}, function(){}); ```
用户进入聊天室后,服务器端首先需要完成用户的session注册同时绑定用户离开事件:
```javascript session.bind(uid); session.on('closed', onUserLeave.bind(null, app)); ```
另外,服务器端需要通过调用rpc方法将用户加入到相应的channel中;同时在rpc方法中,服务器端需要将该用户的上线消息广播给其他用户,最后服务器端向客户端返回当前channel中的用户列表信息。
```javascript app.rpc.chat.chatRemote.add(session, uid,
serverId,function(){}); ```
客户端向服务端发起聊天请求,请求消息包括聊天内容,发送者和发送目标信息。消息的接收者可以聊天室里所有的用户,也可以是某一特定用户。
服务器端根据客户端的发送的请求,进行不同形式的消息广播。如果发送目标是所有用户,服务器端首先会选择channel中的所有用户,然后向channel发送消息,最后前端服务器就会将消息分别发送给channel中取到的用户;如果发送目标只是某一特定用户,发送过程和之前完全一样,只是服务器端首先从channel中选择的只是一个用户,而不是所有用户。
```javascript
if(msg.target == '*')
channel.pushMessage(param);
channelService.pushMessageByUids(param,
[{uid:uid, sid:sid}]); ```
接收聊天消息
客户端接收广播消息,并将消息并显示即可。
```javascript
pomelo.on('onChat', function() {
addMessage(data.from, data.target, data.msg);
$(&#chatHistory&).show();
退出聊天室
用户在退出聊天室时,必须完成一些清理工作。在session断开连接时,通过rpc调用将用户从channel中移除。在用户退出前,还需要将自己下线的消息广播给所有其他用户。
```javascript
app.rpc.chat.chatRemote.kick(session, uid, serverId,
channelName, null); ```
聊天服务器的可伸缩性与扩展讨论
上一讲已经谈到pomelo在提供了一个高可伸缩性的运行架构,对于聊天服务器同样如此。如果想从单频道聊天室扩展到多频道聊天室,增加的代码几乎为零。大部分工作只是在进行服务器和路由的配置。对于服务器配置只需要修改json配置文件即可,而对于路由配置只需要增加一个路由算法即可。在pomelo中,开发者可以自己配置客户端到服务器的路由规则,这样会使得游戏开发更加灵活。
我们来看一下配置json文件对服务器运行架构的影响:
最简服务器与运行架构:
扩展后的服务器与运行架构:
另外,在0.3版本的pomelo中增加了动态增删服务器的功能,开发者可以在不停服务的情况下根据当前应用运行的负载情况添加新的服务器或者停止闲置的服务器,这样可以让服务器资源得到更充分的利用。
本文通过聊天服务器的搭建过程,分析了pomelo开发应用的基本流程,基本架构与相关概念。有了这些知识我们可以轻松地使用pomelo搭建高实时应用了。 在后文中我们将分析更复杂的游戏案例,并且会对架构中的一些实现深入剖析。
原帖地址:q.com/cn/articles/game-server-development-2
感谢关注 Ithao123Node.js频道,是专门为互联网人打造的学习交流平台,全面满足互联网人工作与学习需求,更多互联网资讯尽在 IThao123!
Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework)。它可以让你从面条一样杂乱的代码中解脱出来;它可以帮你构建一个完美的网络APP,而且每行代码都可以简洁、富于表达力。
Hadoop是一个由Apache基金会所开发的分布式系统基础架构。
用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。
Hadoop实现了一个分布式文件系统(Hadoop Distributed File System),简称HDFS。HDFS有高容错性的特点,并且设计用来部署在低廉的(low-cost)硬件上;而且它提供高吞吐量(high throughput)来访问应用程序的数据,适合那些有着超大数据集(large data set)的应用程序。HDFS放宽了(relax)POSIX的要求,可以以流的形式访问(streaming access)文件系统中的数据。
Hadoop的框架最核心的设计就是:HDFS和MapReduce。HDFS为海量的数据提供了存储,则MapReduce为海量的数据提供了计算。
产品设计是互联网产品经理的核心能力,一个好的产品经理一定在产品设计方面有扎实的功底,本专题将从互联网产品设计的几个方面谈谈产品设计
随着国内互联网的发展,产品经理岗位需求大幅增加,在国内,从事产品工作的大部分岗位为产品经理,其实现实中,很多从事产品工作的岗位是不能称为产品经理,主要原因是对产品经理的职责不明确,那产品经理的职责有哪些,本专题将详细介绍产品经理的主要职责
IThao123周刊

我要回帖

更多关于 libpomelo 客户端 的文章

 

随机推荐