websocket理解

简介

在实际开发中,可能会出现一个需求场景,要求网页的数据可以实时更新。在这种情况下,我们一般会采用轮询的方式,间隔性获取数据,即通过定时器间隔性请求相应接口获取数据,此方式由于是不断请求服务器,资源开销相对较大,且由于数据更新是间隔性,会导致数据时效性欠缺,可能会出现部分延迟,因此衍生出另一种方式:长轮询,长轮询一般是客户端请求服务端,但服务器不是即时返回,而当内容有所更新时,服务器会返回相应内容给客户端,从名义上为服务器向客户端推送信息。
综合以上,可以总结出轮询、长轮询的优缺点
轮询:

1.服务器的CPU、带宽资源消耗较大,也可能会存在大量的无效请求
2.由于是间隔性获取数据,数据时效性欠缺,会有部分延迟,当然,间隔时间越短,相对的数据的时效性越好,但会导致资源开销大

长轮询

1.解决了轮询过程中的数据时效性较差的问题
2.由于一直在等待服务器返回,对于内存的消耗会相对较大

轮询、长轮询都或多或少存在一些缺陷,因此websocket协议就此诞生,它就是为了实现消息的实时更新而出现的。

websocket简介

websocket协议是网络传输协议,位于应用层,可在单个tcp连接上进行全双工通信,能够更好的节省资源开销实现消息通讯,websocket基于tcp协议,所以也是需要建立连接和关闭连接的,不像tcp的三次握手,客户端与服务器建立一次连接后即可实现双向通信。
为了实现和HTTP的兼容性,WebSocket握手使用HTTP的Upgrade头将HTTP协议转换成Websocket协议。
作为一种协议,websocket自然也是有其用于协议控制的头部信息的,但是相对于HTTP请求每次都要带上完整的头部信息,传输数据时,websocket数据包的头部信息就相对较小,从而降低了控制开销。

特点

  • 全双工通信;
    在数据传输中,有三种类型;单工通信、半双工通信、全双工通信,
单工通信:只能向一个方向进行传输
半双工通信:可以双向传输,但是不能同时进行
全双工通信:可以双向传输,也可以同时传输
  • 二进制帧
    采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,相比http/2,WebSocket更侧重于“实时通信”,而HTTP/2 更侧重于提高传输效率
  • 协议名与http协议类似
    引入ws和wss分别代表明文和密文的websocket协议,且默认端口使用80或443,与http几乎一致

websocket建立连接的流程

1.建立连接, 客户端与服务器端连接
2.握手:验证服务器是否支持websocket
    发送的数据如下:
    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

    Sec-WebSocket-Key:浏览器随机生成的key,base64生成
    Sec-WebSocket-Protocol:websocket的协议
    Sec-WebSocket-Version:websocket的版本
    其中客户端有一个:Sec-WebSocket-Key字段,服务器有一个:Sec-WebSocket-Accept字段,
        浏览器发送随机生成的key到服务器,
        服务器返回对应Sec-WebSocket-Accept key的原理:
            取出Sec-WebSocket-Key(随机产生),与一个magic string “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” 连接成一个新的key串
            将新的key串SHA1编码,生成一个由多组两位16进制数构成的加密串
            把加密串进行base64编码生成最终的key,这个key就是Sec-WebSocket-Accept
            简化:取出浏览器发送的key 与magic string连接成一个新字符串
                对新key进行sha1加密返回新的加密串
                对新的加密串进行base64加密并将结果返回给浏览器
        浏览器接收到key与自己本地加密的key进行比较,如果验证通过则说明支持websocket协议,则进行下一步收发数据动作
     为了表示服务器同意和客户端进行Socket连接, 服务器端需要使用客户端发送的这个Key进行校验 ,然后返回一个校验过的字符串给客户端,客户端验证通过后才能正式建立Socket连接。       
3.收发数据
4.关闭连接

简易流程如下:

1.建立连接
2.握手:校验服务器是否支持websocket协议:
3.收发数据
4.关闭连接(一般为长连接)

websocket的优点

  • 较少的控制开销:数据包头部协议较小,不同于http每次请求需要携带完整的头部
  • 更强的实时性:相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少
  • 保持连接状态:创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证
  • 更好的二进制支持:定义了二进制帧,更好处理二进制内容
    支持扩展:用户可以扩展websocket协议、实现部分自定义的子协议
  • 更好的压缩效果:Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率

websocket的异常处理

由于websocket可能由于网络波动、连接异常断开导致无法正常进行通信,因此对于一般websocket而言,需要添加异常处理机制,即在出现异常断开时进行重连,因此衍生出心跳重连机制。

心跳重连机制:
    原因:由于网络原因导致的websocket连接未能正常关闭,从而导致服务器继续推送数据导致数据丢失,因此需要一种判断客户端和服务端是否存货的方式
    概念: 心跳机制是每隔一段时间会向服务器发送一个数据包,告诉服务器自己还活着,同时客户端会确认服务器端是否还活着,如果还活着的话,就会回传一个数据包给客户端来确定服务器端也还活着,否则的话,有可能是网络断开连接了。需要重连~

需要注意的是,在websocket的一方出现异常时,需要先将原始连接关闭后再建立新的连接,如果原始连接还在,可能会出现消息数据发送错误的情况

websocket的使用场景

  • 视频平台的弹幕发送
  • 聊天室
  • 协同编辑:例如在线文档的多人协作
  • 服务器推送客户端消息
  • 其他需要保持长时间实时更新的任务或需求

websocket在python中的简单使用

客户端

首先先创建一个简易的服务端,使用flask_sockets创建

from flask import Flask
from flask_sockets import Sockets

app = Flask(__name__)
socket = Sockets(app=app)

@socket.route('/start_websocket')
def start_websocket(ws):
    try:
        while not ws.closed:
            data = ws.receive()
            print(f'recv data:{data}')
            ws.send(data)
    except Exception as e:
      print(f'start_websocket error:{e}')


if __name__ ==  "__main__":
    from gevent import pywsgi
    from geventwebsocket.handler import WebSocketHandler
    server = pywsgi.WSGIServer(listener=("0.0.0.0", 5050), application=app, handler_class=WebSocketHandler)
    print('server start')
    server.serve_forever()
  • create_connection连接
    此方法不建议使用,链接不稳定,容易断,并且连接很耗时
pip3 install websocket-client
from websocket import create_connection

url='ws://127.0.0.1:5050/start_websocket'
ws =  create_connection(url)
ws.send('hello')
while True:
    recv_data = ws.recv()
    print(f'recv_data:{recv_data}')

  • ws4py.client.threadedclient import WebSocketClient
    此方法很容易连接,获取数据的速度也挺快
# 需要安装ws4py
pip3 install ws4py
from ws4py.client.threadedclient import WebSocketClient

class EchoClient(WebSocketClient):
    def opened(self):
        self.send('hello')

    def closed(self, code, reason):
        print(("Closed down", code, reason))

    def received_message(self, m):
        print("=> %d %s" % (len(m), str(m)))

try:
    ws = EchoClient('ws://127.0.0.1:5050/start_websocket', protocols=['http-only', 'chat'])
    ws.connect()
    ws.run_forever()
except KeyboardInterrupt:
    ws.close()
  • 使用websocket自带的Websocket
    与第一种方式类似,容易断,连接耗时长
pip install websocket
from websocket import WebSocket

try:
    ws = WebSocket()
    url='ws://127.0.0.1:5050/start_websocket'
    ws.connect(url)
    ws.send('hello')
    while True:
        data = ws.recv()
        print(f'data:{data}')
except Exception as e:
    print(f'start websocket fail:{e}')
  • 使用websocket模块的WebSocketApp
    使用方法类似于js的websocket api
# 需要安装websocket
pip3 install websocket
from websocket import WebSocketApp
import websocket

def open(ws:WebSocketApp):
    print(f'ws:{ws}')
    ws.send('hello')


def receive_msg(ws, msg):
    print(f'ws:{ws}, msg:{msg}')

def error(ws, error):
    print(f'ws:{ws}, error:{error}')

def close(ws, status, code):
    print(f'ws:{ws}, status:{status}, code:{code}')


if __name__ == "__main__":
    # 打开可追溯性,打印项目日志debug
    websocket.enableTrace(True)
    url='ws://127.0.0.1:5050/start_websocket'
    ws = WebSocketApp(url=url, on_open=open, on_message=receive_msg, on_error=error, on_close=close)
    ws.run_forever()

结果如下

--- request header ---
GET /start_websocket HTTP/1.1
Upgrade: websocket
Host: 127.0.0.1:5050
Origin: http://127.0.0.1:5050
Sec-WebSocket-Key: xP5FfO+M3UzHuug2rjAMMA==
Sec-WebSocket-Version: 13
Connection: Upgrade


-----------------------
--- response header ---
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: eKkIqEQa+7niXqEbW6bHN1tMnA0=
-----------------------
ws:<websocket._app.WebSocketApp object at 0x7f8d8721acc0>
++Sent raw: b'\x81\x85\xa7\xc1\xf2\x11\xcf\xa4\x9e}\xc8'
++Sent decoded: fin=1 opcode=1 data=b'hello'
++Rcv raw: b'\x81\x05hello'
++Rcv decoded: fin=1 opcode=1 data=b'hello'
ws:<websocket._app.WebSocketApp object at 0x7f8d8721acc0>, msg:hello

由此可以看出,发送的key为eKkIqEQa+7niXqEbW6bHN1tMnA0=,协议为websocket,版本为13

服务端

  • flask_sockets
    上文已经写了一个简单的服务端程序
from flask import Flask
from flask_sockets import Sockets

app = Flask(__name__)
socket = Sockets(app=app)

@socket.route('/start_websocket')
def start_websocket(ws):
    try:
        while not ws.closed:
            data = ws.receive()
            print(f'recv data:{data}')
            ws.send(data)
    except Exception as e:
      print(f'start_websocket error:{e}')


if __name__ ==  "__main__":
    from gevent import pywsgi
    # 将连接转为websocket协议,具体连接过程详见上述连接过程的建立连接
    from geventwebsocket.handler import WebSocketHandler
    server = pywsgi.WSGIServer(listener=("0.0.0.0", 5050), application=app, handler_class=WebSocketHandler)
    print('server start')
    server.serve_forever()

WebSocketHandler:就是将连接转为websocket协议,存在一个magic string,以及支持的版本

SUPPORTED_VERSIONS = ('13', '8', '7')
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

注意事项

flask和Flask-Sockets版本不能过高, 过高会导致无法正常启动websocket
flask:1.1.2
Flask-Sockets:1.0.1
Werkzeug:1.0.0
另外,此方法在官方文档中已经不再推荐

参考链接 flask_websockts

  • flask-sockeio
pip3 install flask-socketio

必要条件:
Flask SocketIO与Python 3.6+兼容。此包所依赖的异步服务可以在三个选项中选择:
1.eventlet
2.gevent
3.Werkzeug

from flask import Flask
# send:用于自带的event发送数据, emit用于定制的event发送数据
from flask_socketio import SocketIO,send, emit


app = Flask(__name__)
app.config['SECRET_KEY'] = 'secret'
socketio = SocketIO(app)

'''
使用socketio.on装饰器来接收从客户端发送来的WebSocket信息。
socketio.on的第一个参数是event名称。connect, disconnect, message和json是SocketIO产生的特殊events。
其它event名称被认为是定制events。
'''
@socketio.on('message', namespace='/start_websocket')
def handle_message(message):
    print(f'handle_message:{message}')
    send(message)

@socketio.on('json')
def handle_json(json):
    print('received json: '+ str(json))

@socketio.on('my event')
def handle_my_custom_event(json):
    print('received json: ' + str(json))

# 声明命名空间,方便多路复用, 类似于flask_websockets下的socket.route中的参数
@socketio.on('my event2', namespace='/test')
def handle_my_custom_namespace_event(json):
    print('received json: ' + str(json))

if __name__ == "__main__":
    # 函数socketio.run()封装了网络服务器的启动部分,并且代替了flask开发服务器的标准启动语句app.run()
    socketio.run(app)

与其他服务端不同,客户端还需引用Socket.IO库并且建立一个连接:

<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js" integrity="sha512-q/dWJ3kcmjBLU4Qc47E4A9kTB4m3wuTY7vkFJDTZKjTs8jhyGQnaUrxa0Ytd0ssMZhbNua9hE+E7Qv1j+DyZwA==" crossorigin="anonymous"></script>
<script type="text/javascript" charset="utf-8">
    var socket = io();
    socket.on('connect', function() {
        socket.emit('my event', {data: 'I\'m connected!'});
    });
</script>

目前发现比较适用与前端界面到后端的交互

参考文档:flask_socketio官网flask_socketio中文翻译

  • flask-sock
    此方法不再依赖于其他的异步组件
pip3 install flask-sock
from flask import Flask
from flask_sock import Sock


app = Flask(__name__)
sock = Sock(app)
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 25}

@sock.route('/start_websocket')
def start_websocket(ws):
    while True:
        data = ws.receive()
        print(f'data:{data}')
        if data != 'close':
            ws.send(data)
        else:
            ws.close()
if __name__ == "__main__":
    app.run(port=5050)

flask-sock的适用方式与flask_sockets类似,但是,它不依赖于gevent等,类似于http请求接口一样,并且自带发送ping消息的功能。
参考文档:flask_sockflask_sock文档

posted @   形同陌路love  阅读(457)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示