Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

465 строки
20KB

  1. import asyncio
  2. import logging
  3. import random
  4. import engineio
  5. import six
  6. from . import client
  7. from . import exceptions
  8. from . import packet
  9. default_logger = logging.getLogger('socketio.client')
  10. class AsyncClient(client.Client):
  11. """A Socket.IO client for asyncio.
  12. This class implements a fully compliant Socket.IO web client with support
  13. for websocket and long-polling transports.
  14. :param reconnection: ``True`` if the client should automatically attempt to
  15. reconnect to the server after an interruption, or
  16. ``False`` to not reconnect. The default is ``True``.
  17. :param reconnection_attempts: How many reconnection attempts to issue
  18. before giving up, or 0 for infinity attempts.
  19. The default is 0.
  20. :param reconnection_delay: How long to wait in seconds before the first
  21. reconnection attempt. Each successive attempt
  22. doubles this delay.
  23. :param reconnection_delay_max: The maximum delay between reconnection
  24. attempts.
  25. :param randomization_factor: Randomization amount for each delay between
  26. reconnection attempts. The default is 0.5,
  27. which means that each delay is randomly
  28. adjusted by +/- 50%.
  29. :param logger: To enable logging set to ``True`` or pass a logger object to
  30. use. To disable logging set to ``False``. The default is
  31. ``False``.
  32. :param binary: ``True`` to support binary payloads, ``False`` to treat all
  33. payloads as text. On Python 2, if this is set to ``True``,
  34. ``unicode`` values are treated as text, and ``str`` and
  35. ``bytes`` values are treated as binary. This option has no
  36. effect on Python 3, where text and binary payloads are
  37. always automatically discovered.
  38. :param json: An alternative json module to use for encoding and decoding
  39. packets. Custom json modules must have ``dumps`` and ``loads``
  40. functions that are compatible with the standard library
  41. versions.
  42. The Engine.IO configuration supports the following settings:
  43. :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
  44. a logger object to use. To disable logging set to
  45. ``False``. The default is ``False``.
  46. """
  47. def is_asyncio_based(self):
  48. return True
  49. async def connect(self, url, headers={}, transports=None,
  50. namespaces=None, socketio_path='socket.io'):
  51. """Connect to a Socket.IO server.
  52. :param url: The URL of the Socket.IO server. It can include custom
  53. query string parameters if required by the server.
  54. :param headers: A dictionary with custom headers to send with the
  55. connection request.
  56. :param transports: The list of allowed transports. Valid transports
  57. are ``'polling'`` and ``'websocket'``. If not
  58. given, the polling transport is connected first,
  59. then an upgrade to websocket is attempted.
  60. :param namespaces: The list of custom namespaces to connect, in
  61. addition to the default namespace. If not given,
  62. the namespace list is obtained from the registered
  63. event handlers.
  64. :param socketio_path: The endpoint where the Socket.IO server is
  65. installed. The default value is appropriate for
  66. most cases.
  67. Note: this method is a coroutine.
  68. Example usage::
  69. sio = socketio.Client()
  70. sio.connect('http://localhost:5000')
  71. """
  72. self.connection_url = url
  73. self.connection_headers = headers
  74. self.connection_transports = transports
  75. self.connection_namespaces = namespaces
  76. self.socketio_path = socketio_path
  77. if namespaces is None:
  78. namespaces = set(self.handlers.keys()).union(
  79. set(self.namespace_handlers.keys()))
  80. elif isinstance(namespaces, six.string_types):
  81. namespaces = [namespaces]
  82. self.connection_namespaces = namespaces
  83. self.namespaces = [n for n in namespaces if n != '/']
  84. try:
  85. await self.eio.connect(url, headers=headers,
  86. transports=transports,
  87. engineio_path=socketio_path)
  88. except engineio.exceptions.ConnectionError as exc:
  89. six.raise_from(exceptions.ConnectionError(exc.args[0]), None)
  90. self.connected = True
  91. async def wait(self):
  92. """Wait until the connection with the server ends.
  93. Client applications can use this function to block the main thread
  94. during the life of the connection.
  95. Note: this method is a coroutine.
  96. """
  97. while True:
  98. await self.eio.wait()
  99. await self.sleep(1) # give the reconnect task time to start up
  100. if not self._reconnect_task:
  101. break
  102. await self._reconnect_task
  103. if self.eio.state != 'connected':
  104. break
  105. async def emit(self, event, data=None, namespace=None, callback=None):
  106. """Emit a custom event to one or more connected clients.
  107. :param event: The event name. It can be any string. The event names
  108. ``'connect'``, ``'message'`` and ``'disconnect'`` are
  109. reserved and should not be used.
  110. :param data: The data to send to the client or clients. Data can be of
  111. type ``str``, ``bytes``, ``list`` or ``dict``. If a
  112. ``list`` or ``dict``, the data will be serialized as JSON.
  113. :param namespace: The Socket.IO namespace for the event. If this
  114. argument is omitted the event is emitted to the
  115. default namespace.
  116. :param callback: If given, this function will be called to acknowledge
  117. the the client has received the message. The arguments
  118. that will be passed to the function are those provided
  119. by the client. Callback functions can only be used
  120. when addressing an individual client.
  121. Note: this method is a coroutine.
  122. """
  123. namespace = namespace or '/'
  124. if namespace != '/' and namespace not in self.namespaces:
  125. raise exceptions.BadNamespaceError(
  126. namespace + ' is not a connected namespace.')
  127. self.logger.info('Emitting event "%s" [%s]', event, namespace)
  128. if callback is not None:
  129. id = self._generate_ack_id(namespace, callback)
  130. else:
  131. id = None
  132. if six.PY2 and not self.binary:
  133. binary = False # pragma: nocover
  134. else:
  135. binary = None
  136. # tuples are expanded to multiple arguments, everything else is sent
  137. # as a single argument
  138. if isinstance(data, tuple):
  139. data = list(data)
  140. elif data is not None:
  141. data = [data]
  142. else:
  143. data = []
  144. await self._send_packet(packet.Packet(
  145. packet.EVENT, namespace=namespace, data=[event] + data, id=id,
  146. binary=binary))
  147. async def send(self, data, namespace=None, callback=None):
  148. """Send a message to one or more connected clients.
  149. This function emits an event with the name ``'message'``. Use
  150. :func:`emit` to issue custom event names.
  151. :param data: The data to send to the client or clients. Data can be of
  152. type ``str``, ``bytes``, ``list`` or ``dict``. If a
  153. ``list`` or ``dict``, the data will be serialized as JSON.
  154. :param namespace: The Socket.IO namespace for the event. If this
  155. argument is omitted the event is emitted to the
  156. default namespace.
  157. :param callback: If given, this function will be called to acknowledge
  158. the the client has received the message. The arguments
  159. that will be passed to the function are those provided
  160. by the client. Callback functions can only be used
  161. when addressing an individual client.
  162. Note: this method is a coroutine.
  163. """
  164. await self.emit('message', data=data, namespace=namespace,
  165. callback=callback)
  166. async def call(self, event, data=None, namespace=None, timeout=60):
  167. """Emit a custom event to a client and wait for the response.
  168. :param event: The event name. It can be any string. The event names
  169. ``'connect'``, ``'message'`` and ``'disconnect'`` are
  170. reserved and should not be used.
  171. :param data: The data to send to the client or clients. Data can be of
  172. type ``str``, ``bytes``, ``list`` or ``dict``. If a
  173. ``list`` or ``dict``, the data will be serialized as JSON.
  174. :param namespace: The Socket.IO namespace for the event. If this
  175. argument is omitted the event is emitted to the
  176. default namespace.
  177. :param timeout: The waiting timeout. If the timeout is reached before
  178. the client acknowledges the event, then a
  179. ``TimeoutError`` exception is raised.
  180. Note: this method is a coroutine.
  181. """
  182. callback_event = self.eio.create_event()
  183. callback_args = []
  184. def event_callback(*args):
  185. callback_args.append(args)
  186. callback_event.set()
  187. await self.emit(event, data=data, namespace=namespace,
  188. callback=event_callback)
  189. try:
  190. await asyncio.wait_for(callback_event.wait(), timeout)
  191. except asyncio.TimeoutError:
  192. six.raise_from(exceptions.TimeoutError(), None)
  193. return callback_args[0] if len(callback_args[0]) > 1 \
  194. else callback_args[0][0] if len(callback_args[0]) == 1 \
  195. else None
  196. async def disconnect(self):
  197. """Disconnect from the server.
  198. Note: this method is a coroutine.
  199. """
  200. # here we just request the disconnection
  201. # later in _handle_eio_disconnect we invoke the disconnect handler
  202. for n in self.namespaces:
  203. await self._send_packet(packet.Packet(packet.DISCONNECT,
  204. namespace=n))
  205. await self._send_packet(packet.Packet(
  206. packet.DISCONNECT, namespace='/'))
  207. self.connected = False
  208. await self.eio.disconnect(abort=True)
  209. def start_background_task(self, target, *args, **kwargs):
  210. """Start a background task using the appropriate async model.
  211. This is a utility function that applications can use to start a
  212. background task using the method that is compatible with the
  213. selected async mode.
  214. :param target: the target function to execute.
  215. :param args: arguments to pass to the function.
  216. :param kwargs: keyword arguments to pass to the function.
  217. This function returns an object compatible with the `Thread` class in
  218. the Python standard library. The `start()` method on this object is
  219. already called by this function.
  220. """
  221. return self.eio.start_background_task(target, *args, **kwargs)
  222. async def sleep(self, seconds=0):
  223. """Sleep for the requested amount of time using the appropriate async
  224. model.
  225. This is a utility function that applications can use to put a task to
  226. sleep without having to worry about using the correct call for the
  227. selected async mode.
  228. Note: this method is a coroutine.
  229. """
  230. return await self.eio.sleep(seconds)
  231. async def _send_packet(self, pkt):
  232. """Send a Socket.IO packet to the server."""
  233. encoded_packet = pkt.encode()
  234. if isinstance(encoded_packet, list):
  235. binary = False
  236. for ep in encoded_packet:
  237. await self.eio.send(ep, binary=binary)
  238. binary = True
  239. else:
  240. await self.eio.send(encoded_packet, binary=False)
  241. async def _handle_connect(self, namespace):
  242. namespace = namespace or '/'
  243. self.logger.info('Namespace {} is connected'.format(namespace))
  244. await self._trigger_event('connect', namespace=namespace)
  245. if namespace == '/':
  246. for n in self.namespaces:
  247. await self._send_packet(packet.Packet(packet.CONNECT,
  248. namespace=n))
  249. elif namespace not in self.namespaces:
  250. self.namespaces.append(namespace)
  251. async def _handle_disconnect(self, namespace):
  252. if not self.connected:
  253. return
  254. namespace = namespace or '/'
  255. if namespace == '/':
  256. for n in self.namespaces:
  257. await self._trigger_event('disconnect', namespace=n)
  258. self.namespaces = []
  259. await self._trigger_event('disconnect', namespace=namespace)
  260. if namespace in self.namespaces:
  261. self.namespaces.remove(namespace)
  262. if namespace == '/':
  263. self.connected = False
  264. async def _handle_event(self, namespace, id, data):
  265. namespace = namespace or '/'
  266. self.logger.info('Received event "%s" [%s]', data[0], namespace)
  267. r = await self._trigger_event(data[0], namespace, *data[1:])
  268. if id is not None:
  269. # send ACK packet with the response returned by the handler
  270. # tuples are expanded as multiple arguments
  271. if r is None:
  272. data = []
  273. elif isinstance(r, tuple):
  274. data = list(r)
  275. else:
  276. data = [r]
  277. if six.PY2 and not self.binary:
  278. binary = False # pragma: nocover
  279. else:
  280. binary = None
  281. await self._send_packet(packet.Packet(
  282. packet.ACK, namespace=namespace, id=id, data=data,
  283. binary=binary))
  284. async def _handle_ack(self, namespace, id, data):
  285. namespace = namespace or '/'
  286. self.logger.info('Received ack [%s]', namespace)
  287. callback = None
  288. try:
  289. callback = self.callbacks[namespace][id]
  290. except KeyError:
  291. # if we get an unknown callback we just ignore it
  292. self.logger.warning('Unknown callback received, ignoring.')
  293. else:
  294. del self.callbacks[namespace][id]
  295. if callback is not None:
  296. if asyncio.iscoroutinefunction(callback):
  297. await callback(*data)
  298. else:
  299. callback(*data)
  300. def _handle_error(self, namespace):
  301. namespace = namespace or '/'
  302. self.logger.info('Connection to namespace {} was rejected'.format(
  303. namespace))
  304. if namespace in self.namespaces:
  305. self.namespaces.remove(namespace)
  306. if namespace == '/':
  307. self.namespaces = []
  308. self.connected = False
  309. async def _trigger_event(self, event, namespace, *args):
  310. """Invoke an application event handler."""
  311. # first see if we have an explicit handler for the event
  312. if namespace in self.handlers and event in self.handlers[namespace]:
  313. if asyncio.iscoroutinefunction(self.handlers[namespace][event]):
  314. try:
  315. ret = await self.handlers[namespace][event](*args)
  316. except asyncio.CancelledError: # pragma: no cover
  317. ret = None
  318. else:
  319. ret = self.handlers[namespace][event](*args)
  320. return ret
  321. # or else, forward the event to a namepsace handler if one exists
  322. elif namespace in self.namespace_handlers:
  323. return await self.namespace_handlers[namespace].trigger_event(
  324. event, *args)
  325. async def _handle_reconnect(self):
  326. self._reconnect_abort.clear()
  327. client.reconnecting_clients.append(self)
  328. attempt_count = 0
  329. current_delay = self.reconnection_delay
  330. while True:
  331. delay = current_delay
  332. current_delay *= 2
  333. if delay > self.reconnection_delay_max:
  334. delay = self.reconnection_delay_max
  335. delay += self.randomization_factor * (2 * random.random() - 1)
  336. self.logger.info(
  337. 'Connection failed, new attempt in {:.02f} seconds'.format(
  338. delay))
  339. try:
  340. await asyncio.wait_for(self._reconnect_abort.wait(), delay)
  341. self.logger.info('Reconnect task aborted')
  342. break
  343. except (asyncio.TimeoutError, asyncio.CancelledError):
  344. pass
  345. attempt_count += 1
  346. try:
  347. await self.connect(self.connection_url,
  348. headers=self.connection_headers,
  349. transports=self.connection_transports,
  350. namespaces=self.connection_namespaces,
  351. socketio_path=self.socketio_path)
  352. except (exceptions.ConnectionError, ValueError):
  353. pass
  354. else:
  355. self.logger.info('Reconnection successful')
  356. self._reconnect_task = None
  357. break
  358. if self.reconnection_attempts and \
  359. attempt_count >= self.reconnection_attempts:
  360. self.logger.info(
  361. 'Maximum reconnection attempts reached, giving up')
  362. break
  363. client.reconnecting_clients.remove(self)
  364. def _handle_eio_connect(self):
  365. """Handle the Engine.IO connection event."""
  366. self.logger.info('Engine.IO connection established')
  367. self.sid = self.eio.sid
  368. async def _handle_eio_message(self, data):
  369. """Dispatch Engine.IO messages."""
  370. if self._binary_packet:
  371. pkt = self._binary_packet
  372. if pkt.add_attachment(data):
  373. self._binary_packet = None
  374. if pkt.packet_type == packet.BINARY_EVENT:
  375. await self._handle_event(pkt.namespace, pkt.id, pkt.data)
  376. else:
  377. await self._handle_ack(pkt.namespace, pkt.id, pkt.data)
  378. else:
  379. pkt = packet.Packet(encoded_packet=data)
  380. if pkt.packet_type == packet.CONNECT:
  381. await self._handle_connect(pkt.namespace)
  382. elif pkt.packet_type == packet.DISCONNECT:
  383. await self._handle_disconnect(pkt.namespace)
  384. elif pkt.packet_type == packet.EVENT:
  385. await self._handle_event(pkt.namespace, pkt.id, pkt.data)
  386. elif pkt.packet_type == packet.ACK:
  387. await self._handle_ack(pkt.namespace, pkt.id, pkt.data)
  388. elif pkt.packet_type == packet.BINARY_EVENT or \
  389. pkt.packet_type == packet.BINARY_ACK:
  390. self._binary_packet = pkt
  391. elif pkt.packet_type == packet.ERROR:
  392. self._handle_error(pkt.namespace)
  393. else:
  394. raise ValueError('Unknown packet type.')
  395. async def _handle_eio_disconnect(self):
  396. """Handle the Engine.IO disconnection event."""
  397. self.logger.info('Engine.IO connection dropped')
  398. self._reconnect_abort.set()
  399. if self.connected:
  400. for n in self.namespaces:
  401. await self._trigger_event('disconnect', namespace=n)
  402. await self._trigger_event('disconnect', namespace='/')
  403. self.namespaces = []
  404. self.connected = False
  405. self.callbacks = {}
  406. self._binary_packet = None
  407. self.sid = None
  408. if self.eio.state == 'connected' and self.reconnection:
  409. self._reconnect_task = self.start_background_task(
  410. self._handle_reconnect)
  411. def _engineio_client_class(self):
  412. return engineio.AsyncClient