/danmu_abc/conn.py

https://github.com/yjqiang/danmu · Python · 216 lines · 164 code · 36 blank · 16 comment · 32 complexity · a03b0adea752be14653926d343183f86 MD5 · raw file

  1. import json
  2. import asyncio
  3. from typing import Optional, Any
  4. from urllib.parse import urlparse
  5. from abc import ABC, abstractmethod
  6. from aiohttp import ClientSession, WSMsgType, ClientError
  7. class Conn(ABC):
  8. __slots__ = ('_url', '_receive_timeout',)
  9. # receive_timeout 推荐为 heartbeat 间隔加 10s 5s
  10. @abstractmethod
  11. def __init__(self, url: str, receive_timeout: Optional[float] = None):
  12. self._url = url
  13. self._receive_timeout = receive_timeout
  14. @abstractmethod
  15. async def open(self) -> bool:
  16. return False
  17. @abstractmethod
  18. async def close(self) -> bool:
  19. return True
  20. # 用于永久 close 之后一些数据清理等
  21. @abstractmethod
  22. async def clean(self) -> None:
  23. pass
  24. @abstractmethod
  25. async def send_bytes(self, bytes_data: bytes) -> bool:
  26. return True
  27. @abstractmethod
  28. async def read_bytes(self) -> Optional[bytes]:
  29. return None
  30. @abstractmethod
  31. async def read_json(self) -> Any:
  32. return None
  33. # 类似于 https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader.readexactly
  34. # Read exactly n bytes.
  35. @abstractmethod
  36. async def read_exactly_bytes(self, n: int) -> Optional[bytes]:
  37. return None
  38. # 类似于 https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader.readexactly
  39. # Read exactly n bytes.
  40. @abstractmethod
  41. async def read_exactly_json(self, n: int) -> Any:
  42. return None
  43. class TcpConn(Conn):
  44. __slots__ = ('_host', '_port', '_reader', '_writer')
  45. # url 格式 tcp://hostname:port
  46. def __init__(self, url: str, receive_timeout: Optional[float] = None):
  47. super().__init__(url, receive_timeout)
  48. result = urlparse(url)
  49. if result.scheme != 'tcp':
  50. raise TypeError(f'url scheme must be tcp ({result.scheme})')
  51. self._host = result.hostname
  52. self._port = result.port
  53. self._reader = None
  54. self._writer = None
  55. async def open(self) -> bool:
  56. try:
  57. self._reader, self._writer = await asyncio.wait_for(
  58. asyncio.open_connection(self._host, self._port), timeout=3)
  59. except (OSError, asyncio.TimeoutError):
  60. return False
  61. return True
  62. async def close(self) -> bool:
  63. if self._writer is not None:
  64. self._writer.close()
  65. # py3.7 才有妈的你们真的磨叽
  66. # await self._writer.wait_closed()
  67. return True
  68. async def clean(self) -> None:
  69. pass
  70. async def send_bytes(self, bytes_data: bytes) -> bool:
  71. try:
  72. self._writer.write(bytes_data)
  73. await self._writer.drain()
  74. except OSError:
  75. return False
  76. except asyncio.CancelledError:
  77. # print('asyncio.CancelledError', 'send_bytes')
  78. return False
  79. return True
  80. async def read_bytes(self) -> Optional[bytes]:
  81. # 不支持的原因是tcp 流式传输自己拼装过于复杂
  82. raise NotImplementedError("Sorry, but I don't think we need this in TCP.")
  83. async def read_json(self) -> Any:
  84. # 不支持的原因是tcp 流式传输自己拼装过于复杂
  85. raise NotImplementedError("Sorry, but I don't think we need this in TCP.")
  86. async def read_exactly_bytes(self, n: int) -> Optional[bytes]:
  87. if n <= 0:
  88. return None
  89. try:
  90. bytes_data = await asyncio.wait_for(
  91. self._reader.readexactly(n), timeout=self._receive_timeout)
  92. except (OSError, asyncio.TimeoutError, asyncio.IncompleteReadError):
  93. return None
  94. except asyncio.CancelledError:
  95. # print('asyncio.CancelledError', 'read_bytes')
  96. return None
  97. return bytes_data
  98. async def read_exactly_json(self, n: int) -> Any:
  99. data = await self.read_exactly_bytes(n)
  100. if not data:
  101. return None
  102. return json.loads(data.decode('utf8'))
  103. class WsConn(Conn):
  104. __slots__ = ('_is_sharing_session', '_session', '_ws_receive_timeout', '_ws_heartbeat', '_ws')
  105. # url 格式 ws://hostname:port/… 或者 wss://hostname:port/…
  106. def __init__(
  107. self, url: str,
  108. receive_timeout: Optional[float] = None,
  109. session: Optional[ClientSession] = None,
  110. ws_receive_timeout: Optional[float] = None, # 自动 ping pong 时候用的
  111. ws_heartbeat: Optional[float] = None): # 自动 ping pong 时候用的
  112. super().__init__(url, receive_timeout)
  113. result = urlparse(url)
  114. if result.scheme != 'ws' and result.scheme != 'wss':
  115. raise TypeError(f'url scheme must be websocket ({result.scheme})')
  116. self._url = url
  117. if session is None:
  118. self._is_sharing_session = False
  119. self._session = ClientSession()
  120. else:
  121. self._is_sharing_session = True
  122. self._session = session
  123. self._ws_receive_timeout = ws_receive_timeout
  124. self._ws_heartbeat = ws_heartbeat
  125. self._ws = None
  126. async def open(self) -> bool:
  127. try:
  128. self._ws = await asyncio.wait_for(
  129. self._session.ws_connect(
  130. self._url,
  131. receive_timeout=self._ws_receive_timeout,
  132. heartbeat=self._ws_heartbeat), timeout=3)
  133. except (ClientError, asyncio.TimeoutError):
  134. return False
  135. return True
  136. async def close(self) -> bool:
  137. if self._ws is not None:
  138. await self._ws.close()
  139. return True
  140. async def clean(self) -> None:
  141. if not self._is_sharing_session:
  142. await self._session.close()
  143. async def send_bytes(self, bytes_data: bytes) -> bool:
  144. try:
  145. await self._ws.send_bytes(bytes_data)
  146. except ClientError:
  147. return False
  148. except asyncio.CancelledError:
  149. return False
  150. return True
  151. async def read_bytes(self) -> Optional[bytes]:
  152. try:
  153. msg = await asyncio.wait_for(
  154. self._ws.receive(), timeout=self._receive_timeout)
  155. if msg.type == WSMsgType.BINARY:
  156. return msg.data
  157. except (ClientError, asyncio.TimeoutError):
  158. return None
  159. except asyncio.CancelledError:
  160. # print('asyncio.CancelledError', 'read_bytes')
  161. return None
  162. return None
  163. async def read_json(self) -> Any:
  164. try:
  165. msg = await asyncio.wait_for(
  166. self._ws.receive(), timeout=self._receive_timeout)
  167. if msg.type == WSMsgType.TEXT:
  168. return json.loads(msg.data)
  169. elif msg.type == WSMsgType.BINARY:
  170. return json.loads(msg.data.decode('utf8'))
  171. except (ClientError, asyncio.TimeoutError):
  172. return None
  173. except asyncio.CancelledError:
  174. # print('asyncio.CancelledError', 'read_json')
  175. return None
  176. return None
  177. async def read_exactly_bytes(self, n: int) -> Optional[bytes]:
  178. raise NotImplementedError("Sorry, but I don't think we need this in WebSocket.")
  179. async def read_exactly_json(self, n: int) -> Any:
  180. raise NotImplementedError("Sorry, but I don't think we need this in WebSocket.")