python学习笔记-websocket介绍

发布时间 2023-11-25 18:55:44作者: 子不语332

一、websocket介绍

概述
-http,socket实现,短链接,请求响应
-websocket,socket实现,双工通道,请求响应,推送
socket创建连接,不断开

二、websocket握手过程分析

socket入手
-服务端(socket服务端)
1、服务端开启socket,监听IP和端口
3、允许连接
*5、服务端接收特殊值【加密sha1,特殊值,magic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】
*6、加密后的值发送给客户端

-客户端(浏览器)
2、客服端发起连接请求(IP和端口)
*4、客户端生成一个xxx,【加密sha1,特殊值,magic string="258EAFA5-E914-47DA-95CA-C5AB0DC85B11"】,向服务端发送一段特殊值
*7、客户端接收到加密的值

收发数据:

三、基于python实现websocket握手

服务端:

import socket
import base64
import hashlib

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)

#等待用户连接
conn,address=sock.accept()
#握手消息
data=conn.recv(8096)

def 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_dict

#获取【握手消息】,magic string,sha1加密
#发送回客户端
headers=get_headers(data)
for k,v in headers.items():
    print(k,v)

magic_string='258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value=headers["Sec-WebSocket-Key"]+magic_string
ac=base64.b64encode(hashlib.sha1(value.encode("utf-8")).digest())

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"
response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
conn.send(bytes(response_str, encoding='utf-8'))
服务端

客户端浏览器

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        ws=new WebSocket("ws://127.0.0.1:8002");
        ws.onopen=function(){
            console.log(1111);
        }
    </script>
</body>
</html>
客户端

结果:

 四、WebSocket数据解析过程

1、位运算知识回顾

1.1、位运算
10010001
右移4位:00001001
左移4位:100100010000

1.2、异或运算
都是1:1
0,1:0
0,0:0
10010001

2、WebSocket数据解析

客户端和服务端传输数据时,需要对数据进行【封包】和【解包】。客户端的JavaScript类库已经封装【封包】和【解包】过程,但Socket服务端需要手动实现。

websocke服务端接收到的数据是字节,不能直接获得,要进行位运算,

例:获取的数据如下,info[0]就表示第一个字节
info=b'\x81\x85\x02\xe9\xee\xf0j\x8c\x82\x9cm'

解读:
opcode=info[0]&15 #opcode是第一个字节后四位,可通过跟15做异或运算获得

fin=info[0]>>7 #向右移7位获得

payload_len=info[1]&127 #payload_len是第二个字节与127进行运算获得。payload_len决定后面占的数据头长度

如果payload_len<126,头只占用这2个字节
如果payload_len=126,头要往后扩展2个字节,共占用4字节
如果payload_len=127(最大),头要往后扩展8个字节,共占用10字节

读取解码数据:后面数据不全占用,后面的前4个字节是masking key,后面才是客户端发送的数据,需要用masking key解密获得

解密方法,(后面的数据一个一个拿,然后一个一个解)

 

  info = conn.recv(8096)



  payload_len = info[1] & 127
 
    if 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)):
 
        chunk = decoded[i] ^ mask[i % 4]
 
        bytes_list.append(chunk)
 
    body = str(bytes_list, encoding='utf-8')
 
    print(body)
解包过程
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)
 
    return True
客户端封包

实例1:

服务端:

import socket
import base64
import hashlib

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)

#等待用户连接
conn,address=sock.accept()
#握手消息
data=conn.recv(8096)

def 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_dict

#获取【握手消息】,magic string,sha1加密
#发送回客户端
headers=get_headers(data)
# for k,v in headers.items():
#     print(k,v)

magic_string='258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value=headers["Sec-WebSocket-Key"]+magic_string
ac=base64.b64encode(hashlib.sha1(value.encode("utf-8")).digest())

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"
response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url'])
conn.send(bytes(response_str, encoding='utf-8'))

#发送消息封包
def send_msg(conn, msg_bytes):
    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)
    return True
#接收消息,处理并回复
while True:
    info = conn.recv(8096)
    payload_len = info[1] & 127
    if 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)):
        chunk = decoded[i] ^ mask[i % 4]
        bytes_list.append(chunk)
    body = str(bytes_list, encoding='utf-8')
    print(body)
    body=body+"==服务端已收到!"
    send_msg(conn,bytes(body,encoding="utf-8"))
服务端

客户端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <script>
        ws=new WebSocket("ws://127.0.0.1:8002");
        ws.onopen=function(){
            console.log(1111);
            ws.send('hello村长');
        };

        ws.onmessage=function(event){
            console.log(event)
        };
    </script>
</body>
</html>
客户端

结果:

另外:
*客户端ws.onclose()方法是服务器中断链接时执行
*FIN和opcode是接收到的数据过长分批处理时用到

五、通过框架实现,tornado的websocket实例

要点

服务端

Handler继承自tornado.websocket.WebSocketHandler

open #建立链接
on_message #接收消息
on_close #客户端关闭链接时执行

self.write_message #发送消息

render_string 用模板引擎去渲染

客户端

ws.send #发消息
ws.onmessage #收消息
ws.onclose #服务器端关闭时运行

》实例2:聊天室功能

 

服务端:

import tornado.web
import tornado.websocket

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html")

class ChatHandler(tornado.websocket.WebSocketHandler):
    def open(self,*args,**kwargs):
        '''
        客户端和服务器建立链接
        1、链接
        2、握手
        :param args:
        :param kwargs:
        :return:
        '''
        print("欢迎。。")

    def on_message(self, message):
        print(message)
        message=message+'good good'

        self.write_message(message)

    def on_close(self):
        pass

def run():

    settings={
        "template_path":"templates",
        "static_path":"static"}
    application=tornado.web.Application([
        (r"/",IndexHandler),
        (r"/chat",ChatHandler),
    ],**settings)
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    run()

客户端:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .container{
            border:1px solid #dddddd;
            height: 400px;
            overflow: auto;
        }
    </style>
</head>
<body>
<div style="width:750px;margin:0 auto;">
<h1>佛学聊天室</h1>
<div class="container"></div>
    <div class="input">
        <input type="text" id="txt"/>
        <input type="button" value="发送" id="btn" onclick="sendMessage();" />
    </div>

</div>
<script src="/static/jquery-3.4.1.min.js"></script>
<script>
    ws=new WebSocket("ws://127.0.0.1:8888/chat");
    ws.onmessage=function(event){
        console.log(event)
    };
     ws.onclose=function(){

        };
    function sendMessage() {
        ws.send($('#txt').val());
    }
</script>
</body>
</html>

》实例3:聊天室完善,将消息更新到聊天窗口。。

目录:

服务端:

import tornado.web
import tornado.websocket

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html")
users=set()
class ChatHandler(tornado.websocket.WebSocketHandler):
    def open(self,*args,**kwargs):
        '''
        客户端和服务器建立链接
        1、链接
        2、握手
        :param args:
        :param kwargs:
        :return:
        '''
        users.add(self)
        print("欢迎光临...")

    def on_message(self, message):
        print(message)
        message = message + ' - -(来自iPhone 15 Pro Max)'

        content=self.render_string('message.html',msg=message)
        for client in users:
            client.write_message(content)

    def on_close(self):
        users.remove(self)

def run():

    settings={
        "template_path":"templates",
        "static_path":"static"}
    application=tornado.web.Application([
        (r"/",IndexHandler),
        (r"/chat",ChatHandler),
    ],**settings)
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

if __name__ == "__main__":
    run()
服务端

模板message.html

<div style="margin: 20px;background-color: antiquewhite">{{ msg }}</div>

客户端:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .container{
            border:1px solid #dddddd;
            height: 400px;
            overflow: auto;
        }
    </style>
</head>
<body>
<div style="width:750px;margin:0 auto;">
<h1>佛学聊天室</h1>
<div class="container"></div>
    <div class="input">
        <input type="text" id="txt"/>
        <input type="button" value="发送" id="btn" onclick="sendMessage();" />
    </div>

</div>
<script src="/static/jquery-3.4.1.min.js"></script>
<script>
    ws=new WebSocket("ws://127.0.0.1:8888/chat");
    ws.onmessage=function(event){
        $('.container').append(event.data);
    };
     ws.onclose=function(){

        };
    function sendMessage() {
        ws.send($('#txt').val());
    }
</script>
</body>
</html>
客户端

效果: