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。
geventwebsocket安装方法:
pip3 install geventwebsocket
实时更新基本的实现思路:
- 客户端发起 ws 连接请求
- 服务器响应,并且把 ws 加入到 observer 数组中。
- 当某一 ws 向服务器发送信息时,服务器遍历 observers 数组向每一个元素发送信息。
- ws 断开连接时,从 observer 中剔除。
具体实现代码:
from flask import Flask,render_template,request from geventwebsocket.handler import WebSocketHandler from geventwebsocket.websocket import WebSocket from gevent.pywsgi import WSGIServer import json app=Flask(__name__) user_socket_list = [] user_socket_dict = { } @app.route("/ws/<username>") def ws(username): user_socket = request.environ.get("wsgi.websocket")#type:WebSocket if user_socket: user_socket_dict[username] = user_socket print(len(user_socket_dict),user_socket_dict) while 1: msg = user_socket.receive() msg_dict = json.load(msg) msg_dict["from_user"] = username to_user = msg_dict.get("to_user") # chat= msg_dict.get("msg") u_socket = user_socket_dict.get(to_user) #type:WebSocket u_socket.send(json.dumps(msg_dict)) # for u_socket in user_socket_list: # if u_socket.send(json.dumps(msg_dict)) # # for u_socket in user_socket_list: # if u_socket == user_socket: # continue # try: # u_socket.send(msg) # except: # continue @app.route("/") def index(): return render_template("ws.html") if __name__ == '__main__': app.run("0.0.0.0",9527,debug=True) http_serv = WSGIServer(("0.0.0.0",9527),app,handle_class=WebSocketHandler) http_serv.serve_forever()
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <meta name="viewport" content="width=device-width,initial-scale=1"> </head> <body> <input type="text" id="username"> <button onclick="login()">登录聊天室</button> 给<input type="text" id="to_user">发送:<input type="text" id="msg"> <button onclick="send_msg()">发送</button> <div id="chat_list" style="500px;height: 500px;"></div> </body> <script type="application/javascript"> var ws = null; function login() { var username = document.getElementById("username").value; ws = new WebSocket("ws://192.168.13.209:9527/ws/"+username); ws.onmessage = function (data) { console.log(data.data); var recv_msg = JSON.parse(data.data); var ptag = document.createElement("p"); ptag.innerText = recv_msg.from_user + ":" + recv_msg.msg; document.getElementById("chat_list").appendChild(ptag); } } function send_msg() { var to_user = document.getElementById("to_user").value; var msg = document.getElementById("msg").value; var send_str = { "to_user":to_user, "msg":msg }; ws.send(JSON.stringify(send_str)); } </script> </html>