├── .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 | [](https://github.com/miguelgrinberg/python-socketio/actions) [](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://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 |
63 |
67 |
71 |
75 |
80 |
84 |
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 |
63 |
67 |
71 |
75 |
80 |
84 |
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 |
63 |
67 |
71 |
75 |
80 |
84 |
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 |
63 |
67 |
71 |
75 |
80 |
84 |
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 |
63 |
67 |
71 |
75 |
80 |
84 |
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 |
63 |
67 |
71 |
75 |
80 |
84 |
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 |
--------------------------------------------------------------------------------