WebSocket、HTTP 与 TCP
HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。我们可以把这些高级协议理解成对 TCP 的封装。
既然大家都使用 TCP 协议,那么大家的连接和断开,都要遵循 TCP 协议中的三次握手和四次挥手 ,只是在连接之后发送的内容不同,或者是断开的时间不同.
我们知道http是基于tcp实现的,浏览器可以看做是客户端socket,这个socket在开发的时候遵循http规范,数据在发送并接收成功的时候就断开socket了,那么如果我们的socket一直连接不断开,并且发送数据的时候都把数据进行加密,这样是不是也是可以的呢?这种想法就可以引出websocket了,值得一提的是,websocket的兼容性不是所有版本的浏览器都支持(取决于是否能够new WebSocket), 但是随着技术的发展,浏览器对websocket的支持肯定会越来越好。
对于 WebSocket 来说,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
Websocket和socket不同,Websocket工作在应用层,而socket是基于门面模式的一种封装,可以让程序员简便地写网络通信程序
flask实现websocket
安装gevent-websocket模块
from flask import Flask,render_template,request
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
import json
app = Flask(__name__)
USERS = {
\'1\':{\'name\':\'jack\',\'count\':0},
\'2\':{\'name\':\'tuple\',\'count\':0},
\'3\':{\'name\':\'lily\',\'count\':100},
}
@app.route(\'/index\')
def index():
return render_template(\'index.html\',users=USERS)
# http://127.0.0.1:5000/message 既可以接受http,也可以接受websocket
WEBSOCKET_LIST = []
@app.route(\'/message\')
def message():
ws = request.environ.get(\'wsgi.websocket\')
if not ws:
return \'您使用的是Http协议\'
WEBSOCKET_LIST.append(ws)
while True:
# 当关闭webscoekt的时候cid 得到None
cid = ws.receive()
if not cid:
WEBSOCKET_LIST.remove(ws)
ws.close()
break
old_count = USERS[cid][\'count\']
new_count = old_count + 1
USERS[cid][\'count\'] = new_count
for client in WEBSOCKET_LIST:
client.send(json.dumps({\'cid\':cid,\'count\':new_count}))
return \'\'
if __name__ == \'__main__\':
# 当请求是websocket的时候,那么就用WebSocketHandler进行处理
http_server = WSGIServer((\'0.0.0.0\', 5000), app, handler_class=WebSocketHandler)
http_server.serve_forever()
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>投票系统</h1>
<ul>
{% for k,v in users.items() %}
<li onclick="vote({{k}})" id="id_{{k}}">{{v.name}}<span>{{v.count}}</span></li>
{% endfor %}
</ul>
<script src="{{ url_for(\'static\',filename=\'jquery-3.3.1.min.js\')}}"></script>
<script>
var ws = new WebSocket(\'ws://127.0.0.1:5000/message\');
// 事件监听绑定回调函数,一旦有消息发送过来就会调用函数
ws.onmessage = function (event) {
/* 服务器端向客户端发送数据时,自动执行 */
// {\'cid\':cid,\'count\':new}
var response = JSON.parse(event.data);
$(\'#id_\'+response.cid).find(\'span\').text(response.count);
};
function vote(cid) {
ws.send(cid)
}
</script>
</body>
</html>
webscoket 原理
websocket的握手需要借助http, new WebSocket的时候,服务端接收到的的请求头信息有有三个比较特殊:
Sec-WebSocket-Key:FUH2Nige4Npq/InFS0OoJQ==
Upgrade:websocket
Connection: Upgrade
表示这次请求是用于升级http为websocket,并把一个随机字符串发送过去,此时服务端应该拿到这个字符串进行加密然后发送过去,浏览器对这个字符串也加密,比较加密后的字符串和服务端发送过来的字符串是否一致,一致就表名握手成功,此时response header为
Connection:Upgrade
Sec-WebSocket-Accept:+lZCKceYfJsmNj6q0GYPa9r9LXE=
Upgrade:websocket
状态码为:101 Switching Protocols,表示协议切换成功,此时,后面的通信就和http没有半毛钱关系了。
自定义实现websocket服务端
def get_headers(data):
"""
将请求头格式化成字典
:param data:
:return:
"""
header_dict = {}
data = str(data, encoding=\'utf-8\')
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_dict
def send_msg(conn, msg_bytes):
"""
WebSocket服务端向客户端发送消息
:param conn: 客户端连接到服务器端的socket对象,即: conn,address = socket.accept()
:param msg_bytes: 向客户端发送的字节
:return:
"""
import struct
token = b"\\x81"
length = len(msg_bytes)
if length < 126:
token += struct.pack("B", length)
elif length <= 0xFFFF:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)
msg = token + msg_bytes
conn.send(msg)
import base64
import hashlib
import socket
sock = 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)
while 1:
# 1. 等待用户连接
conn, address = sock.accept()
# 2. 接收验证消息
msg = conn.recv(8096)
msg_dict = get_headers(msg)
# 3. 对数据加密, 其中魔法字符串\'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\' 是固定的值
value = msg_dict[\'Sec-WebSocket-Key\'] + \'258EAFA5-E914-47DA-95CA-C5AB0DC85B11\'
ac = base64.b64encode(hashlib.sha1(value.encode(\'utf-8\')).digest())
# 4. 将加密后的结果返回给浏览器,发送的时候是用http通信的,回消息的时候也要封装成http格式
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://127.0.0.1:8002\\r\\n\\r\\n"
response = response_tpl %(ac.decode(\'utf-8\'),)
conn.send(response.encode(\'utf-8\'))
# 5. 接受浏览器发送过来的加密数据,并进行解密,发送数据也需要进行加密,加密和解密规则可以参考官方
while True:
info = conn.recv(8096)
# 10111111 去掉第一位,和 01111111 相与就行
payload_len = info[1] & 127
if payload_len == 127:
# 如果payload_len是127,那么需要往后面取8个字节作为头信息,头部总共10个字节
extend_payload_len = info[2:10]
mask = info[10:14]
decoded = info[14:]
elif payload_len == 126:
# 如果payload_len是126,那么需要往后面取2个字节作为头信息,头部总共4个字节
extend_payload_len = info[2:4]
mask = info[4:8]
decoded = info[8:]
else:
# 如果payload_len小于126,那么头部总共2个字节
extend_payload_len = None
mask = info[2:6]
decoded = info[6:]
bytes_list = bytearray()
# decoded 是真实的数据
for i in range(len(decoded)):
chunk = decoded[i] ^ mask[i % 4]
bytes_list.append(chunk)
body = str(bytes_list, encoding=\'utf-8\')
print(\'浏览器发送过来的数据\',body)
#
# import time
# time.sleep(2)
send_msg(conn,body.encode(\'utf8\'))