HTML5 以前,HTML 还不支持 WebSocket ,当时如果要进行实时的内容更新,要么使用 Ajax轮询(Polling) 或者使用 Comet 技术。

Non-Websocket

Ajax 轮询

在 2005 年, Jesse James Garrett 提出 Ajax (Asynchronous JavaScript and XML, 异步 Javascript 和 XML)。具体请看 Ajax: A New Approach to Web Applications 。并且从那时开始流行使用 Ajax 进行异步处理客户端请求。【关于异步处理请求的历史,可以看 http://en.wikipedia.org/wiki/Ajax_(programming) 中相关的介绍】。 XMLHttpRequest 在后台对服务器发起 request ,当收到 response 的时候,进行 DOM 操作,从而达到部分更新页面内容的目的(而不需要整个页面刷新)。

Ajax 轮询 可以做到接近实时的更新内容,但是因为是由客户端发起请求,即服务器处于被动的状态,这种“实时”存在缺陷: (1) 伪实时。服务器有更新的时候,只有客户端发起请求,服务器才能将更新返回到客户端。 (2) 数据更新量少的时候,容易造成浪费带宽、流量。 (3) 请求频率难以把握。太快会对服务器造成过大的压力,而太慢又不够“实时”,权衡频率需要考虑的因素很多。

Comet 技术

Comet 是指不需要客户端浏览器安装任何插件,仅靠浏览器和服务器之间的长 HTTP 连接实现服务器向客户端通信(服务器推)的技术。 Comet 有两种方式: Ajax长轮询iframe with htmlfile stream

iframe with htmlfile streaming

这种技术,暂时没使用过。基本原理是使用 iframe 标签在 html 中插入一个隐藏的帧,向服务器建立长连接,服务器不断地向 iframe 输入数据。

Ajax 长轮询

Ajax 长轮询本质上也是 Ajax 轮询,不同的是,在服务器端做了些修改。当服务器没有更新的时候,服务器将请求阻塞,直到 有更新连接超时。当请求结束之后再进行第二次的请求。

这种方式基本上可以避开 Ajax 轮询的缺陷。 Tornado 框架中的 Asynchronous 功能就是通过阻塞请求实现异步更新。 通过 Tornado 框架提供的 Asynchronous 功能可以实现实时数据传递。欢迎参考我在学习使用 tornado 异步功能时实现的两段应用:

  1. https://github.com/shonenada/chat-in-command-line
  2. https://github.com/shonenada/guess-number // 这程序功能不完善,但实现了异步的功能。

WebSocket

WebSocket 是 HTML5 的新功能,它是一种 TCP 协议。当客户端和服务器完成握手,建立连接之后,ws 就如普通 socket 一样,在两者之间进行通信。

理解了基本通信原理,就可以进行编程了。

前面已说,WS 是一种 TCP 协议,所以是语言无关的,用任何语言都可以实现服务器端的编程。我选择了 Python,使用 _flask: http://flask.pocoo.org/ 作为框架,以 _Gevent: http://www.gevent.org/ 和 _gevent-websocket: https://pypi.python.org/pypi/gevent-websocket/ 做 HttpServer。

实时更新基本的实现思路:

  1. 客户端发起 ws 连接请求
  2. 服务器响应,并且把 ws 加入到 observer 数组中。
  3. 当某一 ws 向服务器发送信息时,服务器遍历 observers 数组向每一个元素发送信息。
  4. ws 断开连接时,从 observer 中剔除。

具体实现代码:

# manage.py
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
from flask import Flask, request, render_template, abort

import message


msgsrv = message.MessageServer()

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('message.html')


@app.route('/message/')
def message():
    if request.environ.get('wsgi.websocket'):
        ws = request.environ['wsgi.websocket']
        msgsrv.observers.append(ws)
        while True:
            if ws.socket:
                message = ws.receive()
                if message:
                    msgsrv.add_message("%s" % message)
            else:
                abort(404)
    return "Connected!"


if __name__ == '__main__':
    http_server = WSGIServer(('', 5000), app, handler_class=WebSocketHandler)
    http_server.serve_forever()
# message.py
from geventwebsocket import WebSocketError


class MessageServer(object):

    def __init__(self):
        self.observers = []

    def add_message(self, msg):
        for ws in self.observers:
            try:
                ws.send(msg)
            except WebSocketError:
                self.observers.pop(self.observers.index(ws))
                print ws, 'is closed'
                continue
<!-- templates/message.html -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <meta http-equiv="Content-Language" content="zh-CN"/>
        <title>实时消息</title>
        <link rel="stylesheet" href="http://getbootstrap.com/2.3.2/assets/css/bootstrap.css">
        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
        <script type="text/javascript" charset="utf-8">
        $(document).ready(function(){
            $('form').submit(function(event){
                ws.send($(this).serialize());
                return false;
            });
            if ("WebSocket" in window) {
                ws = new WebSocket("ws://" + document.domain + ":5000/message/");
                ws.onmessage = function (msg) {
                    console.log(msg.data);
                };
            } else {
                alert("WebSocket not supported");
            }
            window.onbeforeunload = function() {
                ws.onclose = function () {
                    console.log('unlodad')
                };
                ws.close()
            };
        });
        </script>
    </head>
    <body>
        <div class="header container">
            <h1>实时消息</h1>
            <ul class="tabs">
                <li class="active">
                    <a href="/">DEMO</a>
                </li>
            </ul>
        </div>
        <div class="container">
            Pls check your Chrome console.
            <form class="row" id="message_form">
                <div class="span10">
                    <div class="clearfix">
                        <label for="chat_content">消息</label>
                        <div class="input">
                            <textarea id="chat_content" name="content" class="xlarge" rows="6"></textarea>
                        </div>
                    </div>
                    <div class="well align-center">
                        <input type="submit" class="btn primary" value="发布">
                        &nbsp;
                        <input type="reset" class="btn" value="清空">
                    </div>
                </div>
            </form>
        </div>
        <div class="footer container">
            <p>
                &copy; Copyright by shonenada
            </p>
        </div>
    </body>
</html>

~接下来可以实现 HTML5 的桌面直播了。