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

610 строки
25KB

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