使用Flask-Socketio进行WebSocket通信

需要写一个Web页面监控后台程序的运行状态,一开始的想法是将后台程序的log保存到redis,再从Web端使用ajax定时获取redis里的数据。还没开始撸代码就觉得这种方法有问题,一定有更优雅的方法实现。

HTTP协议都需要从客户端发起,服务器应答。而这里的情况是客户端并不知道后台程序的状态什么时候发生改变,所以需要让服务器可以主动地发送数据给客户端。

google一圈后发现WebSocket可以完美满足要求,所以了解了一下这个HTML5的协议,并且学习了下它的Flask插件—Flask-Socketio。

WebSocket

WebSocket一种在单个 TCP 连接上进行全双工通讯的协议。
WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的101状态码进行协议切换,使用的 TCP 端口是80,可以用于绕过大多数防火墙的限制。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端直接向客户端推送数据而不需要客户端进行请求,在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并允许数据进行双向传送。


WebSocket是一个持久化的协议,在浏览器中可以看出,请求一直处于pending状态。而一般HTTP协议的request一旦收到response,这个请求就结束了。


Websocket使用HTTP协议建立连接,并转换成WebSocket。查看它的请求头和响应头,会发现很多和一般HTTP请求不一样的地方。
请求头中:

  • Upgrade和Connection字段告诉服务器切换协议;
  • Sec-WebSocket开头的字段用于验证和统一协议版本细节;

响应头中:

  • Upgrade和Connection表明已经切换到Websocket协议;
  • Sec-WebSocket-Accept用于确认验证信息。

Socket.IO

Socket.IO 是一个面向实时 web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库。两者有着几乎一样的API。像Node.js一样,它也是事件驱动的.
Socket.IO 主要使用WebSocket协议。但是如果需要的话,Socket.io可以回退到几种其它方法,例如Adobe Flash Sockets,JSONP拉取,或是传统的AJAX拉取,并且在同时提供完全相同的接口。尽管它可以被用作WebSocket的包装库,它还是提供了许多其它功能,比如广播至多个套接字,存储与不同客户有关的数据,和异步IO操作。

Socket.IO的客户端常用API比较简单,先看一段示例代码:

<script src="socket.io.min.js"></script>
<script>
  var socket = io.connect();
  socket.on('news', function (data) {
    console.log(data); 
    socket.emit('my other event', { my: 'data' });
  });
</script>
  • io.connect() 建立连接
  • socket.on() 监听事件
  • socket.emit() 发送消息

socket.emit()直接发送中文会出现乱码导致服务器返回400,所以发送中文时需要将中文进行编码:

var data = encodeURI('你好');
socket.emit('my event', data);

这里使用默认namespace,如果要指定,需要在建立连接时声明。

Flask-Socketio

Flask-Socketio是Socketio的Flask插件,常用API和Socket.io非常类似,上手比较容易。Flask-Socketio基于异步处理各种事件,可以选择使用的异步服务有:eventlet、gevent和Flask自带的Werkzeug 。我对这几种异步包都还不太了解,就直接使用默认的eventlet了。

初始化

Flask-Socketio的初始化方法和其他Flask插件基本一样:

from flask import Flask, render_template
from flask_socketio import SocketIO

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

if __name__ == '__main__':
    socketio.run(app)

Socketio的run方法封装了Flask的run方法,依然可以指定host和port。

监听事件

Socketio的通信基于事件,不同名称的事件对应不同的处理函数,类似于Web服务器处理路由的机制。

@socketio.on('message')
def handle_message(msg):
    print('received message: ' + msg)

on装饰器指定接收事件的名称和所在的命名空间(这里使用默认namespace),处理函数可以接收各种类型的参数:string, bytes, int, 或是JSON。
事件名称可以自由指定,但也有几个内置事件,如connect,disconnect...自定义事件名时,不要跟内置事件重复,否则不会按照自己的代码触发事件。
如果不使用装饰器,on_event函数也可以用于监听事件。

def my_function_handler(data):
    pass

socketio.on_event('my event', my_function_handler, namespace='/test')

发送消息

发送消息使用send()或是emit(),对于未命名的事件使用send(),已经命名的事件用emit()。

from flask_socketio import send, emit

@socketio.on('message')
def handle_message(message):
    send(message)

@socketio.on('json')
def handle_json(json):
    send(json, json=True)

@socketio.on('my event')
def handle_my_custom_event(json):
    emit('my response', json)

发送事件时,如果把broadcasting参数设为True,那么同一个namespace下的所有连接都会收到该事件。

Namespace

Namespace允许客户端与服务器通过同一个socket建立多个连接。如果不指定则使用默认namespce。

@socketio.on('my event', namespace='/chat')
def handle_my_custom_event(json):
    emit('my response', json, namespace='/chat')

Room

顾名思义,Room把namespace分成了很多小“房间”,如果一个事件指定了room,那么只有处在同一个room内的连接才能收到该事件。每一个连接都默认属于名为request.sid的房间。
与room有关的常用方法有:

  • join_room() 创建房间
  • leave_room() 离开房间
  • close_room() 关闭房间
  • rooms() 当前连接属于的房间列表

简单demo

最后附一个简单demo,客户端将字符串发送给服务器,服务器收到数据后,又将数据返回给客户端显示。

app.py

#!/usr/bin/env python
from flask import Flask, render_template, session, request
from flask_socketio import SocketIO, emit

app = Flask(__name__, template_folder='./')
app.config['SECRET_KEY'] = 'secret!'

socketio = SocketIO(app)

@app.route('/')
def index():
    return render_template('index.html')

@socketio.on('client_event')
def client_msg(msg):
    emit('server_response', {'data': msg['data']})
    
@socketio.on('connect_event')
def connected_msg(msg):
    emit('server_response', {'data': msg['data']})

if __name__ == '__main__':
    socketio.run(app, host='0.0.0.0')

index.html

<!DOCTYPE HTML>
<html>
<head>
    <title>Flask-SocketIO Test</title>
    <script type="text/javascript" src="//cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
    <script type="text/javascript" src="//cdn.bootcss.com/socket.io/1.5.1/socket.io.min.js"></script>
    <script type="text/javascript" charset="utf-8">
    $(document).ready(function() {
        var socket = io.connect();

        socket.on('connect', function() {
            socket.emit('connect_event', {data: 'connected!'});
        })

        socket.on('server_response', function(msg) {
            $('#log').append('<br>' + $('<div/>').text('Received #' + ': ' + msg.data).html());
        });

        $('form#emit').submit(function(event) {
                socket.emit('client_event', {data: $('#emit_data').val()});
                return false;
            });
    });
    
    </script>   
</head>
<body>
    <h2>WebSokect</h2>
    <form id="emit" method="POST" action='#'>
        <input type="text" name="emit_data" id="emit_data" placeholder="Message">
        <input type="submit" value="Echo">
    </form>
    <div id='log'></div>
</body>
</html>
Comments
Write a Comment
  • Yyyyyyyyyyyyy reply

    66666666666666

  • vinceeent reply

    兄弟,牛逼

  • Tianshanghong reply

    感谢!很有帮助!

  • david reply

    WebSocket transport not available. Install eventlet or gevent and gevent-websocket for improved performance.

    C:\workplace\JDGoods\venv\lib\site-packages\flask_socketio\__init__.py:496: Warning: Silently ignoring app.run() because the application is run from the flask command line executable. Consider putting app.run() behind an if __name__ == "__main__" guard to silence this warning.

    use_reloader=use_reloader, **kwargs)

    代码都是一样的,我这边怎么总是提示这个错。有帮忙解答的嘛

    • Melw00d reply

      @david 装个异步库嘛,pip install eventlet