├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── docs ├── Makefile ├── _static │ ├── README.md │ └── custom.css ├── api.rst ├── client.rst ├── conf.py ├── index.rst ├── intro.rst ├── make.bat └── server.rst ├── examples ├── README.rst ├── client │ ├── README.rst │ ├── async │ │ ├── README.rst │ │ ├── fiddle_client.py │ │ └── latency_client.py │ ├── javascript │ │ ├── README.md │ │ ├── fiddle_client.js │ │ ├── latency_client.js │ │ ├── package-lock.json │ │ └── package.json │ └── sync │ │ ├── README.rst │ │ ├── fiddle_client.py │ │ └── latency_client.py ├── server │ ├── README.rst │ ├── aiohttp │ │ ├── README.rst │ │ ├── app.html │ │ ├── app.py │ │ ├── fiddle.html │ │ ├── fiddle.py │ │ ├── latency.html │ │ ├── latency.py │ │ ├── requirements.txt │ │ └── static │ │ │ ├── fiddle.js │ │ │ └── style.css │ ├── asgi │ │ ├── README.rst │ │ ├── app.html │ │ ├── app.py │ │ ├── fastapi-fiddle.py │ │ ├── fiddle.html │ │ ├── fiddle.py │ │ ├── latency.html │ │ ├── latency.py │ │ ├── litestar-fiddle.py │ │ ├── requirements.txt │ │ └── static │ │ │ ├── fiddle.js │ │ │ └── style.css │ ├── javascript │ │ ├── README.md │ │ ├── fiddle.js │ │ ├── fiddle_public │ │ │ ├── index.html │ │ │ └── main.js │ │ ├── latency.js │ │ ├── latency_public │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── style.css │ │ ├── package-lock.json │ │ └── package.json │ ├── sanic │ │ ├── README.rst │ │ ├── app.html │ │ ├── app.py │ │ ├── fiddle.html │ │ ├── fiddle.py │ │ ├── latency.html │ │ ├── latency.py │ │ ├── requirements.txt │ │ └── static │ │ │ ├── fiddle.js │ │ │ └── style.css │ ├── tornado │ │ ├── README.rst │ │ ├── app.py │ │ ├── fiddle.py │ │ ├── latency.py │ │ ├── requirements.txt │ │ ├── static │ │ │ ├── fiddle.js │ │ │ └── style.css │ │ └── templates │ │ │ ├── app.html │ │ │ ├── fiddle.html │ │ │ └── latency.html │ └── wsgi │ │ ├── README.rst │ │ ├── app.py │ │ ├── django_socketio │ │ ├── README.md │ │ ├── django_socketio │ │ │ ├── __init__.py │ │ │ ├── asgi.py │ │ │ ├── settings.py │ │ │ ├── urls.py │ │ │ └── wsgi.py │ │ ├── manage.py │ │ ├── requirements.txt │ │ └── socketio_app │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── migrations │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── static │ │ │ └── index.html │ │ │ ├── tests.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── fiddle.py │ │ ├── latency.py │ │ ├── requirements.txt │ │ ├── static │ │ ├── fiddle.js │ │ └── style.css │ │ └── templates │ │ ├── fiddle.html │ │ ├── index.html │ │ └── latency.html └── simple-client │ ├── README.rst │ ├── async │ ├── README.rst │ ├── fiddle_client.py │ └── latency_client.py │ └── sync │ ├── README.rst │ ├── fiddle_client.py │ └── latency_client.py ├── pyproject.toml ├── src └── socketio │ ├── __init__.py │ ├── admin.py │ ├── asgi.py │ ├── async_admin.py │ ├── async_aiopika_manager.py │ ├── async_client.py │ ├── async_manager.py │ ├── async_namespace.py │ ├── async_pubsub_manager.py │ ├── async_redis_manager.py │ ├── async_server.py │ ├── async_simple_client.py │ ├── base_client.py │ ├── base_manager.py │ ├── base_namespace.py │ ├── base_server.py │ ├── client.py │ ├── exceptions.py │ ├── kafka_manager.py │ ├── kombu_manager.py │ ├── manager.py │ ├── middleware.py │ ├── msgpack_packet.py │ ├── namespace.py │ ├── packet.py │ ├── pubsub_manager.py │ ├── redis_manager.py │ ├── server.py │ ├── simple_client.py │ ├── tornado.py │ └── zmq_manager.py ├── tests ├── __init__.py ├── async │ ├── __init__.py │ ├── test_admin.py │ ├── test_client.py │ ├── test_manager.py │ ├── test_namespace.py │ ├── test_pubsub_manager.py │ ├── test_server.py │ └── test_simple_client.py ├── asyncio_web_server.py ├── common │ ├── __init__.py │ ├── test_admin.py │ ├── test_client.py │ ├── test_manager.py │ ├── test_middleware.py │ ├── test_msgpack_packet.py │ ├── test_namespace.py │ ├── test_packet.py │ ├── test_pubsub_manager.py │ ├── test_redis_manager.py │ ├── test_server.py │ └── test_simple_client.py ├── performance │ ├── README.md │ ├── binary_packet.py │ ├── json_packet.py │ ├── namespace_packet.py │ ├── run.sh │ ├── server_receive.py │ ├── server_send.py │ ├── server_send_broadcast.py │ └── text_packet.py └── web_server.py └── tox.ini /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **IMPORTANT**: If you have a question, or you are not sure if you have found a bug in this package, then you are in the wrong place. Hit back in your web browser, and then open a GitHub Discussion instead. Likewise, if you are unable to provide the information requested below, open a discussion to troubleshoot your issue. 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. If you are getting errors, please include the complete error message, including the stack trace. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Logs** 26 | Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: GitHub Discussions 4 | url: https://github.com/miguelgrinberg/python-socketio/discussions 5 | about: Ask questions here. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Logs** 20 | Please provide relevant logs from the server and the client. On the Python server and client, add the `logger=True` and `engineio_logger=True` arguments to your `Server()` or `Client()` objects to get logs dumped on your terminal. If you are using the JavaScript client, see [here](https://socket.io/docs/v4/logging-and-debugging/) for how to enable logs. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request, workflow_dispatch] 3 | jobs: 4 | lint: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-python@v5 10 | - run: python -m pip install --upgrade pip wheel 11 | - run: pip install tox tox-gh-actions 12 | - run: tox -eflake8 13 | - run: tox -edocs 14 | tests: 15 | name: tests 16 | strategy: 17 | matrix: 18 | os: [windows-latest, macos-latest, ubuntu-latest] 19 | python: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', 'pypy-3.10'] 20 | exclude: 21 | # pypy3 currently fails to run on Windows 22 | - os: windows-latest 23 | python: pypy-3.10 24 | fail-fast: false 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-python@v5 29 | with: 30 | python-version: ${{ matrix.python }} 31 | - run: python -m pip install --upgrade pip wheel 32 | - run: pip install tox tox-gh-actions 33 | - run: tox 34 | coverage: 35 | name: coverage 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: actions/setup-python@v5 40 | - run: python -m pip install --upgrade pip wheel 41 | - run: pip install tox tox-gh-actions 42 | - run: tox 43 | - uses: codecov/codecov-action@v3 44 | with: 45 | files: ./coverage.xml 46 | fail_ci_if_error: true 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | var 14 | sdist 15 | develop-eggs 16 | .installed.cfg 17 | lib 18 | lib64 19 | __pycache__ 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | docs/_build 38 | venv* 39 | .eggs 40 | .ropeproject 41 | .idea 42 | .vscode 43 | tags 44 | htmlcov 45 | *.swp 46 | 47 | node_modules 48 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | extra_requirements: 16 | - docs 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Miguel Grinberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE tox.ini 2 | recursive-include docs * 3 | recursive-exclude docs/_build * 4 | recursive-include tests * 5 | exclude **/*.pyc 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-socketio 2 | =============== 3 | 4 | [![Build status](https://github.com/miguelgrinberg/python-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/python-socketio/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/python-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/python-socketio) 5 | 6 | Python implementation of the `Socket.IO` realtime client and server. 7 | 8 | Sponsors 9 | -------- 10 | 11 | The following organizations are funding this project: 12 | 13 | ![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| 14 | -|- 15 | 16 | Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? 17 | 18 | Version compatibility 19 | --------------------- 20 | 21 | The Socket.IO protocol has been through a number of revisions, and some of these 22 | introduced backward incompatible changes, which means that the client and the 23 | server must use compatible versions for everything to work. 24 | 25 | If you are using the Python client and server, the easiest way to ensure compatibility 26 | is to use the same version of this package for the client and the server. If you are 27 | using this package with a different client or server, then you must ensure the 28 | versions are compatible. 29 | 30 | The version compatibility chart below maps versions of this package to versions 31 | of the JavaScript reference implementation and the versions of the Socket.IO and 32 | Engine.IO protocols. 33 | 34 | JavaScript Socket.IO version | Socket.IO protocol revision | Engine.IO protocol revision | python-socketio version 35 | -|-|-|- 36 | 0.9.x | 1, 2 | 1, 2 | Not supported 37 | 1.x and 2.x | 3, 4 | 3 | 4.x 38 | 3.x and 4.x | 5 | 4 | 5.x 39 | 40 | Resources 41 | --------- 42 | 43 | - [Documentation](http://python-socketio.readthedocs.io/) 44 | - [PyPI](https://pypi.python.org/pypi/python-socketio) 45 | - [Change Log](https://github.com/miguelgrinberg/python-socketio/blob/main/CHANGES.md) 46 | - Questions? See the [questions](https://stackoverflow.com/questions/tagged/python-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+python-socketio) your own question. 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | If you think you've found a vulnerability on this project, please send me (Miguel Grinberg) an email at mailto:miguel.grinberg@gmail.com with a description of the problem. I will personally review the issue and respond to you with next steps. 6 | 7 | If the issue is highly sensitive, you are welcome to encrypt your message. Here is my [PGP key](http://pgp.mit.edu/pks/lookup?search=miguel.grinberg%40gmail.com&op=index). 8 | 9 | Please do not disclose vulnerabilities publicly before discussing how to proceed with me. 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/README.md: -------------------------------------------------------------------------------- 1 | Place static files used by the documentation here. 2 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | div.sphinxsidebar { 2 | max-height: calc(100% - 30px); 3 | overflow-y: auto; 4 | overflow-x: hidden; 5 | } 6 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | .. module:: socketio 8 | 9 | .. autoclass:: SimpleClient 10 | :members: 11 | :inherited-members: 12 | 13 | .. autoclass:: AsyncSimpleClient 14 | :members: 15 | :inherited-members: 16 | 17 | .. autoclass:: Client 18 | :members: 19 | :inherited-members: 20 | 21 | .. autoclass:: AsyncClient 22 | :members: 23 | :inherited-members: 24 | 25 | .. autoclass:: Server 26 | :members: 27 | :inherited-members: 28 | 29 | .. autoclass:: AsyncServer 30 | :members: 31 | :inherited-members: 32 | 33 | .. autoclass:: socketio.exceptions.ConnectionRefusedError 34 | :members: 35 | 36 | .. autoclass:: WSGIApp 37 | :members: 38 | 39 | .. autoclass:: ASGIApp 40 | :members: 41 | 42 | .. autoclass:: Middleware 43 | :members: 44 | 45 | .. autoclass:: ClientNamespace 46 | :members: 47 | :inherited-members: 48 | 49 | .. autoclass:: Namespace 50 | :members: 51 | :inherited-members: 52 | 53 | .. autoclass:: AsyncClientNamespace 54 | :members: 55 | :inherited-members: 56 | 57 | .. autoclass:: AsyncNamespace 58 | :members: 59 | :inherited-members: 60 | 61 | .. autoclass:: Manager 62 | :members: 63 | :inherited-members: 64 | 65 | .. autoclass:: PubSubManager 66 | :members: 67 | :inherited-members: 68 | 69 | .. autoclass:: KombuManager 70 | :members: 71 | :inherited-members: 72 | 73 | .. autoclass:: RedisManager 74 | :members: 75 | :inherited-members: 76 | 77 | .. autoclass:: KafkaManager 78 | :members: 79 | :inherited-members: 80 | 81 | .. autoclass:: AsyncManager 82 | :members: 83 | :inherited-members: 84 | 85 | .. autoclass:: AsyncRedisManager 86 | :members: 87 | :inherited-members: 88 | 89 | .. autoclass:: AsyncAioPikaManager 90 | :members: 91 | :inherited-members: 92 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'python-socketio' 23 | copyright = '2018, Miguel Grinberg' 24 | author = 'Miguel Grinberg' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | ] 44 | 45 | autodoc_member_order = 'alphabetical' 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = 'en' 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path. 69 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 70 | 71 | # The name of the Pygments (syntax highlighting) style to use. 72 | pygments_style = None 73 | 74 | 75 | # -- Options for HTML output ------------------------------------------------- 76 | 77 | # The theme to use for HTML and HTML Help pages. See the documentation for 78 | # a list of builtin themes. 79 | # 80 | html_theme = 'alabaster' 81 | 82 | # Theme options are theme-specific and customize the look and feel of a theme 83 | # further. For a list of options available for each theme, see the 84 | # documentation. 85 | # 86 | html_theme_options = { 87 | 'github_user': 'miguelgrinberg', 88 | 'github_repo': 'python-socketio', 89 | 'github_banner': True, 90 | 'github_button': True, 91 | 'github_type': 'star', 92 | 'fixed_sidebar': True, 93 | 94 | } 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | # Custom sidebar templates, must be a dictionary that maps document names 102 | # to template names. 103 | # 104 | # The default sidebars (for documents that don't match any pattern) are 105 | # defined by theme itself. Builtin themes are using these templates by 106 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 107 | # 'searchbox.html']``. 108 | # 109 | # html_sidebars = {} 110 | 111 | 112 | # -- Options for HTMLHelp output --------------------------------------------- 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = 'python-socketiodoc' 116 | 117 | 118 | # -- Options for LaTeX output ------------------------------------------------ 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'python-socketio.tex', 'python-socketio Documentation', 143 | 'Miguel Grinberg', 'manual'), 144 | ] 145 | 146 | 147 | # -- Options for manual page output ------------------------------------------ 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [ 152 | (master_doc, 'python-socketio', 'python-socketio Documentation', 153 | [author], 1) 154 | ] 155 | 156 | 157 | # -- Options for Texinfo output ---------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | (master_doc, 'python-socketio', 'python-socketio Documentation', 164 | author, 'python-socketio', 'One line description of project.', 165 | 'Miscellaneous'), 166 | ] 167 | 168 | 169 | # -- Options for Epub output ------------------------------------------------- 170 | 171 | # Bibliographic Dublin Core info. 172 | epub_title = project 173 | 174 | # The unique identifier of the text. This can be a ISBN number 175 | # or the project homepage. 176 | # 177 | # epub_identifier = '' 178 | 179 | # A unique identification for the text. 180 | # 181 | # epub_uid = '' 182 | 183 | # A list of files that should not be packed into the epub file. 184 | epub_exclude_files = ['search.html'] 185 | 186 | 187 | # -- Extension configuration ------------------------------------------------- 188 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python-socketio documentation master file, created by 2 | sphinx-quickstart on Sun Nov 25 11:52:38 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | python-socketio 7 | =============== 8 | 9 | This projects implements Socket.IO clients and servers that can run standalone 10 | or integrated with a variety of Python web frameworks. 11 | 12 | .. toctree:: 13 | :maxdepth: 3 14 | 15 | intro 16 | client 17 | server 18 | api 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Examples 2 | ================== 3 | 4 | This directory contains several example Socket.IO applications. Look in the 5 | `server` directory for Socket.IO servers, and in the `client` and 6 | `simple-client` directories for Socket.IO clients. 7 | -------------------------------------------------------------------------------- /examples/client/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Client Examples 2 | ========================= 3 | 4 | This directory contains several example Socket.IO client applications, 5 | organized by directory: 6 | 7 | sync 8 | ---- 9 | 10 | Examples that use standard Python thread concurrency. 11 | 12 | async 13 | ----- 14 | 15 | Examples that use Python's `asyncio` package for concurrency. 16 | 17 | javascript 18 | ---------- 19 | 20 | Examples that use the JavaScript version of Socket.IO for compatiblity testing. 21 | -------------------------------------------------------------------------------- /examples/client/async/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Async Client Examples 2 | =============================== 3 | 4 | This directory contains example Socket.IO clients that work with the 5 | ``asyncio`` package of the Python standard library. 6 | 7 | latency_client.py 8 | ----------------- 9 | 10 | In this application the client sends *ping* messages to the server, which are 11 | responded by the server with a *pong*. The client measures the time it takes 12 | for each of these exchanges. 13 | 14 | This is an ideal application to measure the performance of the different 15 | asynchronous modes supported by the Socket.IO server. 16 | 17 | fiddle_client.py 18 | ---------------- 19 | 20 | This is an extemely simple application based on the JavaScript example of the 21 | same name. 22 | 23 | Running the Examples 24 | -------------------- 25 | 26 | These examples work with the server examples of the same name. First run one 27 | of the ``latency.py`` or ``fiddle.py`` versions from one of the 28 | ``examples/server`` subdirectories. On another terminal, then start the 29 | corresponding client:: 30 | 31 | $ python latency_client.py 32 | $ python fiddle_client.py 33 | -------------------------------------------------------------------------------- /examples/client/async/fiddle_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socketio 3 | 4 | sio = socketio.AsyncClient() 5 | 6 | 7 | @sio.event 8 | async def connect(): 9 | print('connected to server') 10 | 11 | 12 | @sio.event 13 | async def disconnect(reason): 14 | print('disconnected from server, reason:', reason) 15 | 16 | 17 | @sio.event 18 | def hello(a, b, c): 19 | print(a, b, c) 20 | 21 | 22 | async def start_server(): 23 | await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) 24 | await sio.wait() 25 | 26 | 27 | if __name__ == '__main__': 28 | asyncio.run(start_server()) 29 | -------------------------------------------------------------------------------- /examples/client/async/latency_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import socketio 4 | 5 | loop = asyncio.get_event_loop() 6 | sio = socketio.AsyncClient() 7 | start_timer = None 8 | 9 | 10 | async def send_ping(): 11 | global start_timer 12 | start_timer = time.time() 13 | await sio.emit('ping_from_client') 14 | 15 | 16 | @sio.event 17 | async def connect(): 18 | print('connected to server') 19 | await send_ping() 20 | 21 | 22 | @sio.event 23 | async def pong_from_server(): 24 | latency = time.time() - start_timer 25 | print(f'latency is {latency * 1000:.2f} ms') 26 | await sio.sleep(1) 27 | if sio.connected: 28 | await send_ping() 29 | 30 | 31 | async def start_server(): 32 | await sio.connect('http://localhost:5000') 33 | await sio.wait() 34 | 35 | 36 | if __name__ == '__main__': 37 | loop.run_until_complete(start_server()) 38 | -------------------------------------------------------------------------------- /examples/client/javascript/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Socket.IO Fiddle 3 | 4 | ``` 5 | $ npm install 6 | $ node fiddle-client.js # to run the fiddle example 7 | $ node latency-client.js # to run the latency example 8 | ``` 9 | 10 | Optionally, specify a port by supplying the `PORT` env variable. 11 | -------------------------------------------------------------------------------- /examples/client/javascript/fiddle_client.js: -------------------------------------------------------------------------------- 1 | const io = require('socket.io-client') 2 | const port = process.env.PORT || 5000; 3 | 4 | const socket = io('http://localhost:' + port, {auth: {token: 'my-token'}}); 5 | 6 | socket.on('connect', () => { 7 | console.log(`connect ${socket.id}`); 8 | }); 9 | 10 | socket.on('disconnect', () => { 11 | console.log(`disconnect ${socket.id}`); 12 | }); 13 | 14 | socket.on('hello', (a, b, c) => { 15 | console.log(a, b, c); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/client/javascript/latency_client.js: -------------------------------------------------------------------------------- 1 | const io = require('socket.io-client') 2 | const port = process.env.PORT || 5000; 3 | 4 | const socket = io('http://localhost:' + port); 5 | let last; 6 | function send () { 7 | last = new Date(); 8 | socket.emit('ping_from_client'); 9 | } 10 | 11 | socket.on('connect', () => { 12 | console.log(`connect ${socket.id}`); 13 | send(); 14 | }); 15 | 16 | socket.on('disconnect', () => { 17 | console.log(`disconnect ${socket.id}`); 18 | }); 19 | 20 | socket.on('pong_from_server', () => { 21 | const latency = new Date() - last; 22 | console.log('latency is ' + latency + ' ms'); 23 | setTimeout(send, 1000); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/client/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socketio-examples", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "express": "^4.21.2", 6 | "smoothie": "1.19.0", 7 | "socket.io": "^4.8.0", 8 | "socket.io-client": "^4.6.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/client/sync/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Client Examples 2 | ========================= 3 | 4 | This directory contains example Socket.IO clients that work with the 5 | Python standard library. 6 | 7 | latency_client.py 8 | ----------------- 9 | 10 | In this application the client sends *ping* messages to the server, which are 11 | responded by the server with a *pong*. The client measures the time it takes 12 | for each of these exchanges. 13 | 14 | This is an ideal application to measure the performance of the different 15 | asynchronous modes supported by the Socket.IO server. 16 | 17 | fiddle_client.py 18 | ---------------- 19 | 20 | This is an extemely simple application based on the JavaScript example of the 21 | same name. 22 | 23 | Running the Examples 24 | -------------------- 25 | 26 | These examples work with the server examples of the same name. First run one 27 | of the ``latency.py`` or ``fiddle.py`` versions from one of the 28 | ``examples/server`` subdirectories. On another terminal, then start the 29 | corresponding client:: 30 | 31 | $ python latency_client.py 32 | $ python fiddle_client.py 33 | -------------------------------------------------------------------------------- /examples/client/sync/fiddle_client.py: -------------------------------------------------------------------------------- 1 | import socketio 2 | 3 | sio = socketio.Client() 4 | 5 | 6 | @sio.event 7 | def connect(): 8 | print('connected to server') 9 | 10 | 11 | @sio.event 12 | def disconnect(reason): 13 | print('disconnected from server, reason:', reason) 14 | 15 | 16 | @sio.event 17 | def hello(a, b, c): 18 | print(a, b, c) 19 | 20 | 21 | if __name__ == '__main__': 22 | sio.connect('http://localhost:5000', auth={'token': 'my-token'}) 23 | sio.wait() 24 | -------------------------------------------------------------------------------- /examples/client/sync/latency_client.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socketio 3 | 4 | sio = socketio.Client(logger=True, engineio_logger=True) 5 | start_timer = None 6 | 7 | 8 | def send_ping(): 9 | global start_timer 10 | start_timer = time.time() 11 | sio.emit('ping_from_client') 12 | 13 | 14 | @sio.event 15 | def connect(): 16 | print('connected to server') 17 | send_ping() 18 | 19 | 20 | @sio.event 21 | def pong_from_server(): 22 | latency = time.time() - start_timer 23 | print(f'latency is {latency * 1000:.2f} ms') 24 | sio.sleep(1) 25 | if sio.connected: 26 | send_ping() 27 | 28 | 29 | if __name__ == '__main__': 30 | sio.connect('http://localhost:5000') 31 | sio.wait() 32 | -------------------------------------------------------------------------------- /examples/server/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Server Examples 2 | ========================= 3 | 4 | This directory contains several example Socket.IO applications, organized by 5 | directory: 6 | 7 | wsgi 8 | ---- 9 | 10 | Examples that are compatible with the WSGI protocol and frameworks. 11 | 12 | asgi 13 | ---- 14 | 15 | Examples that are compatible with the ASGI specification. 16 | 17 | aiohttp 18 | ------- 19 | 20 | Examples that are compatible with the aiohttp framework for asyncio. 21 | 22 | sanic 23 | ----- 24 | 25 | Examples that are compatible with the sanic framework for asyncio. 26 | 27 | tornado 28 | ------- 29 | 30 | Examples that are compatible with the tornado framework. 31 | 32 | javascript 33 | ---------- 34 | 35 | Examples that use the JavaScript version of Socket.IO for compatiblity testing. 36 | -------------------------------------------------------------------------------- /examples/server/aiohttp/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO aiohttp Examples 2 | ========================== 3 | 4 | This directory contains example Socket.IO applications that are compatible with 5 | asyncio and the aiohttp framework. These applications require Python 3.5 or 6 | later. 7 | 8 | app.py 9 | ------ 10 | 11 | A basic "kitchen sink" type application that allows the user to experiment 12 | with most of the available features of the Socket.IO server. 13 | 14 | latency.py 15 | ---------- 16 | 17 | A port of the latency application included in the official Engine.IO 18 | Javascript server. In this application the client sends *ping* messages to 19 | the server, which are responded by the server with a *pong*. The client 20 | measures the time it takes for each of these exchanges and plots these in real 21 | time to the page. 22 | 23 | This is an ideal application to measure the performance of the different 24 | asynchronous modes supported by the Socket.IO server. 25 | 26 | fiddle.py 27 | --------- 28 | 29 | This is a very simple application based on a JavaScript example of the same 30 | name. 31 | 32 | Running the Examples 33 | -------------------- 34 | 35 | To run these examples, create a virtual environment, install the requirements 36 | and then run one of the following:: 37 | 38 | $ python app.py 39 | 40 | :: 41 | 42 | $ python latency.py 43 | 44 | :: 45 | 46 | $ python fiddle.py 47 | 48 | You can then access the application from your web browser at 49 | ``http://localhost:8080``. 50 | -------------------------------------------------------------------------------- /examples/server/aiohttp/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | python-socketio test 5 | 6 | 7 | 55 | 56 | 57 |

python-socketio test

58 |

Send:

59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |

Receive:

88 |

89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/server/aiohttp/app.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | import socketio 4 | 5 | sio = socketio.AsyncServer(async_mode='aiohttp') 6 | app = web.Application() 7 | sio.attach(app) 8 | 9 | 10 | async def background_task(): 11 | """Example of how to send server generated events to clients.""" 12 | count = 0 13 | while True: 14 | await sio.sleep(10) 15 | count += 1 16 | await sio.emit('my_response', {'data': 'Server generated event'}) 17 | 18 | 19 | async def index(request): 20 | with open('app.html') as f: 21 | return web.Response(text=f.read(), content_type='text/html') 22 | 23 | 24 | @sio.event 25 | async def my_event(sid, message): 26 | await sio.emit('my_response', {'data': message['data']}, room=sid) 27 | 28 | 29 | @sio.event 30 | async def my_broadcast_event(sid, message): 31 | await sio.emit('my_response', {'data': message['data']}) 32 | 33 | 34 | @sio.event 35 | async def join(sid, message): 36 | await sio.enter_room(sid, message['room']) 37 | await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, 38 | room=sid) 39 | 40 | 41 | @sio.event 42 | async def leave(sid, message): 43 | await sio.leave_room(sid, message['room']) 44 | await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, 45 | room=sid) 46 | 47 | 48 | @sio.event 49 | async def close_room(sid, message): 50 | await sio.emit('my_response', 51 | {'data': 'Room ' + message['room'] + ' is closing.'}, 52 | room=message['room']) 53 | await sio.close_room(message['room']) 54 | 55 | 56 | @sio.event 57 | async def my_room_event(sid, message): 58 | await sio.emit('my_response', {'data': message['data']}, 59 | room=message['room']) 60 | 61 | 62 | @sio.event 63 | async def disconnect_request(sid): 64 | await sio.disconnect(sid) 65 | 66 | 67 | @sio.event 68 | async def connect(sid, environ): 69 | await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) 70 | 71 | 72 | @sio.event 73 | def disconnect(sid, reason): 74 | print('Client disconnected, reason:', reason) 75 | 76 | 77 | app.router.add_static('/static', 'static') 78 | app.router.add_get('/', index) 79 | 80 | 81 | async def init_app(): 82 | sio.start_background_task(background_task) 83 | return app 84 | 85 | 86 | if __name__ == '__main__': 87 | web.run_app(init_app(), port=5000) 88 | -------------------------------------------------------------------------------- /examples/server/aiohttp/fiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Fiddle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/server/aiohttp/fiddle.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | import socketio 4 | 5 | sio = socketio.AsyncServer(async_mode='aiohttp') 6 | app = web.Application() 7 | sio.attach(app) 8 | 9 | 10 | async def index(request): 11 | with open('fiddle.html') as f: 12 | return web.Response(text=f.read(), content_type='text/html') 13 | 14 | 15 | @sio.event 16 | async def connect(sid, environ, auth): 17 | print(f'connected auth={auth} sid={sid}') 18 | await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 19 | 20 | 21 | @sio.event 22 | def disconnect(sid, reason): 23 | print('disconnected', sid, reason) 24 | 25 | 26 | app.router.add_static('/static', 'static') 27 | app.router.add_get('/', index) 28 | 29 | 30 | if __name__ == '__main__': 31 | web.run_app(app, port=5000) 32 | -------------------------------------------------------------------------------- /examples/server/aiohttp/latency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Latency 5 | 6 | 7 | 8 |

Socket.IO Latency

9 |

(connecting)

10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/server/aiohttp/latency.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | 3 | import socketio 4 | 5 | sio = socketio.AsyncServer(async_mode='aiohttp') 6 | app = web.Application() 7 | sio.attach(app) 8 | 9 | 10 | async def index(request): 11 | with open('latency.html') as f: 12 | return web.Response(text=f.read(), content_type='text/html') 13 | 14 | 15 | @sio.event 16 | async def ping_from_client(sid): 17 | await sio.emit('pong_from_server', room=sid) 18 | 19 | 20 | app.router.add_static('/static', 'static') 21 | app.router.add_get('/', index) 22 | 23 | 24 | if __name__ == '__main__': 25 | web.run_app(app) 26 | -------------------------------------------------------------------------------- /examples/server/aiohttp/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.10.11 2 | async-timeout==1.1.0 3 | chardet==2.3.0 4 | multidict==2.1.4 5 | python-engineio 6 | python_socketio 7 | six==1.10.0 8 | yarl==0.9.2 9 | -------------------------------------------------------------------------------- /examples/server/aiohttp/static/fiddle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | const socket = io(); 6 | 7 | socket.on('connect', () => { 8 | console.log(`connect ${socket.id}`); 9 | }); 10 | 11 | socket.on('disconnect', () => { 12 | console.log(`disconnect ${socket.id}`); 13 | }); 14 | 15 | socket.on('hello', (a, b, c) => { 16 | console.log(a, b, c); 17 | }); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/server/aiohttp/static/style.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; font-family: Helvetica Neue; } 2 | h1 { margin: 100px 100px 10px; } 3 | h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } 4 | #latency { color: red; } 5 | -------------------------------------------------------------------------------- /examples/server/asgi/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO ASGI Examples 2 | ========================== 3 | 4 | This directory contains example Socket.IO applications that are compatible with 5 | asyncio and the ASGI specification. 6 | 7 | app.py 8 | ------ 9 | 10 | A basic "kitchen sink" type application that allows the user to experiment 11 | with most of the available features of the Socket.IO server. 12 | 13 | latency.py 14 | ---------- 15 | 16 | A port of the latency application included in the official Engine.IO 17 | Javascript server. In this application the client sends *ping* messages to 18 | the server, which are responded by the server with a *pong*. The client 19 | measures the time it takes for each of these exchanges and plots these in real 20 | time to the page. 21 | 22 | This is an ideal application to measure the performance of the different 23 | asynchronous modes supported by the Socket.IO server. 24 | 25 | fiddle.py 26 | --------- 27 | 28 | This is a very simple application based on a JavaScript example of the same 29 | name. 30 | 31 | fastapi-fiddle.py 32 | ----------------- 33 | 34 | A version of `fiddle.py` that is integrated with the FastAPI framework. 35 | 36 | litestar-fiddle.py 37 | ------------------ 38 | 39 | A version of `fiddle.py` that is integrated with the Litestar framework. 40 | 41 | Running the Examples 42 | -------------------- 43 | 44 | To run these examples, create a virtual environment, install the requirements 45 | and then run:: 46 | 47 | $ python app.py 48 | 49 | or:: 50 | 51 | $ python latency.py 52 | 53 | You can then access the application from your web browser at 54 | ``http://localhost:5000``. 55 | -------------------------------------------------------------------------------- /examples/server/asgi/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | python-socketio test 5 | 6 | 7 | 55 | 56 | 57 |

python-socketio test

58 |

Send:

59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |

Receive:

88 |

89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/server/asgi/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # set instrument to `True` to accept connections from the official Socket.IO 4 | # Admin UI hosted at https://admin.socket.io 5 | instrument = True 6 | admin_login = { 7 | 'username': 'admin', 8 | 'password': 'python', # change this to a strong secret for production use! 9 | } 10 | 11 | import uvicorn 12 | import socketio 13 | 14 | sio = socketio.AsyncServer( 15 | async_mode='asgi', 16 | cors_allowed_origins=None if not instrument else [ 17 | 'http://localhost:5000', 18 | 'https://admin.socket.io', # edit the allowed origins if necessary 19 | ]) 20 | if instrument: 21 | sio.instrument(auth=admin_login) 22 | 23 | app = socketio.ASGIApp(sio, static_files={ 24 | '/': 'app.html', 25 | }) 26 | background_task_started = False 27 | 28 | 29 | async def background_task(): 30 | """Example of how to send server generated events to clients.""" 31 | count = 0 32 | while True: 33 | await sio.sleep(10) 34 | count += 1 35 | await sio.emit('my_response', {'data': 'Server generated event'}) 36 | 37 | 38 | @sio.on('my_event') 39 | async def test_message(sid, message): 40 | await sio.emit('my_response', {'data': message['data']}, room=sid) 41 | 42 | 43 | @sio.on('my_broadcast_event') 44 | async def test_broadcast_message(sid, message): 45 | await sio.emit('my_response', {'data': message['data']}) 46 | 47 | 48 | @sio.on('join') 49 | async def join(sid, message): 50 | await sio.enter_room(sid, message['room']) 51 | await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, 52 | room=sid) 53 | 54 | 55 | @sio.on('leave') 56 | async def leave(sid, message): 57 | await sio.leave_room(sid, message['room']) 58 | await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, 59 | room=sid) 60 | 61 | 62 | @sio.on('close room') 63 | async def close(sid, message): 64 | await sio.emit('my_response', 65 | {'data': 'Room ' + message['room'] + ' is closing.'}, 66 | room=message['room']) 67 | await sio.close_room(message['room']) 68 | 69 | 70 | @sio.on('my_room_event') 71 | async def send_room_message(sid, message): 72 | await sio.emit('my_response', {'data': message['data']}, 73 | room=message['room']) 74 | 75 | 76 | @sio.on('disconnect request') 77 | async def disconnect_request(sid): 78 | await sio.disconnect(sid) 79 | 80 | 81 | @sio.on('connect') 82 | async def test_connect(sid, environ): 83 | global background_task_started 84 | if not background_task_started: 85 | sio.start_background_task(background_task) 86 | background_task_started = True 87 | await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) 88 | 89 | 90 | @sio.on('disconnect') 91 | def test_disconnect(sid, reason): 92 | print('Client disconnected, reason:', reason) 93 | 94 | 95 | if __name__ == '__main__': 96 | if instrument: 97 | print('The server is instrumented for remote administration.') 98 | print( 99 | 'Use the official Socket.IO Admin UI at https://admin.socket.io ' 100 | 'with the following connection details:' 101 | ) 102 | print(' - Server URL: http://localhost:5000') 103 | print(' - Username:', admin_login['username']) 104 | print(' - Password:', admin_login['password']) 105 | print('') 106 | uvicorn.run(app, host='127.0.0.1', port=5000) 107 | -------------------------------------------------------------------------------- /examples/server/asgi/fastapi-fiddle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from fastapi import FastAPI 3 | from fastapi.responses import FileResponse 4 | from fastapi.staticfiles import StaticFiles 5 | import socketio 6 | import uvicorn 7 | 8 | app = FastAPI() 9 | app.mount('/static', StaticFiles(directory='static'), name='static') 10 | 11 | sio = socketio.AsyncServer(async_mode='asgi') 12 | combined_asgi_app = socketio.ASGIApp(sio, app) 13 | 14 | 15 | @app.get('/') 16 | async def index(): 17 | return FileResponse('fiddle.html') 18 | 19 | 20 | @app.get('/hello') 21 | async def hello(): 22 | return {'message': 'Hello, World!'} 23 | 24 | 25 | @sio.event 26 | async def connect(sid, environ, auth): 27 | print(f'connected auth={auth} sid={sid}') 28 | await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 29 | 30 | 31 | @sio.event 32 | def disconnect(sid): 33 | print('disconnected', sid) 34 | 35 | 36 | if __name__ == '__main__': 37 | uvicorn.run(combined_asgi_app, host='127.0.0.1', port=5000) 38 | -------------------------------------------------------------------------------- /examples/server/asgi/fiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Fiddle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/server/asgi/fiddle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import uvicorn 3 | 4 | import socketio 5 | 6 | sio = socketio.AsyncServer(async_mode='asgi') 7 | app = socketio.ASGIApp(sio, static_files={ 8 | '/': 'fiddle.html', 9 | '/static': 'static', 10 | }) 11 | 12 | 13 | @sio.event 14 | async def connect(sid, environ, auth): 15 | print(f'connected auth={auth} sid={sid}') 16 | await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 17 | 18 | 19 | @sio.event 20 | def disconnect(sid, reason): 21 | print('disconnected', sid, reason) 22 | 23 | 24 | if __name__ == '__main__': 25 | uvicorn.run(app, host='127.0.0.1', port=5000) 26 | -------------------------------------------------------------------------------- /examples/server/asgi/latency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Latency 5 | 6 | 7 | 8 |

Socket.IO Latency

9 |

(connecting)

10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/server/asgi/latency.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import uvicorn 3 | 4 | import socketio 5 | 6 | sio = socketio.AsyncServer(async_mode='asgi') 7 | app = socketio.ASGIApp(sio, static_files={ 8 | '/': 'latency.html', 9 | '/static': 'static', 10 | }) 11 | 12 | 13 | @sio.event 14 | async def ping_from_client(sid): 15 | await sio.emit('pong_from_server', room=sid) 16 | 17 | 18 | if __name__ == '__main__': 19 | uvicorn.run(app, host='127.0.0.1', port=5000) 20 | -------------------------------------------------------------------------------- /examples/server/asgi/litestar-fiddle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from litestar import Litestar, get, MediaType 3 | from litestar.response import File 4 | from litestar.static_files.config import StaticFilesConfig 5 | import socketio 6 | import uvicorn 7 | 8 | sio = socketio.AsyncServer(async_mode='asgi') 9 | 10 | 11 | @get('/', media_type=MediaType.HTML) 12 | async def index() -> File: 13 | return File('fiddle.html', content_disposition_type='inline') 14 | 15 | 16 | @get('/hello') 17 | async def hello() -> dict: 18 | return {'message': 'Hello, World!'} 19 | 20 | 21 | @sio.event 22 | async def connect(sid, environ, auth): 23 | print(f'connected auth={auth} sid={sid}') 24 | await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 25 | 26 | 27 | @sio.event 28 | def disconnect(sid): 29 | print('disconnected', sid) 30 | 31 | 32 | app = Litestar([index, hello], static_files_config=[ 33 | StaticFilesConfig('static', directories=['static'])]) 34 | combined_asgi_app = socketio.ASGIApp(sio, app) 35 | 36 | 37 | if __name__ == '__main__': 38 | uvicorn.run(combined_asgi_app, host='127.0.0.1', port=5000) 39 | -------------------------------------------------------------------------------- /examples/server/asgi/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.1.2 2 | h11==0.16.0 3 | httptools==0.1.1 4 | python-engineio 5 | python_socketio 6 | uvicorn==0.13.1 7 | uvloop==0.14.0 8 | websockets==9.1 9 | -------------------------------------------------------------------------------- /examples/server/asgi/static/fiddle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | const socket = io(); 6 | 7 | socket.on('connect', () => { 8 | console.log(`connect ${socket.id}`); 9 | }); 10 | 11 | socket.on('disconnect', () => { 12 | console.log(`disconnect ${socket.id}`); 13 | }); 14 | 15 | socket.on('hello', (a, b, c) => { 16 | console.log(a, b, c); 17 | }); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/server/asgi/static/style.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; font-family: Helvetica Neue; } 2 | h1 { margin: 100px 100px 10px; } 3 | h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } 4 | #latency { color: red; } 5 | -------------------------------------------------------------------------------- /examples/server/javascript/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Socket.IO JavaScript Examples 3 | 4 | ``` 5 | $ npm install 6 | $ node fiddle # to run the fiddle server example 7 | $ node latency # to run the latency server example 8 | ``` 9 | 10 | And point your browser to `http://localhost:5000`. Optionally, specify 11 | a port by supplying the `PORT` env variable. 12 | -------------------------------------------------------------------------------- /examples/server/javascript/fiddle.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { createServer } = require("http"); 3 | const { Server } = require("socket.io"); 4 | const { instrument } = require("@socket.io/admin-ui"); 5 | 6 | const app = express(); 7 | const httpServer = createServer(app); 8 | const io = new Server(httpServer, { 9 | cors: { origin: 'https://admin.socket.io', credentials: true }, 10 | }); 11 | const port = process.env.PORT || 5000; 12 | 13 | app.use(express.static(__dirname + '/fiddle_public')); 14 | 15 | io.on('connection', socket => { 16 | console.log(`connect auth=${JSON.stringify(socket.handshake.auth)} sid=${socket.id}`); 17 | 18 | socket.emit('hello', 1, '2', { 19 | hello: 'you' 20 | }); 21 | 22 | socket.on('disconnect', (reason) => { 23 | console.log(`disconnect ${socket.id}, reason: ${reason}`); 24 | }); 25 | }); 26 | 27 | instrument(io, {auth: false, mode: 'development'}); 28 | httpServer.listen(port, () => console.log(`server listening on port ${port}`)); 29 | -------------------------------------------------------------------------------- /examples/server/javascript/fiddle_public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Fiddle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/server/javascript/fiddle_public/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | const socket = io(); 6 | 7 | socket.on('connect', () => { 8 | console.log(`connect ${socket.id}`); 9 | }); 10 | 11 | socket.on('disconnect', () => { 12 | console.log(`disconnect ${socket.id}`); 13 | }); 14 | 15 | socket.on('hello', (a, b, c) => { 16 | console.log(a, b, c); 17 | }); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/server/javascript/latency.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { createServer } = require("http"); 3 | const { Server } = require("socket.io"); 4 | 5 | const app = express(); 6 | const httpServer = createServer(app); 7 | const io = new Server(httpServer); 8 | 9 | const port = process.env.PORT || 5000; 10 | 11 | app.use(express.static(__dirname + '/latency_public')); 12 | 13 | io.on('connection', socket => { 14 | console.log(`connect ${socket.id}`); 15 | 16 | socket.on('ping_from_client', () => { 17 | socket.emit('pong_from_server'); 18 | }); 19 | 20 | socket.on('disconnect', () => { 21 | console.log(`disconnect ${socket.id}`); 22 | }); 23 | }); 24 | 25 | httpServer.listen(port, () => console.log(`server listening on port ${port}`)); 26 | -------------------------------------------------------------------------------- /examples/server/javascript/latency_public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Latency 5 | 6 | 7 | 8 |

Socket.IO Latency

9 |

(connecting)

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/server/javascript/latency_public/index.js: -------------------------------------------------------------------------------- 1 | // helper 2 | function $ (id) { return document.getElementById(id); } 3 | 4 | // chart 5 | let smoothie; 6 | let time; 7 | 8 | function render () { 9 | if (smoothie) smoothie.stop(); 10 | $('chart').width = document.body.clientWidth; 11 | smoothie = new SmoothieChart(); 12 | smoothie.streamTo($('chart'), 1000); 13 | time = new TimeSeries(); 14 | smoothie.addTimeSeries(time, { 15 | strokeStyle: 'rgb(255, 0, 0)', 16 | fillStyle: 'rgba(255, 0, 0, 0.4)', 17 | lineWidth: 2 18 | }); 19 | } 20 | 21 | // socket 22 | const socket = io(); 23 | let last; 24 | function send () { 25 | last = new Date(); 26 | socket.emit('ping_from_client'); 27 | $('transport').innerHTML = socket.io.engine.transport.name; 28 | } 29 | 30 | socket.on('connect', () => { 31 | if ($('chart').getContext) { 32 | render(); 33 | window.onresize = render; 34 | } 35 | send(); 36 | }); 37 | 38 | socket.on('disconnect', () => { 39 | if (smoothie) smoothie.stop(); 40 | $('transport').innerHTML = '(disconnected)'; 41 | }); 42 | 43 | socket.on('pong_from_server', () => { 44 | const latency = new Date() - last; 45 | $('latency').innerHTML = latency + 'ms'; 46 | if (time) time.append(+new Date(), latency); 47 | setTimeout(send, 100); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/server/javascript/latency_public/style.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; font-family: Helvetica Neue; } 2 | h1 { margin: 100px 100px 10px; } 3 | h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } 4 | #latency { color: red; } 5 | -------------------------------------------------------------------------------- /examples/server/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "socketio-examples", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "@socket.io/admin-ui": "^0.5.1", 6 | "express": "^4.21.2", 7 | "smoothie": "1.19.0", 8 | "socket.io": "^4.8.0", 9 | "socket.io-client": "^4.6.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/server/sanic/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO sanic Examples 2 | ======================== 3 | 4 | This directory contains example Socket.IO applications that are compatible with 5 | asyncio and the sanic framework. These applications require Python 3.5 or 6 | later. 7 | 8 | Note that Sanic versions older than 0.4.0 do not support the WebSocket 9 | protocol, so on those versions the only available transport is long-polling. 10 | 11 | app.py 12 | ------ 13 | 14 | A basic "kitchen sink" type application that allows the user to experiment 15 | with most of the available features of the Socket.IO server. 16 | 17 | latency.py 18 | ---------- 19 | 20 | A port of the latency application included in the official Engine.IO 21 | Javascript server. In this application the client sends *ping* messages to 22 | the server, which are responded by the server with a *pong*. The client 23 | measures the time it takes for each of these exchanges and plots these in real 24 | time to the page. 25 | 26 | This is an ideal application to measure the performance of the different 27 | asynchronous modes supported by the Socket.IO server. 28 | 29 | fiddle.py 30 | --------- 31 | 32 | This is a very simple application based on a JavaScript example of the same 33 | name. 34 | 35 | Running the Examples 36 | -------------------- 37 | 38 | To run these examples, create a virtual environment, install the requirements 39 | and then run one of the following:: 40 | 41 | $ python app.py 42 | 43 | :: 44 | 45 | $ python latency.py 46 | 47 | :: 48 | 49 | $ python fiddle.py 50 | 51 | You can then access the application from your web browser at 52 | ``http://localhost:8000``. 53 | -------------------------------------------------------------------------------- /examples/server/sanic/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Python-SocketIO Test 5 | 6 | 7 | 55 | 56 | 57 |

Python-SocketIO Test

58 |

Send:

59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |

Receive:

88 |

89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/server/sanic/app.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.response import html 3 | 4 | import socketio 5 | 6 | sio = socketio.AsyncServer(async_mode='sanic') 7 | app = Sanic(__name__) 8 | sio.attach(app) 9 | 10 | 11 | async def background_task(): 12 | """Example of how to send server generated events to clients.""" 13 | count = 0 14 | while True: 15 | await sio.sleep(10) 16 | count += 1 17 | await sio.emit('my_response', {'data': 'Server generated event'}) 18 | 19 | 20 | @app.listener('before_server_start') 21 | def before_server_start(sanic, loop): 22 | sio.start_background_task(background_task) 23 | 24 | 25 | @app.route('/') 26 | async def index(request): 27 | with open('app.html') as f: 28 | return html(f.read()) 29 | 30 | 31 | @sio.event 32 | async def my_event(sid, message): 33 | await sio.emit('my_response', {'data': message['data']}, room=sid) 34 | 35 | 36 | @sio.event 37 | async def my_broadcast_event(sid, message): 38 | await sio.emit('my_response', {'data': message['data']}) 39 | 40 | 41 | @sio.event 42 | async def join(sid, message): 43 | await sio.enter_room(sid, message['room']) 44 | await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, 45 | room=sid) 46 | 47 | 48 | @sio.event 49 | async def leave(sid, message): 50 | await sio.leave_room(sid, message['room']) 51 | await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, 52 | room=sid) 53 | 54 | 55 | @sio.event 56 | async def close_room(sid, message): 57 | await sio.emit('my_response', 58 | {'data': 'Room ' + message['room'] + ' is closing.'}, 59 | room=message['room']) 60 | await sio.close_room(message['room']) 61 | 62 | 63 | @sio.event 64 | async def my_room_event(sid, message): 65 | await sio.emit('my_response', {'data': message['data']}, 66 | room=message['room']) 67 | 68 | 69 | @sio.event 70 | async def disconnect_request(sid): 71 | await sio.disconnect(sid) 72 | 73 | 74 | @sio.event 75 | async def connect(sid, environ): 76 | await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) 77 | 78 | 79 | @sio.event 80 | def disconnect(sid, reason): 81 | print('Client disconnected, reason:', reason) 82 | 83 | 84 | app.static('/static', './static') 85 | 86 | 87 | if __name__ == '__main__': 88 | app.run() 89 | -------------------------------------------------------------------------------- /examples/server/sanic/fiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Fiddle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/server/sanic/fiddle.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.response import html 3 | 4 | import socketio 5 | 6 | sio = socketio.AsyncServer(async_mode='sanic') 7 | app = Sanic(__name__) 8 | sio.attach(app) 9 | 10 | 11 | @app.route('/') 12 | def index(request): 13 | with open('fiddle.html') as f: 14 | return html(f.read()) 15 | 16 | 17 | @sio.event 18 | async def connect(sid, environ, auth): 19 | print(f'connected auth={auth} sid={sid}') 20 | await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 21 | 22 | 23 | @sio.event 24 | def disconnect(sid, reason): 25 | print('disconnected', sid, reason) 26 | 27 | 28 | app.static('/static', './static') 29 | 30 | 31 | if __name__ == '__main__': 32 | app.run() 33 | -------------------------------------------------------------------------------- /examples/server/sanic/latency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Latency 5 | 6 | 7 | 8 |

Socket.IO Latency

9 |

(connecting)

10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/server/sanic/latency.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.response import html 3 | 4 | import socketio 5 | 6 | sio = socketio.AsyncServer(async_mode='sanic') 7 | app = Sanic(__name__) 8 | sio.attach(app) 9 | 10 | 11 | @app.route('/') 12 | def index(request): 13 | with open('latency.html') as f: 14 | return html(f.read()) 15 | 16 | 17 | @sio.event 18 | async def ping_from_client(sid): 19 | await sio.emit('pong_from_server', room=sid) 20 | 21 | app.static('/static', './static') 22 | 23 | 24 | if __name__ == '__main__': 25 | app.run() 26 | -------------------------------------------------------------------------------- /examples/server/sanic/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.3.0 2 | httptools==0.0.9 3 | python_engineio 4 | python_socketio 5 | sanic==20.12.7 6 | six==1.10.0 7 | ujson==5.4.0 8 | uvloop==0.8.0 9 | -------------------------------------------------------------------------------- /examples/server/sanic/static/fiddle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | const socket = io(); 6 | 7 | socket.on('connect', () => { 8 | console.log(`connect ${socket.id}`); 9 | }); 10 | 11 | socket.on('disconnect', () => { 12 | console.log(`disconnect ${socket.id}`); 13 | }); 14 | 15 | socket.on('hello', (a, b, c) => { 16 | console.log(a, b, c); 17 | }); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/server/sanic/static/style.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; font-family: Helvetica Neue; } 2 | h1 { margin: 100px 100px 10px; } 3 | h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } 4 | #latency { color: red; } 5 | -------------------------------------------------------------------------------- /examples/server/tornado/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Tornado Examples 2 | ========================== 3 | 4 | This directory contains example Socket.IO applications that are compatible 5 | with the Tornado framework. These applications require Tornado 5 and Python 6 | 3.5 or later. 7 | 8 | app.py 9 | ------ 10 | 11 | A basic "kitchen sink" type application that allows the user to experiment 12 | with most of the available features of the Socket.IO server. 13 | 14 | latency.py 15 | ---------- 16 | 17 | A port of the latency application included in the official Engine.IO 18 | Javascript server. In this application the client sends *ping* messages to 19 | the server, which are responded by the server with a *pong*. The client 20 | measures the time it takes for each of these exchanges and plots these in real 21 | time to the page. 22 | 23 | This is an ideal application to measure the performance of the different 24 | asynchronous modes supported by the Socket.IO server. 25 | 26 | fiddle.py 27 | --------- 28 | 29 | This is a very simple application based on a JavaScript example of the same 30 | name. 31 | 32 | Running the Examples 33 | -------------------- 34 | 35 | To run these examples, create a virtual environment, install the requirements 36 | and then run one of the following:: 37 | 38 | $ python app.py 39 | 40 | :: 41 | 42 | $ python latency.py 43 | 44 | :: 45 | 46 | $ python fiddle.py 47 | 48 | You can then access the application from your web browser at 49 | ``http://localhost:5000``. 50 | -------------------------------------------------------------------------------- /examples/server/tornado/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import tornado.ioloop 4 | from tornado.options import define, options, parse_command_line 5 | import tornado.web 6 | 7 | import socketio 8 | 9 | define("port", default=5000, help="run on the given port", type=int) 10 | define("debug", default=False, help="run in debug mode") 11 | 12 | sio = socketio.AsyncServer(async_mode='tornado') 13 | 14 | 15 | async def background_task(): 16 | """Example of how to send server generated events to clients.""" 17 | count = 0 18 | while True: 19 | await sio.sleep(10) 20 | count += 1 21 | await sio.emit('my_response', {'data': 'Server generated event'}) 22 | 23 | 24 | class MainHandler(tornado.web.RequestHandler): 25 | def get(self): 26 | self.render("app.html") 27 | 28 | 29 | @sio.event 30 | async def my_event(sid, message): 31 | await sio.emit('my_response', {'data': message['data']}, room=sid) 32 | 33 | 34 | @sio.event 35 | async def my_broadcast_event(sid, message): 36 | await sio.emit('my_response', {'data': message['data']}) 37 | 38 | 39 | @sio.event 40 | async def join(sid, message): 41 | await sio.enter_room(sid, message['room']) 42 | await sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, 43 | room=sid) 44 | 45 | 46 | @sio.event 47 | async def leave(sid, message): 48 | await sio.leave_room(sid, message['room']) 49 | await sio.emit('my_response', {'data': 'Left room: ' + message['room']}, 50 | room=sid) 51 | 52 | 53 | @sio.event 54 | async def close_room(sid, message): 55 | await sio.emit('my_response', 56 | {'data': 'Room ' + message['room'] + ' is closing.'}, 57 | room=message['room']) 58 | await sio.close_room(message['room']) 59 | 60 | 61 | @sio.event 62 | async def my_room_event(sid, message): 63 | await sio.emit('my_response', {'data': message['data']}, 64 | room=message['room']) 65 | 66 | 67 | @sio.event 68 | async def disconnect_request(sid): 69 | await sio.disconnect(sid) 70 | 71 | 72 | @sio.event 73 | async def connect(sid, environ): 74 | await sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) 75 | 76 | 77 | @sio.event 78 | def disconnect(sid, reason): 79 | print('Client disconnected, reason:', reason) 80 | 81 | 82 | def main(): 83 | parse_command_line() 84 | app = tornado.web.Application( 85 | [ 86 | (r"/", MainHandler), 87 | (r"/socket.io/", socketio.get_tornado_handler(sio)), 88 | ], 89 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 90 | static_path=os.path.join(os.path.dirname(__file__), "static"), 91 | debug=options.debug, 92 | ) 93 | app.listen(options.port) 94 | tornado.ioloop.IOLoop.current().start() 95 | 96 | 97 | if __name__ == "__main__": 98 | main() 99 | -------------------------------------------------------------------------------- /examples/server/tornado/fiddle.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import tornado.ioloop 4 | from tornado.options import define, options, parse_command_line 5 | import tornado.web 6 | 7 | import socketio 8 | 9 | define("port", default=5000, help="run on the given port", type=int) 10 | define("debug", default=False, help="run in debug mode") 11 | 12 | sio = socketio.AsyncServer(async_mode='tornado') 13 | 14 | 15 | class MainHandler(tornado.web.RequestHandler): 16 | def get(self): 17 | self.render("fiddle.html") 18 | 19 | 20 | @sio.event 21 | async def connect(sid, environ, auth): 22 | print(f'connected auth={auth} sid={sid}') 23 | await sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 24 | 25 | 26 | @sio.event 27 | def disconnect(sid, reason): 28 | print('disconnected', sid, reason) 29 | 30 | 31 | def main(): 32 | parse_command_line() 33 | app = tornado.web.Application( 34 | [ 35 | (r"/", MainHandler), 36 | (r"/socket.io/", socketio.get_tornado_handler(sio)), 37 | ], 38 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 39 | static_path=os.path.join(os.path.dirname(__file__), "static"), 40 | debug=options.debug, 41 | ) 42 | app.listen(options.port) 43 | tornado.ioloop.IOLoop.current().start() 44 | 45 | 46 | if __name__ == "__main__": 47 | main() 48 | -------------------------------------------------------------------------------- /examples/server/tornado/latency.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import tornado.ioloop 4 | from tornado.options import define, options, parse_command_line 5 | import tornado.web 6 | 7 | import socketio 8 | 9 | define("port", default=5000, help="run on the given port", type=int) 10 | define("debug", default=False, help="run in debug mode") 11 | 12 | sio = socketio.AsyncServer(async_mode='tornado') 13 | 14 | 15 | class MainHandler(tornado.web.RequestHandler): 16 | def get(self): 17 | self.render("latency.html") 18 | 19 | 20 | @sio.event 21 | async def ping_from_client(sid): 22 | await sio.emit('pong_from_server', room=sid) 23 | 24 | 25 | def main(): 26 | parse_command_line() 27 | app = tornado.web.Application( 28 | [ 29 | (r"/", MainHandler), 30 | (r"/socket.io/", socketio.get_tornado_handler(sio)), 31 | ], 32 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 33 | static_path=os.path.join(os.path.dirname(__file__), "static"), 34 | debug=options.debug, 35 | ) 36 | app.listen(options.port) 37 | tornado.ioloop.IOLoop.current().start() 38 | 39 | 40 | if __name__ == "__main__": 41 | main() 42 | -------------------------------------------------------------------------------- /examples/server/tornado/requirements.txt: -------------------------------------------------------------------------------- 1 | tornado==6.5.1 2 | python-engineio 3 | python_socketio 4 | six==1.10.0 5 | -------------------------------------------------------------------------------- /examples/server/tornado/static/fiddle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | const socket = io(); 6 | 7 | socket.on('connect', () => { 8 | console.log(`connect ${socket.id}`); 9 | }); 10 | 11 | socket.on('disconnect', () => { 12 | console.log(`disconnect ${socket.id}`); 13 | }); 14 | 15 | socket.on('hello', (a, b, c) => { 16 | console.log(a, b, c); 17 | }); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/server/tornado/static/style.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; font-family: Helvetica Neue; } 2 | h1 { margin: 100px 100px 10px; } 3 | h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } 4 | #latency { color: red; } 5 | -------------------------------------------------------------------------------- /examples/server/tornado/templates/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | python-socketio test 5 | 6 | 7 | 55 | 56 | 57 |

python-socketio test

58 |

Send:

59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |

Receive:

88 |

89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/server/tornado/templates/fiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Fiddle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/server/tornado/templates/latency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Latency 5 | 6 | 7 | 8 |

Socket.IO Latency

9 |

(connecting)

10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/server/wsgi/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO WSGI Examples 2 | ======================= 3 | 4 | This directory contains example Socket.IO applications that work together with 5 | WSGI frameworks. These examples use Flask or Django to serve the client 6 | application to the web browser, but they should be easily adapted to use other 7 | WSGI compliant frameworks. 8 | 9 | app.py 10 | ------ 11 | 12 | A basic "kitchen sink" type application that allows the user to experiment 13 | with most of the available features of the Socket.IO server. 14 | 15 | latency.py 16 | ---------- 17 | 18 | A port of the latency application included in the official Engine.IO 19 | Javascript server. In this application the client sends *ping* messages to 20 | the server, which are responded by the server with a *pong*. The client 21 | measures the time it takes for each of these exchanges and plots these in real 22 | time to the page. 23 | 24 | This is an ideal application to measure the performance of the different 25 | asynchronous modes supported by the Socket.IO server. 26 | 27 | django_socketio 28 | --------------- 29 | 30 | This is a version of the "app.py" application described above, that is based 31 | on the Django web framework. 32 | 33 | fiddle.py 34 | --------- 35 | 36 | This is a very simple application based on a JavaScript example of the same 37 | name. 38 | 39 | Running the Examples 40 | -------------------- 41 | 42 | To run these examples, create a virtual environment, install the requirements 43 | and then run one of the following:: 44 | 45 | $ python app.py 46 | 47 | :: 48 | 49 | $ python latency.py 50 | 51 | :: 52 | 53 | $ cd django_example 54 | $ ./manage.py runserver 55 | 56 | :: 57 | 58 | $ python fiddle 59 | 60 | You can then access the application from your web browser at 61 | ``http://localhost:5000`` (``app.py``, ``latency.py`` and ``fiddle.py``) or 62 | ``http://localhost:8000`` (``django_example``). 63 | 64 | Near the top of the ``app.py``, ``latency.py`` and ``fiddle.py`` source files 65 | there is a ``async_mode`` variable that can be edited to switch to the other 66 | asynchornous modes. Accepted values for ``async_mode`` are ``'threading'``, 67 | ``'eventlet'`` and ``'gevent'``. For ``django_example``, the async mode can be 68 | set in the ``django_example/socketio_app/views.py`` module. 69 | 70 | Note 1: when using the ``'eventlet'`` mode, the eventlet package must be 71 | installed in the virtual environment:: 72 | 73 | $ pip install eventlet 74 | 75 | Note 2: when using the ``'gevent'`` mode, the gevent and gevent-websocket 76 | packages must be installed in the virtual environment:: 77 | 78 | $ pip install gevent gevent-websocket 79 | -------------------------------------------------------------------------------- /examples/server/wsgi/app.py: -------------------------------------------------------------------------------- 1 | # set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to 2 | # force a mode else, the best mode is selected automatically from what's 3 | # installed 4 | async_mode = None 5 | 6 | # set instrument to `True` to accept connections from the official Socket.IO 7 | # Admin UI hosted at https://admin.socket.io 8 | instrument = True 9 | admin_login = { 10 | 'username': 'admin', 11 | 'password': 'python', # change this to a strong secret for production use! 12 | } 13 | 14 | from flask import Flask, render_template 15 | import socketio 16 | 17 | sio = socketio.Server( 18 | async_mode=async_mode, 19 | cors_allowed_origins=None if not instrument else [ 20 | 'http://localhost:5000', 21 | 'https://admin.socket.io', # edit the allowed origins if necessary 22 | ]) 23 | if instrument: 24 | sio.instrument(auth=admin_login) 25 | 26 | app = Flask(__name__) 27 | app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) 28 | app.config['SECRET_KEY'] = 'secret!' 29 | thread = None 30 | 31 | 32 | def background_thread(): 33 | """Example of how to send server generated events to clients.""" 34 | count = 0 35 | while True: 36 | sio.sleep(10) 37 | count += 1 38 | sio.emit('my_response', {'data': 'Server generated event'}) 39 | 40 | 41 | @app.route('/') 42 | def index(): 43 | global thread 44 | if thread is None: 45 | thread = sio.start_background_task(background_thread) 46 | return render_template('index.html') 47 | 48 | 49 | @sio.event 50 | def my_event(sid, message): 51 | sio.emit('my_response', {'data': message['data']}, room=sid) 52 | 53 | 54 | @sio.event 55 | def my_broadcast_event(sid, message): 56 | sio.emit('my_response', {'data': message['data']}) 57 | 58 | 59 | @sio.event 60 | def join(sid, message): 61 | sio.enter_room(sid, message['room']) 62 | sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, 63 | room=sid) 64 | 65 | 66 | @sio.event 67 | def leave(sid, message): 68 | sio.leave_room(sid, message['room']) 69 | sio.emit('my_response', {'data': 'Left room: ' + message['room']}, 70 | room=sid) 71 | 72 | 73 | @sio.event 74 | def close_room(sid, message): 75 | sio.emit('my_response', 76 | {'data': 'Room ' + message['room'] + ' is closing.'}, 77 | room=message['room']) 78 | sio.close_room(message['room']) 79 | 80 | 81 | @sio.event 82 | def my_room_event(sid, message): 83 | sio.emit('my_response', {'data': message['data']}, room=message['room']) 84 | 85 | 86 | @sio.event 87 | def disconnect_request(sid): 88 | sio.disconnect(sid) 89 | 90 | 91 | @sio.event 92 | def connect(sid, environ): 93 | sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) 94 | 95 | 96 | @sio.event 97 | def disconnect(sid, reason): 98 | print('Client disconnected, reason:', reason) 99 | 100 | 101 | if __name__ == '__main__': 102 | if instrument: 103 | print('The server is instrumented for remote administration.') 104 | print( 105 | 'Use the official Socket.IO Admin UI at https://admin.socket.io ' 106 | 'with the following connection details:' 107 | ) 108 | print(' - Server URL: http://localhost:5000') 109 | print(' - Username:', admin_login['username']) 110 | print(' - Password:', admin_login['password']) 111 | print('') 112 | if sio.async_mode == 'threading': 113 | # deploy with Werkzeug 114 | app.run(threaded=True) 115 | elif sio.async_mode == 'eventlet': 116 | # deploy with eventlet 117 | import eventlet 118 | import eventlet.wsgi 119 | eventlet.wsgi.server(eventlet.listen(('', 5000)), app) 120 | elif sio.async_mode == 'gevent': 121 | # deploy with gevent 122 | from gevent import pywsgi 123 | try: 124 | from geventwebsocket.handler import WebSocketHandler 125 | websocket = True 126 | except ImportError: 127 | websocket = False 128 | if websocket: 129 | pywsgi.WSGIServer(('', 5000), app, 130 | handler_class=WebSocketHandler).serve_forever() 131 | else: 132 | pywsgi.WSGIServer(('', 5000), app).serve_forever() 133 | elif sio.async_mode == 'gevent_uwsgi': 134 | print('Start the application through the uwsgi server. Example:') 135 | print('uwsgi --http :5000 --gevent 1000 --http-websockets --master ' 136 | '--wsgi-file app.py --callable app') 137 | else: 138 | print('Unknown async_mode: ' + sio.async_mode) 139 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/README.md: -------------------------------------------------------------------------------- 1 | django-socketio 2 | =============== 3 | 4 | This is an example Django application integrated with Socket.IO. 5 | 6 | You can run it with the Django development web server: 7 | 8 | ```bash 9 | python manage.py runserver 10 | ``` 11 | 12 | When running in this mode, you will get an error message: 13 | 14 | RuntimeError: Cannot obtain socket from WSGI environment. 15 | 16 | This is expected, and it happens because the Django web server does not support 17 | the WebSocket protocol. You can ignore the error, as the server will still work 18 | using long-polling. 19 | 20 | To run the application with WebSocket enabled, you can use the Gunicorn web 21 | server as follows: 22 | 23 | gunicorn -b :8000 --threads 100 --access-logfile - django_socketio.wsgi:application 24 | 25 | See the documentation for information on other supported deployment methods 26 | that you can use to add support for WebSocket. 27 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/django_socketio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/python-socketio/d3a9b82d5816a2f13413af769c05193ecd6d2422/examples/server/wsgi/django_socketio/django_socketio/__init__.py -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/django_socketio/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_socketio project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_socketio.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/django_socketio/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_socketio project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-&@-nkbrpe@%1_%ljh#oe@sw)6+k(&yn#r_)!5p)$22c^u#0@lj' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'socketio_app', 41 | ] 42 | 43 | MIDDLEWARE = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ] 52 | 53 | ROOT_URLCONF = 'django_socketio.urls' 54 | 55 | TEMPLATES = [ 56 | { 57 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 58 | 'DIRS': [], 59 | 'APP_DIRS': True, 60 | 'OPTIONS': { 61 | 'context_processors': [ 62 | 'django.template.context_processors.debug', 63 | 'django.template.context_processors.request', 64 | 'django.contrib.auth.context_processors.auth', 65 | 'django.contrib.messages.context_processors.messages', 66 | ], 67 | }, 68 | }, 69 | ] 70 | 71 | WSGI_APPLICATION = 'django_socketio.wsgi.application' 72 | 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': BASE_DIR / 'db.sqlite3', 81 | } 82 | } 83 | 84 | 85 | # Password validation 86 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 87 | 88 | AUTH_PASSWORD_VALIDATORS = [ 89 | { 90 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 91 | }, 92 | { 93 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 94 | }, 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 100 | }, 101 | ] 102 | 103 | 104 | # Internationalization 105 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 106 | 107 | LANGUAGE_CODE = 'en-us' 108 | 109 | TIME_ZONE = 'UTC' 110 | 111 | USE_I18N = True 112 | 113 | USE_TZ = True 114 | 115 | 116 | # Static files (CSS, JavaScript, Images) 117 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 118 | 119 | STATIC_URL = 'static/' 120 | 121 | # Default primary key field type 122 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 123 | 124 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 125 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/django_socketio/urls.py: -------------------------------------------------------------------------------- 1 | """django_socketio URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from django.conf.urls import include 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path(r'', include('socketio_app.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/django_socketio/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_socketio project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | import socketio 14 | 15 | from socketio_app.views import sio 16 | 17 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_socketio.settings') 18 | 19 | django_app = get_wsgi_application() 20 | application = socketio.WSGIApp(sio, django_app) 21 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_socketio.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.6.0 2 | bidict==0.22.1 3 | Django==4.2.22 4 | gunicorn==23.0.0 5 | h11==0.16.0 6 | python-engineio 7 | python-socketio 8 | simple-websocket 9 | sqlparse==0.5.0 10 | wsproto==1.2.0 11 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/python-socketio/d3a9b82d5816a2f13413af769c05193ecd6d2422/examples/server/wsgi/django_socketio/socketio_app/__init__.py -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SocketioAppConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'socketio_app' 7 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/python-socketio/d3a9b82d5816a2f13413af769c05193ecd6d2422/examples/server/wsgi/django_socketio/socketio_app/migrations/__init__.py -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Django + SocketIO Test 5 | 6 | 7 | 55 | 56 | 57 |

Django + SocketIO Test

58 |

Send:

59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |

Receive:

88 |

89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path(r'', views.index, name='index'), 7 | ] 8 | -------------------------------------------------------------------------------- /examples/server/wsgi/django_socketio/socketio_app/views.py: -------------------------------------------------------------------------------- 1 | # set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to 2 | # force a mode else, the best mode is selected automatically from what's 3 | # installed 4 | async_mode = None 5 | 6 | import os 7 | 8 | from django.http import HttpResponse 9 | import socketio 10 | 11 | basedir = os.path.dirname(os.path.realpath(__file__)) 12 | sio = socketio.Server(async_mode=async_mode) 13 | thread = None 14 | 15 | 16 | def index(request): 17 | global thread 18 | if thread is None: 19 | thread = sio.start_background_task(background_thread) 20 | return HttpResponse(open(os.path.join(basedir, 'static/index.html'))) 21 | 22 | 23 | def background_thread(): 24 | """Example of how to send server generated events to clients.""" 25 | count = 0 26 | while True: 27 | sio.sleep(10) 28 | count += 1 29 | sio.emit('my_response', {'data': 'Server generated event'}, 30 | namespace='/test') 31 | 32 | 33 | @sio.event 34 | def my_event(sid, message): 35 | sio.emit('my_response', {'data': message['data']}, room=sid) 36 | 37 | 38 | @sio.event 39 | def my_broadcast_event(sid, message): 40 | sio.emit('my_response', {'data': message['data']}) 41 | 42 | 43 | @sio.event 44 | def join(sid, message): 45 | sio.enter_room(sid, message['room']) 46 | sio.emit('my_response', {'data': 'Entered room: ' + message['room']}, 47 | room=sid) 48 | 49 | 50 | @sio.event 51 | def leave(sid, message): 52 | sio.leave_room(sid, message['room']) 53 | sio.emit('my_response', {'data': 'Left room: ' + message['room']}, 54 | room=sid) 55 | 56 | 57 | @sio.event 58 | def close_room(sid, message): 59 | sio.emit('my_response', 60 | {'data': 'Room ' + message['room'] + ' is closing.'}, 61 | room=message['room']) 62 | sio.close_room(message['room']) 63 | 64 | 65 | @sio.event 66 | def my_room_event(sid, message): 67 | sio.emit('my_response', {'data': message['data']}, room=message['room']) 68 | 69 | 70 | @sio.event 71 | def disconnect_request(sid): 72 | sio.disconnect(sid) 73 | 74 | 75 | @sio.event 76 | def connect(sid, environ): 77 | sio.emit('my_response', {'data': 'Connected', 'count': 0}, room=sid) 78 | 79 | 80 | @sio.event 81 | def disconnect(sid, reason): 82 | print('Client disconnected, reason:', reason) 83 | -------------------------------------------------------------------------------- /examples/server/wsgi/fiddle.py: -------------------------------------------------------------------------------- 1 | # set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to 2 | # force a mode else, the best mode is selected automatically from what's 3 | # installed 4 | async_mode = None 5 | 6 | from flask import Flask, render_template 7 | import socketio 8 | 9 | sio = socketio.Server(async_mode=async_mode) 10 | app = Flask(__name__) 11 | app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) 12 | 13 | 14 | @app.route('/') 15 | def index(): 16 | return render_template('fiddle.html') 17 | 18 | 19 | @sio.event 20 | def connect(sid, environ, auth): 21 | print(f'connected auth={auth} sid={sid}') 22 | sio.emit('hello', (1, 2, {'hello': 'you'}), to=sid) 23 | 24 | 25 | @sio.event 26 | def disconnect(sid, reason): 27 | print('disconnected', sid, reason) 28 | 29 | 30 | if __name__ == '__main__': 31 | if sio.async_mode == 'threading': 32 | # deploy with Werkzeug 33 | app.run(threaded=True) 34 | elif sio.async_mode == 'eventlet': 35 | # deploy with eventlet 36 | import eventlet 37 | import eventlet.wsgi 38 | eventlet.wsgi.server(eventlet.listen(('', 5000)), app) 39 | elif sio.async_mode == 'gevent': 40 | # deploy with gevent 41 | from gevent import pywsgi 42 | try: 43 | from geventwebsocket.handler import WebSocketHandler 44 | websocket = True 45 | except ImportError: 46 | websocket = False 47 | if websocket: 48 | pywsgi.WSGIServer(('', 5000), app, 49 | handler_class=WebSocketHandler).serve_forever() 50 | else: 51 | pywsgi.WSGIServer(('', 5000), app).serve_forever() 52 | elif sio.async_mode == 'gevent_uwsgi': 53 | print('Start the application through the uwsgi server. Example:') 54 | print('uwsgi --http :5000 --gevent 1000 --http-websockets --master ' 55 | '--wsgi-file latency.py --callable app') 56 | else: 57 | print('Unknown async_mode: ' + sio.async_mode) 58 | -------------------------------------------------------------------------------- /examples/server/wsgi/latency.py: -------------------------------------------------------------------------------- 1 | # set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to 2 | # force a mode else, the best mode is selected automatically from what's 3 | # installed 4 | async_mode = None 5 | 6 | from flask import Flask, render_template 7 | import socketio 8 | 9 | sio = socketio.Server(async_mode=async_mode) 10 | app = Flask(__name__) 11 | app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) 12 | 13 | 14 | @app.route('/') 15 | def index(): 16 | return render_template('latency.html') 17 | 18 | 19 | @sio.event 20 | def ping_from_client(sid): 21 | sio.emit('pong_from_server', room=sid) 22 | 23 | 24 | if __name__ == '__main__': 25 | if sio.async_mode == 'threading': 26 | # deploy with Werkzeug 27 | app.run(threaded=True) 28 | elif sio.async_mode == 'eventlet': 29 | # deploy with eventlet 30 | import eventlet 31 | import eventlet.wsgi 32 | eventlet.wsgi.server(eventlet.listen(('', 5000)), app) 33 | elif sio.async_mode == 'gevent': 34 | # deploy with gevent 35 | from gevent import pywsgi 36 | try: 37 | from geventwebsocket.handler import WebSocketHandler 38 | websocket = True 39 | except ImportError: 40 | websocket = False 41 | if websocket: 42 | pywsgi.WSGIServer(('', 5000), app, 43 | handler_class=WebSocketHandler).serve_forever() 44 | else: 45 | pywsgi.WSGIServer(('', 5000), app).serve_forever() 46 | elif sio.async_mode == 'gevent_uwsgi': 47 | print('Start the application through the uwsgi server. Example:') 48 | print('uwsgi --http :5000 --gevent 1000 --http-websockets --master ' 49 | '--wsgi-file latency.py --callable app') 50 | else: 51 | print('Unknown async_mode: ' + sio.async_mode) 52 | -------------------------------------------------------------------------------- /examples/server/wsgi/requirements.txt: -------------------------------------------------------------------------------- 1 | Click==7.0 2 | enum-compat==0.0.2 3 | enum34==1.1.6 4 | eventlet==0.35.2 5 | Flask==1.0.2 6 | greenlet==0.4.12 7 | itsdangerous==1.1.0 8 | Jinja2==3.1.6 9 | MarkupSafe==1.1.0 10 | packaging==16.8 11 | pyparsing==2.1.10 12 | python-engineio 13 | python-socketio 14 | six==1.11.0 15 | Werkzeug==2.2.3 16 | -------------------------------------------------------------------------------- /examples/server/wsgi/static/fiddle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | const socket = io(); 6 | 7 | socket.on('connect', () => { 8 | console.log(`connect ${socket.id}`); 9 | }); 10 | 11 | socket.on('disconnect', () => { 12 | console.log(`disconnect ${socket.id}`); 13 | }); 14 | 15 | socket.on('hello', (a, b, c) => { 16 | console.log(a, b, c); 17 | }); 18 | 19 | })(); 20 | -------------------------------------------------------------------------------- /examples/server/wsgi/static/style.css: -------------------------------------------------------------------------------- 1 | body { margin: 0; padding: 0; font-family: Helvetica Neue; } 2 | h1 { margin: 100px 100px 10px; } 3 | h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } 4 | #latency { color: red; } 5 | -------------------------------------------------------------------------------- /examples/server/wsgi/templates/fiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Socket.IO Fiddle 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/server/wsgi/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Python-SocketIO Test 5 | 6 | 7 | 55 | 56 | 57 |

Python-SocketIO Test

58 |

Send:

59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |

Receive:

88 |

89 | 90 | 91 | -------------------------------------------------------------------------------- /examples/server/wsgi/templates/latency.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Socket.IO Latency 5 | 6 | 7 | 8 |

Socket.IO Latency

9 |

(connecting)

10 | 11 | 12 | 13 | 14 | 15 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/simple-client/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Simple Client Examples 2 | ================================ 3 | 4 | This directory contains several example Socket.IO client applications built 5 | with the simplified client and organized by directory: 6 | 7 | sync 8 | ---- 9 | 10 | Examples that use standard Python. 11 | 12 | async 13 | ----- 14 | 15 | Examples that use Python's `asyncio` package. 16 | -------------------------------------------------------------------------------- /examples/simple-client/async/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Async Simple Client Examples 2 | ====================================== 3 | 4 | This directory contains example Socket.IO clients that work with the 5 | `asyncio` package of the Python standard library, built with the simplified 6 | client. 7 | 8 | latency_client.py 9 | ----------------- 10 | 11 | In this application the client sends *ping* messages to the server, which are 12 | responded by the server with a *pong*. The client measures the time it takes 13 | for each of these exchanges. 14 | 15 | This is an ideal application to measure the performance of the different 16 | asynchronous modes supported by the Socket.IO server. 17 | 18 | fiddle_client.py 19 | ---------------- 20 | 21 | This is an extemely simple application based on the JavaScript example of the 22 | same name. 23 | 24 | Running the Examples 25 | -------------------- 26 | 27 | These examples work with the server examples of the same name. First run one 28 | of the ``latency.py`` or ``fiddle.py`` versions from one of the 29 | ``examples/server`` subdirectories. On another terminal, then start the 30 | corresponding client:: 31 | 32 | $ python latency_client.py 33 | $ python fiddle_client.py 34 | -------------------------------------------------------------------------------- /examples/simple-client/async/fiddle_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socketio 3 | 4 | 5 | async def main(): 6 | async with socketio.AsyncSimpleClient() as sio: 7 | await sio.connect('http://localhost:5000', auth={'token': 'my-token'}) 8 | print(await sio.receive()) 9 | 10 | 11 | if __name__ == '__main__': 12 | asyncio.run(main()) 13 | -------------------------------------------------------------------------------- /examples/simple-client/async/latency_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import socketio 4 | 5 | 6 | async def main(): 7 | async with socketio.AsyncSimpleClient() as sio: 8 | await sio.connect('http://localhost:5000') 9 | while True: 10 | start_timer = time.time() 11 | await sio.emit('ping_from_client') 12 | while (await sio.receive()) != ['pong_from_server']: 13 | pass 14 | latency = time.time() - start_timer 15 | print(f'latency is {latency * 1000:.2f} ms') 16 | 17 | await asyncio.sleep(1) 18 | 19 | 20 | if __name__ == '__main__': 21 | asyncio.run(main()) 22 | -------------------------------------------------------------------------------- /examples/simple-client/sync/README.rst: -------------------------------------------------------------------------------- 1 | Socket.IO Simple Client Examples 2 | ================================ 3 | 4 | This directory contains example Socket.IO clients that are built using the 5 | simplified client. 6 | 7 | latency_client.py 8 | ----------------- 9 | 10 | In this application the client sends *ping* messages to the server, which are 11 | responded by the server with a *pong*. The client measures the time it takes 12 | for each of these exchanges. 13 | 14 | This is an ideal application to measure the performance of the different 15 | asynchronous modes supported by the Socket.IO server. 16 | 17 | fiddle_client.py 18 | ---------------- 19 | 20 | This is an extemely simple application based on the JavaScript example of the 21 | same name. 22 | 23 | Running the Examples 24 | -------------------- 25 | 26 | These examples work with the server examples of the same name. First run one 27 | of the ``latency.py`` or ``fiddle.py`` versions from one of the 28 | ``examples/server`` subdirectories. On another terminal, then start the 29 | corresponding client:: 30 | 31 | $ python latency_client.py 32 | $ python fiddle_client.py 33 | -------------------------------------------------------------------------------- /examples/simple-client/sync/fiddle_client.py: -------------------------------------------------------------------------------- 1 | import socketio 2 | 3 | 4 | def main(): 5 | with socketio.SimpleClient() as sio: 6 | sio.connect('http://localhost:5000', auth={'token': 'my-token'}) 7 | print(sio.receive()) 8 | 9 | 10 | if __name__ == '__main__': 11 | main() 12 | -------------------------------------------------------------------------------- /examples/simple-client/sync/latency_client.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socketio 3 | 4 | 5 | def main(): 6 | with socketio.SimpleClient() as sio: 7 | sio.connect('http://localhost:5000') 8 | while True: 9 | start_timer = time.time() 10 | sio.emit('ping_from_client') 11 | while sio.receive() != ['pong_from_server']: 12 | pass 13 | latency = time.time() - start_timer 14 | print(f'latency is {latency * 1000:.2f} ms') 15 | 16 | time.sleep(1) 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "python-socketio" 3 | version = "5.13.1.dev0" 4 | license = {text = "MIT"} 5 | authors = [ 6 | { name = "Miguel Grinberg", email = "miguel.grinberg@gmail.com" }, 7 | ] 8 | description = "Socket.IO server and client for Python" 9 | classifiers = [ 10 | "Environment :: Web Environment", 11 | "Intended Audience :: Developers", 12 | "Programming Language :: Python :: 3", 13 | "Operating System :: OS Independent", 14 | ] 15 | requires-python = ">=3.8" 16 | dependencies = [ 17 | "bidict >= 0.21.0", 18 | "python-engineio >= 4.11.0", 19 | ] 20 | 21 | [project.readme] 22 | file = "README.md" 23 | content-type = "text/markdown" 24 | 25 | [project.urls] 26 | Homepage = "https://github.com/miguelgrinberg/python-socketio" 27 | "Bug Tracker" = "https://github.com/miguelgrinberg/python-socketio/issues" 28 | 29 | [project.optional-dependencies] 30 | client = [ 31 | "requests >= 2.21.0", 32 | "websocket-client >= 0.54.0", 33 | ] 34 | asyncio_client = [ 35 | "aiohttp >= 3.4", 36 | ] 37 | docs = [ 38 | "sphinx", 39 | ] 40 | 41 | [tool.setuptools] 42 | zip-safe = false 43 | include-package-data = true 44 | 45 | [tool.setuptools.package-dir] 46 | "" = "src" 47 | 48 | [tool.setuptools.packages.find] 49 | where = [ 50 | "src", 51 | ] 52 | namespaces = false 53 | 54 | [build-system] 55 | requires = ["setuptools>=61.2"] 56 | build-backend = "setuptools.build_meta" 57 | 58 | [tool.pytest.ini_options] 59 | asyncio_mode = "auto" 60 | asyncio_default_fixture_loop_scope = "session" 61 | -------------------------------------------------------------------------------- /src/socketio/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client 2 | from .simple_client import SimpleClient 3 | from .manager import Manager 4 | from .pubsub_manager import PubSubManager 5 | from .kombu_manager import KombuManager 6 | from .redis_manager import RedisManager 7 | from .kafka_manager import KafkaManager 8 | from .zmq_manager import ZmqManager 9 | from .server import Server 10 | from .namespace import Namespace, ClientNamespace 11 | from .middleware import WSGIApp, Middleware 12 | from .tornado import get_tornado_handler 13 | from .async_client import AsyncClient 14 | from .async_simple_client import AsyncSimpleClient 15 | from .async_server import AsyncServer 16 | from .async_manager import AsyncManager 17 | from .async_namespace import AsyncNamespace, AsyncClientNamespace 18 | from .async_redis_manager import AsyncRedisManager 19 | from .async_aiopika_manager import AsyncAioPikaManager 20 | from .asgi import ASGIApp 21 | 22 | __all__ = ['SimpleClient', 'Client', 'Server', 'Manager', 'PubSubManager', 23 | 'KombuManager', 'RedisManager', 'ZmqManager', 'KafkaManager', 24 | 'Namespace', 'ClientNamespace', 'WSGIApp', 'Middleware', 25 | 'AsyncSimpleClient', 'AsyncClient', 'AsyncServer', 26 | 'AsyncNamespace', 'AsyncClientNamespace', 'AsyncManager', 27 | 'AsyncRedisManager', 'ASGIApp', 'get_tornado_handler', 28 | 'AsyncAioPikaManager'] 29 | -------------------------------------------------------------------------------- /src/socketio/asgi.py: -------------------------------------------------------------------------------- 1 | import engineio 2 | 3 | 4 | class ASGIApp(engineio.ASGIApp): # pragma: no cover 5 | """ASGI application middleware for Socket.IO. 6 | 7 | This middleware dispatches traffic to an Socket.IO application. It can 8 | also serve a list of static files to the client, or forward unrelated 9 | HTTP traffic to another ASGI application. 10 | 11 | :param socketio_server: The Socket.IO server. Must be an instance of the 12 | ``socketio.AsyncServer`` class. 13 | :param static_files: A dictionary with static file mapping rules. See the 14 | documentation for details on this argument. 15 | :param other_asgi_app: A separate ASGI app that receives all other traffic. 16 | :param socketio_path: The endpoint where the Socket.IO application should 17 | be installed. The default value is appropriate for 18 | most cases. With a value of ``None``, all incoming 19 | traffic is directed to the Socket.IO server, with the 20 | assumption that routing, if necessary, is handled by 21 | a different layer. When this option is set to 22 | ``None``, ``static_files`` and ``other_asgi_app`` are 23 | ignored. 24 | :param on_startup: function to be called on application startup; can be 25 | coroutine 26 | :param on_shutdown: function to be called on application shutdown; can be 27 | coroutine 28 | 29 | Example usage:: 30 | 31 | import socketio 32 | import uvicorn 33 | 34 | sio = socketio.AsyncServer() 35 | app = socketio.ASGIApp(sio, static_files={ 36 | '/': 'index.html', 37 | '/static': './public', 38 | }) 39 | uvicorn.run(app, host='127.0.0.1', port=5000) 40 | """ 41 | def __init__(self, socketio_server, other_asgi_app=None, 42 | static_files=None, socketio_path='socket.io', 43 | on_startup=None, on_shutdown=None): 44 | super().__init__(socketio_server, other_asgi_app, 45 | static_files=static_files, 46 | engineio_path=socketio_path, on_startup=on_startup, 47 | on_shutdown=on_shutdown) 48 | -------------------------------------------------------------------------------- /src/socketio/async_aiopika_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pickle 3 | 4 | from .async_pubsub_manager import AsyncPubSubManager 5 | 6 | try: 7 | import aio_pika 8 | except ImportError: 9 | aio_pika = None 10 | 11 | 12 | class AsyncAioPikaManager(AsyncPubSubManager): # pragma: no cover 13 | """Client manager that uses aio_pika for inter-process messaging under 14 | asyncio. 15 | 16 | This class implements a client manager backend for event sharing across 17 | multiple processes, using RabbitMQ 18 | 19 | To use a aio_pika backend, initialize the :class:`Server` instance as 20 | follows:: 21 | 22 | url = 'amqp://user:password@hostname:port//' 23 | server = socketio.Server(client_manager=socketio.AsyncAioPikaManager( 24 | url)) 25 | 26 | :param url: The connection URL for the backend messaging queue. Example 27 | connection URLs are ``'amqp://guest:guest@localhost:5672//'`` 28 | for RabbitMQ. 29 | :param channel: The channel name on which the server sends and receives 30 | notifications. Must be the same in all the servers. 31 | With this manager, the channel name is the exchange name 32 | in rabbitmq 33 | :param write_only: If set to ``True``, only initialize to emit events. The 34 | default of ``False`` initializes the class for emitting 35 | and receiving. 36 | """ 37 | 38 | name = 'asyncaiopika' 39 | 40 | def __init__(self, url='amqp://guest:guest@localhost:5672//', 41 | channel='socketio', write_only=False, logger=None): 42 | if aio_pika is None: 43 | raise RuntimeError('aio_pika package is not installed ' 44 | '(Run "pip install aio_pika" in your ' 45 | 'virtualenv).') 46 | self.url = url 47 | self._lock = asyncio.Lock() 48 | self.publisher_connection = None 49 | self.publisher_channel = None 50 | self.publisher_exchange = None 51 | super().__init__(channel=channel, write_only=write_only, logger=logger) 52 | 53 | async def _connection(self): 54 | return await aio_pika.connect_robust(self.url) 55 | 56 | async def _channel(self, connection): 57 | return await connection.channel() 58 | 59 | async def _exchange(self, channel): 60 | return await channel.declare_exchange(self.channel, 61 | aio_pika.ExchangeType.FANOUT) 62 | 63 | async def _queue(self, channel, exchange): 64 | queue = await channel.declare_queue(durable=False, 65 | arguments={'x-expires': 300000}) 66 | await queue.bind(exchange) 67 | return queue 68 | 69 | async def _publish(self, data): 70 | if self.publisher_connection is None: 71 | async with self._lock: 72 | if self.publisher_connection is None: 73 | self.publisher_connection = await self._connection() 74 | self.publisher_channel = await self._channel( 75 | self.publisher_connection 76 | ) 77 | self.publisher_exchange = await self._exchange( 78 | self.publisher_channel 79 | ) 80 | retry = True 81 | while True: 82 | try: 83 | await self.publisher_exchange.publish( 84 | aio_pika.Message( 85 | body=pickle.dumps(data), 86 | delivery_mode=aio_pika.DeliveryMode.PERSISTENT 87 | ), routing_key='*', 88 | ) 89 | break 90 | except aio_pika.AMQPException: 91 | if retry: 92 | self._get_logger().error('Cannot publish to rabbitmq... ' 93 | 'retrying') 94 | retry = False 95 | else: 96 | self._get_logger().error( 97 | 'Cannot publish to rabbitmq... giving up') 98 | break 99 | except aio_pika.exceptions.ChannelInvalidStateError: 100 | # aio_pika raises this exception when the task is cancelled 101 | raise asyncio.CancelledError() 102 | 103 | async def _listen(self): 104 | async with (await self._connection()) as connection: 105 | channel = await self._channel(connection) 106 | await channel.set_qos(prefetch_count=1) 107 | exchange = await self._exchange(channel) 108 | queue = await self._queue(channel, exchange) 109 | 110 | retry_sleep = 1 111 | while True: 112 | try: 113 | async with queue.iterator() as queue_iter: 114 | async for message in queue_iter: 115 | async with message.process(): 116 | yield pickle.loads(message.body) 117 | retry_sleep = 1 118 | except aio_pika.AMQPException: 119 | self._get_logger().error( 120 | 'Cannot receive from rabbitmq... ' 121 | 'retrying in {} secs'.format(retry_sleep)) 122 | await asyncio.sleep(retry_sleep) 123 | retry_sleep = min(retry_sleep * 2, 60) 124 | except aio_pika.exceptions.ChannelInvalidStateError: 125 | # aio_pika raises this exception when the task is cancelled 126 | raise asyncio.CancelledError() 127 | -------------------------------------------------------------------------------- /src/socketio/async_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from engineio import packet as eio_packet 4 | from socketio import packet 5 | from .base_manager import BaseManager 6 | 7 | 8 | class AsyncManager(BaseManager): 9 | """Manage a client list for an asyncio server.""" 10 | async def can_disconnect(self, sid, namespace): 11 | return self.is_connected(sid, namespace) 12 | 13 | async def emit(self, event, data, namespace, room=None, skip_sid=None, 14 | callback=None, to=None, **kwargs): 15 | """Emit a message to a single client, a room, or all the clients 16 | connected to the namespace. 17 | 18 | Note: this method is a coroutine. 19 | """ 20 | room = to or room 21 | if namespace not in self.rooms: 22 | return 23 | if isinstance(data, tuple): 24 | # tuples are expanded to multiple arguments, everything else is 25 | # sent as a single argument 26 | data = list(data) 27 | elif data is not None: 28 | data = [data] 29 | else: 30 | data = [] 31 | if not isinstance(skip_sid, list): 32 | skip_sid = [skip_sid] 33 | tasks = [] 34 | if not callback: 35 | # when callbacks aren't used the packets sent to each recipient are 36 | # identical, so they can be generated once and reused 37 | pkt = self.server.packet_class( 38 | packet.EVENT, namespace=namespace, data=[event] + data) 39 | encoded_packet = pkt.encode() 40 | if not isinstance(encoded_packet, list): 41 | encoded_packet = [encoded_packet] 42 | eio_pkt = [eio_packet.Packet(eio_packet.MESSAGE, p) 43 | for p in encoded_packet] 44 | for sid, eio_sid in self.get_participants(namespace, room): 45 | if sid not in skip_sid: 46 | for p in eio_pkt: 47 | tasks.append(asyncio.create_task( 48 | self.server._send_eio_packet(eio_sid, p))) 49 | else: 50 | # callbacks are used, so each recipient must be sent a packet that 51 | # contains a unique callback id 52 | # note that callbacks when addressing a group of people are 53 | # implemented but not tested or supported 54 | for sid, eio_sid in self.get_participants(namespace, room): 55 | if sid not in skip_sid: # pragma: no branch 56 | id = self._generate_ack_id(sid, callback) 57 | pkt = self.server.packet_class( 58 | packet.EVENT, namespace=namespace, data=[event] + data, 59 | id=id) 60 | tasks.append(asyncio.create_task( 61 | self.server._send_packet(eio_sid, pkt))) 62 | if tasks == []: # pragma: no cover 63 | return 64 | await asyncio.wait(tasks) 65 | 66 | async def connect(self, eio_sid, namespace): 67 | """Register a client connection to a namespace. 68 | 69 | Note: this method is a coroutine. 70 | """ 71 | return super().connect(eio_sid, namespace) 72 | 73 | async def disconnect(self, sid, namespace, **kwargs): 74 | """Disconnect a client. 75 | 76 | Note: this method is a coroutine. 77 | """ 78 | return self.basic_disconnect(sid, namespace, **kwargs) 79 | 80 | async def enter_room(self, sid, namespace, room, eio_sid=None): 81 | """Add a client to a room. 82 | 83 | Note: this method is a coroutine. 84 | """ 85 | return self.basic_enter_room(sid, namespace, room, eio_sid=eio_sid) 86 | 87 | async def leave_room(self, sid, namespace, room): 88 | """Remove a client from a room. 89 | 90 | Note: this method is a coroutine. 91 | """ 92 | return self.basic_leave_room(sid, namespace, room) 93 | 94 | async def close_room(self, room, namespace): 95 | """Remove all participants from a room. 96 | 97 | Note: this method is a coroutine. 98 | """ 99 | return self.basic_close_room(room, namespace) 100 | 101 | async def trigger_callback(self, sid, id, data): 102 | """Invoke an application callback. 103 | 104 | Note: this method is a coroutine. 105 | """ 106 | callback = None 107 | try: 108 | callback = self.callbacks[sid][id] 109 | except KeyError: 110 | # if we get an unknown callback we just ignore it 111 | self._get_logger().warning('Unknown callback received, ignoring.') 112 | else: 113 | del self.callbacks[sid][id] 114 | if callback is not None: 115 | ret = callback(*data) 116 | if asyncio.iscoroutine(ret): 117 | try: 118 | await ret 119 | except asyncio.CancelledError: # pragma: no cover 120 | pass 121 | -------------------------------------------------------------------------------- /src/socketio/async_redis_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pickle 3 | 4 | try: # pragma: no cover 5 | from redis import asyncio as aioredis 6 | from redis.exceptions import RedisError 7 | except ImportError: # pragma: no cover 8 | try: 9 | import aioredis 10 | from aioredis.exceptions import RedisError 11 | except ImportError: 12 | aioredis = None 13 | RedisError = None 14 | 15 | from .async_pubsub_manager import AsyncPubSubManager 16 | from .redis_manager import parse_redis_sentinel_url 17 | 18 | 19 | class AsyncRedisManager(AsyncPubSubManager): # pragma: no cover 20 | """Redis based client manager for asyncio servers. 21 | 22 | This class implements a Redis backend for event sharing across multiple 23 | processes. 24 | 25 | To use a Redis backend, initialize the :class:`AsyncServer` instance as 26 | follows:: 27 | 28 | url = 'redis://hostname:port/0' 29 | server = socketio.AsyncServer( 30 | client_manager=socketio.AsyncRedisManager(url)) 31 | 32 | :param url: The connection URL for the Redis server. For a default Redis 33 | store running on the same host, use ``redis://``. To use a 34 | TLS connection, use ``rediss://``. To use Redis Sentinel, use 35 | ``redis+sentinel://`` with a comma-separated list of hosts 36 | and the service name after the db in the URL path. Example: 37 | ``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``. 38 | :param channel: The channel name on which the server sends and receives 39 | notifications. Must be the same in all the servers. 40 | :param write_only: If set to ``True``, only initialize to emit events. The 41 | default of ``False`` initializes the class for emitting 42 | and receiving. 43 | :param redis_options: additional keyword arguments to be passed to 44 | ``Redis.from_url()`` or ``Sentinel()``. 45 | """ 46 | name = 'aioredis' 47 | 48 | def __init__(self, url='redis://localhost:6379/0', channel='socketio', 49 | write_only=False, logger=None, redis_options=None): 50 | if aioredis is None: 51 | raise RuntimeError('Redis package is not installed ' 52 | '(Run "pip install redis" in your virtualenv).') 53 | if not hasattr(aioredis.Redis, 'from_url'): 54 | raise RuntimeError('Version 2 of aioredis package is required.') 55 | self.redis_url = url 56 | self.redis_options = redis_options or {} 57 | self._redis_connect() 58 | super().__init__(channel=channel, write_only=write_only, logger=logger) 59 | 60 | def _redis_connect(self): 61 | if not self.redis_url.startswith('redis+sentinel://'): 62 | self.redis = aioredis.Redis.from_url(self.redis_url, 63 | **self.redis_options) 64 | else: 65 | sentinels, service_name, connection_kwargs = \ 66 | parse_redis_sentinel_url(self.redis_url) 67 | kwargs = self.redis_options 68 | kwargs.update(connection_kwargs) 69 | sentinel = aioredis.sentinel.Sentinel(sentinels, **kwargs) 70 | self.redis = sentinel.master_for(service_name or self.channel) 71 | self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) 72 | 73 | async def _publish(self, data): 74 | retry = True 75 | while True: 76 | try: 77 | if not retry: 78 | self._redis_connect() 79 | return await self.redis.publish( 80 | self.channel, pickle.dumps(data)) 81 | except RedisError: 82 | if retry: 83 | self._get_logger().error('Cannot publish to redis... ' 84 | 'retrying') 85 | retry = False 86 | else: 87 | self._get_logger().error('Cannot publish to redis... ' 88 | 'giving up') 89 | break 90 | 91 | async def _redis_listen_with_retries(self): 92 | retry_sleep = 1 93 | connect = False 94 | while True: 95 | try: 96 | if connect: 97 | self._redis_connect() 98 | await self.pubsub.subscribe(self.channel) 99 | retry_sleep = 1 100 | async for message in self.pubsub.listen(): 101 | yield message 102 | except RedisError: 103 | self._get_logger().error('Cannot receive from redis... ' 104 | 'retrying in ' 105 | '{} secs'.format(retry_sleep)) 106 | connect = True 107 | await asyncio.sleep(retry_sleep) 108 | retry_sleep *= 2 109 | if retry_sleep > 60: 110 | retry_sleep = 60 111 | 112 | async def _listen(self): 113 | channel = self.channel.encode('utf-8') 114 | await self.pubsub.subscribe(self.channel) 115 | async for message in self._redis_listen_with_retries(): 116 | if message['channel'] == channel and \ 117 | message['type'] == 'message' and 'data' in message: 118 | yield message['data'] 119 | await self.pubsub.unsubscribe(self.channel) 120 | -------------------------------------------------------------------------------- /src/socketio/base_manager.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import logging 3 | 4 | from bidict import bidict, ValueDuplicationError 5 | 6 | default_logger = logging.getLogger('socketio') 7 | 8 | 9 | class BaseManager: 10 | def __init__(self): 11 | self.logger = None 12 | self.server = None 13 | self.rooms = {} # self.rooms[namespace][room][sio_sid] = eio_sid 14 | self.eio_to_sid = {} 15 | self.callbacks = {} 16 | self.pending_disconnect = {} 17 | 18 | def set_server(self, server): 19 | self.server = server 20 | 21 | def initialize(self): 22 | """Invoked before the first request is received. Subclasses can add 23 | their initialization code here. 24 | """ 25 | pass 26 | 27 | def get_namespaces(self): 28 | """Return an iterable with the active namespace names.""" 29 | return self.rooms.keys() 30 | 31 | def get_participants(self, namespace, room): 32 | """Return an iterable with the active participants in a room.""" 33 | ns = self.rooms.get(namespace, {}) 34 | if hasattr(room, '__len__') and not isinstance(room, str): 35 | participants = ns[room[0]]._fwdm.copy() if room[0] in ns else {} 36 | for r in room[1:]: 37 | participants.update(ns[r]._fwdm if r in ns else {}) 38 | else: 39 | participants = ns[room]._fwdm.copy() if room in ns else {} 40 | yield from participants.items() 41 | 42 | def connect(self, eio_sid, namespace): 43 | """Register a client connection to a namespace.""" 44 | sid = self.server.eio.generate_id() 45 | try: 46 | self.basic_enter_room(sid, namespace, None, eio_sid=eio_sid) 47 | except ValueDuplicationError: 48 | # already connected 49 | return None 50 | self.basic_enter_room(sid, namespace, sid, eio_sid=eio_sid) 51 | return sid 52 | 53 | def is_connected(self, sid, namespace): 54 | if namespace in self.pending_disconnect and \ 55 | sid in self.pending_disconnect[namespace]: 56 | # the client is in the process of being disconnected 57 | return False 58 | try: 59 | return self.rooms[namespace][None][sid] is not None 60 | except KeyError: 61 | pass 62 | return False 63 | 64 | def sid_from_eio_sid(self, eio_sid, namespace): 65 | try: 66 | return self.rooms[namespace][None]._invm[eio_sid] 67 | except KeyError: 68 | pass 69 | 70 | def eio_sid_from_sid(self, sid, namespace): 71 | if namespace in self.rooms: 72 | return self.rooms[namespace][None].get(sid) 73 | 74 | def pre_disconnect(self, sid, namespace): 75 | """Put the client in the to-be-disconnected list. 76 | 77 | This allows the client data structures to be present while the 78 | disconnect handler is invoked, but still recognize the fact that the 79 | client is soon going away. 80 | """ 81 | if namespace not in self.pending_disconnect: 82 | self.pending_disconnect[namespace] = [] 83 | self.pending_disconnect[namespace].append(sid) 84 | return self.rooms[namespace][None].get(sid) 85 | 86 | def basic_disconnect(self, sid, namespace, **kwargs): 87 | if namespace not in self.rooms: 88 | return 89 | rooms = [] 90 | for room_name, room in self.rooms[namespace].copy().items(): 91 | if sid in room: 92 | rooms.append(room_name) 93 | for room in rooms: 94 | self.basic_leave_room(sid, namespace, room) 95 | if sid in self.callbacks: 96 | del self.callbacks[sid] 97 | if namespace in self.pending_disconnect and \ 98 | sid in self.pending_disconnect[namespace]: 99 | self.pending_disconnect[namespace].remove(sid) 100 | if len(self.pending_disconnect[namespace]) == 0: 101 | del self.pending_disconnect[namespace] 102 | 103 | def basic_enter_room(self, sid, namespace, room, eio_sid=None): 104 | if eio_sid is None and namespace not in self.rooms: 105 | raise ValueError('sid is not connected to requested namespace') 106 | if namespace not in self.rooms: 107 | self.rooms[namespace] = {} 108 | if room not in self.rooms[namespace]: 109 | self.rooms[namespace][room] = bidict() 110 | if eio_sid is None: 111 | eio_sid = self.rooms[namespace][None][sid] 112 | self.rooms[namespace][room][sid] = eio_sid 113 | 114 | def basic_leave_room(self, sid, namespace, room): 115 | try: 116 | del self.rooms[namespace][room][sid] 117 | if len(self.rooms[namespace][room]) == 0: 118 | del self.rooms[namespace][room] 119 | if len(self.rooms[namespace]) == 0: 120 | del self.rooms[namespace] 121 | except KeyError: 122 | pass 123 | 124 | def basic_close_room(self, room, namespace): 125 | try: 126 | for sid, _ in self.get_participants(namespace, room): 127 | self.basic_leave_room(sid, namespace, room) 128 | except KeyError: # pragma: no cover 129 | pass 130 | 131 | def get_rooms(self, sid, namespace): 132 | """Return the rooms a client is in.""" 133 | r = [] 134 | try: 135 | for room_name, room in self.rooms[namespace].items(): 136 | if room_name is not None and sid in room: 137 | r.append(room_name) 138 | except KeyError: 139 | pass 140 | return r 141 | 142 | def _generate_ack_id(self, sid, callback): 143 | """Generate a unique identifier for an ACK packet.""" 144 | if sid not in self.callbacks: 145 | self.callbacks[sid] = {0: itertools.count(1)} 146 | id = next(self.callbacks[sid][0]) 147 | self.callbacks[sid][id] = callback 148 | return id 149 | 150 | def _get_logger(self): 151 | """Get the appropriate logger 152 | 153 | Prevents uninitialized servers in write-only mode from failing. 154 | """ 155 | 156 | if self.logger: 157 | return self.logger 158 | elif self.server: 159 | return self.server.logger 160 | else: 161 | return default_logger 162 | -------------------------------------------------------------------------------- /src/socketio/base_namespace.py: -------------------------------------------------------------------------------- 1 | class BaseNamespace: 2 | def __init__(self, namespace=None): 3 | self.namespace = namespace or '/' 4 | 5 | def is_asyncio_based(self): 6 | return False 7 | 8 | 9 | class BaseServerNamespace(BaseNamespace): 10 | def __init__(self, namespace=None): 11 | super().__init__(namespace=namespace) 12 | self.server = None 13 | 14 | def _set_server(self, server): 15 | self.server = server 16 | 17 | def rooms(self, sid, namespace=None): 18 | """Return the rooms a client is in. 19 | 20 | The only difference with the :func:`socketio.Server.rooms` method is 21 | that when the ``namespace`` argument is not given the namespace 22 | associated with the class is used. 23 | """ 24 | return self.server.rooms(sid, namespace=namespace or self.namespace) 25 | 26 | 27 | class BaseClientNamespace(BaseNamespace): 28 | def __init__(self, namespace=None): 29 | super().__init__(namespace=namespace) 30 | self.client = None 31 | 32 | def _set_client(self, client): 33 | self.client = client 34 | -------------------------------------------------------------------------------- /src/socketio/exceptions.py: -------------------------------------------------------------------------------- 1 | class SocketIOError(Exception): 2 | pass 3 | 4 | 5 | class ConnectionError(SocketIOError): 6 | pass 7 | 8 | 9 | class ConnectionRefusedError(ConnectionError): 10 | """Connection refused exception. 11 | 12 | This exception can be raised from a connect handler when the connection 13 | is not accepted. The positional arguments provided with the exception are 14 | returned with the error packet to the client. 15 | """ 16 | def __init__(self, *args): 17 | if len(args) == 0: 18 | self.error_args = {'message': 'Connection rejected by server'} 19 | elif len(args) == 1: 20 | self.error_args = {'message': str(args[0])} 21 | else: 22 | self.error_args = {'message': str(args[0])} 23 | if len(args) == 2: 24 | self.error_args['data'] = args[1] 25 | else: 26 | self.error_args['data'] = args[1:] 27 | 28 | 29 | class TimeoutError(SocketIOError): 30 | pass 31 | 32 | 33 | class BadNamespaceError(SocketIOError): 34 | pass 35 | 36 | 37 | class DisconnectedError(SocketIOError): 38 | pass 39 | -------------------------------------------------------------------------------- /src/socketio/kafka_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pickle 3 | 4 | try: 5 | import kafka 6 | except ImportError: 7 | kafka = None 8 | 9 | from .pubsub_manager import PubSubManager 10 | 11 | logger = logging.getLogger('socketio') 12 | 13 | 14 | class KafkaManager(PubSubManager): # pragma: no cover 15 | """Kafka based client manager. 16 | 17 | This class implements a Kafka backend for event sharing across multiple 18 | processes. 19 | 20 | To use a Kafka backend, initialize the :class:`Server` instance as 21 | follows:: 22 | 23 | url = 'kafka://hostname:port' 24 | server = socketio.Server(client_manager=socketio.KafkaManager(url)) 25 | 26 | :param url: The connection URL for the Kafka server. For a default Kafka 27 | store running on the same host, use ``kafka://``. For a highly 28 | available deployment of Kafka, pass a list with all the 29 | connection URLs available in your cluster. 30 | :param channel: The channel name (topic) on which the server sends and 31 | receives notifications. Must be the same in all the 32 | servers. 33 | :param write_only: If set to ``True``, only initialize to emit events. The 34 | default of ``False`` initializes the class for emitting 35 | and receiving. 36 | """ 37 | name = 'kafka' 38 | 39 | def __init__(self, url='kafka://localhost:9092', channel='socketio', 40 | write_only=False): 41 | if kafka is None: 42 | raise RuntimeError('kafka-python package is not installed ' 43 | '(Run "pip install kafka-python" in your ' 44 | 'virtualenv).') 45 | 46 | super().__init__(channel=channel, write_only=write_only) 47 | 48 | urls = [url] if isinstance(url, str) else url 49 | self.kafka_urls = [url[8:] if url != 'kafka://' else 'localhost:9092' 50 | for url in urls] 51 | self.producer = kafka.KafkaProducer(bootstrap_servers=self.kafka_urls) 52 | self.consumer = kafka.KafkaConsumer(self.channel, 53 | bootstrap_servers=self.kafka_urls) 54 | 55 | def _publish(self, data): 56 | self.producer.send(self.channel, value=pickle.dumps(data)) 57 | self.producer.flush() 58 | 59 | def _kafka_listen(self): 60 | yield from self.consumer 61 | 62 | def _listen(self): 63 | for message in self._kafka_listen(): 64 | if message.topic == self.channel: 65 | yield pickle.loads(message.value) 66 | -------------------------------------------------------------------------------- /src/socketio/kombu_manager.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import time 3 | import uuid 4 | 5 | try: 6 | import kombu 7 | except ImportError: 8 | kombu = None 9 | 10 | from .pubsub_manager import PubSubManager 11 | 12 | 13 | class KombuManager(PubSubManager): # pragma: no cover 14 | """Client manager that uses kombu for inter-process messaging. 15 | 16 | This class implements a client manager backend for event sharing across 17 | multiple processes, using RabbitMQ, Redis or any other messaging mechanism 18 | supported by `kombu `_. 19 | 20 | To use a kombu backend, initialize the :class:`Server` instance as 21 | follows:: 22 | 23 | url = 'amqp://user:password@hostname:port//' 24 | server = socketio.Server(client_manager=socketio.KombuManager(url)) 25 | 26 | :param url: The connection URL for the backend messaging queue. Example 27 | connection URLs are ``'amqp://guest:guest@localhost:5672//'`` 28 | and ``'redis://localhost:6379/'`` for RabbitMQ and Redis 29 | respectively. Consult the `kombu documentation 30 | `_ for more on how to construct 32 | connection URLs. 33 | :param channel: The channel name on which the server sends and receives 34 | notifications. Must be the same in all the servers. 35 | :param write_only: If set to ``True``, only initialize to emit events. The 36 | default of ``False`` initializes the class for emitting 37 | and receiving. 38 | :param connection_options: additional keyword arguments to be passed to 39 | ``kombu.Connection()``. 40 | :param exchange_options: additional keyword arguments to be passed to 41 | ``kombu.Exchange()``. 42 | :param queue_options: additional keyword arguments to be passed to 43 | ``kombu.Queue()``. 44 | :param producer_options: additional keyword arguments to be passed to 45 | ``kombu.Producer()``. 46 | """ 47 | name = 'kombu' 48 | 49 | def __init__(self, url='amqp://guest:guest@localhost:5672//', 50 | channel='socketio', write_only=False, logger=None, 51 | connection_options=None, exchange_options=None, 52 | queue_options=None, producer_options=None): 53 | if kombu is None: 54 | raise RuntimeError('Kombu package is not installed ' 55 | '(Run "pip install kombu" in your ' 56 | 'virtualenv).') 57 | super().__init__(channel=channel, write_only=write_only, logger=logger) 58 | self.url = url 59 | self.connection_options = connection_options or {} 60 | self.exchange_options = exchange_options or {} 61 | self.queue_options = queue_options or {} 62 | self.producer_options = producer_options or {} 63 | self.publisher_connection = self._connection() 64 | 65 | def initialize(self): 66 | super().initialize() 67 | 68 | monkey_patched = True 69 | if self.server.async_mode == 'eventlet': 70 | from eventlet.patcher import is_monkey_patched 71 | monkey_patched = is_monkey_patched('socket') 72 | elif 'gevent' in self.server.async_mode: 73 | from gevent.monkey import is_module_patched 74 | monkey_patched = is_module_patched('socket') 75 | if not monkey_patched: 76 | raise RuntimeError( 77 | 'Kombu requires a monkey patched socket library to work ' 78 | 'with ' + self.server.async_mode) 79 | 80 | def _connection(self): 81 | return kombu.Connection(self.url, **self.connection_options) 82 | 83 | def _exchange(self): 84 | options = {'type': 'fanout', 'durable': False} 85 | options.update(self.exchange_options) 86 | return kombu.Exchange(self.channel, **options) 87 | 88 | def _queue(self): 89 | queue_name = 'python-socketio.' + str(uuid.uuid4()) 90 | options = {'durable': False, 'queue_arguments': {'x-expires': 300000}} 91 | options.update(self.queue_options) 92 | return kombu.Queue(queue_name, self._exchange(), **options) 93 | 94 | def _producer_publish(self, connection): 95 | producer = connection.Producer(exchange=self._exchange(), 96 | **self.producer_options) 97 | return connection.ensure(producer, producer.publish) 98 | 99 | def _publish(self, data): 100 | retry = True 101 | while True: 102 | try: 103 | producer_publish = self._producer_publish( 104 | self.publisher_connection) 105 | producer_publish(pickle.dumps(data)) 106 | break 107 | except (OSError, kombu.exceptions.KombuError): 108 | if retry: 109 | self._get_logger().error('Cannot publish to rabbitmq... ' 110 | 'retrying') 111 | retry = False 112 | else: 113 | self._get_logger().error( 114 | 'Cannot publish to rabbitmq... giving up') 115 | break 116 | 117 | def _listen(self): 118 | reader_queue = self._queue() 119 | retry_sleep = 1 120 | while True: 121 | try: 122 | with self._connection() as connection: 123 | with connection.SimpleQueue(reader_queue) as queue: 124 | while True: 125 | message = queue.get(block=True) 126 | message.ack() 127 | yield message.payload 128 | retry_sleep = 1 129 | except (OSError, kombu.exceptions.KombuError): 130 | self._get_logger().error( 131 | 'Cannot receive from rabbitmq... ' 132 | 'retrying in {} secs'.format(retry_sleep)) 133 | time.sleep(retry_sleep) 134 | retry_sleep = min(retry_sleep * 2, 60) 135 | -------------------------------------------------------------------------------- /src/socketio/manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from engineio import packet as eio_packet 4 | from . import base_manager 5 | from . import packet 6 | 7 | default_logger = logging.getLogger('socketio') 8 | 9 | 10 | class Manager(base_manager.BaseManager): 11 | """Manage client connections. 12 | 13 | This class keeps track of all the clients and the rooms they are in, to 14 | support the broadcasting of messages. The data used by this class is 15 | stored in a memory structure, making it appropriate only for single process 16 | services. More sophisticated storage backends can be implemented by 17 | subclasses. 18 | """ 19 | def can_disconnect(self, sid, namespace): 20 | return self.is_connected(sid, namespace) 21 | 22 | def emit(self, event, data, namespace, room=None, skip_sid=None, 23 | callback=None, to=None, **kwargs): 24 | """Emit a message to a single client, a room, or all the clients 25 | connected to the namespace.""" 26 | room = to or room 27 | if namespace not in self.rooms: 28 | return 29 | if isinstance(data, tuple): 30 | # tuples are expanded to multiple arguments, everything else is 31 | # sent as a single argument 32 | data = list(data) 33 | elif data is not None: 34 | data = [data] 35 | else: 36 | data = [] 37 | if not isinstance(skip_sid, list): 38 | skip_sid = [skip_sid] 39 | if not callback: 40 | # when callbacks aren't used the packets sent to each recipient are 41 | # identical, so they can be generated once and reused 42 | pkt = self.server.packet_class( 43 | packet.EVENT, namespace=namespace, data=[event] + data) 44 | encoded_packet = pkt.encode() 45 | if not isinstance(encoded_packet, list): 46 | encoded_packet = [encoded_packet] 47 | eio_pkt = [eio_packet.Packet(eio_packet.MESSAGE, p) 48 | for p in encoded_packet] 49 | for sid, eio_sid in self.get_participants(namespace, room): 50 | if sid not in skip_sid: 51 | for p in eio_pkt: 52 | self.server._send_eio_packet(eio_sid, p) 53 | else: 54 | # callbacks are used, so each recipient must be sent a packet that 55 | # contains a unique callback id 56 | # note that callbacks when addressing a group of people are 57 | # implemented but not tested or supported 58 | for sid, eio_sid in self.get_participants(namespace, room): 59 | if sid not in skip_sid: # pragma: no branch 60 | id = self._generate_ack_id(sid, callback) 61 | pkt = self.server.packet_class( 62 | packet.EVENT, namespace=namespace, data=[event] + data, 63 | id=id) 64 | self.server._send_packet(eio_sid, pkt) 65 | 66 | def disconnect(self, sid, namespace, **kwargs): 67 | """Register a client disconnect from a namespace.""" 68 | return self.basic_disconnect(sid, namespace) 69 | 70 | def enter_room(self, sid, namespace, room, eio_sid=None): 71 | """Add a client to a room.""" 72 | return self.basic_enter_room(sid, namespace, room, eio_sid=eio_sid) 73 | 74 | def leave_room(self, sid, namespace, room): 75 | """Remove a client from a room.""" 76 | return self.basic_leave_room(sid, namespace, room) 77 | 78 | def close_room(self, room, namespace): 79 | """Remove all participants from a room.""" 80 | return self.basic_close_room(room, namespace) 81 | 82 | def trigger_callback(self, sid, id, data): 83 | """Invoke an application callback.""" 84 | callback = None 85 | try: 86 | callback = self.callbacks[sid][id] 87 | except KeyError: 88 | # if we get an unknown callback we just ignore it 89 | self._get_logger().warning('Unknown callback received, ignoring.') 90 | else: 91 | del self.callbacks[sid][id] 92 | if callback is not None: 93 | callback(*data) 94 | -------------------------------------------------------------------------------- /src/socketio/middleware.py: -------------------------------------------------------------------------------- 1 | import engineio 2 | 3 | 4 | class WSGIApp(engineio.WSGIApp): 5 | """WSGI middleware for Socket.IO. 6 | 7 | This middleware dispatches traffic to a Socket.IO application. It can also 8 | serve a list of static files to the client, or forward unrelated HTTP 9 | traffic to another WSGI application. 10 | 11 | :param socketio_app: The Socket.IO server. Must be an instance of the 12 | ``socketio.Server`` class. 13 | :param wsgi_app: The WSGI app that receives all other traffic. 14 | :param static_files: A dictionary with static file mapping rules. See the 15 | documentation for details on this argument. 16 | :param socketio_path: The endpoint where the Socket.IO application should 17 | be installed. The default value is appropriate for 18 | most cases. 19 | 20 | Example usage:: 21 | 22 | import socketio 23 | import eventlet 24 | from . import wsgi_app 25 | 26 | sio = socketio.Server() 27 | app = socketio.WSGIApp(sio, wsgi_app) 28 | eventlet.wsgi.server(eventlet.listen(('', 8000)), app) 29 | """ 30 | def __init__(self, socketio_app, wsgi_app=None, static_files=None, 31 | socketio_path='socket.io'): 32 | super().__init__(socketio_app, wsgi_app, static_files=static_files, 33 | engineio_path=socketio_path) 34 | 35 | 36 | class Middleware(WSGIApp): 37 | """This class has been renamed to WSGIApp and is now deprecated.""" 38 | def __init__(self, socketio_app, wsgi_app=None, 39 | socketio_path='socket.io'): 40 | super().__init__(socketio_app, wsgi_app, socketio_path=socketio_path) 41 | -------------------------------------------------------------------------------- /src/socketio/msgpack_packet.py: -------------------------------------------------------------------------------- 1 | import msgpack 2 | from . import packet 3 | 4 | 5 | class MsgPackPacket(packet.Packet): 6 | uses_binary_events = False 7 | 8 | def encode(self): 9 | """Encode the packet for transmission.""" 10 | return msgpack.dumps(self._to_dict()) 11 | 12 | def decode(self, encoded_packet): 13 | """Decode a transmitted package.""" 14 | decoded = msgpack.loads(encoded_packet) 15 | self.packet_type = decoded['type'] 16 | self.data = decoded.get('data') 17 | self.id = decoded.get('id') 18 | self.namespace = decoded['nsp'] 19 | -------------------------------------------------------------------------------- /src/socketio/redis_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pickle 3 | import time 4 | from urllib.parse import urlparse 5 | 6 | try: 7 | import redis 8 | except ImportError: 9 | redis = None 10 | 11 | from .pubsub_manager import PubSubManager 12 | 13 | logger = logging.getLogger('socketio') 14 | 15 | 16 | def parse_redis_sentinel_url(url): 17 | """Parse a Redis Sentinel URL with the format: 18 | redis+sentinel://[:password]@host1:port1,host2:port2,.../db/service_name 19 | """ 20 | parsed_url = urlparse(url) 21 | if parsed_url.scheme != 'redis+sentinel': 22 | raise ValueError('Invalid Redis Sentinel URL') 23 | sentinels = [] 24 | for host_port in parsed_url.netloc.split('@')[-1].split(','): 25 | host, port = host_port.rsplit(':', 1) 26 | sentinels.append((host, int(port))) 27 | kwargs = {} 28 | if parsed_url.username: 29 | kwargs['username'] = parsed_url.username 30 | if parsed_url.password: 31 | kwargs['password'] = parsed_url.password 32 | service_name = None 33 | if parsed_url.path: 34 | parts = parsed_url.path.split('/') 35 | if len(parts) >= 2 and parts[1] != '': 36 | kwargs['db'] = int(parts[1]) 37 | if len(parts) >= 3 and parts[2] != '': 38 | service_name = parts[2] 39 | return sentinels, service_name, kwargs 40 | 41 | 42 | class RedisManager(PubSubManager): # pragma: no cover 43 | """Redis based client manager. 44 | 45 | This class implements a Redis backend for event sharing across multiple 46 | processes. Only kept here as one more example of how to build a custom 47 | backend, since the kombu backend is perfectly adequate to support a Redis 48 | message queue. 49 | 50 | To use a Redis backend, initialize the :class:`Server` instance as 51 | follows:: 52 | 53 | url = 'redis://hostname:port/0' 54 | server = socketio.Server(client_manager=socketio.RedisManager(url)) 55 | 56 | :param url: The connection URL for the Redis server. For a default Redis 57 | store running on the same host, use ``redis://``. To use a 58 | TLS connection, use ``rediss://``. To use Redis Sentinel, use 59 | ``redis+sentinel://`` with a comma-separated list of hosts 60 | and the service name after the db in the URL path. Example: 61 | ``redis+sentinel://user:pw@host1:1234,host2:2345/0/myredis``. 62 | :param channel: The channel name on which the server sends and receives 63 | notifications. Must be the same in all the servers. 64 | :param write_only: If set to ``True``, only initialize to emit events. The 65 | default of ``False`` initializes the class for emitting 66 | and receiving. 67 | :param redis_options: additional keyword arguments to be passed to 68 | ``Redis.from_url()`` or ``Sentinel()``. 69 | """ 70 | name = 'redis' 71 | 72 | def __init__(self, url='redis://localhost:6379/0', channel='socketio', 73 | write_only=False, logger=None, redis_options=None): 74 | if redis is None: 75 | raise RuntimeError('Redis package is not installed ' 76 | '(Run "pip install redis" in your ' 77 | 'virtualenv).') 78 | self.redis_url = url 79 | self.redis_options = redis_options or {} 80 | self._redis_connect() 81 | super().__init__(channel=channel, write_only=write_only, logger=logger) 82 | 83 | def initialize(self): 84 | super().initialize() 85 | 86 | monkey_patched = True 87 | if self.server.async_mode == 'eventlet': 88 | from eventlet.patcher import is_monkey_patched 89 | monkey_patched = is_monkey_patched('socket') 90 | elif 'gevent' in self.server.async_mode: 91 | from gevent.monkey import is_module_patched 92 | monkey_patched = is_module_patched('socket') 93 | if not monkey_patched: 94 | raise RuntimeError( 95 | 'Redis requires a monkey patched socket library to work ' 96 | 'with ' + self.server.async_mode) 97 | 98 | def _redis_connect(self): 99 | if not self.redis_url.startswith('redis+sentinel://'): 100 | self.redis = redis.Redis.from_url(self.redis_url, 101 | **self.redis_options) 102 | else: 103 | sentinels, service_name, connection_kwargs = \ 104 | parse_redis_sentinel_url(self.redis_url) 105 | kwargs = self.redis_options 106 | kwargs.update(connection_kwargs) 107 | sentinel = redis.sentinel.Sentinel(sentinels, **kwargs) 108 | self.redis = sentinel.master_for(service_name or self.channel) 109 | self.pubsub = self.redis.pubsub(ignore_subscribe_messages=True) 110 | 111 | def _publish(self, data): 112 | retry = True 113 | while True: 114 | try: 115 | if not retry: 116 | self._redis_connect() 117 | return self.redis.publish(self.channel, pickle.dumps(data)) 118 | except redis.exceptions.RedisError: 119 | if retry: 120 | logger.error('Cannot publish to redis... retrying') 121 | retry = False 122 | else: 123 | logger.error('Cannot publish to redis... giving up') 124 | break 125 | 126 | def _redis_listen_with_retries(self): 127 | retry_sleep = 1 128 | connect = False 129 | while True: 130 | try: 131 | if connect: 132 | self._redis_connect() 133 | self.pubsub.subscribe(self.channel) 134 | retry_sleep = 1 135 | yield from self.pubsub.listen() 136 | except redis.exceptions.RedisError: 137 | logger.error('Cannot receive from redis... ' 138 | 'retrying in {} secs'.format(retry_sleep)) 139 | connect = True 140 | time.sleep(retry_sleep) 141 | retry_sleep *= 2 142 | if retry_sleep > 60: 143 | retry_sleep = 60 144 | 145 | def _listen(self): 146 | channel = self.channel.encode('utf-8') 147 | self.pubsub.subscribe(self.channel) 148 | for message in self._redis_listen_with_retries(): 149 | if message['channel'] == channel and \ 150 | message['type'] == 'message' and 'data' in message: 151 | yield message['data'] 152 | self.pubsub.unsubscribe(self.channel) 153 | -------------------------------------------------------------------------------- /src/socketio/tornado.py: -------------------------------------------------------------------------------- 1 | try: 2 | from engineio.async_drivers.tornado import get_tornado_handler as \ 3 | get_engineio_handler 4 | except ImportError: # pragma: no cover 5 | get_engineio_handler = None 6 | 7 | 8 | def get_tornado_handler(socketio_server): # pragma: no cover 9 | return get_engineio_handler(socketio_server.eio) 10 | -------------------------------------------------------------------------------- /src/socketio/zmq_manager.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import re 3 | 4 | from .pubsub_manager import PubSubManager 5 | 6 | 7 | class ZmqManager(PubSubManager): # pragma: no cover 8 | """zmq based client manager. 9 | 10 | NOTE: this zmq implementation should be considered experimental at this 11 | time. At this time, eventlet is required to use zmq. 12 | 13 | This class implements a zmq backend for event sharing across multiple 14 | processes. To use a zmq backend, initialize the :class:`Server` instance as 15 | follows:: 16 | 17 | url = 'zmq+tcp://hostname:port1+port2' 18 | server = socketio.Server(client_manager=socketio.ZmqManager(url)) 19 | 20 | :param url: The connection URL for the zmq message broker, 21 | which will need to be provided and running. 22 | :param channel: The channel name on which the server sends and receives 23 | notifications. Must be the same in all the servers. 24 | :param write_only: If set to ``True``, only initialize to emit events. The 25 | default of ``False`` initializes the class for emitting 26 | and receiving. 27 | 28 | A zmq message broker must be running for the zmq_manager to work. 29 | you can write your own or adapt one from the following simple broker 30 | below:: 31 | 32 | import zmq 33 | 34 | receiver = zmq.Context().socket(zmq.PULL) 35 | receiver.bind("tcp://*:5555") 36 | 37 | publisher = zmq.Context().socket(zmq.PUB) 38 | publisher.bind("tcp://*:5556") 39 | 40 | while True: 41 | publisher.send(receiver.recv()) 42 | """ 43 | name = 'zmq' 44 | 45 | def __init__(self, url='zmq+tcp://localhost:5555+5556', 46 | channel='socketio', 47 | write_only=False, 48 | logger=None): 49 | try: 50 | from eventlet.green import zmq 51 | except ImportError: 52 | raise RuntimeError('zmq package is not installed ' 53 | '(Run "pip install pyzmq" in your ' 54 | 'virtualenv).') 55 | 56 | r = re.compile(r':\d+\+\d+$') 57 | if not (url.startswith('zmq+tcp://') and r.search(url)): 58 | raise RuntimeError('unexpected connection string: ' + url) 59 | 60 | url = url.replace('zmq+', '') 61 | (sink_url, sub_port) = url.split('+') 62 | sink_port = sink_url.split(':')[-1] 63 | sub_url = sink_url.replace(sink_port, sub_port) 64 | 65 | sink = zmq.Context().socket(zmq.PUSH) 66 | sink.connect(sink_url) 67 | 68 | sub = zmq.Context().socket(zmq.SUB) 69 | sub.setsockopt_string(zmq.SUBSCRIBE, '') 70 | sub.connect(sub_url) 71 | 72 | self.sink = sink 73 | self.sub = sub 74 | self.channel = channel 75 | super().__init__(channel=channel, write_only=write_only, logger=logger) 76 | 77 | def _publish(self, data): 78 | pickled_data = pickle.dumps( 79 | { 80 | 'type': 'message', 81 | 'channel': self.channel, 82 | 'data': data 83 | } 84 | ) 85 | return self.sink.send(pickled_data) 86 | 87 | def zmq_listen(self): 88 | while True: 89 | response = self.sub.recv() 90 | if response is not None: 91 | yield response 92 | 93 | def _listen(self): 94 | for message in self.zmq_listen(): 95 | if isinstance(message, bytes): 96 | try: 97 | message = pickle.loads(message) 98 | except Exception: 99 | pass 100 | if isinstance(message, dict) and \ 101 | message['type'] == 'message' and \ 102 | message['channel'] == self.channel and \ 103 | 'data' in message: 104 | yield message['data'] 105 | return 106 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/python-socketio/d3a9b82d5816a2f13413af769c05193ecd6d2422/tests/__init__.py -------------------------------------------------------------------------------- /tests/async/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/python-socketio/d3a9b82d5816a2f13413af769c05193ecd6d2422/tests/async/__init__.py -------------------------------------------------------------------------------- /tests/asyncio_web_server.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import threading 3 | import time 4 | import uvicorn 5 | import socketio 6 | 7 | 8 | class SocketIOWebServer: 9 | """A simple web server used for running Socket.IO servers in tests. 10 | 11 | :param sio: a Socket.IO server instance. 12 | 13 | Note 1: This class is not production-ready and is intended for testing. 14 | Note 2: This class only supports the "asgi" async_mode. 15 | """ 16 | def __init__(self, sio, on_shutdown=None): 17 | if sio.async_mode != 'asgi': 18 | raise ValueError('The async_mode must be "asgi"') 19 | 20 | async def http_app(scope, receive, send): 21 | await send({'type': 'http.response.start', 22 | 'status': 200, 23 | 'headers': [('Content-Type', 'text/plain')]}) 24 | await send({'type': 'http.response.body', 25 | 'body': b'OK'}) 26 | 27 | self.sio = sio 28 | self.app = socketio.ASGIApp(sio, http_app, on_shutdown=on_shutdown) 29 | self.httpd = None 30 | self.thread = None 31 | 32 | def start(self, port=8900): 33 | """Start the web server. 34 | 35 | :param port: the port to listen on. Defaults to 8900. 36 | 37 | The server is started in a background thread. 38 | """ 39 | self.httpd = uvicorn.Server(config=uvicorn.Config(self.app, port=port)) 40 | self.thread = threading.Thread(target=self.httpd.run) 41 | self.thread.start() 42 | 43 | # wait for the server to start 44 | while True: 45 | try: 46 | r = requests.get(f'http://localhost:{port}/') 47 | r.raise_for_status() 48 | if r.text == 'OK': 49 | break 50 | except: 51 | time.sleep(0.1) 52 | 53 | def stop(self): 54 | """Stop the web server.""" 55 | self.httpd.should_exit = True 56 | self.thread.join() 57 | self.thread = None 58 | -------------------------------------------------------------------------------- /tests/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelgrinberg/python-socketio/d3a9b82d5816a2f13413af769c05193ecd6d2422/tests/common/__init__.py -------------------------------------------------------------------------------- /tests/common/test_middleware.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from socketio import middleware 4 | 5 | 6 | class TestMiddleware: 7 | def test_wsgi_routing(self): 8 | mock_wsgi_app = mock.MagicMock() 9 | mock_sio_app = 'foo' 10 | m = middleware.Middleware(mock_sio_app, mock_wsgi_app) 11 | environ = {'PATH_INFO': '/foo'} 12 | start_response = "foo" 13 | m(environ, start_response) 14 | mock_wsgi_app.assert_called_once_with(environ, start_response) 15 | 16 | def test_sio_routing(self): 17 | mock_wsgi_app = 'foo' 18 | mock_sio_app = mock.Mock() 19 | mock_sio_app.handle_request = mock.MagicMock() 20 | m = middleware.Middleware(mock_sio_app, mock_wsgi_app) 21 | environ = {'PATH_INFO': '/socket.io/'} 22 | start_response = "foo" 23 | m(environ, start_response) 24 | mock_sio_app.handle_request.assert_called_once_with( 25 | environ, start_response 26 | ) 27 | 28 | def test_404(self): 29 | mock_wsgi_app = None 30 | mock_sio_app = mock.Mock() 31 | m = middleware.Middleware(mock_sio_app, mock_wsgi_app) 32 | environ = {'PATH_INFO': '/foo/bar'} 33 | start_response = mock.MagicMock() 34 | r = m(environ, start_response) 35 | assert r == [b'Not Found'] 36 | start_response.assert_called_once_with( 37 | "404 Not Found", [('Content-Type', 'text/plain')] 38 | ) 39 | -------------------------------------------------------------------------------- /tests/common/test_msgpack_packet.py: -------------------------------------------------------------------------------- 1 | from socketio import msgpack_packet 2 | from socketio import packet 3 | 4 | 5 | class TestMsgPackPacket: 6 | def test_encode_decode(self): 7 | p = msgpack_packet.MsgPackPacket( 8 | packet.CONNECT, data={'auth': {'token': '123'}}, namespace='/foo') 9 | p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) 10 | assert p.packet_type == p2.packet_type 11 | assert p.data == p2.data 12 | assert p.id == p2.id 13 | assert p.namespace == p2.namespace 14 | 15 | def test_encode_decode_with_id(self): 16 | p = msgpack_packet.MsgPackPacket( 17 | packet.EVENT, data=['ev', 42], id=123, namespace='/foo') 18 | p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) 19 | assert p.packet_type == p2.packet_type 20 | assert p.data == p2.data 21 | assert p.id == p2.id 22 | assert p.namespace == p2.namespace 23 | 24 | def test_encode_binary_event_packet(self): 25 | p = msgpack_packet.MsgPackPacket(packet.EVENT, data={'foo': b'bar'}) 26 | assert p.packet_type == packet.EVENT 27 | p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) 28 | assert p2.data == {'foo': b'bar'} 29 | 30 | def test_encode_binary_ack_packet(self): 31 | p = msgpack_packet.MsgPackPacket(packet.ACK, data={'foo': b'bar'}) 32 | assert p.packet_type == packet.ACK 33 | p2 = msgpack_packet.MsgPackPacket(encoded_packet=p.encode()) 34 | assert p2.data == {'foo': b'bar'} 35 | -------------------------------------------------------------------------------- /tests/common/test_redis_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from socketio.redis_manager import parse_redis_sentinel_url 4 | 5 | 6 | class TestPubSubManager: 7 | def test_sentinel_url_parser(self): 8 | with pytest.raises(ValueError): 9 | parse_redis_sentinel_url('redis://localhost:6379/0') 10 | 11 | assert parse_redis_sentinel_url( 12 | 'redis+sentinel://localhost:6379' 13 | ) == ( 14 | [('localhost', 6379)], 15 | None, 16 | {} 17 | ) 18 | assert parse_redis_sentinel_url( 19 | 'redis+sentinel://192.168.0.1:6379,192.168.0.2:6379/' 20 | ) == ( 21 | [('192.168.0.1', 6379), ('192.168.0.2', 6379)], 22 | None, 23 | {} 24 | ) 25 | assert parse_redis_sentinel_url( 26 | 'redis+sentinel://h1:6379,h2:6379/0' 27 | ) == ( 28 | [('h1', 6379), ('h2', 6379)], 29 | None, 30 | {'db': 0} 31 | ) 32 | assert parse_redis_sentinel_url( 33 | 'redis+sentinel://user:password@h1:6379,h2:6379,h1:6380/0/myredis' 34 | ) == ( 35 | [('h1', 6379), ('h2', 6379), ('h1', 6380)], 36 | 'myredis', 37 | {'username': 'user', 'password': 'password', 'db': 0} 38 | ) 39 | -------------------------------------------------------------------------------- /tests/common/test_simple_client.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | import pytest 3 | from socketio import SimpleClient 4 | from socketio.exceptions import SocketIOError, TimeoutError, DisconnectedError 5 | 6 | 7 | class TestSimpleClient: 8 | def test_constructor(self): 9 | client = SimpleClient(1, '2', a='3', b=4) 10 | assert client.client_args == (1, '2') 11 | assert client.client_kwargs == {'a': '3', 'b': 4} 12 | assert client.client is None 13 | assert client.input_buffer == [] 14 | assert not client.connected 15 | 16 | def test_connect(self): 17 | mock_client = mock.MagicMock() 18 | original_client_class = SimpleClient.client_class 19 | SimpleClient.client_class = mock_client 20 | 21 | client = SimpleClient(123, a='b') 22 | client.connect('url', headers='h', auth='a', transports='t', 23 | namespace='n', socketio_path='s', wait_timeout='w') 24 | mock_client.assert_called_once_with(123, a='b') 25 | assert client.client == mock_client() 26 | mock_client().connect.assert_called_once_with( 27 | 'url', headers='h', auth='a', transports='t', 28 | namespaces=['n'], socketio_path='s', wait_timeout='w') 29 | mock_client().event.call_count == 3 30 | mock_client().on.assert_called_once_with('*', namespace='n') 31 | assert client.namespace == 'n' 32 | assert not client.input_event.is_set() 33 | 34 | SimpleClient.client_class = original_client_class 35 | 36 | def test_connect_context_manager(self): 37 | mock_client = mock.MagicMock() 38 | original_client_class = SimpleClient.client_class 39 | SimpleClient.client_class = mock_client 40 | 41 | with SimpleClient(123, a='b') as client: 42 | client.connect('url', headers='h', auth='a', transports='t', 43 | namespace='n', socketio_path='s', 44 | wait_timeout='w') 45 | mock_client.assert_called_once_with(123, a='b') 46 | assert client.client == mock_client() 47 | mock_client().connect.assert_called_once_with( 48 | 'url', headers='h', auth='a', transports='t', 49 | namespaces=['n'], socketio_path='s', wait_timeout='w') 50 | mock_client().event.call_count == 3 51 | mock_client().on.assert_called_once_with('*', namespace='n') 52 | assert client.namespace == 'n' 53 | assert not client.input_event.is_set() 54 | 55 | SimpleClient.client_class = original_client_class 56 | 57 | def test_connect_twice(self): 58 | client = SimpleClient(123, a='b') 59 | client.client = mock.MagicMock() 60 | client.connected = True 61 | 62 | with pytest.raises(RuntimeError): 63 | client.connect('url') 64 | 65 | def test_properties(self): 66 | client = SimpleClient() 67 | client.client = mock.MagicMock(transport='websocket') 68 | client.client.get_sid.return_value = 'sid' 69 | client.connected_event.set() 70 | client.connected = True 71 | 72 | assert client.sid == 'sid' 73 | assert client.transport == 'websocket' 74 | 75 | def test_emit(self): 76 | client = SimpleClient() 77 | client.client = mock.MagicMock() 78 | client.namespace = '/ns' 79 | client.connected_event.set() 80 | client.connected = True 81 | 82 | client.emit('foo', 'bar') 83 | client.client.emit.assert_called_once_with('foo', 'bar', 84 | namespace='/ns') 85 | 86 | def test_emit_disconnected(self): 87 | client = SimpleClient() 88 | client.connected_event.set() 89 | client.connected = False 90 | with pytest.raises(DisconnectedError): 91 | client.emit('foo', 'bar') 92 | 93 | def test_emit_retries(self): 94 | client = SimpleClient() 95 | client.connected_event.set() 96 | client.connected = True 97 | client.client = mock.MagicMock() 98 | client.client.emit.side_effect = [SocketIOError(), None] 99 | 100 | client.emit('foo', 'bar') 101 | client.client.emit.assert_called_with('foo', 'bar', namespace='/') 102 | 103 | def test_call(self): 104 | client = SimpleClient() 105 | client.client = mock.MagicMock() 106 | client.client.call.return_value = 'result' 107 | client.namespace = '/ns' 108 | client.connected_event.set() 109 | client.connected = True 110 | 111 | assert client.call('foo', 'bar') == 'result' 112 | client.client.call.assert_called_once_with('foo', 'bar', 113 | namespace='/ns', timeout=60) 114 | 115 | def test_call_disconnected(self): 116 | client = SimpleClient() 117 | client.connected_event.set() 118 | client.connected = False 119 | with pytest.raises(DisconnectedError): 120 | client.call('foo', 'bar') 121 | 122 | def test_call_retries(self): 123 | client = SimpleClient() 124 | client.connected_event.set() 125 | client.connected = True 126 | client.client = mock.MagicMock() 127 | client.client.call.side_effect = [SocketIOError(), 'result'] 128 | 129 | assert client.call('foo', 'bar') == 'result' 130 | client.client.call.assert_called_with('foo', 'bar', namespace='/', 131 | timeout=60) 132 | 133 | def test_receive_with_input_buffer(self): 134 | client = SimpleClient() 135 | client.input_buffer = ['foo', 'bar'] 136 | assert client.receive() == 'foo' 137 | assert client.receive() == 'bar' 138 | 139 | def test_receive_without_input_buffer(self): 140 | client = SimpleClient() 141 | client.connected_event.set() 142 | client.connected = True 143 | client.input_event = mock.MagicMock() 144 | 145 | def fake_wait(timeout=None): 146 | client.input_buffer = ['foo'] 147 | return True 148 | 149 | client.input_event.wait = fake_wait 150 | assert client.receive() == 'foo' 151 | 152 | def test_receive_with_timeout(self): 153 | client = SimpleClient() 154 | client.connected_event.set() 155 | client.connected = True 156 | with pytest.raises(TimeoutError): 157 | client.receive(timeout=0.01) 158 | 159 | def test_receive_disconnected(self): 160 | client = SimpleClient() 161 | client.connected_event.set() 162 | client.connected = False 163 | with pytest.raises(DisconnectedError): 164 | client.receive() 165 | 166 | def test_disconnect(self): 167 | client = SimpleClient() 168 | mc = mock.MagicMock() 169 | client.client = mc 170 | client.connected = True 171 | client.disconnect() 172 | client.disconnect() 173 | mc.disconnect.assert_called_once_with() 174 | assert client.client is None 175 | -------------------------------------------------------------------------------- /tests/performance/README.md: -------------------------------------------------------------------------------- 1 | Performance 2 | =========== 3 | 4 | This directory contains several scripts and tools to test the performance of 5 | the project. 6 | -------------------------------------------------------------------------------- /tests/performance/binary_packet.py: -------------------------------------------------------------------------------- 1 | import time 2 | from socketio import packet 3 | 4 | 5 | def test(): 6 | p = packet.Packet(packet.EVENT, {'foo': b'bar'}) 7 | start = time.time() 8 | count = 0 9 | while True: 10 | eps = p.encode() 11 | p = packet.Packet(encoded_packet=eps[0]) 12 | for ep in eps[1:]: 13 | p.add_attachment(ep) 14 | count += 1 15 | if time.time() - start >= 5: 16 | break 17 | return count 18 | 19 | 20 | if __name__ == '__main__': 21 | count = test() 22 | print('binary_packet:', count, 'packets processed.') 23 | -------------------------------------------------------------------------------- /tests/performance/json_packet.py: -------------------------------------------------------------------------------- 1 | import time 2 | from socketio import packet 3 | 4 | 5 | def test(): 6 | p = packet.Packet(packet.EVENT, {'foo': 'bar'}) 7 | start = time.time() 8 | count = 0 9 | while True: 10 | p = packet.Packet(encoded_packet=p.encode()) 11 | count += 1 12 | if time.time() - start >= 5: 13 | break 14 | return count 15 | 16 | 17 | if __name__ == '__main__': 18 | count = test() 19 | print('json_packet:', count, 'packets processed.') 20 | -------------------------------------------------------------------------------- /tests/performance/namespace_packet.py: -------------------------------------------------------------------------------- 1 | import time 2 | from socketio import packet 3 | 4 | 5 | def test(): 6 | p = packet.Packet(packet.EVENT, 'hello', namespace='/foo') 7 | start = time.time() 8 | count = 0 9 | while True: 10 | p = packet.Packet(encoded_packet=p.encode()) 11 | count += 1 12 | if time.time() - start >= 5: 13 | break 14 | return count 15 | 16 | 17 | if __name__ == '__main__': 18 | count = test() 19 | print('namespace_packet:', count, 'packets processed.') 20 | -------------------------------------------------------------------------------- /tests/performance/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python text_packet.py 3 | python binary_packet.py 4 | python json_packet.py 5 | python namespace_packet.py 6 | python server_receive.py 7 | python server_send.py 8 | python server_send_broadcast.py 9 | -------------------------------------------------------------------------------- /tests/performance/server_receive.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socketio 3 | 4 | 5 | def test(): 6 | s = socketio.Server(async_handlers=False) 7 | start = time.time() 8 | count = 0 9 | s._handle_eio_connect('123', 'environ') 10 | s._handle_eio_message('123', '0') 11 | while True: 12 | s._handle_eio_message('123', '2["test","hello"]') 13 | count += 1 14 | if time.time() - start >= 5: 15 | break 16 | return count 17 | 18 | 19 | if __name__ == '__main__': 20 | count = test() 21 | print('server_receive:', count, 'packets received.') 22 | -------------------------------------------------------------------------------- /tests/performance/server_send.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socketio 3 | 4 | 5 | class Server(socketio.Server): 6 | def _send_packet(self, eio_sid, pkt): 7 | pass 8 | 9 | def _send_eio_packet(self, eio_sid, eio_pkt): 10 | pass 11 | 12 | 13 | def test(): 14 | s = Server() 15 | start = time.time() 16 | count = 0 17 | s._handle_eio_connect('123', 'environ') 18 | s._handle_eio_message('123', '0') 19 | while True: 20 | s.emit('test', 'hello') 21 | count += 1 22 | if time.time() - start >= 5: 23 | break 24 | return count 25 | 26 | 27 | if __name__ == '__main__': 28 | count = test() 29 | print('server_send:', count, 'packets received.') 30 | -------------------------------------------------------------------------------- /tests/performance/server_send_broadcast.py: -------------------------------------------------------------------------------- 1 | import time 2 | import socketio 3 | 4 | 5 | class Server(socketio.Server): 6 | def _send_packet(self, eio_sid, pkt): 7 | pass 8 | 9 | def _send_eio_packet(self, eio_sid, eio_pkt): 10 | pass 11 | 12 | 13 | def test(): 14 | s = Server() 15 | start = time.time() 16 | count = 0 17 | for i in range(100): 18 | s._handle_eio_connect(str(i), 'environ') 19 | s._handle_eio_message(str(i), '0') 20 | while True: 21 | s.emit('test', 'hello') 22 | count += 1 23 | if time.time() - start >= 5: 24 | break 25 | return count 26 | 27 | 28 | if __name__ == '__main__': 29 | count = test() 30 | print('server_send:', count, 'packets received.') 31 | -------------------------------------------------------------------------------- /tests/performance/text_packet.py: -------------------------------------------------------------------------------- 1 | import time 2 | from socketio import packet 3 | 4 | 5 | def test(): 6 | p = packet.Packet(packet.EVENT, 'hello') 7 | start = time.time() 8 | count = 0 9 | while True: 10 | p = packet.Packet(encoded_packet=p.encode()) 11 | count += 1 12 | if time.time() - start >= 5: 13 | break 14 | return count 15 | 16 | 17 | if __name__ == '__main__': 18 | count = test() 19 | print('text_packet:', count, 'packets processed.') 20 | -------------------------------------------------------------------------------- /tests/web_server.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import time 3 | from socketserver import ThreadingMixIn 4 | from wsgiref.simple_server import make_server, WSGIServer, WSGIRequestHandler 5 | import requests 6 | import socketio 7 | 8 | 9 | class SocketIOWebServer: 10 | """A simple web server used for running Socket.IO servers in tests. 11 | 12 | :param sio: a Socket.IO server instance. 13 | 14 | Note 1: This class is not production-ready and is intended for testing. 15 | Note 2: This class only supports the "threading" async_mode, with WebSocket 16 | support provided by the simple-websocket package. 17 | """ 18 | def __init__(self, sio): 19 | if sio.async_mode != 'threading': 20 | raise ValueError('The async_mode must be "threading"') 21 | 22 | def http_app(environ, start_response): 23 | start_response('200 OK', [('Content-Type', 'text/plain')]) 24 | return [b'OK'] 25 | 26 | self.sio = sio 27 | self.app = socketio.WSGIApp(sio, http_app) 28 | self.httpd = None 29 | self.thread = None 30 | 31 | def start(self, port=8900): 32 | """Start the web server. 33 | 34 | :param port: the port to listen on. Defaults to 8900. 35 | 36 | The server is started in a background thread. 37 | """ 38 | class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): 39 | pass 40 | 41 | class WebSocketRequestHandler(WSGIRequestHandler): 42 | def get_environ(self): 43 | env = super().get_environ() 44 | 45 | # pass the raw socket to the WSGI app so that it can be used 46 | # by WebSocket connections (hack copied from gunicorn) 47 | env['gunicorn.socket'] = self.connection 48 | return env 49 | 50 | self.httpd = make_server('', port, self._app_wrapper, 51 | ThreadingWSGIServer, WebSocketRequestHandler) 52 | self.thread = threading.Thread(target=self.httpd.serve_forever) 53 | self.thread.start() 54 | 55 | # wait for the server to start 56 | while True: 57 | try: 58 | r = requests.get(f'http://localhost:{port}/') 59 | r.raise_for_status() 60 | if r.text == 'OK': 61 | break 62 | except: 63 | time.sleep(0.1) 64 | 65 | def stop(self): 66 | """Stop the web server.""" 67 | self.sio.shutdown() 68 | self.httpd.shutdown() 69 | self.httpd.server_close() 70 | self.thread.join() 71 | self.httpd = None 72 | self.thread = None 73 | 74 | def _app_wrapper(self, environ, start_response): 75 | try: 76 | return self.app(environ, start_response) 77 | except StopIteration: 78 | # end the WebSocket request without sending a response 79 | # (this is a hack that was copied from gunicorn's threaded worker) 80 | start_response('200 OK', []) 81 | return [] 82 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=flake8,py{38,39,310,311,312,313},docs 3 | skip_missing_interpreters=True 4 | 5 | [gh-actions] 6 | python = 7 | 3.8: py38 8 | 3.9: py39 9 | 3.10: py310 10 | 3.11: py311 11 | 3.12: py312 12 | 3.13: py313 13 | pypy-3: pypy3 14 | 15 | [testenv] 16 | commands= 17 | pip install -e . 18 | pytest -p no:logging --timeout=60 --cov=socketio --cov-branch --cov-report=term-missing --cov-report=xml 19 | deps= 20 | simple-websocket 21 | uvicorn 22 | requests 23 | websocket-client 24 | aiohttp 25 | msgpack 26 | pytest 27 | pytest-asyncio 28 | pytest-timeout 29 | pytest-cov 30 | 31 | [testenv:flake8] 32 | deps= 33 | flake8 34 | commands= 35 | flake8 --exclude=".*" --exclude="examples/server/wsgi/django_socketio" --ignore=W503,E402,E722 src/socketio tests examples 36 | 37 | [testenv:docs] 38 | changedir=docs 39 | deps= 40 | sphinx 41 | allowlist_externals= 42 | make 43 | commands= 44 | make html 45 | --------------------------------------------------------------------------------