1. 程式人生 > >使用nodeJs+web Socket構建即時通訊(WebIM)

使用nodeJs+web Socket構建即時通訊(WebIM)

在文章的開頭,我要解釋一下,為什麼不直接使用web Socket實現即時通訊,因為一部分瀏覽器並不相容web Socket,導致即時通訊在這些瀏覽器上無法正常使用,所以才需要用到nodeJs裡封裝好webSocket協議的socket.io包模組來提供不相容websocket瀏覽器的解決方案並接管客戶與伺服器端互動的IO流。(一般處理不相容websocket的瀏覽器的方法就是考慮 commit 方式,或者用 Flash sockect.)

這個即時通訊的demo包括最基本的聊天室功能和私聊功能,其他附加的功能就是傳送圖片和表情及改變字型顏色,實現最基本的對話聊天功能。需要其他功能的可以懂得原理後自行新增模組進來。話不多說,直接用程式碼說話。(其中將會使用到express和socket.io兩個包模組,一定要先安裝好。PS:

express是node.js中管理路由響應請求的模組,根據請求的URL返回相應的HTML頁面。這裡我們使用一個事先寫好的靜態頁面返回給客戶端,只需使用express指定要返回的頁面的路徑即可。如果不用這個包,我們需要將HTML程式碼與後臺JavaScript程式碼寫在一起進行請求的響應,不太方便。socket.io是Node.js中使用socket的一個包。使用它可以很方便地建立伺服器到客戶端的sockets連線,傳送事件與接收特定事件。)
首先,我們來看伺服器端程式碼:

var express = require('express'),
    app = express(),
    server = require
('http').createServer(app), io = require('socket.io').listen(server); //指定靜態HTML檔案的位置 app.use('/', express.static(__dirname + '/www')); server.listen(3000);//監聽埠是否有來自客戶端的請求 var onlineUsers = {};//線上使用者列表 var socketList = {};//每個使用者所持有的與伺服器互動的socket列表 var onlineCount = 0;//線上人數 //處理socket事件 io.sockets.on('connection'
, function(socket) { //新使用者登陸 socket.on('login', function(obj) { if (onlineUsers.hasOwnProperty(obj.userid)) { socket.emit('userExisted'); } else { socket.name = obj.userid; socketList[obj.userid] = socket; //檢查線上使用者列表,如果不存在,則將該使用者加入線上使用者表 if(!onlineUsers.hasOwnProperty(obj.userid)) { onlineUsers[obj.userid] = obj.nickname; //線上人數+1 onlineCount++; } socket.emit('loginSuccess',{onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj}); io.sockets.emit('system', obj, onlineCount, 'login'); }; }); //使用者離線 socket.on('disconnect', function() { //將退出的使用者從線上列表中刪除 if(onlineUsers.hasOwnProperty(socket.name)) { //退出使用者的資訊 var obj = {userid:socket.name, nickname:onlineUsers[socket.name]}; //刪除 delete onlineUsers[socket.name]; delete socketList[socket.name]; //線上人數-1 onlineCount--; //向所有客戶端廣播使用者退出 socket.broadcast.emit('system', obj, onlineCount, 'logout'); } }); //接收新資訊 socket.on('postMsg', function(msg, color) { socket.broadcast.emit('newMsg', onlineUsers[socket.name], msg, color); }); //接收新私信(P2P) socket.on('privateMsg', function(msg, color, userid) { socketList[userid].emit('newMsg', onlineUsers[socket.name], msg, color); }); //接收新圖片 socket.on('img', function(imgData, color) { socket.broadcast.emit('newImg', onlineUsers[socket.name], imgData, color); }); //接收新私人圖片(P2P) socket.on('privateimg', function(imgData, color, userid) { socketList[userid].emit('newImg', onlineUsers[socket.name], imgData, color); }); });

學過JS的小夥伴們肯定都能看得懂大概的意思吧,如果不明白socket.io中的伺服器推技術是怎麼實現的,可以百度一下相關文件,這裡就不多加贅述了。現在我們來看客戶端的程式碼實現:


window.onload = function() {
    var rdChat = new RdChat();
    rdChat.init();
};
var RdChat = function() {
    this.socket = null;
};
RdChat.prototype = {
    init: function() {
        var that = this;
        var userList = {};//使用者列表
        var userCount = null;//使用者數
        this.socket = io.connect();
        this.socket.on('connect', function() {//使用者登入
            document.getElementById('info').textContent = 'get yourself a nickname :)';
            document.getElementById('nickWrapper').style.display = 'block';
            document.getElementById('nicknameInput').focus();
        });
        this.socket.on('nickExisted', function() {
            document.getElementById('info').textContent = '!nickname is taken, choose another pls';
        });
        this.socket.on('loginSuccess', function(o) {
            document.title = 'RdChat | ' + document.getElementById('nicknameInput').value;
            this.userList = o.onlineUsers;
            that._initUserList(o.onlineUsers);
            document.getElementById('loginWrapper').style.display = 'none';
            document.getElementById('messageInput').focus();
        });
        this.socket.on('error', function(err) {
            if (document.getElementById('loginWrapper').style.display == 'none') {
                document.getElementById('status').textContent = '!fail to connect :(';
            } else {
                document.getElementById('info').textContent = '!fail to connect :(';
            }
        });
        this.socket.on('system', function(obj, userCount, type) {
            var msg = obj.nickname + (type == 'login' ? ' joined' : ' left');
            that._displayNewMsg('system ', msg, 'red');
            if(type == 'login' && !this.userList.hasOwnProperty(obj.userid)){
                that._updateUserList(obj);
            }
            if(document.getElementById('userlist').value == ""){
                document.getElementById('status').textContent = userCount + (userCount > 1 ? ' users' : ' user') + ' online';
            }else{
                if(document.getElementById('userlist').value == obj.userid){
                    document.getElementById('status').textContent = obj.nickname + "  " + type;
                }
            }

        });
        this.socket.on('newMsg', function(user, msg, color) {
            that._displayNewMsg(user, msg, color);
        });
        this.socket.on('newImg', function(user, img, color) {
            that._displayImage(user, img, color);
        });
        document.getElementById('loginBtn').addEventListener('click', function() {//監聽登入按鈕的click事件
            var nickName = document.getElementById('nicknameInput').value;
            var userid = that._getUid();
            if (nickName.trim().length != 0) {
                that.socket.emit('login', {userid:userid, nickname:nickName});
            } else {
                document.getElementById('nicknameInput').focus();
            };
        }, false);
        document.getElementById('nicknameInput').addEventListener('keyup', function(e) {//監聽回車鍵事件
            if (e.keyCode == 13) {
                var nickName = document.getElementById('nicknameInput').value;
                var userid = that._getUid();
                if (nickName.trim().length != 0) {
                    that.socket.emit('login', {userid:userid, nickname:nickName});
                };
            };
        }, false);
        document.getElementById('sendBtn').addEventListener('click', function() {//監聽訊息傳送的click事件
            var messageInput = document.getElementById('messageInput'),
                msg = messageInput.value,
                color = document.getElementById('colorStyle').value;
            messageInput.value = '';
            messageInput.focus();
            if (msg.trim().length != 0) {
                if(document.getElementById('userlist').value == ''){//判斷是廣播訊息還是私聊
                    that.socket.emit('postMsg', msg, color);
                    that._displayNewMsg('me', msg, color);
                    return;
                }else{
                    that.socket.emit('privateMsg', msg, color, document.getElementById('userlist').value);
                    that._displayNewMsg('me', msg, color);
                    return;
                }

            };
        }, false);
        document.getElementById('messageInput').addEventListener('keyup', function(e) {//監聽鍵盤事件(回車傳送訊息)
            var messageInput = document.getElementById('messageInput'),
                msg = messageInput.value,
                color = document.getElementById('colorStyle').value;
            if (e.keyCode == 13 && msg.trim().length != 0) {
                messageInput.value = '';
                if(document.getElementById('userlist').value == ''){
                    that.socket.emit('postMsg', msg, color);
                    that._displayNewMsg('me', msg, color);
                }else{
                    that.socket.emit('privateMsg', msg, color, document.getElementById('userlist').value);
                    that._displayNewMsg('me', msg, color);
                }
            };
        }, false);
        document.getElementById('clearBtn').addEventListener('click', function() {
            document.getElementById('historyMsg').innerHTML = '';
        }, false);
        document.getElementById('sendImage').addEventListener('change', function() {//傳送檔案按鈕監聽事件
            if (this.files.length != 0) {
                var file = this.files[0],
                    reader = new FileReader(),
                    color = document.getElementById('colorStyle').value;
                if (!reader) {
                    that._displayNewMsg('system', '!your browser doesn\'t support fileReader', 'red');
                    this.value = '';
                    return;
                };
                reader.onload = function(e) {
                    this.value = '';
                    if(document.getElementById('userlist').value == ""){
                        that.socket.emit('img', e.target.result, color);
                        that._displayImage('me', e.target.result, color);//在聊天視窗顯示自己傳送的圖片
                    }else{
                        that.socket.emit('privateimg', e.target.result, color, document.getElementById('userlist').value);
                        that._displayImage('me', e.target.result, color);
                    }

                };
                reader.readAsDataURL(file);
            };
        }, false);
        this._initialEmoji();//初始化聊天表情
        document.getElementById('emoji').addEventListener('click', function(e) {//傳送聊天表情
            var emojiwrapper = document.getElementById('emojiWrapper');
            emojiwrapper.style.display = 'block';
            e.stopPropagation();
        }, false);
        document.body.addEventListener('click', function(e) {
            var emojiwrapper = document.getElementById('emojiWrapper');
            if (e.target != emojiwrapper) {
                emojiwrapper.style.display = 'none';
            };
        });
        document.getElementById('emojiWrapper').addEventListener('click', function(e) {
            var target = e.target;
            if (target.nodeName.toLowerCase() == 'img') {
                var messageInput = document.getElementById('messageInput');
                messageInput.focus();
                messageInput.value = messageInput.value + '[emoji:' + target.title + ']';
            };
        }, false);
    },
    _initialEmoji: function() {//建立表情的html內容
        var emojiContainer = document.getElementById('emojiWrapper'),
            docFragment = document.createDocumentFragment();
        for (var i = 69; i > 0; i--) {
            var emojiItem = document.createElement('img');
            emojiItem.src = '../content/emoji/' + i + '.gif';
            emojiItem.title = i;
            docFragment.appendChild(emojiItem);
        };
        emojiContainer.appendChild(docFragment);
    },
    _displayNewMsg: function(user, msg, color) {//展示新資訊
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8),
            //判斷訊息中是否包含表情
            msg = this._showEmoji(msg);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span>' + msg;
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    },
    _displayImage: function(user, imgData, color) {
        var container = document.getElementById('historyMsg'),
            msgToDisplay = document.createElement('p'),
            date = new Date().toTimeString().substr(0, 8);
        msgToDisplay.style.color = color || '#000';
        msgToDisplay.innerHTML = user + '<span class="timespan">(' + date + '): </span> <br/>' + '<a href="' + imgData + '" target="_blank"><img src="' + imgData + '"/></a>';
        container.appendChild(msgToDisplay);
        container.scrollTop = container.scrollHeight;
    },
    _showEmoji: function(msg) {
        var match, result = msg,
            reg = /\[emoji:\d+\]/g,
            emojiIndex,
            totalEmojiNum = document.getElementById('emojiWrapper').children.length;
        while (match = reg.exec(msg)) {
            emojiIndex = match[0].slice(7, -1);
            if (emojiIndex > totalEmojiNum) {
                result = result.replace(match[0], '[X]');
            } else {
                result = result.replace(match[0], '<img class="emoji" src="../content/emoji/' + emojiIndex + '.gif" />');//todo:fix this in chrome it will cause a new request for the image
            };
        };
        return result;
    },
    _getUid:function(){//得到使用者標識
            return new Date().getTime()+""+Math.floor(Math.random()*899+100);
    },
    _updateUserList:function(obj){//更新使用者列表
        var container = document.getElementById('userlist'),
            optDisplay = document.createElement('option');
        optDisplay.style.color = '#000';
        optDisplay.value = obj.userid;
        optDisplay.innerHTML = obj.nickname;
        container.appendChild(optDisplay);
        container.scrollTop = container.scrollHeight;

    },
    _initUserList:function(userList){//初始化使用者列表
        var container = document.getElementById('userlist');
        for(key in userList) {
            if(userList.hasOwnProperty(key)){
                var optDisplay = document.createElement('option');
                optDisplay.style.color = '#000';
                optDisplay.value = key;
                optDisplay.innerHTML = userList[key];
                container.appendChild(optDisplay);
                container.scrollTop = container.scrollHeight;
            }
        }
    }
};

最後我們來看看聊天展示頁面的html程式碼吧~

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <meta name="author" content="Xj">
        <meta name="description" content="RdChat">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>RdChat</title>
        <link rel="stylesheet" href="styles/main.css">
        <link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
        <link rel="icon" href="favicon.ico" type="image/x-icon">
    </head>
    <body>
        <div class="wrapper">
            <div class="left">
                <div class="banner">
                    <span id="status"></span>
                </div>
                <div id="historyMsg">
                </div>
                <div class="controls" >
                    <div class="items">
                        <input id="colorStyle" type="color" placeHolder='#000' title="font color" />
                        <input id="emoji" type="button" value="emoji" title="emoji" />
                        <label for="sendImage" class="imageLable">
                            <input type="button" value="image"  />
                            <input id="sendImage" type="file" value="image"/>
                        </label>
                        <input id="clearBtn" type="button" value="clear" title="clear screen" />
                    </div>
                    <textarea id="messageInput" placeHolder="enter to send"></textarea>
                    <input id="sendBtn" type="button" value="SEND">
                    <div id="emojiWrapper">
                    </div>
                </div>
            </div>
            <!--右側聯絡人列表-->
            <div class="right">
                <select name="select" id="userlist" size="15" id="select">
                </select>
            </div>
        </div>

        <div id="loginWrapper">
            <p id="info">connecting to server...</p>
            <div id="nickWrapper">
                <input type="text" placeHolder="nickname" id="nicknameInput" />
                <input type="button" value="OK" id="loginBtn" />
            </div>
        </div>
        <script src="/socket.io/socket.io.js"></script><!--一定要這個js-->
        <script src="scripts/rdChat.js"></script>
    </body>
</html>

至此,一個簡單的WebIM就完成了。(這個程式碼經過測試,可以直接複製執行,至於樣式,自己做一下吧,哈哈~)

最後,為了讓我們更清楚即時通訊其中的工作原理,我們來了解幾個web通訊中比較重要的概念知識吧:(摘自博主hoojo的http://www.cnblogs.com/hoojo/p/longPolling_comet_jquery_iframe_ajax.html。)
Web 通訊 之 長連線、長輪詢(long polling)
基於HTTP的長連線,是一種通過長輪詢方式實現”伺服器推”的技術,它彌補了HTTP簡單的請求應答模式的不足,極大地增強了程式的實時性和互動性。
一、什麼是長連線、長輪詢?
用通俗易懂的話來說,就是客戶端不停的向伺服器傳送請求以獲取最新的資料資訊。這裡的“不停”其實是有停止的,只是我們人眼無法分辨是否停止,它只是一種快速的停下然後又立即開始連線而已。

二、長連線、長輪詢的應用場景
長連線、長輪詢一般應用與WebIM、ChatRoom和一些需要及時互動的網站應用中。其真實案例有:WebQQ、Hi網頁版、Facebook IM等。
如果你對伺服器端的反向Ajax感興趣,可以參考這篇文章 DWR 反向Ajax 伺服器端推的方式:http://www.cnblogs.com/hoojo/category/276235.html

三、優缺點
輪詢:客戶端定時向伺服器傳送Ajax請求,伺服器接到請求後馬上返回響應資訊並關閉連線。
優點:後端程式編寫比較容易。
缺點:請求中有大半是無用,浪費頻寬和伺服器資源。
例項:適於小型應用。

長輪詢:客戶端向伺服器傳送Ajax請求,伺服器接到請求後hold住連線,直到有新訊息才返回響應資訊並關閉連線,客戶端處理完響應資訊後再向伺服器傳送新的請求。
優點:在無訊息的情況下不會頻繁的請求,耗費資源小。
缺點:伺服器hold連線會消耗資源,返回資料順序無保證,難於管理維護。
例項:WebQQ、Hi網頁版、Facebook IM。

長連線:在頁面裡嵌入一個隱蔵iframe,將這個隱蔵iframe的src屬性設為對一個長連線的請求或是採用xhr請求,伺服器端就能源源不斷地往客戶端輸入資料。
優點:訊息即時到達,不發無用請求;管理起來也相對方便。
缺點:伺服器維護一個長連線會增加開銷。
例項:Gmail聊天

Flash Socket:在頁面中內嵌入一個使用了Socket類的 Flash 程式JavaScript通過呼叫此Flash程式提供的Socket介面與伺服器端的Socket介面進行通訊,JavaScript在收到伺服器端傳送的資訊後控制頁面的顯示。
優點:實現真正的即時通訊,而不是偽即時。
缺點:客戶端必須安裝Flash外掛;非HTTP協議,無法自動穿越防火牆。
例項:網路互動遊戲。

四、實現原理

所謂長連線,就是要在客戶端與伺服器之間建立和保持穩定可靠的連線。其實它是一種很早就存在的技術,但是由於瀏覽器技術的發展比較緩慢,沒有為這種機制的實現提供很好的支援。所以要達到這種效果,需要客戶端和伺服器的程式共同配合來完成。通常的做法是,在伺服器的程式中加入一個死迴圈,在迴圈中監測資料的變動。當發現新資料時,立即將其輸出給瀏覽器並斷開連線,瀏覽器在收到資料後,再次發起請求以進入下一個週期,這就是常說的長輪詢(long-polling)方式。如下圖所示,它通常包含以下幾個關鍵過程:
這裡寫圖片描述
1. 輪詢的建立
建立輪詢的過程很簡單,瀏覽器發起請求後進入迴圈等待狀態,此時由於伺服器還未做出應答,所以HTTP也一直處於連線狀態中。
2. 資料的推送
在迴圈過程中,伺服器程式對資料變動進行監控,如發現更新,將該資訊輸出給瀏覽器,隨即斷開連線,完成應答過程,實現“伺服器推”。
3. 輪詢的終止
輪詢可能在以下3種情況時終止:
3.1. 有新資料推送
當迴圈過程中伺服器向瀏覽器推送資訊後,應該主動結束程式執行從而讓連線斷開,這樣瀏覽器才能及時收到資料。
3.2. 沒有新資料推送
迴圈不能一直持續下去,應該設定一個最長時限,避免WEB伺服器超時(Timeout),若一直沒有新資訊,伺服器應主動向瀏覽器傳送本次輪詢無新資訊的正常響應,並斷開連線,這也被稱為“心跳”資訊。
3.3. 網路故障或異常
由於網路故障等因素造成的請求超時或出錯也可能導致輪詢的意外中斷,此時瀏覽器將收到錯誤資訊。
4. 輪詢的重建
瀏覽器收到回覆並進行相應處理後,應馬上重新發起請求,開始一個新的輪詢週期。

五、程式設計

1、普通輪詢 Ajax方式
客戶端程式碼片段

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
     <head>
         <meta http-equiv="pragma" content="no-cache">
         <meta http-equiv="cache-control" content="no-cache">
         <meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">
         <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
         <%@ include file="/tags/jquery-lib.jsp"%>

         <script type="text/javascript">
             $(function () {

                 window.setInterval(function () {

                     $.get("${pageContext.request.contextPath}/communication/user/ajax.mvc", 
                         {"timed": new Date().getTime()}, 
                         function (data) {
                             $("#logs").append("[data: " + data + " ]<br/>");
                     });
                 }, 3000);

             });
         </script>
     </head>

     <body>
         <div id="logs"></div>
     </body>
 </html>

客戶端實現的就是用一種普通輪詢的結果,比較簡單。利用setInterval不間斷的重新整理來獲取伺服器的資源,這種方式的優點就是簡單、及時。缺點是連結多數是無效重複的;響應的結果沒有順序(因為是非同步請求,當傳送的請求沒有返回結果的時候,後面的請求又被髮送。而此時如果後面的請求比前面的請求要先返回結果,那麼當前面的請求返回結果資料時已經是過時無效的資料了);請求多,難於維護、浪費伺服器和網路資源。

伺服器端程式碼

@RequestMapping("/ajax")
 public void ajax(long timed, HttpServletResponse response) throws Exception {
      PrintWriter writer = response.getWriter();

      Random rand = new Random();
      // 死迴圈 查詢有無資料變化
      while (true) {
          Thread.sleep(300); // 休眠300毫秒,模擬處理業務等
          int i = rand.nextInt(100); // 產生一個0-100之間的隨機數
          if (i > 20 && i < 56) { // 如果隨機數在20-56之間就視為有效資料,模擬資料發生變化
              long responseTime = System.currentTimeMillis();
              // 返回資料資訊,請求時間、返回資料時間、耗時
              writer.print("result: " + i + ", response time: " + responseTime + ", request time: " + timed + ", use time: " + (responseTime - timed));
              break; // 跳出迴圈,返回資料
          } else { // 模擬沒有資料變化,將休眠 hold住連線
              Thread.sleep(1300);
          }
      }

 }

伺服器端實現,這裡就模擬下程式監控資料的變化。上面程式碼屬於SpringMVC 中controller中的一個方法,相當於Servlet中的一個doPost/doGet方法。如果沒有程式環境適應servlet即可,將方法體中的程式碼copy到servlet的doGet/doPost中即可。
伺服器端在進行長連線的程式設計時,要注意以下幾點:
1. 伺服器程式對輪詢的可控性
由於輪詢是用死迴圈的方式實現的,所以在演算法上要保證程式對何時退出迴圈有完全的控制能力,避免進入死迴圈而耗盡伺服器資源。
2. 合理選擇“心跳”頻率
從圖1可以看出,長連線必須由客戶端不停地進行請求來維持,所以在客戶端和伺服器間保持正常的“心跳”至為關鍵,引數POLLING_LIFE應小於WEB伺服器的超時時間,一般建議在10~20秒左右。
3. 網路因素的影響
在實際應用時,從伺服器做出應答,到下一次迴圈的建立,是有時間延遲的,延遲時間的長短受網路傳輸等多種因素影響,在這段時間內,長連線處於暫時斷開的空檔,如果恰好有資料在這段時間內發生變動,伺服器是無法立即進行推送的,所以,在演算法設計上要注意解決由於延遲可能造成的資料丟失問題。
4. 伺服器的效能
在長連線應用中,伺服器與每個客戶端例項都保持一個持久的連線,這將消耗大量伺服器資源,特別是在一些大型應用系統中更是如此,大量併發的長連線有可能導致新的請求被阻塞甚至系統崩潰,所以,在進行程式設計時應特別注意演算法的優化和改進,必要時還需要考慮伺服器的負載均衡和叢集技術。
這裡寫圖片描述
上圖是返回的結果,可以看到先發出請求,不一定會最先返回結果。這樣就不能保證順序,造成髒資料或無用的連線請求。可見對伺服器或網路的資源浪費。
2、普通輪詢 iframe方式

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
     <head>
         <meta http-equiv="pragma" content="no-cache">
         <meta http-equiv="cache-control" content="no-cache">
         <meta http-equiv="expires" content="0">
         <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
         <%@ include file="/tags/jquery-lib.jsp"%>

         <script type="text/javascript">
             $(function () {

                 window.setInterval(function () {
                     $("#logs").append("[data: " + $($("#frame").get(0).contentDocument).find("body").text() + " ]<br/>");
                     $("#frame").attr("src", "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime());
                     // 延遲1秒再重新請求
                     window.setTimeout(function () {
                         window.frames["polling"].location.reload();
                     }, 1000);
                 }, 5000);

             });
         </script>
     </head>

     <body>
         <iframe id="frame" name="polling" style="display: none;"></iframe>
         <div id="logs"></div>
     </body>
 </html>

這裡的客戶端程式是利用隱藏的iframe向伺服器端不停的拉取資料,將iframe獲取後的資料填充到頁面中即可。同ajax實現的基本原理一樣,唯一不同的是當一個請求沒有響應返回資料的情況下,下一個請求也將開始,這時候前面的請求將被停止。如果要使程式和上面的ajax請求一樣也可以辦到,那就是給每個請求分配一個獨立的iframe即可。下面是返回的結果:
這裡寫圖片描述
其中紅色是沒有成功返回請求就被停止(後面請求開始)掉的請求,黑色是成功返回資料的請求。

3、長連線iframe方式

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" isELIgnored="false" %>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
 <html>
     <head>
         <meta http-equiv="pragma" content="no-cache">
         <meta http-equiv="cache-control" content="no-cache">
         <meta http-equiv="author" content="hoojo & http://hoojo.cnblogs.com">
         <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
         <%@ include file="/tags/jquery-lib.jsp"%>

         <script type="text/javascript">
             $(function () {

                 window.setInterval(function () {
                     var url = "${pageContext.request.contextPath}/communication/user/ajax.mvc?timed=" + new Date().getTime();
                     var $iframe = $('<iframe id="frame" name="polling" style="display: none;" src="' + url + '"></iframe>');
                     $("body").append($iframe);

                     $iframe.load(function () {
                         $("#logs").append("[data: " + $($iframe.get(0).contentDocument).find("body").text() + " ]<br/>");
                         $iframe.remove();
                     });
                 }, 5000);

             });
         </script>
     </head>

     <body>

         <div id="logs"></div>
     </body>
 </html>

這個輪詢方式就是把剛才上面的稍微改下,每個請求都有自己獨立的一個iframe,當這個iframe得到響應的資料後就把資料push到當前頁面上。使用此方