转自:https://blog.shonenada.com/post/websocket-with-flask/
WebSocket with Flask
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 异步功能时实现的两段应用:
- https://github.com/shonenada/chat-in-command-line
- 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。
实时更新基本的实现思路:
- 客户端发起 ws 连接请求
- 服务器响应,并且把 ws 加入到 observer 数组中。
- 当某一 ws 向服务器发送信息时,服务器遍历 observers 数组向每一个元素发送信息。
- 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="发布"> <input type="reset" class="btn" value="清空"> </div> </div> </form> </div> <div class="footer container"> <p> © Copyright by shonenada </p> </div> </body> </html>
~接下来可以实现 HTML5 的桌面直播了。