Ajax/Comet實作聊天室心得

今天為now.in做了一個最陽春版本的聊天室

這聊天室用了AJAX/Comet技術,說穿了AJAX/Comet真的沒什麼,其實不過就一個http request在server端在接受到後,直到有事件發生才回傳,也有另一種做法是利用比較特別的header讓browser對於每次收到的資料分批處理,而這些都在一個response中完成,因為似乎後者還得有browser的支援,所以我用的是前者,而雖然概念上很簡單,但實際做起來會有些細節得注意的

前端不可以有proxy

原本我寫好後放上去結果不能跑,發現原因出在於透過WebFaction前端的Apache再rewrite到我的server,似乎在前端的連線數很快就被擠暴了,因為線上會有數百個連線一直在線上,所以如果前端有proxy,很容易就會被塞暴,後來改用直接連線,問題就解決了

一般的伺服器架構其實根本不適合Ajax/Comet

事實上一般常見的伺服器架構大多都不適合寫Ajax/Comet,為什麼呢? 原因很簡單,大多的伺服器架構都是thread pool之類的做法,一個連線進來,就從thread pool中拿一個worker丟給他去做,在完成之前都屬於在處理的階段,因此你有同時100個連線被Ajax/Comet卡住,就佔掉了thread pool中100個worker,這些worker什麼都不能做,通常只能sleep,定期檢查事件發生了沒,除了伺服器很快就會被塞暴以外,一點效率都沒有,像是Apache就是這類的架構

解決的方案: 非同步的IO

解決方案其實很簡單,就是改用非同步IO的網路框架,我個人最熟的就是Twisted,自然就是選擇這個,身為愛好Python的程式設計師,其實很多時候苦腦的不是沒有選擇,而是選擇太多,Python的非同步網路函式庫不是用一打就能數完的,多到誇張的一個地步,而且一直在增加,不久前又多出了一個Tornado,這也只是冰山一角,有人寫了一篇文章專門是在比較Python非同步網路函式庫的

Asynchronous Servers in Python

當然,這文章裡列出的也只是一部份而已

Twisted的Ajax/Comet做法

class Chatroom(resource.Resource):
    isLeaf = True
    def wait(self, request):
        """Wait for notifications

        """
        self.waitingRequests.append(request)
        log.debug('[%s] %d users are waiting',
                  self.name,
                  len(self.waitingRequests))
        return server.NOT_DONE_YET

    def notify(self):
        """Notify all waiting users

        """
        log.info('[%s] Notify %d users', self.name, len(self.waitingRequests))
        for request in self.waitingRequests:
            if not request._disconnected:
                userTick = int(request.args['tick'][0])
                messages = self.getMessagesByTick(userTick)
                result = dict(tick=self.tick, messages=messages)
                request.write(json.dumps(result))
                request.finish()
        self.waitingRequests = []

省略掉雜七雜八的部份,其實最關鍵的地方很簡單,在於Twisted.web的resource的render可以回傳server.NOT_DONE_YET,來用以延後資料的回傳,接著之後要回傳時,只要呼叫request.write()就可以寫資料回去,而當request要結束時呼叫request.finish()即可,就是這麼簡單,在設計上很簡單,當發現使用者的tick已經跟上最新進度,伺服器這邊沒有新的東西給使用者,既然如此就讓使用者等待事件發生,把request丟進waitingRequest的list中,而當有新的資料進來,就可以呼叫notify,把新的資料丟回給每個在等待的使用者

在一般網頁伺服器也能用Ajax/Comet的解決方案

當然,並不是所有網頁程式用非同步的網路框架寫起來都會輕鬆容易,很多時候我們希望能夠在一般的PHP、Django、TurboGears網頁程式裡也能有Ajax/Comet的好處,但不幸的是這些都是同步式的伺服器架構,兩者各有優缺點,於是我就想到一個解決方案,各取其長,很簡單的解決方案就是,我們將需要長期等待事件發生的request都丟給非同步的伺服器,而一般的網頁都在一般的網頁伺服器處理,要怎麼做呢? 想法很簡單

  1. 當一般的網頁伺服器有需要讓使用者等候事件發生的場合時,只要用xmlrpc呼叫非同步的網頁伺服器,建立並回傳一個對應事件的唯一的url,一般的網頁伺服器拿到url後將user轉向到此url,所有的request連到這個url在timeout前都會一直等待
  2. 當事件發生時,以xmlrpc呼叫非同步網頁伺服器來讓那些request停止等待
  3. 當client發現request已載入或失敗時,會連到一般的網頁伺服器檢查是否有新資料,有的話進行更新,沒有的話重覆步驟1繼續等待

為什麼建立的url需要是唯一的呢? 很簡單,當事件的url已存在,一般的伺服器要重導給client,在這段期間如果事件被觸發,url如果一樣有可能會等到新的事件,而當事件被觸發,url會回傳404,所以整個流程可以重跑一次,可以正確並正常執行

這樣的解決方案有個最大的優點,就是任何網頁程式開發工具,只要是能使用xmlrpc都可以建立Ajax/Comet事件,我原本聊天室的設計就是採用這樣的方案,但是同樣的它也有不少缺點,一來就是當事件url被載入後,還要再開另一個request到我TurboGears2伺服器要資料來更新,再來就是Cross-Domain的問題,Ajax的request有不能跨domain的限制,如果將伺服器分開,這樣就沒辦法用XMLHtmlRequest來送要求,得改用iframe等解決方案,然而這也不是無解,一來是新的browser支援一種新的協定,可以讓Ajax跨domain送request,我曾經有實作過,但發現其實支援的browser實在太有限,我當時試似乎只有FireFox和某些有支援,IE? 甭提了,因此考慮到佔了大半的使用者,那不會是好的解決方案,另一種做法是將兩種server掛在同一個前端server下,用virtual host的方式,但是有個問題,如果前端的伺服器是同步式的伺服器,會遇到我先前提到的問題,所以前端的也得是非同步的伺服器,nginx似乎就是一種,Twisted也可以辦到

因為這樣的寫法還得加資料庫進去,為了方便加上省掉那個檢查新資料用的request,於是我就將聊天室所有東西都放在同一個Twisted的伺服器,包括訊息的暫存,這樣就簡單又可靠

Twisted + Ajax/Comet = TwistedComet

上段中提到的解決方案,因為是用Twisted加上又是Ajax/Comet所以叫TwistedComet,我開了一個Google code project,把程式丟上去,其實程式非常地簡單,就只是如上面提到,提供xmlrpc介面,然後提供非同步request的等待和觸發等動作,專案的網址在此

TwistedComet

有興趣可以玩看看

—-

更新 2012/08/01

依據我對Ajax/Comet開發的經驗,我將它做成雲端服務,現在只要註冊取得API就可以輕易地打造即時的網頁應用程式,有興趣請到

http://ezcomet.com

This entry was posted in Python, 中文文章 and tagged , , , , , , . Bookmark the permalink.

8 Responses to Ajax/Comet實作聊天室心得

  1. K.A says:

    我有朋友說 她用聊天室會出現”網頁發生錯誤”
    這怎麼辦呢??

  2. victor says:

    這我倒是沒遇過,但我猜可能他的網路環境有擋非80 port,因為那聊天室開在非80 port,所以可能因為這樣被擋掉所以出現網頁發生錯誤

  3. 阿咒 says:

    不好意思 我最近開電台 聽眾都會說聽得斷斷續續
    聽到的音樂也大部份發生d-live的情況
    之前沒聊天室時就很少會這樣
    請問是什麼緣故呢 有什麼解決的方法呢
    當dj很苦惱啊

  4. victor says:

    我也很納悶為什麼會有這種情況發生,最主要的原因應該都是聽眾連美國主機的速度太慢,我自己用的是中華電信,我聽是幾乎沒有delay過,即使主機一樣在美國

    要測試是否會不會delay的方式,最簡單的就是去下載MrDJ的檔案來測速看看

    http://static.ez2learn.com/now.in/mrdj.zip

    一般播放音樂的品質都設定在15.xKB/seconds,所以當你連我在美國主機的速度能夠超過這個值時,那聽音樂基本上應該都會是順暢的情況,但如果當你速度低於這個值,因為傳給你的音樂很快就被播完了,但你的頻寬來不及將更新的音樂補上,因此它就只能暫時停下來等待更新的音樂資料送到,這就是主要的delay原因

    我知道當人太多時主機對外頻寬可能有被塞滿的可能,但是目前看來人數都沒之前多,而且也有很多人都能正常收聽,所以應該不是我主機那邊的問題

    目前已知學術網路幾乎是一定會delay,因為學術網路連國外天生就極慢,而像其它人的中華電信之類的,也會delay我就覺得挺納悶的,有可能是鄰居開BT把頻寬佔滿,或是電信商連外頻寬不足,或是什麼情況我目前就不得而知,有空我會找時間研究看看到底是什麼樣的情況

  5. Jeffrey says:

    您好:
    最近在練習comet方面的php程式,
    在網頁載入的時候會送出一個不間斷的request來最查詢資料,
    但此時要再送出第二個request的時候會一直卡著。。。
    應該是因為http 1.1的並發連結限制,
    但是看到您的聊天室似乎在做查詢的時候還可以再送出request去發送發言的訊息,
    不知道可否告訴小弟是用什麼方法呢 ><"

  6. Jeffrey says:

    話說剛剛找到問題原因了。。。
    是因為有 session_start 的關係,
    天啊,好奇怪 = =

  7. victor says:

    @Jeffrey

    因為發言也會順便更新聊天內容,所以把原本正在polling的request給取消掉,就這樣而已

  8. QQ says:

    我也有”網頁發生錯誤”的問題
    按進去說”物件不支援此屬性或方法”
    請問有沒有解決的辦法呢
    (不好意思我不大懂程式XD)
    謝謝大大!!