NodeJS即時聊天

         最近在着手學習NodeJS相關技術,想爲即將開始的職業生涯充點電。那麼,問題來了?如何快速的學習一門新的語言,新的技術呢?記得在面試過程中我也經歷過這樣的面試題,當時面試一家遊戲公司4399,作爲一個只會java的程序猿去面試遊戲公司,難度可想而知。但是語言永遠只是工具,只要你有能力就有無限的可能。4399主要方向有Erlang/C++,爲了掩飾自己不會C++的短板,果斷忽悠面試官,對於Erlang,在校大學生都很陌生,我學習能力強,語言只是工具,重要的是編程思想和算法,跟同齡人比,我一定能較快的掌握Erlang,爲公司創造價值。扒拉扒拉一頓胡扯,最後竟面試過了。羅嗦了半天,到底怎麼學習呢?我個人的理解,首先要對語言、技術有個宏觀的認識和感知,瞭解它解決的什麼問題,做了什麼事,可以試着動手跑一跑別人的簡單的demo。其次,動手纔是王道,在實踐中求真知,簡略瞭解常用的一些API之後,開始動手寫自己的demo和用例,並逐漸瞭解熟悉其API,達到基本能用的程度。最後,開始全面學習其API,讀相關文檔或者源碼,瞭解他的性能,優劣,做到知其然知其所以然。

        迴歸主題吧,從零開始學NodeJS,先熟悉了一下NodeJS的基本API,做了最簡單的helloworld,在這個過程中推薦阿里員工寫的七天學會NodeJS,相當經典,非常適合入門。本着動手實踐的心態,我選擇了用NodeJS實現一個比較經典的在線聊天室。網絡聊天室在web1.0的時代就出現了,但當時技術支持比較有限,大都是通過瀏覽器插件BHO,JavaApplet,Flash實現的。如今HTML5技術風起雲涌,通過websocket實現的網絡聊天室變得非常簡單。

一、websocket協議

          作爲下一代的 Web 標準,HTML5 擁有許多引人注目的新特性,如 Canvas、本地存儲、多媒體編程接口、WebSocket 等等。這其中有“Web 的 TCP ”之稱的 WebSocket 格外吸引開發人員的注意。WebSocket 的出現使得瀏覽器提供對 Socket 的支持成爲可能,從而在瀏覽器和服務器之間提供了一個基於 TCP 連接的雙向通道。Web 開發人員可以非常方便地使用 WebSocket 構建實時 web 應用,開發人員的手中從此又多了一柄神兵利器。

實時 Web 應用的窘境
        Web 應用的信息交互過程通常是客戶端通過瀏覽器發出一個請求,服務器端接收和審覈完請求後進行處理並返回結果給客戶端,然後客戶端瀏覽器將信息呈現出來,這種機制對於信息變化不是特別頻繁的應用尚能相安無事,但是對於那些實時要求比較高的應用來說,比如說在線遊戲、在線證券、設備監控、新聞在線播報、RSS 訂閱推送等等,當客戶端瀏覽器準備呈現這些信息的時候,這些信息在服務器端可能已經過時了。所以保持客戶端和服務器端的信息同步是實時 Web 應用的關鍵要素,對 Web 開發人員來說也是一個難題。在 WebSocket 規範出來之前,開發人員想實現這些實時的 Web 應用,不得不採用一些折衷的方案,其中最常用的就是輪詢 (Polling) 和 Comet 技術,而 Comet 技術實際上是輪詢技術的改進,又可細分爲兩種實現方式,一種是長輪詢機制,一種稱爲流技術。
        綜合這幾種方案,您會發現這些目前我們所使用的所謂的實時技術並不是真正的實時技術,它們只是在用 Ajax 方式來模擬實時的效果,在每次客戶端和服務器端交互的時候都是一次 HTTP 的請求和應答的過程,而每一次的 HTTP 請求和應答都帶有完整的 HTTP 頭信息,這就增加了每次傳輸的數據量,而且這些方案中客戶端和服務器端的編程實現都比較複雜,在實際的應用中,爲了模擬比較真實的實時效果,開發人員往往需要構造兩個 HTTP 連接來模擬客戶端和服務器之間的雙向通訊,一個連接用來處理客戶端到服務器端的數據傳輸,一個連接用來處理服務器端到客戶端的數據傳輸,這不可避免地增加了編程實現的複雜度,也增加了服務器端的負載,制約了應用系統的擴展性。

WebSocket 的拯救
         HTML5 WebSocket 設計出來的目的就是要取代輪詢和 Comet 技術,使客戶端瀏覽器具備像 C/S 架構下桌面系統的實時通訊能力。 瀏覽器通過 JavaScript 向服務器發出建立 WebSocket 連接的請求,連接建立以後,客戶端和服務器端就可以通過 TCP 連接直接交換數據。因爲 WebSocket 連接本質上就是一個 TCP 連接,所以在數據傳輸的穩定性和數據傳輸量的大小方面,和輪詢以及 Comet 技術比較,具有很大的性能優勢。

WebSocket 規範
        WebSocket 協議本質上是一個基於 TCP 的協議。爲了建立一個 WebSocket 連接,客戶端瀏覽器首先要向服務器發起一個 HTTP 請求,這個請求和通常的 HTTP 請求不同,包含了一些附加頭信息,其中附加頭信息”Upgrade: WebSocket”表明這是一個申請協議升級的 HTTP 請求,服務器端解析這些附加的頭信息然後產生應答信息返回給客戶端,客戶端和服務器端的 WebSocket 連接就建立起來了,雙方就可以通過這個連接通道自由的傳遞信息,並且這個連接會持續存在直到客戶端或者服務器端的某一方主動的關閉連接。

         下面我們來詳細介紹一下 WebSocket 規範,由於這個規範目前還是處於草案階段,版本的變化比較快,我們選擇 draft-hixie-thewebsocketprotocol-76版本來描述 WebSocket 協議。因爲這個版本目前在一些主流的瀏覽器上比如 Chrome,、FireFox、Opera 上都得到比較好的支持,您如果參照的是新一些的版本話,內容可能會略有差別。一個典型的 WebSocket 發起請求和得到響應的例子看起來如下:

WebSocket 握手協議
客戶端到服務端: 
GET /demo HTTP/1.1 
Host: example.com 
Connection: Upgrade 
Sec-WebSocket-Key2: 12998 5 Y3 1 .P00 
Upgrade: WebSocket 
Sec-WebSocket-Key1: 4@1 46546xW%0l 1 5 
Origin: http://example.com 
[8-byte security key] 

服務端到客戶端:
HTTP/1.1 101 WebSocket Protocol Handshake 
Upgrade: WebSocket 
Connection: Upgrade 
WebSocket-Origin: http://example.com 
WebSocket-Location: ws://example.com/demo 
[16-byte hash response]

          這些請求和通常的 HTTP 請求很相似,但是其中有些內容是和 WebSocket 協議密切相關的。我們需要簡單介紹一下這些請求和應答信息,”Upgrade:WebSocket”表示這是一個特殊的 HTTP 請求,請求的目的就是要將客戶端和服務器端的通訊協議從 HTTP 協議升級到 WebSocket 協議。從客戶端到服務器端請求的信息裏包含有”Sec-WebSocket-Key1”、“Sec-WebSocket-Key2”和”[8-byte securitykey]”這樣的頭信息。這是客戶端瀏覽器需要向服務器端提供的握手信息,服務器端解析這些頭信息,並在握手的過程中依據這些信息生成一個 16 位的安全密鑰並返回給客戶端,以表明服務器端獲取了客戶端的請求,同意創建 WebSocket 連接。一旦連接建立,客戶端和服務器端就可以通過這個通道雙向傳輸數據了。
          在實際的開發過程中,爲了使用 WebSocket 接口構建 Web 應用,我們首先需要構建一個實現了 WebSocket 規範的服務器,服務器端的實現不受平臺和開發語言的限制,只需要遵從 WebSocket 規範即可,目前已經出現了一些比較成熟的 WebSocket 服務器端實現,比如:

Kaazing WebSocket Gateway — 一個 Java 實現的 WebSocket Server
mod_pywebsocket — 一個 Python 實現的 WebSocket Server
  • Netty —一個 Java 實現的網絡框架其中包括了對 WebSocket 的支持
  • node.js —一個 Server 端的 JavaScript 框架提供了對 WebSocket 的支持

如果以上的 WebSocket 服務端實現還不能滿足您的業務需求的話,開發人員完全可以根據 WebSocket 規範自己實現一個服務器。

二、socket.io介紹

         socket.io一個是基於Nodejs架構體系的,支持websocket的協議用於時時通信的一個軟件包。socket.io 給跨瀏覽器構建實時應用提供了完整的封裝,socket.io完全由javascript實現。
         基於Nodejs實現webscoket其他的框架,請參考文章:Nodejs實現websocket的4種方式

三、服務器和客戶端通信設計

chat

上圖中client1 和 server 描述通信過程,client2描述對其他的客戶端,通過廣播進行消息通信。

1、client1向server發起連接請求
2、server接受client的連接
3、client1輸入登陸用戶名
4、server返回歡迎語
5、server通過廣播告訴其他在線的用戶,client1已登陸
6、client1發送聊天信息
7、server返回聊天信息(可省略)
8、server通過廣播告訴其他在線的用戶,client1的聊天消息
9、client1關閉連接,退出登陸
10、server通過廣播告訴其他在線的用戶,client1已退出

四、服務器端實現

/**
 * Created with JetBrains WebStorm.
 * User: xuwenmin
 * Date: 14-4-19
 * Time: 下午1:20
 * To change this template use File | Settings | File Templates.
 */

var express = require('express'),
    io = require('socket.io');

var app = express();

app.use(express.static(__dirname));

var server = app.listen(8888, 'localhost');


var ws = io.listen(server);


// 檢查暱稱是否重複
var checkNickname = function(name){
    for(var k in ws.sockets.sockets){
        if(ws.sockets.sockets.hasOwnProperty(k)){
            if(ws.sockets.sockets[k] && ws.sockets.sockets[k].nickname == name){
                return true;
            }
        }
    }
    return false;
}
// 獲取所有的暱稱數組
var getAllNickname = function(){
    var result = [];
    for(var k in ws.sockets.sockets){
        if(ws.sockets.sockets.hasOwnProperty(k)){
            result.push({
                name: ws.sockets.sockets[k].nickname
            });
        }
    }
    return result;
}
ws.on('connection', function(client){
    console.log('\033[96msomeone is connect\033[39m \n');
    client.on('join', function(msg){
        // 檢查是否有重複
        if(checkNickname(msg)){
            client.emit('nickname', '暱稱有重複!');
        }else{
            client.nickname = msg;
            ws.sockets.emit('announcement', '系統', msg + ' 加入了聊天室!', {type:'join', name:getAllNickname()});
        }
    });
    // 監聽發送消息
    client.on('send.message', function(msg){
        client.broadcast.emit('send.message',client.nickname,  msg);
    });

    client.on('disconnect', function(){
        if(client.nickname){
            client.broadcast.emit('send.message','系統',  client.nickname + '離開聊天室!', {type:'disconnect', name:client.nickname});
        }
    })

})

五、客戶端實現

<!DOCTYPE html>
<html>
<head>
    <title>socket.io 聊天室例子</title>
    <meta charset="utf-8">

    <link rel="stylesheet" href="css/reset.css"/>
    <link rel="stylesheet" href="css/bootstrap.css"/>
    <link rel="stylesheet" href="css/app.css"/>
</head>
<body>
    <div class="wrapper">
         <div class="content" id="chat">
             <ul id="chat_conatiner">
             </ul>

         </div>
         <div class="action">
             <textarea ></textarea>
             <button class="btn btn-success" id="clear">清屏</button>
             <button class="btn btn-success" id="send">發送</button>
         </div>
    </div>
    <script type="text/javascript" src="js/socket.io.js"></script>
    <script type="text/javascript">

          var ws = io.connect('http://localhost:8888');
          var sendMsg = function(msg){
              ws.emit('send.message', msg);
          }
          var addMessage = function(from, msg){
              var li = document.createElement('li');
              li.innerHTML = '<span>' + from + '</span>' + ' : ' + msg;
              document.querySelector('#chat_conatiner').appendChild(li);

              // 設置內容區的滾動條到底部
              document.querySelector('#chat').scrollTop = document.querySelector('#chat').scrollHeight;

              // 並設置焦點
              document.querySelector('textarea').focus();

          }

          var send = function(){
              var ele_msg = document.querySelector('textarea');
              var msg = ele_msg.value.replace('\r\n', '').trim();
              console.log(msg);
              if(!msg) return;
              sendMsg(msg);
              // 添加消息到自己的內容區
              addMessage('你', msg);
              ele_msg.value = '';
          }

          ws.on('connect', function(){
              var nickname = window.prompt('輸入你的暱稱!');
              while(!nickname){
                  nickname = window.prompt('暱稱不能爲空,請重新輸入!')
              }
              ws.emit('join', nickname);
          });

          // 暱稱有重複
          ws.on('nickname', function(){
              var nickname = window.prompt('暱稱有重複,請重新輸入!');
              while(!nickname){
                  nickname = window.prompt('暱稱不能爲空,請重新輸入!')
              }
              ws.emit('join', nickname);
          });

          ws.on('send.message', function(from, msg){
              addMessage(from, msg);
          });

          ws.on('announcement', function(from, msg){
              addMessage(from, msg);
          });

          document.querySelector('textarea').addEventListener('keypress', function(event){
              if(event.which == 13){
                  send();
              }
          });
          document.querySelector('textarea').addEventListener('keydown', function(event){
              if(event.which == 13){
                  send();
              }
          });
          document.querySelector('#send').addEventListener('click', function(){
              send();
          });

          document.querySelector('#clear').addEventListener('click', function(){
              document.querySelector('#chat_conatiner').innerHTML = '';
          });
    </script>
</body>
</html>


總結:通過這個簡單的demo我對websocket有了一個全新的認識,文中僅提供基於socket.io實現版本的在線聊天室,其實深入學習了websocket協議後,完全可以越過socket.io實現自己的基於websocket的在線聊天室,這是件非常有意義的事。

參考文章:
http://blog.fens.me/nodejs-socketio-chat/
http://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章