您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

923 行
43KB

  1. from functools import wraps
  2. import os
  3. import sys
  4. # make sure gevent-socketio is not installed, as it conflicts with
  5. # python-socketio
  6. gevent_socketio_found = True
  7. try:
  8. from socketio import socketio_manage
  9. except ImportError:
  10. gevent_socketio_found = False
  11. if gevent_socketio_found:
  12. print('The gevent-socketio package is incompatible with this version of '
  13. 'the Flask-SocketIO extension. Please uninstall it, and then '
  14. 'install the latest version of python-socketio in its place.')
  15. sys.exit(1)
  16. import flask
  17. from flask import _request_ctx_stack, json as flask_json
  18. from flask.sessions import SessionMixin
  19. import socketio
  20. from socketio.exceptions import ConnectionRefusedError
  21. from werkzeug.debug import DebuggedApplication
  22. from werkzeug.serving import run_with_reloader
  23. from .namespace import Namespace
  24. from .test_client import SocketIOTestClient
  25. __version__ = '4.2.1'
  26. class _SocketIOMiddleware(socketio.WSGIApp):
  27. """This WSGI middleware simply exposes the Flask application in the WSGI
  28. environment before executing the request.
  29. """
  30. def __init__(self, socketio_app, flask_app, socketio_path='socket.io'):
  31. self.flask_app = flask_app
  32. super(_SocketIOMiddleware, self).__init__(socketio_app,
  33. flask_app.wsgi_app,
  34. socketio_path=socketio_path)
  35. def __call__(self, environ, start_response):
  36. environ = environ.copy()
  37. environ['flask.app'] = self.flask_app
  38. return super(_SocketIOMiddleware, self).__call__(environ,
  39. start_response)
  40. class _ManagedSession(dict, SessionMixin):
  41. """This class is used for user sessions that are managed by
  42. Flask-SocketIO. It is simple dict, expanded with the Flask session
  43. attributes."""
  44. pass
  45. class SocketIO(object):
  46. """Create a Flask-SocketIO server.
  47. :param app: The flask application instance. If the application instance
  48. isn't known at the time this class is instantiated, then call
  49. ``socketio.init_app(app)`` once the application instance is
  50. available.
  51. :param manage_session: If set to ``True``, this extension manages the user
  52. session for Socket.IO events. If set to ``False``,
  53. Flask's own session management is used. When using
  54. Flask's cookie based sessions it is recommended that
  55. you leave this set to the default of ``True``. When
  56. using server-side sessions, a ``False`` setting
  57. enables sharing the user session between HTTP routes
  58. and Socket.IO events.
  59. :param message_queue: A connection URL for a message queue service the
  60. server can use for multi-process communication. A
  61. message queue is not required when using a single
  62. server process.
  63. :param channel: The channel name, when using a message queue. If a channel
  64. isn't specified, a default channel will be used. If
  65. multiple clusters of SocketIO processes need to use the
  66. same message queue without interfering with each other, then
  67. each cluster should use a different channel.
  68. :param path: The path where the Socket.IO server is exposed. Defaults to
  69. ``'socket.io'``. Leave this as is unless you know what you are
  70. doing.
  71. :param resource: Alias to ``path``.
  72. :param kwargs: Socket.IO and Engine.IO server options.
  73. The Socket.IO server options are detailed below:
  74. :param client_manager: The client manager instance that will manage the
  75. client list. When this is omitted, the client list
  76. is stored in an in-memory structure, so the use of
  77. multiple connected servers is not possible. In most
  78. cases, this argument does not need to be set
  79. explicitly.
  80. :param logger: To enable logging set to ``True`` or pass a logger object to
  81. use. To disable logging set to ``False``. The default is
  82. ``False``.
  83. :param binary: ``True`` to support binary payloads, ``False`` to treat all
  84. payloads as text. On Python 2, if this is set to ``True``,
  85. ``unicode`` values are treated as text, and ``str`` and
  86. ``bytes`` values are treated as binary. This option has no
  87. effect on Python 3, where text and binary payloads are
  88. always automatically discovered.
  89. :param json: An alternative json module to use for encoding and decoding
  90. packets. Custom json modules must have ``dumps`` and ``loads``
  91. functions that are compatible with the standard library
  92. versions. To use the same json encoder and decoder as a Flask
  93. application, use ``flask.json``.
  94. :param async_handlers: If set to ``True``, event handlers for a client are
  95. executed in separate threads. To run handlers for a
  96. client synchronously, set to ``False``. The default
  97. is ``True``.
  98. :param always_connect: When set to ``False``, new connections are
  99. provisory until the connect handler returns
  100. something other than ``False``, at which point they
  101. are accepted. When set to ``True``, connections are
  102. immediately accepted, and then if the connect
  103. handler returns ``False`` a disconnect is issued.
  104. Set to ``True`` if you need to emit events from the
  105. connect handler and your client is confused when it
  106. receives events before the connection acceptance.
  107. In any other case use the default of ``False``.
  108. The Engine.IO server configuration supports the following settings:
  109. :param async_mode: The asynchronous model to use. See the Deployment
  110. section in the documentation for a description of the
  111. available options. Valid async modes are
  112. ``threading``, ``eventlet``, ``gevent`` and
  113. ``gevent_uwsgi``. If this argument is not given,
  114. ``eventlet`` is tried first, then ``gevent_uwsgi``,
  115. then ``gevent``, and finally ``threading``. The
  116. first async mode that has all its dependencies installed
  117. is then one that is chosen.
  118. :param ping_timeout: The time in seconds that the client waits for the
  119. server to respond before disconnecting. The default is
  120. 60 seconds.
  121. :param ping_interval: The interval in seconds at which the client pings
  122. the server. The default is 25 seconds.
  123. :param max_http_buffer_size: The maximum size of a message when using the
  124. polling transport. The default is 100,000,000
  125. bytes.
  126. :param allow_upgrades: Whether to allow transport upgrades or not. The
  127. default is ``True``.
  128. :param http_compression: Whether to compress packages when using the
  129. polling transport. The default is ``True``.
  130. :param compression_threshold: Only compress messages when their byte size
  131. is greater than this value. The default is
  132. 1024 bytes.
  133. :param cookie: Name of the HTTP cookie that contains the client session
  134. id. If set to ``None``, a cookie is not sent to the client.
  135. The default is ``'io'``.
  136. :param cors_allowed_origins: Origin or list of origins that are allowed to
  137. connect to this server. Only the same origin
  138. is allowed by default. Set this argument to
  139. ``'*'`` to allow all origins, or to ``[]`` to
  140. disable CORS handling.
  141. :param cors_credentials: Whether credentials (cookies, authentication) are
  142. allowed in requests to this server. The default is
  143. ``True``.
  144. :param monitor_clients: If set to ``True``, a background task will ensure
  145. inactive clients are closed. Set to ``False`` to
  146. disable the monitoring task (not recommended). The
  147. default is ``True``.
  148. :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass
  149. a logger object to use. To disable logging set to
  150. ``False``. The default is ``False``.
  151. """
  152. def __init__(self, app=None, **kwargs):
  153. self.server = None
  154. self.server_options = {}
  155. self.wsgi_server = None
  156. self.handlers = []
  157. self.namespace_handlers = []
  158. self.exception_handlers = {}
  159. self.default_exception_handler = None
  160. self.manage_session = True
  161. # We can call init_app when:
  162. # - we were given the Flask app instance (standard initialization)
  163. # - we were not given the app, but we were given a message_queue
  164. # (standard initialization for auxiliary process)
  165. # In all other cases we collect the arguments and assume the client
  166. # will call init_app from an app factory function.
  167. if app is not None or 'message_queue' in kwargs:
  168. self.init_app(app, **kwargs)
  169. else:
  170. self.server_options.update(kwargs)
  171. def init_app(self, app, **kwargs):
  172. if app is not None:
  173. if not hasattr(app, 'extensions'):
  174. app.extensions = {} # pragma: no cover
  175. app.extensions['socketio'] = self
  176. self.server_options.update(kwargs)
  177. self.manage_session = self.server_options.pop('manage_session',
  178. self.manage_session)
  179. if 'client_manager' not in self.server_options:
  180. url = self.server_options.pop('message_queue', None)
  181. channel = self.server_options.pop('channel', 'flask-socketio')
  182. write_only = app is None
  183. if url:
  184. if url.startswith(('redis://', "rediss://")):
  185. queue_class = socketio.RedisManager
  186. elif url.startswith(('kafka://')):
  187. queue_class = socketio.KafkaManager
  188. elif url.startswith('zmq'):
  189. queue_class = socketio.ZmqManager
  190. else:
  191. queue_class = socketio.KombuManager
  192. queue = queue_class(url, channel=channel,
  193. write_only=write_only)
  194. self.server_options['client_manager'] = queue
  195. if 'json' in self.server_options and \
  196. self.server_options['json'] == flask_json:
  197. # flask's json module is tricky to use because its output
  198. # changes when it is invoked inside or outside the app context
  199. # so here to prevent any ambiguities we replace it with wrappers
  200. # that ensure that the app context is always present
  201. class FlaskSafeJSON(object):
  202. @staticmethod
  203. def dumps(*args, **kwargs):
  204. with app.app_context():
  205. return flask_json.dumps(*args, **kwargs)
  206. @staticmethod
  207. def loads(*args, **kwargs):
  208. with app.app_context():
  209. return flask_json.loads(*args, **kwargs)
  210. self.server_options['json'] = FlaskSafeJSON
  211. resource = self.server_options.pop('path', None) or \
  212. self.server_options.pop('resource', None) or 'socket.io'
  213. if resource.startswith('/'):
  214. resource = resource[1:]
  215. if os.environ.get('FLASK_RUN_FROM_CLI'):
  216. if self.server_options.get('async_mode') is None:
  217. if app is not None:
  218. app.logger.warning(
  219. 'Flask-SocketIO is Running under Werkzeug, WebSocket '
  220. 'is not available.')
  221. self.server_options['async_mode'] = 'threading'
  222. self.server = socketio.Server(**self.server_options)
  223. self.async_mode = self.server.async_mode
  224. for handler in self.handlers:
  225. self.server.on(handler[0], handler[1], namespace=handler[2])
  226. for namespace_handler in self.namespace_handlers:
  227. self.server.register_namespace(namespace_handler)
  228. if app is not None:
  229. # here we attach the SocketIO middlware to the SocketIO object so it
  230. # can be referenced later if debug middleware needs to be inserted
  231. self.sockio_mw = _SocketIOMiddleware(self.server, app,
  232. socketio_path=resource)
  233. app.wsgi_app = self.sockio_mw
  234. def on(self, message, namespace=None):
  235. """Decorator to register a SocketIO event handler.
  236. This decorator must be applied to SocketIO event handlers. Example::
  237. @socketio.on('my event', namespace='/chat')
  238. def handle_my_custom_event(json):
  239. print('received json: ' + str(json))
  240. :param message: The name of the event. This is normally a user defined
  241. string, but a few event names are already defined. Use
  242. ``'message'`` to define a handler that takes a string
  243. payload, ``'json'`` to define a handler that takes a
  244. JSON blob payload, ``'connect'`` or ``'disconnect'``
  245. to create handlers for connection and disconnection
  246. events.
  247. :param namespace: The namespace on which the handler is to be
  248. registered. Defaults to the global namespace.
  249. """
  250. namespace = namespace or '/'
  251. def decorator(handler):
  252. @wraps(handler)
  253. def _handler(sid, *args):
  254. return self._handle_event(handler, message, namespace, sid,
  255. *args)
  256. if self.server:
  257. self.server.on(message, _handler, namespace=namespace)
  258. else:
  259. self.handlers.append((message, _handler, namespace))
  260. return handler
  261. return decorator
  262. def on_error(self, namespace=None):
  263. """Decorator to define a custom error handler for SocketIO events.
  264. This decorator can be applied to a function that acts as an error
  265. handler for a namespace. This handler will be invoked when a SocketIO
  266. event handler raises an exception. The handler function must accept one
  267. argument, which is the exception raised. Example::
  268. @socketio.on_error(namespace='/chat')
  269. def chat_error_handler(e):
  270. print('An error has occurred: ' + str(e))
  271. :param namespace: The namespace for which to register the error
  272. handler. Defaults to the global namespace.
  273. """
  274. namespace = namespace or '/'
  275. def decorator(exception_handler):
  276. if not callable(exception_handler):
  277. raise ValueError('exception_handler must be callable')
  278. self.exception_handlers[namespace] = exception_handler
  279. return exception_handler
  280. return decorator
  281. def on_error_default(self, exception_handler):
  282. """Decorator to define a default error handler for SocketIO events.
  283. This decorator can be applied to a function that acts as a default
  284. error handler for any namespaces that do not have a specific handler.
  285. Example::
  286. @socketio.on_error_default
  287. def error_handler(e):
  288. print('An error has occurred: ' + str(e))
  289. """
  290. if not callable(exception_handler):
  291. raise ValueError('exception_handler must be callable')
  292. self.default_exception_handler = exception_handler
  293. return exception_handler
  294. def on_event(self, message, handler, namespace=None):
  295. """Register a SocketIO event handler.
  296. ``on_event`` is the non-decorator version of ``'on'``.
  297. Example::
  298. def on_foo_event(json):
  299. print('received json: ' + str(json))
  300. socketio.on_event('my event', on_foo_event, namespace='/chat')
  301. :param message: The name of the event. This is normally a user defined
  302. string, but a few event names are already defined. Use
  303. ``'message'`` to define a handler that takes a string
  304. payload, ``'json'`` to define a handler that takes a
  305. JSON blob payload, ``'connect'`` or ``'disconnect'``
  306. to create handlers for connection and disconnection
  307. events.
  308. :param handler: The function that handles the event.
  309. :param namespace: The namespace on which the handler is to be
  310. registered. Defaults to the global namespace.
  311. """
  312. self.on(message, namespace=namespace)(handler)
  313. def on_namespace(self, namespace_handler):
  314. if not isinstance(namespace_handler, Namespace):
  315. raise ValueError('Not a namespace instance.')
  316. namespace_handler._set_socketio(self)
  317. if self.server:
  318. self.server.register_namespace(namespace_handler)
  319. else:
  320. self.namespace_handlers.append(namespace_handler)
  321. def emit(self, event, *args, **kwargs):
  322. """Emit a server generated SocketIO event.
  323. This function emits a SocketIO event to one or more connected clients.
  324. A JSON blob can be attached to the event as payload. This function can
  325. be used outside of a SocketIO event context, so it is appropriate to
  326. use when the server is the originator of an event, outside of any
  327. client context, such as in a regular HTTP request handler or a
  328. background task. Example::
  329. @app.route('/ping')
  330. def ping():
  331. socketio.emit('ping event', {'data': 42}, namespace='/chat')
  332. :param event: The name of the user event to emit.
  333. :param args: A dictionary with the JSON data to send as payload.
  334. :param namespace: The namespace under which the message is to be sent.
  335. Defaults to the global namespace.
  336. :param room: Send the message to all the users in the given room. If
  337. this parameter is not included, the event is sent to
  338. all connected users.
  339. :param skip_sid: The session id of a client to ignore when broadcasting
  340. or addressing a room. This is typically set to the
  341. originator of the message, so that everyone except
  342. that client receive the message. To skip multiple sids
  343. pass a list.
  344. :param callback: If given, this function will be called to acknowledge
  345. that the client has received the message. The
  346. arguments that will be passed to the function are
  347. those provided by the client. Callback functions can
  348. only be used when addressing an individual client.
  349. """
  350. namespace = kwargs.pop('namespace', '/')
  351. room = kwargs.pop('room', None)
  352. include_self = kwargs.pop('include_self', True)
  353. skip_sid = kwargs.pop('skip_sid', None)
  354. if not include_self and not skip_sid:
  355. skip_sid = flask.request.sid
  356. callback = kwargs.pop('callback', None)
  357. if callback:
  358. # wrap the callback so that it sets app app and request contexts
  359. sid = flask.request.sid
  360. original_callback = callback
  361. def _callback_wrapper(*args):
  362. return self._handle_event(original_callback, None, namespace,
  363. sid, *args)
  364. callback = _callback_wrapper
  365. self.server.emit(event, *args, namespace=namespace, room=room,
  366. skip_sid=skip_sid, callback=callback, **kwargs)
  367. def send(self, data, json=False, namespace=None, room=None,
  368. callback=None, include_self=True, skip_sid=None, **kwargs):
  369. """Send a server-generated SocketIO message.
  370. This function sends a simple SocketIO message to one or more connected
  371. clients. The message can be a string or a JSON blob. This is a simpler
  372. version of ``emit()``, which should be preferred. This function can be
  373. used outside of a SocketIO event context, so it is appropriate to use
  374. when the server is the originator of an event.
  375. :param data: The message to send, either a string or a JSON blob.
  376. :param json: ``True`` if ``message`` is a JSON blob, ``False``
  377. otherwise.
  378. :param namespace: The namespace under which the message is to be sent.
  379. Defaults to the global namespace.
  380. :param room: Send the message only to the users in the given room. If
  381. this parameter is not included, the message is sent to
  382. all connected users.
  383. :param skip_sid: The session id of a client to ignore when broadcasting
  384. or addressing a room. This is typically set to the
  385. originator of the message, so that everyone except
  386. that client receive the message. To skip multiple sids
  387. pass a list.
  388. :param callback: If given, this function will be called to acknowledge
  389. that the client has received the message. The
  390. arguments that will be passed to the function are
  391. those provided by the client. Callback functions can
  392. only be used when addressing an individual client.
  393. """
  394. skip_sid = flask.request.sid if not include_self else skip_sid
  395. if json:
  396. self.emit('json', data, namespace=namespace, room=room,
  397. skip_sid=skip_sid, callback=callback, **kwargs)
  398. else:
  399. self.emit('message', data, namespace=namespace, room=room,
  400. skip_sid=skip_sid, callback=callback, **kwargs)
  401. def close_room(self, room, namespace=None):
  402. """Close a room.
  403. This function removes any users that are in the given room and then
  404. deletes the room from the server. This function can be used outside
  405. of a SocketIO event context.
  406. :param room: The name of the room to close.
  407. :param namespace: The namespace under which the room exists. Defaults
  408. to the global namespace.
  409. """
  410. self.server.close_room(room, namespace)
  411. def run(self, app, host=None, port=None, **kwargs):
  412. """Run the SocketIO web server.
  413. :param app: The Flask application instance.
  414. :param host: The hostname or IP address for the server to listen on.
  415. Defaults to 127.0.0.1.
  416. :param port: The port number for the server to listen on. Defaults to
  417. 5000.
  418. :param debug: ``True`` to start the server in debug mode, ``False`` to
  419. start in normal mode.
  420. :param use_reloader: ``True`` to enable the Flask reloader, ``False``
  421. to disable it.
  422. :param extra_files: A list of additional files that the Flask
  423. reloader should watch. Defaults to ``None``
  424. :param log_output: If ``True``, the server logs all incomming
  425. connections. If ``False`` logging is disabled.
  426. Defaults to ``True`` in debug mode, ``False``
  427. in normal mode. Unused when the threading async
  428. mode is used.
  429. :param kwargs: Additional web server options. The web server options
  430. are specific to the server used in each of the supported
  431. async modes. Note that options provided here will
  432. not be seen when using an external web server such
  433. as gunicorn, since this method is not called in that
  434. case.
  435. """
  436. if host is None:
  437. host = '127.0.0.1'
  438. if port is None:
  439. server_name = app.config['SERVER_NAME']
  440. if server_name and ':' in server_name:
  441. port = int(server_name.rsplit(':', 1)[1])
  442. else:
  443. port = 5000
  444. debug = kwargs.pop('debug', app.debug)
  445. log_output = kwargs.pop('log_output', debug)
  446. use_reloader = kwargs.pop('use_reloader', debug)
  447. extra_files = kwargs.pop('extra_files', None)
  448. app.debug = debug
  449. if app.debug and self.server.eio.async_mode != 'threading':
  450. # put the debug middleware between the SocketIO middleware
  451. # and the Flask application instance
  452. #
  453. # mw1 mw2 mw3 Flask app
  454. # o ---- o ---- o ---- o
  455. # /
  456. # o Flask-SocketIO
  457. # \ middleware
  458. # o
  459. # Flask-SocketIO WebSocket handler
  460. #
  461. # BECOMES
  462. #
  463. # dbg-mw mw1 mw2 mw3 Flask app
  464. # o ---- o ---- o ---- o ---- o
  465. # /
  466. # o Flask-SocketIO
  467. # \ middleware
  468. # o
  469. # Flask-SocketIO WebSocket handler
  470. #
  471. self.sockio_mw.wsgi_app = DebuggedApplication(self.sockio_mw.wsgi_app,
  472. evalex=True)
  473. if self.server.eio.async_mode == 'threading':
  474. from werkzeug._internal import _log
  475. _log('warning', 'WebSocket transport not available. Install '
  476. 'eventlet or gevent and gevent-websocket for '
  477. 'improved performance.')
  478. app.run(host=host, port=port, threaded=True,
  479. use_reloader=use_reloader, **kwargs)
  480. elif self.server.eio.async_mode == 'eventlet':
  481. def run_server():
  482. import eventlet
  483. import eventlet.wsgi
  484. import eventlet.green
  485. addresses = eventlet.green.socket.getaddrinfo(host, port)
  486. if not addresses:
  487. raise RuntimeError('Could not resolve host to a valid address')
  488. eventlet_socket = eventlet.listen(addresses[0][4], addresses[0][0])
  489. # If provided an SSL argument, use an SSL socket
  490. ssl_args = ['keyfile', 'certfile', 'server_side', 'cert_reqs',
  491. 'ssl_version', 'ca_certs',
  492. 'do_handshake_on_connect', 'suppress_ragged_eofs',
  493. 'ciphers']
  494. ssl_params = {k: kwargs[k] for k in kwargs if k in ssl_args}
  495. if len(ssl_params) > 0:
  496. for k in ssl_params:
  497. kwargs.pop(k)
  498. ssl_params['server_side'] = True # Listening requires true
  499. eventlet_socket = eventlet.wrap_ssl(eventlet_socket,
  500. **ssl_params)
  501. eventlet.wsgi.server(eventlet_socket, app,
  502. log_output=log_output, **kwargs)
  503. if use_reloader:
  504. run_with_reloader(run_server, extra_files=extra_files)
  505. else:
  506. run_server()
  507. elif self.server.eio.async_mode == 'gevent':
  508. from gevent import pywsgi
  509. try:
  510. from geventwebsocket.handler import WebSocketHandler
  511. websocket = True
  512. except ImportError:
  513. websocket = False
  514. log = 'default'
  515. if not log_output:
  516. log = None
  517. if websocket:
  518. self.wsgi_server = pywsgi.WSGIServer(
  519. (host, port), app, handler_class=WebSocketHandler,
  520. log=log, **kwargs)
  521. else:
  522. self.wsgi_server = pywsgi.WSGIServer((host, port), app,
  523. log=log, **kwargs)
  524. if use_reloader:
  525. # monkey patching is required by the reloader
  526. from gevent import monkey
  527. monkey.patch_thread()
  528. monkey.patch_time()
  529. def run_server():
  530. self.wsgi_server.serve_forever()
  531. run_with_reloader(run_server, extra_files=extra_files)
  532. else:
  533. self.wsgi_server.serve_forever()
  534. def stop(self):
  535. """Stop a running SocketIO web server.
  536. This method must be called from a HTTP or SocketIO handler function.
  537. """
  538. if self.server.eio.async_mode == 'threading':
  539. func = flask.request.environ.get('werkzeug.server.shutdown')
  540. if func:
  541. func()
  542. else:
  543. raise RuntimeError('Cannot stop unknown web server')
  544. elif self.server.eio.async_mode == 'eventlet':
  545. raise SystemExit
  546. elif self.server.eio.async_mode == 'gevent':
  547. self.wsgi_server.stop()
  548. def start_background_task(self, target, *args, **kwargs):
  549. """Start a background task using the appropriate async model.
  550. This is a utility function that applications can use to start a
  551. background task using the method that is compatible with the
  552. selected async mode.
  553. :param target: the target function to execute.
  554. :param args: arguments to pass to the function.
  555. :param kwargs: keyword arguments to pass to the function.
  556. This function returns an object compatible with the `Thread` class in
  557. the Python standard library. The `start()` method on this object is
  558. already called by this function.
  559. """
  560. return self.server.start_background_task(target, *args, **kwargs)
  561. def sleep(self, seconds=0):
  562. """Sleep for the requested amount of time using the appropriate async
  563. model.
  564. This is a utility function that applications can use to put a task to
  565. sleep without having to worry about using the correct call for the
  566. selected async mode.
  567. """
  568. return self.server.sleep(seconds)
  569. def test_client(self, app, namespace=None, query_string=None,
  570. headers=None, flask_test_client=None):
  571. """The Socket.IO test client is useful for testing a Flask-SocketIO
  572. server. It works in a similar way to the Flask Test Client, but
  573. adapted to the Socket.IO server.
  574. :param app: The Flask application instance.
  575. :param namespace: The namespace for the client. If not provided, the
  576. client connects to the server on the global
  577. namespace.
  578. :param query_string: A string with custom query string arguments.
  579. :param headers: A dictionary with custom HTTP headers.
  580. :param flask_test_client: The instance of the Flask test client
  581. currently in use. Passing the Flask test
  582. client is optional, but is necessary if you
  583. want the Flask user session and any other
  584. cookies set in HTTP routes accessible from
  585. Socket.IO events.
  586. """
  587. return SocketIOTestClient(app, self, namespace=namespace,
  588. query_string=query_string, headers=headers,
  589. flask_test_client=flask_test_client)
  590. def _handle_event(self, handler, message, namespace, sid, *args):
  591. if sid not in self.server.environ:
  592. # we don't have record of this client, ignore this event
  593. return '', 400
  594. app = self.server.environ[sid]['flask.app']
  595. with app.request_context(self.server.environ[sid]):
  596. if self.manage_session:
  597. # manage a separate session for this client's Socket.IO events
  598. # created as a copy of the regular user session
  599. if 'saved_session' not in self.server.environ[sid]:
  600. self.server.environ[sid]['saved_session'] = \
  601. _ManagedSession(flask.session)
  602. session_obj = self.server.environ[sid]['saved_session']
  603. else:
  604. # let Flask handle the user session
  605. # for cookie based sessions, this effectively freezes the
  606. # session to its state at connection time
  607. # for server-side sessions, this allows HTTP and Socket.IO to
  608. # share the session, with both having read/write access to it
  609. session_obj = flask.session._get_current_object()
  610. _request_ctx_stack.top.session = session_obj
  611. flask.request.sid = sid
  612. flask.request.namespace = namespace
  613. flask.request.event = {'message': message, 'args': args}
  614. try:
  615. if message == 'connect':
  616. ret = handler()
  617. else:
  618. ret = handler(*args)
  619. except:
  620. err_handler = self.exception_handlers.get(
  621. namespace, self.default_exception_handler)
  622. if err_handler is None:
  623. raise
  624. type, value, traceback = sys.exc_info()
  625. return err_handler(value)
  626. if not self.manage_session:
  627. # when Flask is managing the user session, it needs to save it
  628. if not hasattr(session_obj, 'modified') or session_obj.modified:
  629. resp = app.response_class()
  630. app.session_interface.save_session(app, session_obj, resp)
  631. return ret
  632. def emit(event, *args, **kwargs):
  633. """Emit a SocketIO event.
  634. This function emits a SocketIO event to one or more connected clients. A
  635. JSON blob can be attached to the event as payload. This is a function that
  636. can only be called from a SocketIO event handler, as in obtains some
  637. information from the current client context. Example::
  638. @socketio.on('my event')
  639. def handle_my_custom_event(json):
  640. emit('my response', {'data': 42})
  641. :param event: The name of the user event to emit.
  642. :param args: A dictionary with the JSON data to send as payload.
  643. :param namespace: The namespace under which the message is to be sent.
  644. Defaults to the namespace used by the originating event.
  645. A ``'/'`` can be used to explicitly specify the global
  646. namespace.
  647. :param callback: Callback function to invoke with the client's
  648. acknowledgement.
  649. :param broadcast: ``True`` to send the message to all clients, or ``False``
  650. to only reply to the sender of the originating event.
  651. :param room: Send the message to all the users in the given room. If this
  652. argument is set, then broadcast is implied to be ``True``.
  653. :param include_self: ``True`` to include the sender when broadcasting or
  654. addressing a room, or ``False`` to send to everyone
  655. but the sender.
  656. :param ignore_queue: Only used when a message queue is configured. If
  657. set to ``True``, the event is emitted to the
  658. clients directly, without going through the queue.
  659. This is more efficient, but only works when a
  660. single server process is used, or when there is a
  661. single addresee. It is recommended to always leave
  662. this parameter with its default value of ``False``.
  663. """
  664. if 'namespace' in kwargs:
  665. namespace = kwargs['namespace']
  666. else:
  667. namespace = flask.request.namespace
  668. callback = kwargs.get('callback')
  669. broadcast = kwargs.get('broadcast')
  670. room = kwargs.get('room')
  671. if room is None and not broadcast:
  672. room = flask.request.sid
  673. include_self = kwargs.get('include_self', True)
  674. ignore_queue = kwargs.get('ignore_queue', False)
  675. socketio = flask.current_app.extensions['socketio']
  676. return socketio.emit(event, *args, namespace=namespace, room=room,
  677. include_self=include_self, callback=callback,
  678. ignore_queue=ignore_queue)
  679. def send(message, **kwargs):
  680. """Send a SocketIO message.
  681. This function sends a simple SocketIO message to one or more connected
  682. clients. The message can be a string or a JSON blob. This is a simpler
  683. version of ``emit()``, which should be preferred. This is a function that
  684. can only be called from a SocketIO event handler.
  685. :param message: The message to send, either a string or a JSON blob.
  686. :param json: ``True`` if ``message`` is a JSON blob, ``False``
  687. otherwise.
  688. :param namespace: The namespace under which the message is to be sent.
  689. Defaults to the namespace used by the originating event.
  690. An empty string can be used to use the global namespace.
  691. :param callback: Callback function to invoke with the client's
  692. acknowledgement.
  693. :param broadcast: ``True`` to send the message to all connected clients, or
  694. ``False`` to only reply to the sender of the originating
  695. event.
  696. :param room: Send the message to all the users in the given room.
  697. :param include_self: ``True`` to include the sender when broadcasting or
  698. addressing a room, or ``False`` to send to everyone
  699. but the sender.
  700. :param ignore_queue: Only used when a message queue is configured. If
  701. set to ``True``, the event is emitted to the
  702. clients directly, without going through the queue.
  703. This is more efficient, but only works when a
  704. single server process is used, or when there is a
  705. single addresee. It is recommended to always leave
  706. this parameter with its default value of ``False``.
  707. """
  708. json = kwargs.get('json', False)
  709. if 'namespace' in kwargs:
  710. namespace = kwargs['namespace']
  711. else:
  712. namespace = flask.request.namespace
  713. callback = kwargs.get('callback')
  714. broadcast = kwargs.get('broadcast')
  715. room = kwargs.get('room')
  716. if room is None and not broadcast:
  717. room = flask.request.sid
  718. include_self = kwargs.get('include_self', True)
  719. ignore_queue = kwargs.get('ignore_queue', False)
  720. socketio = flask.current_app.extensions['socketio']
  721. return socketio.send(message, json=json, namespace=namespace, room=room,
  722. include_self=include_self, callback=callback,
  723. ignore_queue=ignore_queue)
  724. def join_room(room, sid=None, namespace=None):
  725. """Join a room.
  726. This function puts the user in a room, under the current namespace. The
  727. user and the namespace are obtained from the event context. This is a
  728. function that can only be called from a SocketIO event handler. Example::
  729. @socketio.on('join')
  730. def on_join(data):
  731. username = session['username']
  732. room = data['room']
  733. join_room(room)
  734. send(username + ' has entered the room.', room=room)
  735. :param room: The name of the room to join.
  736. :param sid: The session id of the client. If not provided, the client is
  737. obtained from the request context.
  738. :param namespace: The namespace for the room. If not provided, the
  739. namespace is obtained from the request context.
  740. """
  741. socketio = flask.current_app.extensions['socketio']
  742. sid = sid or flask.request.sid
  743. namespace = namespace or flask.request.namespace
  744. socketio.server.enter_room(sid, room, namespace=namespace)
  745. def leave_room(room, sid=None, namespace=None):
  746. """Leave a room.
  747. This function removes the user from a room, under the current namespace.
  748. The user and the namespace are obtained from the event context. Example::
  749. @socketio.on('leave')
  750. def on_leave(data):
  751. username = session['username']
  752. room = data['room']
  753. leave_room(room)
  754. send(username + ' has left the room.', room=room)
  755. :param room: The name of the room to leave.
  756. :param sid: The session id of the client. If not provided, the client is
  757. obtained from the request context.
  758. :param namespace: The namespace for the room. If not provided, the
  759. namespace is obtained from the request context.
  760. """
  761. socketio = flask.current_app.extensions['socketio']
  762. sid = sid or flask.request.sid
  763. namespace = namespace or flask.request.namespace
  764. socketio.server.leave_room(sid, room, namespace=namespace)
  765. def close_room(room, namespace=None):
  766. """Close a room.
  767. This function removes any users that are in the given room and then deletes
  768. the room from the server.
  769. :param room: The name of the room to close.
  770. :param namespace: The namespace for the room. If not provided, the
  771. namespace is obtained from the request context.
  772. """
  773. socketio = flask.current_app.extensions['socketio']
  774. namespace = namespace or flask.request.namespace
  775. socketio.server.close_room(room, namespace=namespace)
  776. def rooms(sid=None, namespace=None):
  777. """Return a list of the rooms the client is in.
  778. This function returns all the rooms the client has entered, including its
  779. own room, assigned by the Socket.IO server.
  780. :param sid: The session id of the client. If not provided, the client is
  781. obtained from the request context.
  782. :param namespace: The namespace for the room. If not provided, the
  783. namespace is obtained from the request context.
  784. """
  785. socketio = flask.current_app.extensions['socketio']
  786. sid = sid or flask.request.sid
  787. namespace = namespace or flask.request.namespace
  788. return socketio.server.rooms(sid, namespace=namespace)
  789. def disconnect(sid=None, namespace=None, silent=False):
  790. """Disconnect the client.
  791. This function terminates the connection with the client. As a result of
  792. this call the client will receive a disconnect event. Example::
  793. @socketio.on('message')
  794. def receive_message(msg):
  795. if is_banned(session['username']):
  796. disconnect()
  797. else:
  798. # ...
  799. :param sid: The session id of the client. If not provided, the client is
  800. obtained from the request context.
  801. :param namespace: The namespace for the room. If not provided, the
  802. namespace is obtained from the request context.
  803. :param silent: this option is deprecated.
  804. """
  805. socketio = flask.current_app.extensions['socketio']
  806. sid = sid or flask.request.sid
  807. namespace = namespace or flask.request.namespace
  808. return socketio.server.disconnect(sid, namespace=namespace)