Python订阅Redis主题,实现前端通过Websocket实时获取订阅信息【消息推送】

写在前面

之前玩物联网的时候经常用到MQTT,感觉也很好用,最近要做一个类似实时大屏幕的的系统,屏幕终端可能用很多种平台(Android、Linux、Mac、Windows)
正好之前用JS+HTML做过WS与服务器双向传输信息的东西【用的EMQX引擎】
效果很不错,网页也可以适应各个平台。

我就在想能不能不用MQTT用Redis,反正都是Pub/Sub模式

没想到一钻研就是一两天,也学到了许多东西,项目也做得不错了,在这里分享一下


所需知识

  1. Redis基本操作,这个没得说,不是很难建议都学下
  2. 通过命令行实现Redis的订阅发布
  3. Python异步IO,这个有点难,我也没有彻底理解卡了挺久了
  4. 网络相关知识,Websocket等

心路历程

我刚开始在百度搜了很久,如何实现将redis的订阅发布机制结合到websocket中,资料很少,看到几个用php+swoole这个方案的,但并不适合我的项目环境
所以我就想研究下

  1. Python订阅Redis
  2. Python向Websocket客户端发送信息

这两块,再结合一下可了吗,但远没有想象中这么简单啊


1. Python订阅Redis学习

用redis模块试试

import redis
 
rc = redis.StrictRedis(host='****', port='6379', db=3, password='******')
ps = rc.pubsub()
while True:
    msg = ps.parse_response()
    print(msg)

这么一看还挺简单的对吧,不过有个死循环,再找找

import redis
 
rc = redis.StrictRedis(host='****', port='6379', db=3, password='******')
ps = rc.pubsub()
ps.subscribe('liao')  #从liao订阅消息
for item in ps.listen():		#监听状态:有消息发布了就拿过来
    if item['type'] == 'message':
        print item['channel']
        print item['data']

诶,怎么又有个用for的,查了一波,发现这个listen()方法就是封装了那个死循环
算了,将就用吧

调试也顺利调通,通过命令行向redis发布消息,python顺利print


2. Python向Websocket客户端(多个)发消息学习

网上找了个代码,用的websockets这个库

import websockets
import asyncio
​
​
USERS = set()
​
​
async def notify_users():
    # 对注册列表内的客户端进行推送
    if USERS:  # asyncio.wait doesn't accept an empty list
        message = input('please input:')
        await asyncio.wait([user.send(message) for user in USERS])
​
​
async def register(websocket):
    USERS.add(websocket)
    await notify_users()
​
​
async def unregister(websocket):
    USERS.remove(websocket)
    await notify_users()
​
​
async def counter(websocket, path):
    # register(websocket) sends user_event() to websocket
    await register(websocket)
    try:
        # 处理客户端数据请求 (业务逻辑)
        async for message in websocket:
            print(message)
    finally:
        await unregister(websocket)
​
​
asyncio.get_event_loop().run_until_complete(
    websockets.serve(counter, 'localhost', 6789))
asyncio.get_event_loop().run_forever()

# 原文链接:https://blog.csdn.net/qq_33961117/article/details/94442908

结合HTML代码

DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <title>websocket通信客户端title>
    <script type="text/javascript">
        var ws
        function WebSocketTest() {
            if ("WebSocket" in window) {
                // 打开一个 web socket
                ws = new WebSocket("ws://127.0.0.1:6789");

                // 连接建立后的回调函数
                ws.onopen = function () {
                    // Web Socket 已连接上,使用 send() 方法发送数据
                    //ws.send("admin:123456");
                    alert("连接成功");
                    document.getElementById("ok").innerHTML = "发送消息给服务器"
                };

                // 接收到服务器消息后的回调函数
                ws.onmessage = function (evt) {
                    var received_msg = evt.data;
                    if (received_msg.indexOf("sorry") == -1) {
                        var node = document.createElement("LI");
                        var textnode = document.createTextNode(received_msg);
                        node.appendChild(textnode);
                        document.getElementById("myList").appendChild(node);
                        //alert("收到消息:" + received_msg);
                    }

                };

                // 连接关闭后的回调函数
                ws.onclose = function () {
                    // 关闭 websocket
                    alert("连接已关闭...");
                };
            }
            else {
                // 浏览器不支持 WebSocket
                alert("您的浏览器不支持 WebSocket!");
            }
        }
        function sayHello() {
            ws.send("你好server");
        }
    script>
head>

<button id="ok" onclick="sayHello()">尚未连接button>

<body onload="WebSocketTest()">
    <ul id="myList">
        <li>列表li>
    ul>

body>

html>

嗯,也顺利可以跟多个客户端通信了
去看看官方文档websockets库官方文档
确实,调试成功,就是这个asyncio库要再学习下

3.试试结合?

就在这卡了好久
第一次实验

import asyncio
import logging
import websockets
import redis

# 连接
rc = redis.StrictRedis(host='****', port='6379', db=3, password='******')
ps = rc.pubsub()  # 订阅对象
ps.subscribe('liao')  # 订阅
logging.basicConfig()

USERS = set()


async def notify_users():
    # 对注册列表内的客户端进行推送
    if USERS:  # asyncio.wait doesn't accept an empty list
        message = 'please input:'
        await asyncio.wait([user.send(message) for user in USERS])


async def register(websocket):
    USERS.add(websocket)
    await notify_users()


async def unregister(websocket):
    USERS.remove(websocket)
    await notify_users()


async def counter(websocket, path):
    print("一个")
    await register(websocket)
    print("注册完毕")
    try:
        for item in ps.listen():  # 阻塞监听
        if item['type'] == 'message':
            # print(item['channel'])
            print(item['data'])

    finally:
        print("注销")
        await unregister(websocket)

    print("结束")


asyncio.get_event_loop().run_until_complete(websockets.serve(counter, 'localhost', 6789))
asyncio.get_event_loop().run_forever()

欸,可以了,不过第二个客户端咋连不进去了,一看原来是阻塞监听这死循环了,没有监听到第二个连接
害,再看看这个run_until_complete()啥意思,就在这又研究了好久,发现怎么都还是绕不过这个死循环

后面又尝试了好多种方法,属于是乱试【急了,大家千万别学,磨刀不误砍柴工】

  1. 多添加一个task,实现监听,无奈还是死循环
  2. 不阻塞监听,还是陷入循环
  3. …这里试了好多种,太心塞了,就不写了

无奈搞不来,天色已晚,早点睡觉把


4. 终极解决方案

第二天起来清醒了一点,灵光一现
既然redis老是死循环,那我们能不能照葫芦画瓢,弄个异步的redis订阅?
又恶补了一下异步的知识,上网找答案
功夫不负有心人,找到个aioredis异步redis库

咋实现异步订阅嘞?这次直接在Google上搜了一下
发现了老外的一篇文章
wok,这不就是我要的吗,赶紧copy一下试试

# producer.py

import asyncio
from aioredis import create_connection, Channel
import websockets

async def subscribe_to_redis(path):
    conn = await create_connection(('localhost', 6379))

    # Set up a subscribe channel
    channel = Channel('lightlevel{}'.format(path), is_pattern=False)
    await conn.execute_pubsub('subscribe', channel)
    return channel, conn


async def browser_server(websocket, path):
    channel, conn = await subscribe_to_redis(path)
    try:
        while True:
            # Wait until data is published to this channel
            message = await channel.get()

            # Send unicode decoded data over to the websocket client
            await websocket.send(message.decode('utf-8'))

    except websockets.exceptions.ConnectionClosed:
        # Free up channel if websocket goes down
        await conn.execute_pubsub('unsubscribe', channel)
        conn.close()

if __name__ == '__main__':
    # Runs a server process on 8767. Just do 'python producer.py'
    loop = asyncio.get_event_loop()
    loop.set_debug(True)
    ws_server = websockets.serve(browser_server, 'localhost', 8767)
    loop.run_until_complete(ws_server)
    loop.run_forever()

调试了一下,完美,多客户端也有了,redis订阅也有了。

按照异步逻辑await,每有一个ws客户端连接,python都会创建一个redis订阅连接,果不其然,
在redis命令行测试的时候发现确实如此,会不会占用过多资源,还有待考究

可以说是实现了
websocket连接 转换为 redis订阅连接
也大致满足了我的项目需求了

后期需求(进阶)

redis只需要订阅一次(一个订阅链接)就可以实现对多个客户端发送,以及客户端session的判别

学习

异步IO又是一个大坑,不论是python的asyncio还是PHP的swoole,都还要学习,本文大概就到这里,记录一下研究过过程,有特别多的不足,目前项目需要跟进,有些内容暂时没法深入学习,还请见谅。

你可能感兴趣的:(Python订阅Redis主题,实现前端通过Websocket实时获取订阅信息【消息推送】)