2019-02-19 23:15:00 +08:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2019-02-20 00:25:14 +08:00
|
|
|
|
import asyncio
|
2021-12-12 21:54:07 +08:00
|
|
|
|
import collections
|
|
|
|
|
import enum
|
2019-02-14 18:36:37 +08:00
|
|
|
|
import json
|
2019-02-20 14:53:50 +08:00
|
|
|
|
import logging
|
2019-06-12 22:32:34 +08:00
|
|
|
|
import ssl as ssl_
|
2019-02-19 23:15:00 +08:00
|
|
|
|
import struct
|
2019-09-15 18:46:45 +08:00
|
|
|
|
import zlib
|
2019-06-06 21:50:51 +08:00
|
|
|
|
from typing import *
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2018-06-03 14:06:00 +08:00
|
|
|
|
import aiohttp
|
2018-05-13 21:57:36 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
from . import handlers
|
2021-12-12 21:54:07 +08:00
|
|
|
|
|
2021-12-12 19:28:12 +08:00
|
|
|
|
logger = logging.getLogger('blivedm')
|
2019-02-20 14:53:50 +08:00
|
|
|
|
|
2020-07-19 22:17:32 +08:00
|
|
|
|
ROOM_INIT_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom'
|
|
|
|
|
DANMAKU_SERVER_CONF_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo'
|
2020-10-11 12:43:11 +08:00
|
|
|
|
DEFAULT_DANMAKU_SERVER_LIST = [
|
|
|
|
|
{'host': 'broadcastlv.chat.bilibili.com', 'port': 2243, 'wss_port': 443, 'ws_port': 2244}
|
|
|
|
|
]
|
2018-05-13 21:57:36 +08:00
|
|
|
|
|
2019-04-22 19:47:05 +08:00
|
|
|
|
HEADER_STRUCT = struct.Struct('>I2H2I')
|
2021-12-12 21:54:07 +08:00
|
|
|
|
HeaderTuple = collections.namedtuple('HeaderTuple', ('pack_len', 'raw_header_size', 'ver', 'operation', 'seq_id'))
|
2021-01-31 13:11:20 +08:00
|
|
|
|
WS_BODY_PROTOCOL_VERSION_INFLATE = 0
|
|
|
|
|
WS_BODY_PROTOCOL_VERSION_NORMAL = 1
|
2019-09-15 18:46:45 +08:00
|
|
|
|
WS_BODY_PROTOCOL_VERSION_DEFLATE = 2
|
2019-04-22 19:47:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# go-common\app\service\main\broadcast\model\operation.go
|
2021-12-12 21:54:07 +08:00
|
|
|
|
class Operation(enum.IntEnum):
|
2019-04-22 19:47:05 +08:00
|
|
|
|
HANDSHAKE = 0
|
|
|
|
|
HANDSHAKE_REPLY = 1
|
|
|
|
|
HEARTBEAT = 2
|
|
|
|
|
HEARTBEAT_REPLY = 3
|
|
|
|
|
SEND_MSG = 4
|
|
|
|
|
SEND_MSG_REPLY = 5
|
|
|
|
|
DISCONNECT_REPLY = 6
|
2019-02-19 23:15:00 +08:00
|
|
|
|
AUTH = 7
|
2019-04-22 19:47:05 +08:00
|
|
|
|
AUTH_REPLY = 8
|
|
|
|
|
RAW = 9
|
|
|
|
|
PROTO_READY = 10
|
|
|
|
|
PROTO_FINISH = 11
|
|
|
|
|
CHANGE_ROOM = 12
|
|
|
|
|
CHANGE_ROOM_REPLY = 13
|
|
|
|
|
REGISTER = 14
|
|
|
|
|
REGISTER_REPLY = 15
|
|
|
|
|
UNREGISTER = 16
|
|
|
|
|
UNREGISTER_REPLY = 17
|
|
|
|
|
# B站业务自定义OP
|
|
|
|
|
# MinBusinessOp = 1000
|
|
|
|
|
# MaxBusinessOp = 10000
|
2018-05-13 21:57:36 +08:00
|
|
|
|
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-09-15 18:46:45 +08:00
|
|
|
|
class InitError(Exception):
|
|
|
|
|
"""初始化失败"""
|
|
|
|
|
|
|
|
|
|
|
2019-02-19 23:15:00 +08:00
|
|
|
|
class BLiveClient:
|
2021-12-13 00:07:00 +08:00
|
|
|
|
"""
|
|
|
|
|
B站直播弹幕客户端,负责连接房间
|
|
|
|
|
|
|
|
|
|
:param room_id: URL中的房间ID,可以用短ID
|
|
|
|
|
:param uid: B站用户ID,0表示未登录
|
|
|
|
|
:param session: cookie、连接池
|
|
|
|
|
:param heartbeat_interval: 发送心跳包的间隔时间(秒)
|
|
|
|
|
:param ssl: True表示用默认的SSLContext验证,False表示不验证,也可以传入SSLContext
|
|
|
|
|
:param loop: 协程事件循环
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
room_id,
|
|
|
|
|
uid=0,
|
2021-12-15 00:09:07 +08:00
|
|
|
|
session: Optional[aiohttp.ClientSession] = None,
|
2021-12-13 00:07:00 +08:00
|
|
|
|
heartbeat_interval=30,
|
|
|
|
|
ssl: Union[bool, ssl_.SSLContext] = True,
|
2021-12-15 00:09:07 +08:00
|
|
|
|
loop: Optional[asyncio.BaseEventLoop] = None,
|
2019-03-23 23:58:02 +08:00
|
|
|
|
):
|
2021-12-15 00:09:07 +08:00
|
|
|
|
# 用来init_room的临时房间ID,可以用短ID
|
2019-06-06 21:50:51 +08:00
|
|
|
|
self._tmp_room_id = room_id
|
2019-02-20 00:25:14 +08:00
|
|
|
|
self._uid = uid
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-02-20 14:12:11 +08:00
|
|
|
|
if loop is not None:
|
|
|
|
|
self._loop = loop
|
|
|
|
|
elif session is not None:
|
2021-12-12 21:54:07 +08:00
|
|
|
|
self._loop = session.loop # noqa
|
2019-02-20 14:12:11 +08:00
|
|
|
|
else:
|
|
|
|
|
self._loop = asyncio.get_event_loop()
|
2019-02-20 00:25:14 +08:00
|
|
|
|
|
|
|
|
|
if session is None:
|
2021-03-28 17:47:23 +08:00
|
|
|
|
self._session = aiohttp.ClientSession(loop=self._loop, timeout=aiohttp.ClientTimeout(total=10))
|
2019-02-20 00:25:14 +08:00
|
|
|
|
self._own_session = True
|
|
|
|
|
else:
|
|
|
|
|
self._session = session
|
|
|
|
|
self._own_session = False
|
2021-12-12 21:54:07 +08:00
|
|
|
|
if self._session.loop is not self._loop: # noqa
|
2021-12-15 00:09:07 +08:00
|
|
|
|
raise RuntimeError('BLiveClient and session must use the same event loop')
|
2019-09-15 18:46:45 +08:00
|
|
|
|
|
|
|
|
|
self._heartbeat_interval = heartbeat_interval
|
2021-12-12 21:54:07 +08:00
|
|
|
|
self._ssl = ssl if ssl else ssl_._create_unverified_context() # noqa
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 00:09:07 +08:00
|
|
|
|
# 消息处理器,可动态增删
|
2021-12-13 00:07:00 +08:00
|
|
|
|
self._handlers: List[handlers.HandlerInterface] = []
|
|
|
|
|
|
2021-12-15 00:09:07 +08:00
|
|
|
|
# 在调用init_room后初始化的字段
|
|
|
|
|
# 真实房间ID
|
|
|
|
|
self._room_id = None
|
|
|
|
|
# 房间短ID,没有则为0
|
|
|
|
|
self._room_short_id = None
|
|
|
|
|
# 主播用户ID
|
|
|
|
|
self._room_owner_uid = None
|
|
|
|
|
# 弹幕服务器列表
|
|
|
|
|
# [{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...]
|
|
|
|
|
self._host_server_list: Optional[List[dict]] = None
|
|
|
|
|
# 连接弹幕服务器用的token
|
|
|
|
|
self._host_server_token = None
|
|
|
|
|
|
|
|
|
|
# 在运行时初始化的字段
|
|
|
|
|
# websocket连接
|
|
|
|
|
self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None
|
|
|
|
|
# 网络协程的future
|
|
|
|
|
self._network_future: Optional[asyncio.Future] = None
|
|
|
|
|
# 发心跳包定时器的handle
|
|
|
|
|
self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None
|
|
|
|
|
|
2019-02-20 14:12:11 +08:00
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def is_running(self) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
本客户端正在运行,注意调用stop后还没完全停止也算正在运行
|
|
|
|
|
"""
|
|
|
|
|
return self._network_future is not None
|
2019-06-06 21:50:51 +08:00
|
|
|
|
|
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def room_id(self) -> Optional[int]:
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
|
|
|
|
房间ID,调用init_room后初始化
|
|
|
|
|
"""
|
|
|
|
|
return self._room_id
|
|
|
|
|
|
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def room_short_id(self) -> Optional[int]:
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
|
|
|
|
房间短ID,没有则为0,调用init_room后初始化
|
|
|
|
|
"""
|
|
|
|
|
return self._room_short_id
|
|
|
|
|
|
|
|
|
|
@property
|
2021-12-15 00:09:07 +08:00
|
|
|
|
def room_owner_uid(self) -> Optional[int]:
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
主播用户ID,调用init_room后初始化
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
|
|
|
|
return self._room_owner_uid
|
2019-02-20 14:12:11 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
def add_handler(self, handler: 'handlers.HandlerInterface'):
|
2019-02-20 00:25:14 +08:00
|
|
|
|
"""
|
2021-12-13 00:07:00 +08:00
|
|
|
|
添加消息处理器
|
2021-12-15 23:44:44 +08:00
|
|
|
|
注意多个处理器是并发处理的,不要依赖处理的顺序
|
|
|
|
|
消息处理器和接收消息运行在同一协程,如果处理消息耗时太长会阻塞接收消息,这种情况建议将消息推到队列,让另一个协程处理
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
:param handler: 消息处理器
|
2019-02-20 00:25:14 +08:00
|
|
|
|
"""
|
2021-12-13 00:07:00 +08:00
|
|
|
|
if handler not in self._handlers:
|
|
|
|
|
self._handlers.append(handler)
|
|
|
|
|
|
|
|
|
|
def remove_handler(self, handler: 'handlers.HandlerInterface'):
|
|
|
|
|
"""
|
|
|
|
|
移除消息处理器
|
2021-12-15 00:09:07 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
:param handler: 消息处理器
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
self._handlers.remove(handler)
|
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-06-06 21:50:51 +08:00
|
|
|
|
def start(self):
|
2019-02-19 23:15:00 +08:00
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
启动本客户端
|
2019-02-19 23:15:00 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if self.is_running:
|
2021-12-15 00:09:07 +08:00
|
|
|
|
logger.warning('room %s 已经在运行中,不能再次start', self.room_id)
|
|
|
|
|
return
|
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
self._network_future = asyncio.ensure_future(self._network_coroutine_wrapper(), loop=self._loop)
|
2019-06-12 22:32:34 +08:00
|
|
|
|
|
2019-06-06 21:50:51 +08:00
|
|
|
|
def stop(self):
|
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
停止本客户端
|
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if not self.is_running:
|
2021-12-15 00:09:07 +08:00
|
|
|
|
logger.warning('room %s 已经停止,不能再次stop', self.room_id)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self._network_future.cancel()
|
|
|
|
|
|
|
|
|
|
async def stop_and_close(self):
|
|
|
|
|
"""
|
|
|
|
|
停止本客户端并释放本客户端的资源,调用后本客户端将不可用
|
2019-06-06 21:50:51 +08:00
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
self.stop()
|
|
|
|
|
await self.join()
|
|
|
|
|
await self.close()
|
|
|
|
|
|
|
|
|
|
async def join(self):
|
|
|
|
|
"""
|
|
|
|
|
等待本客户端停止
|
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if not self.is_running:
|
2021-12-15 00:09:07 +08:00
|
|
|
|
logger.warning('room %s 已经停止,不能join', self.room_id)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
await self._network_future
|
2019-06-06 21:50:51 +08:00
|
|
|
|
|
2021-12-13 00:07:00 +08:00
|
|
|
|
async def close(self):
|
|
|
|
|
"""
|
2021-12-15 00:09:07 +08:00
|
|
|
|
释放本客户端的资源,调用后本客户端将不可用
|
2021-12-13 00:07:00 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if self.is_running:
|
2021-12-15 00:09:07 +08:00
|
|
|
|
logger.warning('room %s 在运行状态中调用了close', self.room_id)
|
|
|
|
|
|
|
|
|
|
# 如果session是自己创建的则关闭session
|
2021-12-13 00:07:00 +08:00
|
|
|
|
if self._own_session:
|
|
|
|
|
await self._session.close()
|
|
|
|
|
|
2019-06-06 21:50:51 +08:00
|
|
|
|
async def init_room(self):
|
2020-10-11 12:43:11 +08:00
|
|
|
|
"""
|
2021-12-15 21:12:09 +08:00
|
|
|
|
初始化连接房间需要的字段
|
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
:return: True代表没有降级,如果需要降级后还可用,重载这个函数返回True
|
|
|
|
|
"""
|
|
|
|
|
res = True
|
|
|
|
|
if not await self._init_room_id_and_owner():
|
|
|
|
|
res = False
|
|
|
|
|
# 失败了则降级
|
|
|
|
|
self._room_id = self._room_short_id = self._tmp_room_id
|
|
|
|
|
self._room_owner_uid = 0
|
|
|
|
|
|
|
|
|
|
if not await self._init_host_server():
|
|
|
|
|
res = False
|
|
|
|
|
# 失败了则降级
|
|
|
|
|
self._host_server_list = DEFAULT_DANMAKU_SERVER_LIST
|
|
|
|
|
self._host_server_token = None
|
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
async def _init_room_id_and_owner(self):
|
2019-09-15 18:46:45 +08:00
|
|
|
|
try:
|
2020-07-19 22:17:32 +08:00
|
|
|
|
async with self._session.get(ROOM_INIT_URL, params={'room_id': self._tmp_room_id},
|
2019-09-15 18:46:45 +08:00
|
|
|
|
ssl=self._ssl) as res:
|
|
|
|
|
if res.status != 200:
|
2020-07-19 22:17:32 +08:00
|
|
|
|
logger.warning('room %d init_room失败:%d %s', self._tmp_room_id,
|
2019-09-15 18:46:45 +08:00
|
|
|
|
res.status, res.reason)
|
|
|
|
|
return False
|
|
|
|
|
data = await res.json()
|
|
|
|
|
if data['code'] != 0:
|
2020-10-17 17:09:02 +08:00
|
|
|
|
logger.warning('room %d init_room失败:%s', self._tmp_room_id, data['message'])
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
|
|
|
|
if not self._parse_room_init(data['data']):
|
|
|
|
|
return False
|
2021-03-28 17:47:23 +08:00
|
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2020-07-19 22:17:32 +08:00
|
|
|
|
logger.exception('room %d init_room失败:', self._tmp_room_id)
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
2020-10-11 12:43:11 +08:00
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def _parse_room_init(self, data):
|
|
|
|
|
room_info = data['room_info']
|
|
|
|
|
self._room_id = room_info['room_id']
|
|
|
|
|
self._room_short_id = room_info['short_id']
|
|
|
|
|
self._room_owner_uid = room_info['uid']
|
|
|
|
|
return True
|
2019-09-15 18:46:45 +08:00
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
async def _init_host_server(self):
|
2019-09-15 18:46:45 +08:00
|
|
|
|
try:
|
2020-07-19 22:17:32 +08:00
|
|
|
|
async with self._session.get(DANMAKU_SERVER_CONF_URL, params={'id': self._room_id, 'type': 0},
|
2019-09-15 18:46:45 +08:00
|
|
|
|
ssl=self._ssl) as res:
|
|
|
|
|
if res.status != 200:
|
2020-07-19 22:17:32 +08:00
|
|
|
|
logger.warning('room %d getConf失败:%d %s', self._room_id,
|
2019-09-15 18:46:45 +08:00
|
|
|
|
res.status, res.reason)
|
|
|
|
|
return False
|
|
|
|
|
data = await res.json()
|
|
|
|
|
if data['code'] != 0:
|
2020-10-17 17:09:02 +08:00
|
|
|
|
logger.warning('room %d getConf失败:%s', self._room_id, data['message'])
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
2020-10-11 12:43:11 +08:00
|
|
|
|
if not self._parse_danmaku_server_conf(data['data']):
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
2021-03-28 17:47:23 +08:00
|
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2020-07-19 22:17:32 +08:00
|
|
|
|
logger.exception('room %d getConf失败:', self._room_id)
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return False
|
|
|
|
|
return True
|
2019-06-06 21:50:51 +08:00
|
|
|
|
|
2020-10-11 12:43:11 +08:00
|
|
|
|
def _parse_danmaku_server_conf(self, data):
|
|
|
|
|
self._host_server_list = data['host_list']
|
|
|
|
|
self._host_server_token = data['token']
|
|
|
|
|
if not self._host_server_list:
|
|
|
|
|
logger.warning('room %d getConf失败:host_server_list为空', self._room_id)
|
|
|
|
|
return False
|
2019-09-15 18:46:45 +08:00
|
|
|
|
return True
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-01-31 13:11:20 +08:00
|
|
|
|
@staticmethod
|
2021-12-15 21:12:09 +08:00
|
|
|
|
def _make_packet(data: dict, operation: int) -> bytes:
|
|
|
|
|
"""
|
|
|
|
|
创建一个要发送给服务器的包
|
|
|
|
|
|
|
|
|
|
:param data: 包体JSON数据
|
|
|
|
|
:param operation: 操作码,见Operation
|
|
|
|
|
:return: 整个包的数据
|
|
|
|
|
"""
|
2019-02-19 23:15:00 +08:00
|
|
|
|
body = json.dumps(data).encode('utf-8')
|
2019-04-22 19:47:05 +08:00
|
|
|
|
header = HEADER_STRUCT.pack(
|
2021-12-15 21:12:09 +08:00
|
|
|
|
HEADER_STRUCT.size + len(body), # pack_len
|
|
|
|
|
HEADER_STRUCT.size, # raw_header_size
|
|
|
|
|
1, # ver
|
|
|
|
|
operation, # operation
|
|
|
|
|
1 # seq_id
|
2019-02-19 23:15:00 +08:00
|
|
|
|
)
|
|
|
|
|
return header + body
|
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
async def _network_coroutine_wrapper(self):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
负责处理网络协程的异常,网络协程具体逻辑在_network_coroutine里
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
try:
|
|
|
|
|
await self._network_coroutine()
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
# 正常停止
|
|
|
|
|
pass
|
|
|
|
|
except Exception as e: # noqa
|
|
|
|
|
logger.exception('room %s 网络协程异常结束:', self.room_id)
|
|
|
|
|
finally:
|
|
|
|
|
logger.debug('room %s 网络协程结束', self.room_id)
|
|
|
|
|
self._network_future = None
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 00:09:07 +08:00
|
|
|
|
async def _network_coroutine(self):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
|
|
|
|
网络协程,负责连接服务器、接收消息、解包
|
|
|
|
|
"""
|
2019-09-15 18:46:45 +08:00
|
|
|
|
# 如果之前未初始化则初始化
|
|
|
|
|
if self._host_server_token is None:
|
2021-12-15 23:44:44 +08:00
|
|
|
|
if not await self.init_room():
|
|
|
|
|
raise InitError('初始化失败')
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-06-07 20:02:37 +08:00
|
|
|
|
retry_count = 0
|
2018-05-14 01:02:53 +08:00
|
|
|
|
while True:
|
2019-02-19 23:15:00 +08:00
|
|
|
|
try:
|
|
|
|
|
# 连接
|
2019-09-15 18:46:45 +08:00
|
|
|
|
host_server = self._host_server_list[retry_count % len(self._host_server_list)]
|
|
|
|
|
async with self._session.ws_connect(
|
2021-12-15 21:12:09 +08:00
|
|
|
|
f"wss://{host_server['host']}:{host_server['wss_port']}/sub",
|
2021-01-31 13:47:32 +08:00
|
|
|
|
receive_timeout=self._heartbeat_interval + 5,
|
2019-09-15 18:46:45 +08:00
|
|
|
|
ssl=self._ssl
|
|
|
|
|
) as websocket:
|
2019-02-19 23:15:00 +08:00
|
|
|
|
self._websocket = websocket
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._on_ws_connect()
|
2018-05-14 01:02:53 +08:00
|
|
|
|
|
2019-02-19 23:15:00 +08:00
|
|
|
|
# 处理消息
|
2021-12-12 21:54:07 +08:00
|
|
|
|
message: aiohttp.WSMessage
|
|
|
|
|
async for message in websocket:
|
2019-11-27 01:10:13 +08:00
|
|
|
|
retry_count = 0
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._on_ws_message(message)
|
2018-05-13 23:49:32 +08:00
|
|
|
|
|
2019-06-21 17:45:21 +08:00
|
|
|
|
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
2021-12-15 23:44:44 +08:00
|
|
|
|
# 掉线重连
|
2019-06-07 20:02:37 +08:00
|
|
|
|
pass
|
2019-06-12 22:32:34 +08:00
|
|
|
|
except ssl_.SSLError:
|
2021-12-15 23:44:44 +08:00
|
|
|
|
logger.error('room %d 发生SSL错误,无法重连', self.room_id)
|
|
|
|
|
raise
|
2019-02-19 23:15:00 +08:00
|
|
|
|
finally:
|
|
|
|
|
self._websocket = None
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._on_ws_close()
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 准备重连
|
2019-06-07 20:02:37 +08:00
|
|
|
|
retry_count += 1
|
|
|
|
|
logger.warning('room %d 掉线重连中%d', self.room_id, retry_count)
|
2021-12-15 23:44:44 +08:00
|
|
|
|
await asyncio.sleep(1, loop=self._loop)
|
2019-06-07 20:02:37 +08:00
|
|
|
|
|
2021-12-15 21:12:09 +08:00
|
|
|
|
async def _on_ws_connect(self):
|
|
|
|
|
"""
|
|
|
|
|
websocket连接成功
|
|
|
|
|
"""
|
|
|
|
|
await self._send_auth()
|
|
|
|
|
self._heartbeat_timer_handle = self._loop.call_later(self._heartbeat_interval, self._on_send_heartbeat)
|
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
async def _on_ws_close(self):
|
|
|
|
|
"""
|
|
|
|
|
websocket连接断开
|
|
|
|
|
"""
|
|
|
|
|
if self._heartbeat_timer_handle is not None:
|
|
|
|
|
self._heartbeat_timer_handle.cancel()
|
|
|
|
|
self._heartbeat_timer_handle = None
|
|
|
|
|
|
|
|
|
|
async def _send_auth(self):
|
|
|
|
|
"""
|
|
|
|
|
发送认证包
|
|
|
|
|
"""
|
|
|
|
|
auth_params = {
|
|
|
|
|
'uid': self._uid,
|
|
|
|
|
'roomid': self._room_id,
|
|
|
|
|
'protover': 2,
|
|
|
|
|
'platform': 'web',
|
|
|
|
|
'clientver': '1.14.3',
|
|
|
|
|
'type': 2
|
|
|
|
|
}
|
|
|
|
|
if self._host_server_token is not None:
|
|
|
|
|
auth_params['key'] = self._host_server_token
|
|
|
|
|
await self._websocket.send_bytes(self._make_packet(auth_params, Operation.AUTH))
|
|
|
|
|
|
2021-01-31 13:11:20 +08:00
|
|
|
|
def _on_send_heartbeat(self):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
|
|
|
|
定时发送心跳包的回调
|
|
|
|
|
"""
|
2021-01-31 13:11:20 +08:00
|
|
|
|
coro = self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT))
|
|
|
|
|
asyncio.ensure_future(coro, loop=self._loop)
|
|
|
|
|
self._heartbeat_timer_handle = self._loop.call_later(self._heartbeat_interval, self._on_send_heartbeat)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 21:12:09 +08:00
|
|
|
|
async def _on_ws_message(self, message: aiohttp.WSMessage):
|
|
|
|
|
"""
|
|
|
|
|
收到websocket消息
|
|
|
|
|
|
|
|
|
|
:param message: websocket消息
|
|
|
|
|
"""
|
|
|
|
|
if message.type != aiohttp.WSMsgType.BINARY:
|
|
|
|
|
logger.warning('room %d 未知的websocket消息:type=%s %s', self.room_id,
|
|
|
|
|
message.type, message.data)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await self._parse_ws_message(message.data)
|
|
|
|
|
except asyncio.CancelledError:
|
2021-12-15 23:44:44 +08:00
|
|
|
|
# 正常停止,让外层处理
|
2021-12-15 21:12:09 +08:00
|
|
|
|
raise
|
|
|
|
|
except Exception: # noqa
|
|
|
|
|
logger.exception('room %d 处理websocket消息时发生错误:', self.room_id)
|
|
|
|
|
|
|
|
|
|
async def _parse_ws_message(self, data: bytes):
|
|
|
|
|
"""
|
|
|
|
|
解析websocket消息
|
|
|
|
|
|
|
|
|
|
:param data: websocket消息数据
|
|
|
|
|
"""
|
2019-02-19 23:15:00 +08:00
|
|
|
|
offset = 0
|
2019-09-15 18:46:45 +08:00
|
|
|
|
while offset < len(data):
|
2019-02-19 23:15:00 +08:00
|
|
|
|
try:
|
2019-09-15 18:46:45 +08:00
|
|
|
|
header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset))
|
2019-02-19 23:15:00 +08:00
|
|
|
|
except struct.error:
|
|
|
|
|
break
|
|
|
|
|
|
2019-04-22 19:47:05 +08:00
|
|
|
|
if header.operation == Operation.HEARTBEAT_REPLY:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 心跳包,自己造个消息当成业务消息处理
|
2021-12-13 00:07:00 +08:00
|
|
|
|
popularity = int.from_bytes(
|
|
|
|
|
data[offset + HEADER_STRUCT.size: offset + HEADER_STRUCT.size + 4],
|
|
|
|
|
'big'
|
|
|
|
|
)
|
|
|
|
|
body = {
|
|
|
|
|
'cmd': '_HEARTBEAT',
|
|
|
|
|
'data': {
|
|
|
|
|
'popularity': popularity
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-15 23:44:44 +08:00
|
|
|
|
await self._handle_command(body)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-04-22 19:47:05 +08:00
|
|
|
|
elif header.operation == Operation.SEND_MSG_REPLY:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 业务消息
|
2019-09-15 18:46:45 +08:00
|
|
|
|
body = data[offset + HEADER_STRUCT.size: offset + header.pack_len]
|
2019-10-06 11:06:51 +08:00
|
|
|
|
if header.ver == WS_BODY_PROTOCOL_VERSION_DEFLATE:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 压缩过的先解压,为了避免阻塞网络线程,放在其他线程执行
|
2021-01-31 13:11:20 +08:00
|
|
|
|
body = await self._loop.run_in_executor(None, zlib.decompress, body)
|
2021-12-15 21:12:09 +08:00
|
|
|
|
await self._parse_ws_message(body)
|
2019-09-15 18:46:45 +08:00
|
|
|
|
else:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 没压缩过的
|
2019-11-13 23:26:25 +08:00
|
|
|
|
try:
|
|
|
|
|
body = json.loads(body.decode('utf-8'))
|
2021-12-15 23:44:44 +08:00
|
|
|
|
await self._handle_command(body)
|
2021-01-31 13:11:20 +08:00
|
|
|
|
except Exception:
|
2021-12-15 23:44:44 +08:00
|
|
|
|
logger.error('room %d body=%s', self.room_id, body)
|
2019-11-13 23:26:25 +08:00
|
|
|
|
raise
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-04-22 19:47:05 +08:00
|
|
|
|
elif header.operation == Operation.AUTH_REPLY:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 认证响应
|
2019-04-22 19:47:05 +08:00
|
|
|
|
await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT))
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
|
|
|
|
else:
|
2021-12-15 21:12:09 +08:00
|
|
|
|
# 未知消息
|
2019-09-15 18:46:45 +08:00
|
|
|
|
body = data[offset + HEADER_STRUCT.size: offset + header.pack_len]
|
2019-06-06 21:50:51 +08:00
|
|
|
|
logger.warning('room %d 未知包类型:operation=%d %s%s', self.room_id,
|
|
|
|
|
header.operation, header, body)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2019-04-22 19:47:05 +08:00
|
|
|
|
offset += header.pack_len
|
2019-02-19 23:15:00 +08:00
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
async def _handle_command(self, command: Union[list, dict]):
|
2021-12-15 21:12:09 +08:00
|
|
|
|
"""
|
2021-12-15 23:44:44 +08:00
|
|
|
|
解析并处理业务消息
|
2021-12-15 21:12:09 +08:00
|
|
|
|
|
|
|
|
|
:param command: 业务消息
|
|
|
|
|
"""
|
|
|
|
|
# 这里可能会多个消息一起发
|
2019-02-19 23:15:00 +08:00
|
|
|
|
if isinstance(command, list):
|
|
|
|
|
for one_command in command:
|
2021-12-15 23:44:44 +08:00
|
|
|
|
await self._handle_command(one_command)
|
2019-02-19 23:15:00 +08:00
|
|
|
|
return
|
|
|
|
|
|
2021-12-15 23:44:44 +08:00
|
|
|
|
results = await asyncio.gather(
|
|
|
|
|
*(handler.handle(self, command) for handler in self._handlers),
|
|
|
|
|
loop=self._loop,
|
|
|
|
|
return_exceptions=True
|
|
|
|
|
)
|
|
|
|
|
for res in results:
|
|
|
|
|
if isinstance(res, Exception):
|
|
|
|
|
logger.exception('room %d 处理消息时发生错误,command=%s', self.room_id, command, exc_info=res)
|