flask websocket json_WebSocket及SocketIO技术简介

        我们团队最近对我们的WebSocket服务做了一些修改和升级,也积累了一些经验,借这个契机,我们想用这篇文章,和大家分享一下WebSocket的一些知识和应用经验。

WebSocket简介

103cf8fcbc0af6430a92d672fe501c7c.png

       说到WebSocket,其实不是一个新鲜的技术了,其标准最早在2011年就由IETF颁布,之后由W3C将WebSocket API定为标准。
简单概括一下,WebSocket是一个面向Web领域的网络通信协议,基于TCP提供客户端和服务器之间的全双工的通信。其目的主要是为了在Web中实现双向通信,避免了ajax轮询的查询方式,减少了网络资源损耗。

优点
  • 较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;

  • 更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据;

  • 保持连接状态;

  • 更好的二进制支持;

  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议;

  • 更好的压缩效果;

协议介绍

103cf8fcbc0af6430a92d672fe501c7c.png

       接下来,我们来详细介绍一下WebSocket的通信协议:
       为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。我们看一个握手的例子,然后详细讲解一下:
       客户端请求:

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15

       服务器回应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
  1. 客户端请求时使用101状态码,告诉服务器切换到WebSocket协议(Status Code: 101 );

  2. Connection 必须设置Upgrade,并且Upgrade字段必须设置为WebSocket,表示希望切换到WebSocket协议;

  3. Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行BASE-64编码,将结果作为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议;

  4. Sec-WebSocket-Version 表示支持的Websocket版本;

  5. Sec-WebSocket-Extensions 在握手阶段使用,由浏览器发送到服务器,表示一个或多个WebSocket的扩展,服务器接收后返回其支持的扩展的子集。

       WebSocket在握手过程结束后就可以进行通信,WebSocket的数据通信此时基于TCP,数据被封装进数据帧,其数据帧格式如下所示(本文不再详细解释数据帧),数据在数据帧中的payload处。

be170f718f851e1d191ea5681a7e2d36.png

浏览器API

103cf8fcbc0af6430a92d672fe501c7c.png

      既然知道了WebSocket协议的原理,我们来看看浏览器的原生API是什么样的,下面是W3C的代码示例:

// protocals是可选参数,可以指定WebSocket的子协议
// url既可以是ws协议的,也可以是wss协议的,同http一样,wss协议的WebSocket使用443的端口
let socket = new WebSocket(url [, protocols ] );

// WebSocket连接成功时的回调
socket.onopen = function () {
 setInterval(function() {
   if (socket.bufferedAmount == 0)
     socket.send(getUpdateData());
 }, 50);
};

// WebSocket接收到消息的回调,此处假设接收到的是字符串
mysocket.onmessage = function (event) {
 if (event.data == 'on') {
   turnLampOn();
 } else if (event.data == 'off') {
   turnLampOff();
 }
};

// 向服务器发送数据,data可以是string, Blob, ArrayBuffer, ArrayBufferView
// 这几种类型中的任何一种
socket.send( data );

// 浏览器主动关闭连接
socket.close();

       可以看到WebSocket和服务器之间通信并没有要求数据的格式,在实际应用中,开发者会根据自己的需要开发符合自己要求的数据格式,或者使用现有的其他开源方案。
       接下来我们就来说说,基于WebSocket进行高级封装的SocketIO。

SocketIO简介

103cf8fcbc0af6430a92d672fe501c7c.png

       Socket.IO 是一个面向实时 web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和运行在服务器端的服务端库。

       Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io也可以使用几种其他方法,例如Adobe Flash Sockets,JSONP,或是AJAX,并且提供完全相同的接口。尽管它可以被用作WebSocket的包装库,它还是提供了许多其他功能,比如广播至多个套接字,存储与不同客户有关的数据,和异步IO操作。

客户端使用SocketIO

103cf8fcbc0af6430a92d672fe501c7c.png

       npm安装SocketIO

npm install --save socket.io

       创建连接并监听和发送数据



// 使用websocket协议创建连接
const socket = io.connect(’https://mydomain.com‘, {
    transports: [ 'websocket' ]
});

// 监听连接事件
socket.on('connect', () => {
    console.log('connected !')
});

// 监听服务端传来的事件和数据
socket.on('myevent', (data) => {
   console.log(JSON.stringify(data));
});

// 向服务器发送数据
socket.emit('event_to_server', {
    'name': ’foo‘,
    'type': 'bar'
});

       SocketIO使用WebSocket创建连接时,并不需要显式的把协议指定成为wss,SocketIO会根据通讯协议选择正确的协议字段。

SocketIO特性

103cf8fcbc0af6430a92d672fe501c7c.png

  1. 命名空间(namespace)

       SocketIO允许我们在当前物理连接上划分多个命名空间,不同的命名空间之间的数据和事件相互分离。这个特性可以使我们在尽量少的TCP连接中区分我们不同的业务。

  1. 房间(room)

       在每个命名空间内,我们可以定义逻辑意义上的房间,套接字可以加入和离开任意的房间。在房间内的通讯也只有房间内的套接字可以接收到。

  1. 会话id(sid)

       连接创建后会生成一个唯一的id,该id在处于连接状态时一直生效,当连接发生终端并重新连接之后,该id就会更新。

  1. 安全性

       由于不管WebSocket还是Ajax,建立连接都是通过 HTTP,所以在服务端可以应用多种的授权和验证方式,例如:Basic access authentication, Token, JWT等。

  1. PING / PONG

       PING / PONG用来检测客户端和服务器的存活状态,在使用ajax时可以有效的知晓当前的连接状态,当使用WebSocket时,PING/PONG的数据为递增的整型数字。

Python后端使用SocketIO

103cf8fcbc0af6430a92d672fe501c7c.png

        我们的后端服务基于Flask,接下来我们看看SocketIO在Python后端中的使用。

        就像Django中有Django-SocketIO一样,我们使用的包是:Flask-SocketIO,首先安装Flask-SocketIO:

pip install Flask-SocketIO

        然后我们安装Flask:

pip install Flask

        开始编码:

from flask import Flask, request
from flask_socketio import SocketIO


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


@socketio.on('myevent')
def event_handler():
   sid = request.sid
    print(f'收到sid: {sid}传来的消息')
   send('I got your message')  # 向单个客户端回复
   

@app.route('/broadcast', methods=['GET'])
def broadcast_notification():
   socketio.emit('event', {    # 向所有已连接客户端广播
        'type': 'broadcast',
        'status': 'success'
   })
   return jsonify({})


if __name__ == '__main__':
   app.run('0.0.0.0', port=5001, debug=True)

        之后运行该文件,我们就得到了一个非常简单的SocketIO服务器。
        在我们这个简单的服务器程序中,实现了监听客户端事件,并向单个客户端回复消息,以及向所有已经连接的客户端广播事件。
        当然,我们也可以实现向某个房间(room)内发送消息,这类似于聊天室的功能,只需要加入如下的代码:

from flask_socketio import join_room, leave_room

@socket.on('join')
def join(data):
   room_id = data.get('room_id')
   join_room(room_id)  # 告知服务器加入房间
   

@socket.on('leave')
def leave(data):
   room_id = data.get('room_id')
   leave_room(room_id) # 离开房间


@app.route('/chat_in_room/', methods=['GET'])
def chat_in_room(room_id):
   socketio.emit('room_event', {
        'type': 'chat_in_room',
        'status': 'success'
   }, room=room_id)  # 指定房间名称,只向一个房间内的套接字发送
   return jsonify({})

        同时,客户端也需要加入代码:

const socket = io.connect(’https://mydomain.com‘, {
    transports: [ 'websocket' ]
});

socket.on('connect', () => {
   
   socket.emit('join', {
        'room_id': 'room_topic'
   });
});

// 需要离开房间时调用该方法
function leave_room(){
   socket.emit(’leave‘, () => {
        ’room_id‘: 'room_topic'
   });
}

        如上所示,当服务端的接口'/chat_in_room'被调用时,就会向指定的房间内发送消息,并且只有加入了该房间的客户端套接字可以接收到数据。
        需要强调的是,向房间内发送消息的功能只能由服务器端程序发起。原因是客户端只和服务器端建立了连接,客户端并没有向其他客户端套接字发送数据的能力。想要实现客户端向房间发送数据的功能的话,只能由服务器代为转发。
        接下来给大家演示服务端向指定客户端套接字发送数据的功能:

@app.route('/unicast/', methods=['GET'])
def unicast(sid):
   socketio.emit('unicast_event', 'an unicast message', room=sid)
   return jsonify({})

        只要知道客户端套接字的sid,就可以只向该套接字发送数据。

Flask-SocketIO的部署

103cf8fcbc0af6430a92d672fe501c7c.png

        1. 最简单的部署方式是直接调用app.run()来启动服务,当服务启动后会尝试查找eventlet和gevent,如果eventlet或gevent当前环境下已安装的话,就使用eventlet或gevent做web服务器,并运行为product模式;如果两者都没有安装,就运行为development模式;
        2. 使用gunicorn运行后台服务,这也是我们在使用的方式。当选用gunicorn时,可以使用eventlet或者gevent作为worker,这时gunicorn的启动命令为:

gunicorn --worker-class eventlet -w 1 module:app

        或者

gunicorn -k gevent -w 1 module:app

        这里需要特别注意的是,由于gunicorn负载均衡策略的问题,worker的数量应当设置成1,否则连接将会变得混乱而无法正常使用。

Flask-SocketIO多实例运行

103cf8fcbc0af6430a92d672fe501c7c.png

        Flask-SocketIO在多个实例中运行时会遇到一点问题:每个客户端套接字只能连接一台服务器;同样,一台服务器中只维护了和自己建立连接的客户端列表。当服务器端想要向某一个或者一批客户端套接字发送消息时,发送方不能保证指定的客户端套接字和自己建立了连接。那么怎么解决这个问题呢?

        Python-SocketIO为我们提供了一个很好的解决方案 —— 支持使用消息队列,在多个运行的实例间同步消息发送的状态。Python-SocketIO支持多种消息队列,例如: RabbitMQ, Redis, 或其他Kombu支持的消息队列。

        我们以Redis举例:当服务端实例A要向C1,C2两个套接字发送数据时,在发送的同时向Redis发出一个Pub命令,此时所有其他Flask-SocketIO实例会接到Redis的Sub事件,C1,C2的信息以及待发送的数据会通过事件参数传递,该Flask-SocketIO实例根据参数中的信息,向C1,C2发送数据。这样,可以保证客户端不管连接到的是哪个服务实例,都能正确的接收到消息。

        下面我们看看怎么通过配置实现这个功能:
        1. Redis: 要使用Redis的消息队列,需要保证环境中有redis这个包:

# 安装redis包
pip install redis

# 配置redis消息队列
socketio = SocketIO(app, message_queue='redis://')

        2. RabbitMQ: 要使用RabbitMQ的消息队列,需要安装Kombu这个包,Kombu支持包括RabbitMQ在内的多种消息队列:

# 安装Kombu
pip install kombu

# 配置使用RabbitMQ消息队列
socketio = SocketIO(app, message_queue='amqp://')

        Flask-SocketIO和Python-SocketIO比较好的封装了对于消息队列的使用,这让使用者可以比较方便快速的实现一个高可用的SocketIO服务端集群。

总结

103cf8fcbc0af6430a92d672fe501c7c.png

        我们刚才从WebSocket谈到了SocketIO,又从SocketIO谈到了服务端SocketIO的使用以及关键技术点。可以看到WebSocket及其衍生的技术还是很多的,其主旨都是为了解决Web领域通信的难题,但是在生产环境中使用时还是会遇到很多网络环境带来的问题。

        本篇文章先做个铺垫,以后当我们有更深入地研究,或是对技术相关领域有些新的理解和体会时,也会第一时间分享给大家。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值