websocket应用
手动实现的websocket
你所见过的websocket
你一定见过在网站中,有一个游客聊天的聊天框,比如人人影视。这个聊天框是如何实现即时通讯的呢,就是用到了websocket
你可以打开浏览器的network,会看到有个ws://xxxxx,这就代表了是websocket做的
那么什么是websocket?
websocket就是一套协议。
看名字,虽然有个websocket,但他和http协议一样,也要走socket。
不同的是:http是短连接,处理完一个请求就断开;
? websocket是连上就不断开,一直不断开,属于双工通道,服务端可以主动给客户端推送消息,客户端也可以主动给服务端推送消息
当某一个客户端发送一条消息,服务端接收以后,再推送给所有的客户端,所以才会呈现出所有人都在即时通讯的效果
服务端当然就是我们写的程序了,那客户端是浏览器,所以还需要浏览器支持才行。不要以为浏览器是都支持的,如果所有人都用chrome,前端开发工程师估计就没什么工作了。还有,如果所有的浏览器都支持,腾讯的webQQ,web微信,也不会使用长轮询来做这个事了。
来看一下具体的代码实现
import socketimport base64import hashlibdef get_headers(data): ???""" ???将请求头格式化成字典 ???:param data: ???:return: ???""" ???header_dict = {} ???data = str(data, encoding=‘utf-8‘) ???for i in data.split(‘\r\n‘): ???????print(i) ???header, body = data.split(‘\r\n\r\n‘, 1) ???header_list = header.split(‘\r\n‘) ???for i in range(0, len(header_list)): ???????if i == 0: ???????????if len(header_list[i].split(‘ ‘)) == 3: ???????????????header_dict[‘method‘], header_dict[‘url‘], header_dict[‘protocol‘] = header_list[i].split(‘ ‘) ???????else: ???????????k, v = header_list[i].split(‘:‘, 1) ???????????header_dict[k] = v.strip() ???return header_dictsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)sock.bind((‘127.0.0.1‘, 8002))sock.listen(5)conn, address = sock.accept()data = conn.recv(1024)headers = get_headers(data) ?# 提取请求头信息# 对请求头中的sec-websocket-key进行加密response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ ??????????????"Upgrade:websocket\r\n" \ ??????????????"Connection: Upgrade\r\n" \ ??????????????"Sec-WebSocket-Accept: %s\r\n" \ ??????????????"WebSocket-Location: ws://%s%s\r\n\r\n"magic_string = ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11‘ ??????#固定的,魔法字符串就是这个字符串value = headers[‘Sec-WebSocket-Key‘] + magic_stringac = base64.b64encode(hashlib.sha1(value.encode(‘utf-8‘)).digest()) #把返回消息加密response_str = response_tpl % (ac.decode(‘utf-8‘), headers[‘Host‘], headers[‘url‘])# 响应【握手】信息conn.send(bytes(response_str, encoding=‘utf-8‘))info = conn.recv(8096)#下面是对浏览器发来的消息解密的过程payload_len = info[1] & 127if payload_len == 126: ???extend_payload_len = info[2:4] ???mask = info[4:8] ???decoded = info[8:] # 数据elif payload_len == 127: ???extend_payload_len = info[2:10] ???mask = info[10:14] ???decoded = info[14:]else: ???extend_payload_len = None ???mask = info[2:6] ???decoded = info[6:]bytes_list = bytearray()for i in range(len(decoded)): ??????#上面解密的最终结果,就是拿到这个decode,就是浏览器发来的真实的数据(加密的) ???chunk = decoded[i] ^ mask[i % 4] ???#按位异或 ???bytes_list.append(chunk)body = str(bytes_list, encoding=‘utf-8‘)print(body)
客户端向服务端发送的请求里,有Sec-WebSocket-Key
这样一个key,服务端回消息的时候,就要拿到这个key,加密后再发给浏览器,浏览器会判断自己加密后的值,与浏览器处理的是否一致,一致才能连接。加密的方式,用到一个magic_string
,其实就是一段固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,加密后打包发给浏览器,浏览器验证通过后就可以通讯了,再来看看客户端:
客户端就直接用浏览器运行这个html文件就行
<!DOCTYPE html><html lang="en"><head> ???<meta charset="UTF-8"> ???<title>Title</title> ???<link rel="stylesheet" href="dist/css/bootstrap.css"></head><body> ???<div> ???????<input type="text" id="txt"/> ???????<input type="button" id="btn" value="提交" onclick="sendMsg();"/> ???????<input type="button" id="close" value="关闭连接" onclick="closeConn();"/> ???</div> ???<div id="content"></div> ???<script type="text/javascript"> ?????????var socket = new WebSocket("ws://127.0.0.1:8002"); ???????????socket.onopen = function () { ???????????????/* 与服务器端连接成功后,自动执行 */ ???????????????var newTag = document.createElement(‘div‘); ???????????????newTag.innerHTML = "【连接成功】"; ???????????????document.getElementById(‘content‘).appendChild(newTag); ???????????}; ???????????socket.onmessage = function (event) { ???????????????/* 服务器端向客户端发送数据时,自动执行 */ ???????????????var response = event.data; ???????????????var newTag = document.createElement(‘div‘); ???????????????newTag.innerHTML = response; ???????????????document.getElementById(‘content‘).appendChild(newTag); ???????????}; ???????????socket.onclose = function (event) { ???????????????/* 服务器端主动断开连接时,自动执行 */ ???????????????var newTag = document.createElement(‘div‘); ???????????????newTag.innerHTML = "【关闭连接】"; ???????????????document.getElementById(‘content‘).appendChild(newTag); ???????????}; ???????????function sendMsg() { ???????????????var txt = document.getElementById(‘txt‘); ???????????????socket.send(txt.value); ???????????????txt.value = ""; ???????????} ???????????function closeConn() { ???????????????socket.close(); ???????????????var newTag = document.createElement(‘div‘); ???????????????newTag.innerHTML = "【关闭连接】"; ???????????????document.getElementById(‘content‘).appendChild(newTag); ???????????} ???</script><script></script></body></html>
这里面有三个方法:
- 连接上后,onopen会自动执行
- 发消息时,onmessage自动执行
- 断开连接,onclose自动执行
客户端发送给服务端的数据,还有一层加密,必须通过解密才能拿到正确的消息
payload_len = info[1] & 127if payload_len == 126: ???extend_payload_len = info[2:4] ???mask = info[4:8] ???decoded = info[8:] # 数据elif payload_len == 127: ???extend_payload_len = info[2:10] ???mask = info[10:14] ???decoded = info[14:]else: ???extend_payload_len = None ???mask = info[2:6] ???decoded = info[6:]bytes_list = bytearray()for i in range(len(decoded)): ??????#上面解密的最终结果,就是拿到这个decode,就是浏览器发来的真实的数据(加密的) ???chunk = decoded[i] ^ mask[i % 4] ???#按位异或 ???bytes_list.append(chunk)body = str(bytes_list, encoding=‘utf-8‘)
这段就是解密的过程,用到位运算
Django默认是不支持websocket的,虽然有个第三方的channels插件
但是tornado默认就支持
tornado实现websocket
如果用tornado,客户端不能直接用浏览器运行了,而应该是运行tornado的一个模板文件
服务端代码:
#!/usr/bin/env python# -*- coding:utf-8 -*-import uuidimport jsonimport tornado.ioloopimport tornado.webimport tornado.websocketclass IndexHandler(tornado.web.RequestHandler): ???def get(self): ???????self.render(‘index.html‘)class ChatHandler(tornado.websocket.WebSocketHandler): ???# 用户存储当前聊天室用户 ???waiters = set() ???# 用于存储历时消息 ???messages = [] ???def open(self): ???????""" ???????客户端连接成功时,自动执行 ???????:return: ???????""" ???????ChatHandler.waiters.add(self) ???????uid = str(uuid.uuid4()) ???????self.write_message(uid) ???????# 下面这段代码是给新加入的用户,显示历史信息的 ???????for msg in ChatHandler.messages: ???????????# {‘uid‘:‘xxx‘,‘message‘:asdfasd} ???????????content = self.render_string(‘message.html‘, **msg) ???????????self.write_message(content) ???def on_message(self, message): ???????""" ???????客户端连发送消息时,自动执行 ???????:param message: ???????:return: ???????""" ???????msg = json.loads(message) ???????ChatHandler.messages.append(msg) ???????for client in ChatHandler.waiters: ???????????content = client.render_string(‘message.html‘, **msg) ???????????client.write_message(content) ???def on_close(self): ???????""" ???????客户端关闭连接时,,自动执行 ???????:return: ???????""" ???????ChatHandler.waiters.remove(self)def run(): ???settings = { ???????‘template_path‘: ‘templates‘, ??????# 配置模板文件 ???????‘static_path‘: ‘static‘, ???????????# 配置静态文件路径 ???} ???application = tornado.web.Application([ ????????# 配置路由 ???????(r"/", IndexHandler), ???????(r"/chat", ChatHandler), ???], **settings) ???application.listen(8009) ???tornado.ioloop.IOLoop.instance().start()if __name__ == "__main__": ???run()
模板文件(客户端代码):
<!DOCTYPE html><html lang="en"><head> ???<meta charset="UTF-8"> ???<title>Python聊天室</title></head><body> ???<div> ???????<input type="text" id="txt"/> ???????<input type="button" id="btn" value="提交" onclick="sendMsg();"/> ???????<input type="button" id="close" value="关闭连接" onclick="closeConn();"/> ???</div> ???<div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;"> ???</div> ???<script src="/static/jquery-3.2.1.js"></script> ???<script type="text/javascript"> ???????$(function () { ???????????wsUpdater.start(); ???????}); ???????var wsUpdater = { ???????????socket: null, ???????????uid: null, ???????????start: function() { ???????????????var url = "ws://192.168.16.200:8009/chat"; ???????????????wsUpdater.socket = new WebSocket(url); ???????????????wsUpdater.socket.onmessage = function(event) { ???????????????????if(wsUpdater.uid){ ???????????????????????wsUpdater.showMessage(event.data); ???????????????????}else{ ???????????????????????wsUpdater.uid = event.data; ???????????????????} ???????????????} ???????????}, ???????????showMessage: function(content) { ???????????????$(‘#container‘).append(content); ???????????} ???????}; ???????function sendMsg() { ???????????var msg = { ???????????????uid: wsUpdater.uid, ???????????????message: $("#txt").val() ???????????}; ???????????wsUpdater.socket.send(JSON.stringify(msg)); ???????}</script></body></html>
原理都一样,但是用tornado实现起来,就清爽多了。
ps:再说一下腾讯的长轮询,如果你登录webQQ,或者web微信,你可以在network里面找到 pending的字样,这就是表示是使用的长轮询。
长轮询与轮询的区别就是:
? 轮询是过来以后看到没消息就立马去走了,但是长轮询不会立马走,而是在这等30秒(约定的时间)之后,如果一直没有消息,才返回,下一次来在等30秒,直到有消息了,这样有个缺点就是,拿到的消息并不是即时的。那腾讯这么大的公司,为什么不用性能更好的websocket呢?原因就是他是个大公司,必须要考虑兼容性,必须要保证所有的浏览器都能使用才行。
你可以从这里拿到完整 的示例代码
https://github.com/zEllis/websocket_demo
websocket介绍
原文地址:http://www.cnblogs.com/zhang-can/p/7994913.html