利用扩展服务实现基于websocket的聊天室

简介

BAE提供了丰富的扩展服务,利用这些扩展服务,可以开发出各种各样的应用。今天我们来介绍一下,如何利用扩展服务开发一个聊天室。

该聊天室是一个HTML5的网页,效果图如下图所示。用户打开聊天室的页面后,可以输入姓名和消息,点击发送之后,消息就会通过服务器,转发到所有已经连接到服务器的浏览器,这样消息就能在整个“聊天室”中显示出来。同样的,别的用户发送的消息,当前用户也能即时的显示出来。


架构

首先,要实现浏览器将消息发送给服务器,同时也能实时的接收到来自服务器转发的消息,则需要使用到websocket技术。页面通过websocket与服务器建立长连接,发送的消息通过该连接发送到服务,同时页面监听来自该连接的所有消息,收到消息后显示在页面上。默认情况下,BAE的应用不支持websocket协议。要使用websocket,需要使用BAE的port服务。该服务可以将执行单元的端口映射为一个ip+端口,客户端可以通过这个ip+端口与执行单元建立tcp的长连接或者短连接,tcp上跑什么上层协议都是可以的。

其次,为了提高服务的水平扩展性,需要支持多个执行单元。然而,使用多个执行单元就会涉及到执行单元之间的状态共享。一个页面只会与一个执行单元建立长连接,页面发送给该执行单元的消息,需要广播通知给所有的执行单元,再由这些执行单元通知给所有已经连接上的页面。BAE的redis服务可以完成这个任务:所有的执行单元都订阅(SUBSCRIBE)redis上的同一个频道(channel),当其中一个执行单元接收到来自于页面的消息后,将消息发布(PUBLISH)到这个频道去,所有的执行单元都将得到通知,这时各个执行单元只要把收到的消息再发送给所有连接上的页面即可。

最后,由于有多个执行单元,每个执行单元都会打日志,日志分散在各个执行单元将不利于查看。因此,我们利用log服务对执行单元的日志进行收集和集中存储,这样就可以很方便的查看到同一个应用的所有执行单元的日志。

整个应用的架构图如下图所示:

实现

该聊天室的服务器端使用node.js进行开发。下面的步骤中,将省略应用的创建和代码上传等基本操作,如果读者对这些基本操作不熟悉,可以查看BAE的文档

1. 通过port服务实现websocket

首先在控制台上创建一个port服务,选择关联的部署,端口填18081,同时请记下“IP:Port”一项。

在package.json中增加websocket的依赖:

    "dependencies": {
        "nodejs-websocket": "1.2.0",
        // ...
   }

在server.js中增加处理websocket连接的代码,并监听18081端口:

var ws = require("nodejs-websocket");
var server = ws.createServer(function (conn) {
    logger.info("[websocket] New connection");
    conn.on("text", function (str) {
        logger.info("[websocket] Received " + str);
        // 收到来自页面的消息后,发布到redis
        pubClient.publish(channel, str);
    })
    conn.on("close", function (code, reason) {
        logger.info("[websocket] Connection closed");
    });
}).listen(18081);

2. 通过redis服务通知所有执行单元

首先需要在控制台创建一个redis服务。

然后在package.json 中增加redis依赖:

    "dependencies": {
        "nodejs-websocket": "1.2.0",
        "redis": "0.12.1",
        // ...
    }

最后在server.js中增加redis的处理逻辑,其中的db配置可以从控制台获取:

var channel = ak;
var db = '{db}';
var auth = ak + "-" + sk + "-" + db;
var redis = require("redis"); 
var subClient = redis.createClient(80, "redis.duapp.com", {"no_ready_check": true}); 
var pubClient = redis.createClient(80, "redis.duapp.com", {"no_ready_check": true}); 
subClient.auth(auth); 
pubClient.auth(auth); 
subClient.on("message", function (channel, message) { 
    logger.info("[redis] Got message " + message); 
    // 当收到redis的订阅通知时,将消息发送给所有的websocket连接 
    server.connections.forEach(function (conn) { 
        conn.sendText(message); 
    }); 
}); 
subClient.subscribe(channel);

3. 通过log服务收集日志

首先在控制台上新建一个log服务。

然后下载log服务的sdk并解压到node_modules目录下,同时在package.json中增加依赖:

    "dependencies": {
        "nodejs-websocket": "1.2.0", 
        "redis": "0.12.1", 
        "log4js": "0.6.9", 
        "thrift": "0.9.1" 
    }

最后,在server.js中增加log服务的初始化代码:

var log4js = require('log4js');
log4js.loadAppender('baev3-log'); 
var options = { 
    'user': ak, 
    'passwd': sk 
}; 
log4js.addAppender(log4js.appenders['baev3-log'](options)); 
var logger = log4js.getLogger('node-log-sdk'); 
logger.setLevel('INFO');

收集到的日志可以在控制台上查看:

4. 编写HTML5的页面

由于页面不是本文的重点,因此下面只列出其中的javascript部分。其中ip和port配置就是步骤1中记下的“IP:Port”:

$(function() {
   var websocket = new WebSocket('ws://{ip}:{port}'); 
   websocket.onopen = function(evt) { 
      console.log("Connected"); 
      $('#send').removeAttr('disabled'); 
   }; 
   websocket.onclose = function(evt) { 
      console.log("Disconnected"); 
      $('#send').attr('disabled', 'disabled'); 
   }; 
   websocket.onmessage = function(evt) { 
      console.log("Message: " + evt.data); 
      var json = JSON.parse(evt.data); 
      $('#all-messages').prepend('<h4 class="message"><span>' + json.name + ' 说:</span> ' + json.message + '</h4>'); 
   }; 
   websocket.onerror = function(evt) { 
      console.error("Error: " + evt.data); 
   }; 

   $('#message-form').submit(function() { 
      websocket.send(JSON.stringify({ 
         "name": $('#name').val(), 
         "message": $('#message').val() 
      })); 
      $('#message').val(''); 
      return false; 
   }); 
});

5. 完整的server.js

其中的ak和sk配置可以从控制台获取:

var ak = '{ak}';
var sk = '{sk}'; 

var log4js = require('log4js'); 
log4js.loadAppender('baev3-log'); 
var options = { 
    'user': ak, 
    'passwd': sk 
}; 
log4js.addAppender(log4js.appenders['baev3-log'](options)); 
var logger = log4js.getLogger('node-log-sdk'); 
logger.setLevel('INFO'); 

var channel = ak; 
var db = '{db}'; 
var auth = ak + "-" + sk + "-" + db; 
var redis = require("redis"); 
var subClient = redis.createClient(80, "redis.duapp.com", {"no_ready_check": true}); 
var pubClient = redis.createClient(80, "redis.duapp.com", {"no_ready_check": true}); 
subClient.auth(auth); 
pubClient.auth(auth); 
subClient.on("message", function (channel, message) { 
    logger.info("[redis] Got message " + message); 
    server.connections.forEach(function (conn) { 
        conn.sendText(message); 
    }); 
}); 
subClient.subscribe(channel); 

var ws = require("nodejs-websocket"); 
var server = ws.createServer(function (conn) { 
    logger.info("[websocket] New connection"); 
    conn.on("text", function (str) { 
        logger.info("[websocket] Received " + str); 
        pubClient.publish(channel, str); 
    }); 
    conn.on("close", function (code, reason) { 
        logger.info("[websocket] Connection closed"); 
    }); 
}).listen(18081);

总结

本文介绍了如何利用BAE的port服务、redis服务、log服务,开发出基于websocket的网页聊天室应用。同时,该应用具有很好的水平扩展性,只要通过增加执行单元的数目,就能应对更多的用户和更大的流量。

此条目发表在 未分类 分类目录。将固定链接加入收藏夹。

发表评论