一 Websoket简介
WebSocket 是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
轮询:
- 不断向服务器发起询问,服务器还不断回复
- 浪费带宽, 浪费前后端资源
- 保证数据实时性
长轮询:
- 客户端向服务器发起消息,服务端轮询, 放在另外一个地方,客户端去另外一个地方去拿数据
- 服务端轮询, 放在另外一个地方, 直接推给客户端
释放客户端资源,服务器压力不可避免, 节省带宽资源
数据实时性差
websocket: 是一个新的协议
- 前后端hold住等待消息
- 客户端与服务器建立长连接
- 彻底解决实时性
- 解决了占用带宽的问题
- 解决资源
二 websocket的应用
安装 pip instal gevent-websocket
启动websocket
from flask import Flask, request from geventwebsocket.websocket import WebSocket from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler app = Flask(__name__) @app.route("/ws") def ws(): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket while 1: msg = user_socket.receive() print(msg) user_socket.send(msg) if __name__ == '__main__': http_serv = WSGIServer(("0.0.0.0", 9527), app, handler_class=WebSocketHandler) http_serv.serve_forever() # app.run("0.0.0.0", 9527, debug=True)
前端创建webscoket
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:9527/ws"); ws.onmessage = function (data) { console.log(data.data); } </script> </body> </html>
三 WebSocket群聊
1 带昵称的群聊
flask代码:
from flask import Flask, request, render_template from geventwebsocket.websocket import WebSocket from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler import json app = Flask(__name__) user_socket_dict = {} @app.route("/") def index(): return render_template('index.html') @app.route("/ws/<nickname>") def ws(nickname): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: user_socket_dict[nickname] = user_socket else: return render_template("index.html", message="请使用websocket链接") while 1: try: msg = user_socket.receive() except: user_socket_dict.pop(user_socket) for user_nickname, socket in user_socket_dict.items(): if user_socket != socket: try: socket.send(json.dumps({"sender": nickname, "msg": msg})) except: continue if __name__ == '__main__': http_serv = WSGIServer(("0.0.0.0", 9527), app, handler_class=WebSocketHandler) http_serv.serve_forever()
前端代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>欢迎使用群聊 </h1> <p>{{ message }}</p> <p> 昵称:<input type="text" id="nickname"> <button id="send" onclick="createSocket()">链接群聊</button> </p> 消息:<input type="text" id="send_text"> <button id="send" onclick="send_msg()">发送</button> <div id="text_div" style="border: 2px solid; 500px; height: 800px;"> </div> <script type="text/javascript"> var ws = null; function createSocket() { var nickname = document.getElementById('nickname').value; ws = new WebSocket("ws://127.0.0.1:9527/ws/" + nickname); ws.onmessage = function (data) { var text_div = document.getElementById("text_div"); var obj_data = JSON.parse(data.data); var add_msg = "<p>" + obj_data.sender + ":" + obj_data.msg + "</p>"; text_div.innerHTML += add_msg }; } function send_msg() { var nickname = document.getElementById('nickname').value; var msg = document.getElementById("send_text").value; var text_div = document.getElementById("text_div"); var add_msg = "<p style='text-align:right'>" + msg + ":" + nickname + "</p>"; text_div.innerHTML += add_msg; ws.send(msg) } </script> </body> </html>
2 带昵称私聊
from flask import Flask, request, render_template from geventwebsocket.websocket import WebSocket from gevent.pywsgi import WSGIServer from geventwebsocket.handler import WebSocketHandler import json app = Flask(__name__) user_socket_dict = {} @app.route("/") def index(): return render_template('index.html') @app.route("/ws/<nickname>") def ws(nickname): user_socket = request.environ.get("wsgi.websocket") # type:WebSocket if user_socket: user_socket_dict[nickname] = user_socket else: return render_template("index.html", message="请使用websocket链接") while 1: msg = user_socket.receive() msg_dict = json.loads(msg) # {"to_user": user_id, msg:"hello"} to_user = msg_dict.get("to_user") to_user_socket = user_socket_dict.get(to_user) send_str = json.dumps({"sender": nickname, "msg": msg_dict.get("msg")}) to_user_socket.send(send_str) if __name__ == '__main__': http_serv = WSGIServer(("0.0.0.0", 9527), app, handler_class=WebSocketHandler) http_serv.serve_forever()
html:
<html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> 你的名字:<input type="text" id="nkname"> <button onclick="open_chat()">登录聊天室</button> <p>发送至<input type="text" id="to_user"></p> 消息<input type="text" id="message"> <button onclick="send_msg()">发送</button> <div id="chat_list"> </div> </body> <script type="application/javascript"> var ws = null; function open_chat() { var nkname = document.getElementById('nkname').value; ws = new WebSocket("ws://127.0.0.1:9527/my_ws/" + nkname); ws.onopen = function () { alert('欢迎' + nkname + '登录对骂平台!!') }; ws.onmessage = function (EventMessage) { var chat = JSON.parse(EventMessage.data); var p = document.createElement('p'); p.style.cssText = 'width : 250px; text-align:left'; p.innerText = chat.from_user + '-->' + chat.message; document.getElementById('chat_list').appendChild(p); }; ws.onclose = function () { console.log('断开连接了,啥情况啊? 搞事情啊'); }; } function send_msg() { var message = document.getElementById('message').value; var from_user = document.getElementById('nkname').value; var to_user = document.getElementById('to_user').value; var send_str = { message: message, from_user: from_user, to_user: to_user }; ws.send(JSON.stringify(send_str)); var p = document.createElement('p'); p.style.cssText = " 250px;text-align: right"; p.innerText = send_str.message + "<-我"; document.getElementById('chat_list').appendChild(p); } </script> </html>
四 websocket握手
浏览器 - 链接 - 服务器
浏览器 - 发送 - 字符串(请求头) - Sec-Websocket-Key - 服务器
服务器 - 获取Sec-Websocket-Key的值+magic_string - sha1 - base64加密 - 拼接一个响应头 Sec-Websocket-Accpet
服务器 - 拼接好的响应头 - 浏览器
浏览器 - Sec-Websocket-Accpet解密得到Sec-WebSocket-Key - 握手成功
客户端发送一个请求
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
这段类似HTTP协议的握手请求中多了以下几个东西
Upgrade: websocket
Connection: Upgrade
是告诉服务器我想升级为websocket协议
Sec-WebSocket-Key
是一个Base64
加密的密钥
Sec-WebSocket-Protocol
是用于标识客户端想和服务端使用哪一种子协议(都是应用层的协议,比如 chat 表示采用 “聊天” 这个应用层协议)。
Sec-WebSocket-Version
是告诉服务器所使用的协议版本
Origin
Origin可以预防在浏览器中运行的脚本,在未经 WebSocket 服务器允许的情况下,对其发送跨域的请求。浏览器脚本在使用浏览器提供的 WebSocket 接口对一个 WebSocket 服务发起连接请求时,浏览器会在请求的 Origin 中标识出发出请求的脚本所属的源,然后 WebSocket 在接受到浏览器的连接请求之后,就可以根据其中的源去选择是否接受当前的请求。
服务端响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
上面提到了这个101
是状态码,websocket是使用 HTTP 协议的101状态码进行协议切换。
Sec-WebSocket-Accept
这个值表示服务器同意握手建立连接,是客户端传输过来的Sec-WebSocket-Key跟“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”拼接后,用SHA-1加密,并进行BASE-64编码得来的,Sha1(Sec-WebSocket-Key+258EAFA5-E914-47DA-95CA-C5AB0DC85B11)。
客户端收到Sec-WebSocket-Accept后,将本地的Sec-WebSocket-Key进行同样的编码,然后比对。
五 websocket加解密
解密
# b'x81x83xceHxb6x85xffzx85' hashstr = b'x81x83xceHxb6x85xffzx85' # b'x81 x83 xceHxb6x85xffzx85' # 将第二个字节也就是 x83 第9-16位 进行与127进行位运算 payload = hashstr[1] & 127 # 拿出第二个字节hashstr[1] print(payload) if payload == 127: extend_payload_len = hashstr[2:10] mask = hashstr[10:14] data = hashstr[14:] # 当位运算结果等于127时,则第3-10个字节为数据长度 # 第11-14字节为mask 解密所需字符串 # 则数据为第15字节至结尾 if payload == 126: extend_payload_len = hashstr[2:4] mask = hashstr[4:8] data = hashstr[8:] # 当位运算结果等于126时,则第3-4个字节为数据长度 # 第5-8字节为mask 解密所需字符串 # 则数据为第9字节至结尾 if payload <= 125: extend_payload_len = None mask = hashstr[2:6] data = hashstr[6:] # 当位运算结果小于等于125时,则这个数字就是数据的长度 # 第3-6字节为mask 解密所需字符串 # 则数据为第7字节至结尾 str_byte = bytearray() # 解密数据 for i in range(len(data)): byte = data[i] ^ mask[i % 4] str_byte.append(byte) print(str_byte.decode("utf8"))
加密
import struct msg_bytes = "hello".encode("utf8") token = b"x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) # 固定打包写法 三种不同打包方式 elif length == 126: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes print(msg)