HTML5 WebSocket
一、HTTP协议
传统的HTTP协议是无状态的,每次请求(request)都要由客户端(如 浏览器)主动发起,服务端进行处理后返回response结果,而服务端很难主动向客户端发送数据;
这种客户端是主动方,服务端是被动方的传统Web模式。 对于信息变化不频繁的Web应用来说造成的麻烦较小,而对于涉及实时信息的Web应用却带来了很大的不便,
如带有即时通信、实时数据、订阅推送等功能的应 用。在WebSocket规范提出之前,开发人员若要实现这些实时性较强的功能,经常会使用折衷的解决方法:
轮询(polling)和Comet技术。其实后者本质上也是一种轮询,只不过有所改进。
轮询是最原始的实现实时Web应用的解决方案。轮询技术要求客户端以设定的时间间隔周期性地向服务端发送请求,频繁地查询是否有新的数据改动。明显地,这种方法会导致过多不必要的请求,浪费流量和服务器资源。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。
Comet技术又可以分为长轮询和流技术。长轮询改进了上述的轮询技术,减小了无用的请求。它会为某些数据设定过期时间,当数据过期后才会向服务端发送请求;这种机制适合数据的改动不是特别频繁的情况。流技术通常是指客户端使用一个隐藏的窗口与服务端建立一个HTTP长连接,服务端会不断更新连接状态以保持HTTP长连接存活;这样的话,服务端就可以通过这条长连接主动将数据发送给客户端;流技术在大并发环境下,可能会考验到服务端的性能。
这两种技术都是基于请求-应答模式,都不算是真正意义上的实时技术;它们的每一次请求、应答,都浪费了一定流量在相同的头部信息上,并且开发复杂度也较大。
二、WebSocket
WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过 onmessage 事件来接收服务器返回的数据。
为客户端推送时间消息的 Tornado WebSocket程序:
import tornado.ioloop import tornado.web import tornado.websocket import threading import time import datetime import asyncio from tornado.options import define, options, parse_command_line define("port", default=8888, help="run on the given port", type=int) clients = dict() # 客户端Session 字典 class IndexHandler(tornado.web.RequestHandler): async def get(self): self.render("index.html") class MyWebSocketHandler(tornado.websocket.WebSocketHandler): def open(self, *args): # 有新链接时被调用 self.id = self.get_argument("Id") # 保存Session 到 clients字典中 clients[self.id] = {"id": self.id, "object": self} print("open function", str(clients)) def on_message(self, message): # 接受消息时被调用 print("Client %s received a message : %s" % (self.id, message)) def on_close(self): # 关闭链接时被调用 if self.id in clients: del clients[self.id] print("Client %s is closed" % self.id) def check_origin(self, origin): return True app = tornado.web.Application([(r'/', IndexHandler),(r'/websocket', MyWebSocketHandler)]) # 启动单独的线程运行此函数, 每隔一秒向所有的客户端推送当前时间 def sendTime(): asyncio.set_event_loop(asyncio.new_event_loop()) # 启动异步 event loop while True: for key in clients.keys(): msg = str(datetime.datetime.now()) clients[key]["object"].write_message(msg) print("write to client %s: %s" % (key, msg)) time.sleep(1) if __name__ == "__main__": threading.Thread(target=sendTime).start() parse_command_line() app.listen(options.port) tornado.ioloop.IOLoop.instance().start() # 挂起运行
##############################################################################################
- 定义了全局变量字典clients, 用于保存所有与服务器建立起WebSocket链接的客户端信息。字典的键是客户端的id, 值是一个由id与相应的WebSocketHandler实例构成的元组;
- IndexHandler 是一个普通的页面处理器,用于向客户端渲染主页, 该页面中包含了 Websocket 的客户端程序;
- MyWebSockerHandler 是本例的核心处理器,继承 tornado.web.WebSocketHandler. 其中 open() 函数将所有客户端链接保存到 clients字典中;
- on_message() 函数用于显示客户端发来的消息; on_close()函数用于将已经关闭的 WebSocket链接从 clients 字典中移除。
- 函数 sendTime() 运行在单独的线程中,每隔一秒轮询 clients 中的所有客户端并通过 MyWebSocketHandler.write_message() 函数向客户端推送时间消息。
- 所有 Tornado 线程中必须有一个 event_loop, 该项要求通过 sendTime() 函数中的第一行代码被满足。
- 本例的 tornado.web.Application 实例中只配置了两个路由,分别指向 IndexHandler 和 MyWebSocketHandler, 仍然由 Tornado IOLoop 启动并运行。
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Python后端WebSocket的实现</title> </head> <body> <a href="javascript:WebSocketTest()">Run Websocket</a> <div id="messages" style="height:200px; background:black; color: white;"></div> <script type="text/javascript"> var messageContainer = document.getElementById("messages"); function WebSocketTest(){ // 判断当前浏览器是否支持WebSocket if("WebSocket" in window){ messageContainer.innerHTML = "WebSocket is supported by your browser!"; let ws = new WebSocket("ws://localhost:8888/websocket?Id=666888"); //连接成功建立的回调方法 ws.onopen = function(){ ws.send("Message to send"); }; //接收到消息的回调方法 ws.onmessage = function(evt){ var received_msg = evt.data; messageContainer.innerHTML += "<br/>Message is received: " + received_msg; }; //连接关闭的回调方法 ws.onclose = function(){ messageContainer.innerHTML += "<br/>Connection is closed ..."; }; }else{ messageContainer.innerHTML = "WebSocket Not Supported by your Browser!"; } } </script> </body> </html>
WebSocket程序运行效果: