问题: sanic中使用用redis的订阅发布实现功能,如果没有发布的情况下,redis是阻塞在订阅处的,导致sanic无法处理路由。
原因: sanic自带的websokcet是协程执行的(在一个线程中循环执行一个事件循环),redis也是单线程的,订阅时如果没有任何消息发布,线程会阻塞在订阅消息处,导致线程阻塞,由于整个系统只在一个线程中运行,所以sanic无法处理其他路由
from sanic import Sanic,response
import time
import asyncio
import threading
import redis
import json
app=Sanic()
@app.websocket('/websocket')
async def websockethello(request,ws):
await ws.send('hello')
r=redis.Redis()
p=r.pubsub()
p.subscribe('test')
p.parse_response()
while True:
print(p.parse_response())#----线程阻塞位置----
await asyncio.sleep(5)
@app.route('/print')
async def httphello(request):
return response.text('Hello')
app.run(host="0.0.0.0",port=9000,workers=1)
如前端没有连接websocket,路由print可正常执行,一旦前端连接websokcet之后,线程会执行while True
,运行到print(p.parse_response())
处会阻塞等待redis发布消息
解决:
- 使用异步的reids模块aioredis
- 开启一个子线程专门用来处理redis订阅并发送到websocket
from sanic import Sanic,response
import time
import asyncio
import threading
import redis
import json
app=Sanic()
class webSockets:
__webSocketsList={}
__webSocketNum=0
def add(self,ws,request):
self.__webSocketNum=self.__webSocketNum+1
print(ws.remote_address)
self.__webSocketsList[ws]=request
def remove(self,ws):
self.__webSocketNum=self.__webSocketNum-1
self.__webSocketsList.pop(ws)
async def print(self,message):
print(len(self.__webSocketsList))
for ws,request in self.__webSocketsList.items():
print(ws.remote_address)
await ws.send(message)
webSockets=webSockets()
@app.websocket('/websocket')
async def websockethello(request,ws):
await ws.send('hello')
webSockets.add(ws,request)
while str(ws.state) == 'State.OPEN':
await asyncio.sleep(20)
webSockets.remove(ws)
@app.route('/print')
async def httphello(request):
await webSockets.print('调用print接口')
return response.text('Hello')
def thread_loop_task(loop):
asyncio.set_event_loop(loop)
async def sub():
r=redis.Redis()
p=r.pubsub()
p.subscribe('test')
p.parse_response()
while True:
message=p.parse_response()
topic,content=message[1].decode('utf-8'),message[2].decode('utf-8')
print('发布主题:',topic,'发布内容:',content)
await webSockets.print(json.dumps({topic:content}))
future=asyncio.gather(sub())
loop.run_until_complete(future)
if __name__=="__main__":
thread_loop=asyncio.new_event_loop()
t=threading.Thread(target=thread_loop_task,args=(thread_loop,))
#t=threading.Thread(target=sub)
t.setDaemon(True)
t.start()
app.run(host="0.0.0.0",port=9000,workers=1)
- 首先创建一个全局的websocket列表,有websokcet连接时就加入到列表中,检测到此连接断开时就从列表中删除(此处是个人的一个需求),具体的代码在
class webSockets:
处,用于维护全局websocket列表的一个类,@app.websocket('/websocket')
处用于处理websocket的状态,有新连接时就加入到列表中webSockets.add(ws,request)
,add
函数的形参可改变,每隔20s检测一下websocket的状态,如果已经关闭就从列表中删除:
while str(ws.state) == 'State.OPEN':
await asyncio.sleep(20)
webSockets.remove(ws)
此处的时间可更改,实际操作中发现20s的时间有点长
- 协程websocket发送
async def print(self,message):
print(len(self.__webSocketsList))
for ws,request in self.__webSocketsList.items():
print(ws.remote_address)
await ws.send(message)
此函数是向前端发送消息,ws.send(message)
是sanic自带的发送函数,当调用此print接口时,会向websocket列表中所有的连接发送消息,此处也可根据传入的参数不同向指定用户发送(此功能还未实现),此接口是协程函数,无法直接执行,需注册到时间循环中方可执行
- 打印路由接口
@app.route('/print')
async def httphello(request):
await webSockets.print('调用print接口')
return response.text('Hello')
由于sanic采用asyncio框架,async路由会自动(sanic底层库实现,对于用户来说是自动的)注册到时间循环中,所以在调用webSockets.print()
接口时会执行
- 增加redis订阅发布功能,重新开启一个线程专门用来处理redis订阅,在子线程订阅时同样会阻塞,但是不影响主线程执行,路由接口在主线程中执行,此时就可实现主线程处理路由接口,子线程阻塞订阅,当有消息发布时发送到websocket连接中
def thread_loop_task(loop):
asyncio.set_event_loop(loop)
async def sub():
r=redis.Redis()
p=r.pubsub()
p.subscribe('test')
p.parse_response()
while True:
message=p.parse_response()
topic,content=message[1].decode('utf-8'),message[2].decode('utf-8')
print('发布主题:',topic,'发布内容:',content)
await webSockets.print(json.dumps({topic:content}))
future=asyncio.gather(sub())
loop.run_until_complete(future)
主函数中:
thread_loop=asyncio.new_event_loop()
t=threading.Thread(target=thread_loop_task,args=(thread_loop,))
#t=threading.Thread(target=sub)
t.setDaemon(True)
t.start()
完整的后端代码参考上边
结果: