使用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的功能,客戶端能夠看到好友在線狀態,在線列表,添加好友,刪除好友,新建羣組等,消息的發送除了支持基本的文字外,還能支持表情、圖片和文件。

有興趣的同窗能夠繼續深刻研究。

相關文章
相關標籤/搜索