使用Node.js+Socket.IO搭建WebSocket实时应用

使用Node.js+Socket.IO搭建WebSocket实时应用

Web领域的实时推送技术,也被称做Realtime技术。这种技术要达到的目的是让用户不须要刷新浏览器就能够得到实时更新。它有着普遍的应用场景,好比在线聊天室、在线客服系统、评论系统、WebIM等。javascript

WebSocket简介

谈到Web实时推送,就不得不说WebSocket。在WebSocket出现以前,不少网站为了实现实时推送技术,一般采用的方案是轮询(Polling)和Comet技术,Comet又可细分为两种实现方式,一种是长轮询机制,一种称为流技术,这两种方式其实是对轮询技术的改进,这些方案带来很明显的缺点,须要由浏览器对服务器发出HTTP request,大量消耗服务器带宽和资源。面对这种情况,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并实现真正意义上的实时推送。css

WebSocket协议本质上是一个基于TCP的协议,它由通讯协议和编程API组成,WebSocket可以在浏览器和服务器之间创建双向链接,以基于事件的方式,赋予浏览器实时通讯能力。既然是双向通讯,就意味着服务器端和客户端能够同时发送并响应请求,而再也不像HTTP的请求和响应。html

为了创建一个WebSocket链接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和一般的HTTP请求不一样,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”代表这是一个申请协议升级的HTTP请求,服务器端解析这些附加的头信息而后产生应答信息返回给客户端,客户端和服务器端的WebSocket链接就创建起来了,双方就能够经过这个链接通道自由的传递信息,而且这个链接会持续存在直到客户端或者服务器端的某一方主动的关闭链接。前端

一个典型WebSocket客户端请求头:
java

前面讲到WebSocket是HTML5中新增的一种通讯协议,这意味着一部分老版本浏览器(主要是IE10如下版本)并不具有这个功能, 经过百度统计的公开数据显示,IE8目前仍以33%的市场份额占据榜首,好在chrome浏览器市场份额逐年上升,如今以超过26%的市场份额位居第二,同时微软前不久宣布中止对IE6的技术支持并提示用户更新到新版本浏览器,这个曾经让无数前端工程师为之头疼的浏览器有望退出历史舞台,再加上几乎全部的智能手机浏览器都支持HTML5,因此使得WebSocket的实战意义大增,可是不管如何,咱们实际的项目中,仍然要考虑低版本浏览器的兼容方案:在支持WebSocket的浏览器中采用新技术,而在不支持WebSocket的浏览器里启用Comet来接收发送消息。node

WebSocket实战

本文将以多人在线聊天应用做为实例场景,咱们先来肯定这个聊天应用的基本需求。nginx

需求分析

一、兼容不支持WebSocket的低版本浏览器。
二、容许客户端有相同的用户名。
三、进入聊天室后能够看到当前在线的用户和在线人数。
四、用户上线或退出,全部在线的客户端应该实时更新。
五、用户发送消息,全部客户端实时收取。git

在实际的开发过程当中,为了使用WebSocket接口构建Web应用,咱们首先须要构建一个实现了 WebSocket规范的服务端,服务端的实现不受平台和开发语言的限制,只须要听从WebSocket规范便可,目前已经出现了一些比较成熟的WebSocket服务端实现,好比本文使用的Node.js+Socket.IO。为何选用这个方案呢?先来简单介绍下他们两。程序员

Node.js

Node.js采用C++语言编写而成,它不是Javascript应用,而是一个Javascript的运行环境,据Node.js创始人Ryan Dahl回忆,他最初但愿采用Ruby来写Node.js,可是后来发现Ruby虚拟机的性能不能知足他的要求,后来他尝试采用V8引擎,因此选择了C++语言。github

Node.js支持的系统包括*nux、Windows,这意味着程序员能够编写系统级或者服务器端的Javascript代码,交给Node.js来解释执行。Node.js的Web开发框架Express,能够帮助程序员快速创建web站点,从2009年诞生至今,Node.js的成长的速度有目共睹,其发展前景得到了技术社区的充分确定。

Socket.IO

Socket.IO是一个开源的WebSocket库,它经过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通信,它能够工做在任何平台、浏览器或移动设备。

Socket.IO支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通信方式,从而让开发者能够聚焦到功能的实现而不是平台的兼容性,同时Socket.IO具备不错的稳定性和性能。

编码实现

先上演示效果图:

能够点击这里查看在线演示。整个开发过程很是简单,下面简单记录了开发步骤:

安装Node.js

根据本身的操做系统,去Node.js官网下载安装便可。若是成功安装。在命令行输入node -vnpm -v应该能看到相应的版本号。

node -v  
v0.10.26  
npm -v  
1.4.6  

搭建WebSocket服务端

这个环节咱们尽量的考虑真实生产环境,把WebSocket后端服务搭建成一个线上能够用域名访问的服务,若是你是在本地开发环境,能够换成本地ip地址,或者使用一个虚拟域名指向本地ip。

先进入到你的工做目录,好比 /workspace/wwwroot/plhwin/realtime.plhwin.com,新建一个名为 package.json的文件,内容以下:

{
  "name": "realtime-server",
  "version": "0.0.1",
  "description": "my first realtime server",
  "dependencies": {}
}

接下来使用npm命令安装expresssocket.io

npm install --save express
npm install --save socket.io
安装成功后,应该能够看到工做目录下生成了一个名为`node_modules`的文件夹,里面分别是`express`和`socket.io`,接下来能够开始编写服务端的代码了,新建一个文件:`index.js`
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
    res.send('<h1>Welcome Realtime Server</h1>');
});

http.listen(3000, function(){
    console.log('listening on *:3000');
});

命令行运行node index.js,若是一切顺利,你应该会看到返回的listening on *:3000字样,这说明服务已经成功搭建了。此时浏览器中打开http://localhost:3000应该能够看到正常的欢迎页面。

若是你想要让服务运行在线上服务器,而且能够经过域名访问的话,可使用Nginx作代理,再nginx.conf中添加以下配置,而后将域名(好比:realtime.plhwin.com)解析到服务器IP便可。

  server
  {
    listen       80;
    server_name  realtime.plhwin.com;
    location / {
      proxy_pass http://127.0.0.1:3000;
    }
  }

完成以上步骤,http://realtime.plhwin.com:3000的后端服务就正常搭建了。

服务端代码实现

前面讲到的index.js运行在服务端,以前的代码只是一个简单的WebServer欢迎内容,让咱们把WebSocket服务端完整的实现代码加入进去,整个服务端就能够处理客户端的请求了。完整的index.js代码以下:

var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/', function(req, res){
    res.send('<h1>Welcome Realtime Server</h1>');
});

//在线用户
var onlineUsers = {};
//当前在线人数
var onlineCount = 0;

io.on('connection', function(socket){
    console.log('a user connected');

    //监听新用户加入
    socket.on('login', function(obj){
        //将新加入用户的惟一标识看成socket的名称,后面退出的时候会用到
        socket.name = obj.userid;

        //检查在线列表,若是不在里面就加入
        if(!onlineUsers.hasOwnProperty(obj.userid)) {
            onlineUsers[obj.userid] = obj.username;
            //在线人数+1
            onlineCount++;
        }

        //向全部客户端广播用户加入
        io.emit('login', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj});
        console.log(obj.username+'加入了聊天室');
    });

    //监听用户退出
    socket.on('disconnect', function(){
        //将退出的用户从在线列表中删除
        if(onlineUsers.hasOwnProperty(socket.name)) {
            //退出用户的信息
            var obj = {userid:socket.name, username:onlineUsers[socket.name]};

            //删除
            delete onlineUsers[socket.name];
            //在线人数-1
            onlineCount--;

            //向全部客户端广播用户退出
            io.emit('logout', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj});
            console.log(obj.username+'退出了聊天室');
        }
    });

    //监听用户发布聊天内容
    socket.on('message', function(obj){
        //向全部客户端广播发布的消息
        io.emit('message', obj);
        console.log(obj.username+'说:'+obj.content);
    });

});

http.listen(3000, function(){
    console.log('listening on *:3000');
});

客户端代码实现

进入客户端工做目录/workspace/wwwroot/plhwin/demo.plhwin.com/chat,新建一个index.html:

<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no"/>
<meta name="format-detection" content="email=no"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" name="viewport">
<title>多人聊天室</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<!--[if lt IE 8]><script src="./json3.min.js"></script><![endif]-->
<script src="http://realtime.plhwin.com:3000/socket.io/socket.io.js"></script>
</head>
<body>

<div id="loginbox">
<div style="width:260px;margin:200px auto;">
请先输入你在聊天室的昵称<br/><br/>
<input type="text" style="width:180px;" placeholder="请输入用户名" id="username" name="username" />
<input type="button" style="width:50px;" value="提交" onclick="CHAT.usernameSubmit();"/>
</div>
</div>

<div id="chatbox" style="display:none;">
<div style="background:#3d3d3d;height: 28px; width: 100%;font-size:12px;">
<div style="line-height: 28px;color:#fff;">
<span style="text-align:left;margin-left:10px;">Websocket多人聊天室</span>
<span style="float:right; margin-right:10px;"><span id="showusername"></span>| <a href="javascript:;" onclick="CHAT.logout()" style="color:#fff;">退出</span>
</div>
</div>
<div id="doc">
<div id="chat">
<div id="message" class="message">
<div id="onlinecount" style="width:background:#EFEFF4; font-size:12px; margin-top:10px; margin-left:10px; color:#666;">
</div>
</div>
<div class="input-box">
<div class="input">
<input type="text" maxlength="140" placeholder="请输入聊天内容,按Ctrl提交" id="content" name="content">
</div>
<div class="action">
<button type="button" id="mjr_send" onclick="CHAT.submit();">
提交
</button>
</div>
</div>
</div>
</div>
</div>

<script type="text/javascript" src="./client.js"></script>
</body>
</html>

上面的html内容自己没有什么好说的,咱们主要看看里面的4个文件请求:

一、realtime.plhwin.com:3000/socket.io/socket.io.js
二、style.css
三、json3.min.js
四、client.js

第1个JS是Socket.IO提供的客户端JS文件,在前面安装服务端的步骤中,当npm安装完socket.io并搭建起WebServer后,这个JS文件就能够正常访问了。

第2个style.css文件没什么好说的,就是样式文件而已。

第3个JS只在IE8如下版本的IE浏览器中加载,目的是让这些低版本的IE浏览器也能处理json,这是一个开源的JS,详见:http://bestiejs.github.io/json3/

第4个client.js是完整的客户端的业务逻辑实现代码,它的内容以下:

(function () {
    var d = document,
    w = window,
    p = parseInt,
    dd = d.documentElement,
    db = d.body,
    dc = d.compatMode == 'CSS1Compat',
    dx = dc ? dd: db,
    ec = encodeURIComponent;


    w.CHAT = {
        msgObj:d.getElementById("message"),
        screenheight:w.innerHeight ? w.innerHeight : dx.clientHeight,
        username:null,
        userid:null,
        socket:null,
        //让浏览器滚动条保持在最低部
        scrollToBottom:function(){
            w.scrollTo(0, this.msgObj.clientHeight);
        },
        //退出,本例只是一个简单的刷新
        logout:function(){
            //this.socket.disconnect();
            location.reload();
        },
        //提交聊天消息内容
        submit:function(){
            var content = d.getElementById("content").value;
            if(content != ''){
                var obj = {
                    userid: this.userid,
                    username: this.username,
                    content: content
                };
                this.socket.emit('message', obj);
                d.getElementById("content").value = '';
            }
            return false;
        },
        genUid:function(){
            return new Date().getTime()+""+Math.floor(Math.random()*899+100);
        },
        //更新系统消息,本例中在用户加入、退出的时候调用
        updateSysMsg:function(o, action){
            //当前在线用户列表
            var onlineUsers = o.onlineUsers;
            //当前在线人数
            var onlineCount = o.onlineCount;
            //新加入用户的信息
            var user = o.user;

            //更新在线人数
            var userhtml = '';
            var separator = '';
            for(key in onlineUsers) {
                if(onlineUsers.hasOwnProperty(key)){
                    userhtml += separator+onlineUsers[key];
                    separator = '、';
                }
            }
            d.getElementById("onlinecount").innerHTML = '当前共有 '+onlineCount+' 人在线,在线列表:'+userhtml;

            //添加系统消息
            var html = '';
            html += '
'; html += user.username; html += (action == 'login') ? ' 加入了聊天室' : ' 退出了聊天室'; html += '
';
            var section = d.createElement('section');
            section.className = 'system J-mjrlinkWrap J-cutMsg';
            section.innerHTML = html;
            this.msgObj.appendChild(section);    
            this.scrollToBottom();
        },
        //第一个界面用户提交用户名
        usernameSubmit:function(){
            var username = d.getElementById("username").value;
            if(username != ""){
                d.getElementById("username").value = '';
                d.getElementById("loginbox").style.display = 'none';
                d.getElementById("chatbox").style.display = 'block';
                this.init(username);
            }
            return false;
        },
        init:function(username){
            //客户端根据时间和随机数生成uid,这样使得聊天室用户名称能够重复。实际项目中,若是是须要用户登陆,那么直接采用用户的uid来作标识就能够
            this.userid = this.genUid();
            this.username = username;

            d.getElementById("showusername").innerHTML = this.username;
            this.msgObj.style.minHeight = (this.screenheight - db.clientHeight + this.msgObj.clientHeight) + "px";
            this.scrollToBottom();

            //链接websocket后端服务器
            this.socket = io.connect('ws://realtime.plhwin.com:3000');

            //告诉服务器端有用户登陆
            this.socket.emit('login', {userid:this.userid, username:this.username});

            //监听新用户登陆
            this.socket.on('login', function(o){
                CHAT.updateSysMsg(o, 'login');    
            });

            //监听用户退出
            this.socket.on('logout', function(o){
                CHAT.updateSysMsg(o, 'logout');
            });

            //监听消息发送
            this.socket.on('message', function(obj){
                var isme = (obj.userid == CHAT.userid) ? true : false;
                var contentDiv = '
'+obj.content+'
';
                var usernameDiv = ''+obj.username+'';

                var section = d.createElement('section');
                if(isme){
                    section.className = 'user';
                    section.innerHTML = contentDiv + usernameDiv;
                } else {
                    section.className = 'service';
                    section.innerHTML = usernameDiv + contentDiv;
                }
                CHAT.msgObj.appendChild(section);
                CHAT.scrollToBottom();    
            });

        }
    };
    //经过“回车”提交用户名
    d.getElementById("username").onkeydown = function(e) {
        e = e || event;
        if (e.keyCode === 13) {
            CHAT.usernameSubmit();
        }
    };
    //经过“回车”提交信息
    d.getElementById("content").onkeydown = function(e) {
        e = e || event;
        if (e.keyCode === 13) {
            CHAT.submit();
        }
    };
})();

至此全部的编码开发工做所有完成了,在浏览器中打开http://demo.plhwin.com/chat/就能够看到效果了,后续我会把演示代码提交到Github上。

本例只是一个简单的Demo,留下2个有关项目扩展的思考:

一、假设是一个在线客服系统,里面有许多的公司使用你的服务,每一个公司本身的用户能够经过一个专属URL地址进入该公司的聊天室,聊天是一对一的,每一个公司能够新建多个客服人员,每一个客服人员能够同时和客户端的多个用户聊天。

二、又假设是一个在线WebIM系统,实现相似微信,qq的功能,客户端能够看到好友在线状态,在线列表,添加好友,删除好友,新建群组等,消息的发送除了支持基本的文字外,还能支持表情、图片和文件。

有兴趣的同窗能够继续深刻研究。

相关文章
相关标签/搜索