├── .gitignore ├── .travis.yml ├── AUTHORS ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bootstrap.py ├── buildout.cfg ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── rules └── source │ └── format ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── handler.rst │ ├── index.rst │ ├── main.rst │ ├── mixins.rst │ ├── namespace.rst │ ├── packet.rst │ ├── server.rst │ ├── server_integration.rst │ ├── sgunicorn.rst │ ├── transports.rst │ └── virtsocket.rst ├── examples ├── cross_origin │ ├── README.rst │ ├── requirements.txt │ ├── sock.py │ ├── static │ │ ├── 0.8 │ │ │ └── socket.io.js │ │ ├── 0.9 │ │ │ └── socket.io.js │ │ ├── app.js │ │ └── index.html │ └── web.py ├── django_chat │ ├── .gitignore │ ├── README.rst │ ├── bootstrap.py │ ├── buildout.cfg │ ├── chat │ │ ├── __init__.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── runserver_socketio.py │ │ ├── models.py │ │ ├── sockets.py │ │ ├── static │ │ │ ├── css │ │ │ │ └── chat.css │ │ │ ├── flashsocket │ │ │ │ └── WebSocketMain.swf │ │ │ └── js │ │ │ │ ├── chat.js │ │ │ │ └── socket.io.js │ │ ├── templates │ │ │ ├── base.html │ │ │ ├── room.html │ │ │ └── rooms.html │ │ ├── urls.py │ │ └── views.py │ └── chatproject │ │ ├── __init__.py │ │ ├── development.py │ │ ├── settings.py │ │ └── urls.py ├── flask_chat │ ├── README.rst │ ├── chat.py │ ├── init_db.py │ ├── requirements.txt │ ├── run.py │ ├── static │ │ ├── css │ │ │ └── chat.css │ │ └── js │ │ │ ├── chat.js │ │ │ ├── jquery.min.js │ │ │ └── socketio │ │ │ ├── WebSocketMain.swf │ │ │ └── socket.io.min.js │ └── templates │ │ ├── base.html │ │ ├── room.html │ │ └── rooms.html ├── json.js ├── live_cpu_graph │ ├── README │ ├── live_cpu_graph │ │ ├── index.html │ │ ├── serve.py │ │ └── static │ │ │ ├── WebSocketMain.swf │ │ │ ├── css │ │ │ └── style.css │ │ │ ├── excanvas.js │ │ │ ├── jquery-1.6.1.min.js │ │ │ ├── jquery.flot.js │ │ │ └── socket.io.js │ └── setup.py ├── pyramid_backbone_redis_chat │ ├── README │ ├── chatter3 │ │ ├── __init__.py │ │ ├── models.py │ │ ├── scripts │ │ │ ├── __init__.py │ │ │ └── populate.py │ │ ├── static │ │ │ ├── WebSocketMain.swf │ │ │ ├── backbone.js │ │ │ ├── chatter.js │ │ │ ├── handlebars.js │ │ │ ├── jquery.js │ │ │ ├── socket.io.js │ │ │ ├── styles.css │ │ │ └── underscore.js │ │ ├── templates │ │ │ └── index.mako │ │ └── views.py │ ├── development.ini │ ├── serve.py │ └── setup.py ├── pyramid_backbone_redis_chat_persistence │ ├── README │ ├── chatter4 │ │ ├── __init__.py │ │ ├── models.py │ │ ├── scripts │ │ │ ├── __init__.py │ │ │ └── populate.py │ │ ├── static │ │ │ ├── backbone.js │ │ │ ├── chatter.js │ │ │ ├── handlebars.js │ │ │ ├── jquery.js │ │ │ ├── socket.io.js │ │ │ ├── styles.css │ │ │ └── underscore.js │ │ ├── templates │ │ │ └── index.mako │ │ └── views.py │ ├── development.ini │ ├── requirements.txt │ ├── serve.py │ └── setup.py ├── simple_chat │ ├── chat.html │ ├── chat.py │ └── static │ │ ├── WebSocketMain.swf │ │ ├── css │ │ └── style.css │ │ ├── jquery-1.6.1.min.js │ │ └── socket.io.js ├── simple_pyramid_chat │ ├── README │ ├── chatter2 │ │ ├── __init__.py │ │ ├── models.py │ │ ├── scripts │ │ │ ├── __init__.py │ │ │ └── populate.py │ │ ├── static │ │ │ ├── WebSocketMain.swf │ │ │ ├── backbone.js │ │ │ ├── chatter.js │ │ │ ├── handlebars.js │ │ │ ├── jquery.js │ │ │ ├── socket.io.js │ │ │ ├── styles.css │ │ │ └── underscore.js │ │ ├── templates │ │ │ └── index.mako │ │ └── views.py │ ├── development.ini │ ├── serve.py │ ├── setup.py │ └── wsgi.py ├── testapp │ ├── development.ini │ ├── serve.py │ ├── setup.py │ └── testapp │ │ ├── __init__.py │ │ ├── models.py │ │ ├── scripts │ │ ├── __init__.py │ │ └── populate.py │ │ ├── static │ │ ├── backbone.js │ │ ├── chatter.js │ │ ├── favicon.ico │ │ ├── handlebars.js │ │ ├── jquery.js │ │ ├── socket.io.js │ │ ├── styles.css │ │ └── underscore.js │ │ ├── templates │ │ └── index.mako │ │ └── views.py └── twitterstream │ ├── README │ ├── setup.py │ └── twitterstream │ ├── index.html │ ├── serve.py │ └── static │ ├── WebSocketMain.swf │ ├── css │ └── style.css │ ├── jquery-1.6.1.min.js │ └── socket.io.js ├── pip-requirements-test.txt ├── pip-requirements.txt ├── setup.py ├── socketio ├── __init__.py ├── defaultjson.py ├── handler.py ├── mixins.py ├── namespace.py ├── packet.py ├── policyserver.py ├── sdjango.py ├── server.py ├── sgunicorn.py ├── transports.py └── virtsocket.py ├── tests ├── __init__.py ├── jstests │ ├── jstests.py │ ├── static │ │ ├── WebSocketMain.swf │ │ ├── qunit.css │ │ ├── qunit.js │ │ └── socket.io.js │ └── tests │ │ └── suite.js ├── test_namespace.py ├── test_packet.py └── test_socket.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | jst.js 3 | prod.ini 4 | data/ 5 | *.swo 6 | *~ 7 | # web assets generated directory 8 | gen/ 9 | .webassets-cache/ 10 | 11 | # Ignore built docs.. 12 | docs/build 13 | 14 | *.py[co] 15 | 16 | # Packages 17 | *.egg 18 | *.egg-info 19 | dist 20 | build 21 | _build 22 | eggs 23 | parts 24 | bin 25 | develop-eggs 26 | .installed.cfg 27 | .mr.developer.cfg 28 | external 29 | 30 | # Installer logs 31 | pip-log.txt 32 | logs 33 | *.log 34 | *.pid 35 | 36 | # Unit test / coverage reports 37 | .coverage 38 | .tox 39 | 40 | # Vim 41 | *.swp 42 | .rope* 43 | 44 | # Tags for git-helpers 45 | tags 46 | 47 | nohup.out 48 | .DS_Store 49 | 50 | .project 51 | .pydevproject 52 | .idea/ 53 | /.cache/ 54 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | sudo: true 8 | 9 | before_install: "sudo apt-get install python-dev libevent-dev" 10 | 11 | # command to install dependencies 12 | #install: pip install nose gevent>=1.1rc5; python setup.py install 13 | install: "pip install -r pip-requirements.txt; pip install -r pip-requirements-test.txt;" 14 | 15 | # command to run tests 16 | #script: nosetests 17 | script: py.test 18 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | This SocketIO server based on Gevent and was written by: 2 | 3 | Jeffrey Gelens 4 | 5 | Current Maintainers: 6 | 7 | Alexandre Bourget 8 | John Anderson 9 | 10 | Contributors: 11 | Denis Bilenko 12 | Sébastien Béal 13 | jpellerin (JP) 14 | Philip Neustrom 15 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | New in 0.3.6 (since 0.3.5-rc3) 2 | ----------------------------- 3 | 4 | * initialize() on Namespace objects, as well as Mixins (and all 5 | descendants) is now called by the framework, with ``self`` as a 6 | parameter. 7 | * xhr-longpoll fixed for compat with gevent 1.0bX 8 | * gevent 1.0 bugfixes 9 | * added a paster server integration , use with: egg:gevent-socketio#paster 10 | * Fixed memory leak 11 | 12 | New in 0.3.5-rc3 (since 0.3.5-rc2) 13 | ----------------------------- 14 | * Compatibility with gevent 1.0 15 | * Configurable json serialization 16 | * Django server support 17 | * Better jsonp support 18 | 19 | New in 0.3.5-rc2 (since 0.3.5-rc1) 20 | ----------------------------- 21 | * We echo the connect packet back to the client now 22 | * namespace on handler is now called 'resource' to match node.js 23 | * Even better flash policy support, well tested in IE 24 | * Memory Leak Fixes 25 | 26 | New in 0.3.5-rc1 (since 0.3.5-beta) 27 | ----------------------------- 28 | * Better test coverage on packets and namespace 29 | * recv_disconnect now works from heartbeats 30 | * ACLs weren't being checked when events fired 31 | * Handle decimals in messages now 32 | * Flash policy server support was improved, now default to port 10843 33 | 34 | New in 0.3.5-beta (since 0.3) 35 | ----------------------------- 36 | 37 | * Official repository now on Github: https://github.com/abourget/gevent-socketio 38 | * Official documentation now on ReadTheDocs: http://readthedocs.org/docs/gevent-socketio/en/latest/ 39 | * Largely improved documentation 40 | * Added tests for packets, namespace and socket 41 | * Added several examples (chatter2, testapp, chatter3, chatter4, cross-origin), fixed up the chat.py example. 42 | * Added the ``initialize()`` method to namespaces. 43 | * Added ``reset_acl()`` on namespaces. 44 | * Renamed ``call_method`` to ``call_method_with_acl`` on namespaces. 45 | * Added a Gunicorn worker (GeventSocketIOWorker) and docs 46 | * Implemented the 'ack' callback functions, on both the Python and Javascript sides (when you emit with a callback function as the last argument in javascript, or with keyword ``callback=function`` in Python). 47 | * Added support for disconnection clean-up (``disconnect()`` on namespaces) 48 | * Added a ``session`` dict to both Socket and Namespace objects (a session is unique to a Socket, but provided on the Namespace for convenience), which can hold any session data. 49 | * Improved the RoomsMixin, changed signature of the "emit_to_room" function. 50 | * Improved the BroadcastMixin, implemented the broadcast_event_not_me() 51 | * Added XHR access control headers on handshake 52 | 53 | The new release is marked as beta, but is still pretty stable and can be used. All the major features are there, but we might want to change some small bits here and there, for clarity or conciseness. 54 | 55 | 56 | New in 0.3 (since previous releases) 57 | ------------------------------------ 58 | 59 | * Implemented the Socket.IO 0.7+ protocol. 60 | * Added namespaces and a way to catch events and dispatch them 61 | * Added documentation 62 | * Works on most web frameworks where very small integration is required 63 | * ..woah, so much has changed.. 64 | 65 | Don't use pyramid_socketio anymore, as most of the concepts have been 66 | moved to gevent-socketio. Instead of using the old django-socketio 67 | package with single function dispatchs, use gevent-socketio directly. 68 | See examples and documentation for integration. 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Noppo (Jeffrey Gelens) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list 8 | of conditions and the following disclaimer. 9 | Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | Neither the name of the Noppo nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without specific 14 | prior written permission. 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include AUTHORS 3 | include CHANGELOG 4 | include MANIFEST.in 5 | include README.rst 6 | recursive-include socketio *py 7 | exclude socketio/.ropeproject/* 8 | exclude socketio/sample_protocol_for_doc.py 9 | recursive-include docs * 10 | recursive-include tests * 11 | include pip-requirements.txt 12 | include pip-requirements-test.txt 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Presentation 2 | ============ 3 | 4 | .. image:: https://secure.travis-ci.org/abourget/gevent-socketio.png?branch=master 5 | 6 | ``gevent-socketio`` is a Python implementation of the Socket.IO 7 | protocol, developed originally for Node.js by LearnBoost and then 8 | ported to other languages. Socket.IO enables real-time web 9 | communications between a browser and a server, using a WebSocket-like 10 | API. One aim of this project is to provide a single ``gevent``-based 11 | API that works across the different WSGI-based web frameworks out 12 | there (Pyramid, Pylons, Flask, web2py, Django, etc...). Only ~3 lines 13 | of code are required to tie-in ``gevent-socketio`` in your framework. 14 | Note: you need to use the ``gevent`` python WSGI server to use 15 | ``gevent-socketio``. 16 | 17 | Community, rise up! 18 | =================== 19 | 20 | ANNOUNCEMENT: This project is in need of a solid maintainer to navigate through the 27+ open Pull Requests, merge what needs to be merged, and continue on with newer developments. @abourget is not putting as much time as he'd like on this project these days. This project has nearly 1000 GitHub Stars.. it's used by major corporations. It's a great project for you to lead. Contact me on Twitter @bourgetalexndre to take more leadership. 21 | 22 | 23 | Technical overview 24 | ================== 25 | 26 | Most of the ``gevent-socketio`` implementation is pure Python. There 27 | is an obvious dependency on ``gevent``, and another on 28 | ``gevent-websocket``. There are integration examples for Pyramid, Flask, 29 | Django and BYOF (bring your own framework!). 30 | 31 | 32 | Documentation and References 33 | ============================ 34 | 35 | You can read the renderered Sphinx docs at: 36 | 37 | * http://readthedocs.org/docs/gevent-socketio/en/latest/ 38 | 39 | Discussion and questions happen on the mailing list: 40 | 41 | * https://groups.google.com/forum/#!forum/gevent-socketio 42 | 43 | or in the Github issue tracking: 44 | 45 | * https://github.com/abourget/gevent-socketio/issues 46 | 47 | You can also contact the maintainer: 48 | 49 | * https://twitter.com/#!/bourgetalexndre 50 | * https://plus.google.com/109333785244622657612 51 | 52 | 53 | Installation 54 | ============ 55 | 56 | You can install with standard Python methods:: 57 | 58 | pip install gevent-socketio 59 | 60 | or from source:: 61 | 62 | git clone git://github.com/abourget/gevent-socketio.git 63 | cd gevent-socketio 64 | python setup.py install 65 | 66 | For development, run instead of ``install``:: 67 | 68 | python setup.py develop 69 | 70 | If you want to do all of that in a virtualenv, run:: 71 | 72 | virtualenv env 73 | . env/bin/activate 74 | python setup.py develop # or install 75 | 76 | To execute all tests, run: 77 | 78 | tox 79 | 80 | To execute all tests for a specific Python version, run something like: 81 | 82 | tox -e py27 83 | 84 | To execute a specific test for a specific Python version, run something like: 85 | 86 | tox -e py27 -- test_packet.py::TestEncodeMessage::test_encode_event 87 | -------------------------------------------------------------------------------- /bootstrap.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Bootstrap a buildout-based project 15 | 16 | Simply run this script in a directory containing a buildout.cfg. 17 | The script accepts buildout command-line options, so you can 18 | use the -c option to specify an alternate configuration file. 19 | """ 20 | 21 | import os 22 | import shutil 23 | import sys 24 | import tempfile 25 | 26 | from optparse import OptionParser 27 | 28 | tmpeggs = tempfile.mkdtemp() 29 | 30 | usage = '''\ 31 | [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] 32 | 33 | Bootstraps a buildout-based project. 34 | 35 | Simply run this script in a directory containing a buildout.cfg, using the 36 | Python that you want bin/buildout to use. 37 | 38 | Note that by using --find-links to point to local resources, you can keep 39 | this script from going over the network. 40 | ''' 41 | 42 | parser = OptionParser(usage=usage) 43 | parser.add_option("-v", "--version", help="use a specific zc.buildout version") 44 | 45 | parser.add_option("-t", "--accept-buildout-test-releases", 46 | dest='accept_buildout_test_releases', 47 | action="store_true", default=False, 48 | help=("Normally, if you do not specify a --version, the " 49 | "bootstrap script and buildout gets the newest " 50 | "*final* versions of zc.buildout and its recipes and " 51 | "extensions for you. If you use this flag, " 52 | "bootstrap and buildout will get the newest releases " 53 | "even if they are alphas or betas.")) 54 | parser.add_option("-c", "--config-file", 55 | help=("Specify the path to the buildout configuration " 56 | "file to be used.")) 57 | parser.add_option("-f", "--find-links", 58 | help=("Specify a URL to search for buildout releases")) 59 | 60 | 61 | options, args = parser.parse_args() 62 | 63 | ###################################################################### 64 | # load/install setuptools 65 | 66 | to_reload = False 67 | try: 68 | import pkg_resources 69 | import setuptools 70 | except ImportError: 71 | ez = {} 72 | 73 | try: 74 | from urllib.request import urlopen 75 | except ImportError: 76 | from urllib2 import urlopen 77 | 78 | # XXX use a more permanent ez_setup.py URL when available. 79 | exec(urlopen('https://bitbucket.org/pypa/setuptools/raw/0.7.2/ez_setup.py' 80 | ).read(), ez) 81 | setup_args = dict(to_dir=tmpeggs, download_delay=0) 82 | ez['use_setuptools'](**setup_args) 83 | 84 | if to_reload: 85 | reload(pkg_resources) 86 | import pkg_resources 87 | # This does not (always?) update the default working set. We will 88 | # do it. 89 | for path in sys.path: 90 | if path not in pkg_resources.working_set.entries: 91 | pkg_resources.working_set.add_entry(path) 92 | 93 | ###################################################################### 94 | # Install buildout 95 | 96 | ws = pkg_resources.working_set 97 | 98 | cmd = [sys.executable, '-c', 99 | 'from setuptools.command.easy_install import main; main()', 100 | '-mZqNxd', tmpeggs] 101 | 102 | find_links = os.environ.get( 103 | 'bootstrap-testing-find-links', 104 | options.find_links or 105 | ('http://downloads.buildout.org/' 106 | if options.accept_buildout_test_releases else None) 107 | ) 108 | if find_links: 109 | cmd.extend(['-f', find_links]) 110 | 111 | setuptools_path = ws.find( 112 | pkg_resources.Requirement.parse('setuptools')).location 113 | 114 | requirement = 'zc.buildout' 115 | version = options.version 116 | if version is None and not options.accept_buildout_test_releases: 117 | # Figure out the most recent final version of zc.buildout. 118 | import setuptools.package_index 119 | _final_parts = '*final-', '*final' 120 | 121 | def _final_version(parsed_version): 122 | for part in parsed_version: 123 | if (part[:1] == '*') and (part not in _final_parts): 124 | return False 125 | return True 126 | index = setuptools.package_index.PackageIndex( 127 | search_path=[setuptools_path]) 128 | if find_links: 129 | index.add_find_links((find_links,)) 130 | req = pkg_resources.Requirement.parse(requirement) 131 | if index.obtain(req) is not None: 132 | best = [] 133 | bestv = None 134 | for dist in index[req.project_name]: 135 | distv = dist.parsed_version 136 | if _final_version(distv): 137 | if bestv is None or distv > bestv: 138 | best = [dist] 139 | bestv = distv 140 | elif distv == bestv: 141 | best.append(dist) 142 | if best: 143 | best.sort() 144 | version = best[-1].version 145 | if version: 146 | requirement = '=='.join((requirement, version)) 147 | cmd.append(requirement) 148 | 149 | import subprocess 150 | if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: 151 | raise Exception( 152 | "Failed to execute command:\n%s", 153 | repr(cmd)[1:-1]) 154 | 155 | ###################################################################### 156 | # Import and run buildout 157 | 158 | ws.add_entry(tmpeggs) 159 | ws.require(requirement) 160 | import zc.buildout.buildout 161 | 162 | if not [a for a in args if '=' not in a]: 163 | args.append('bootstrap') 164 | 165 | # if -c was provided, we push it back into args for buildout' main function 166 | if options.config_file is not None: 167 | args[0:0] = ['-c', options.config_file] 168 | 169 | zc.buildout.buildout.main(args) 170 | shutil.rmtree(tmpeggs) 171 | -------------------------------------------------------------------------------- /buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | develop = . 3 | parts = python 4 | test 5 | newest = false 6 | eggs = gevent 7 | gevent-websocket 8 | gevent-socketio 9 | greenlet 10 | 11 | extensions = mr.developer 12 | auto-checkout = * 13 | sources-dir = external 14 | 15 | [sources] 16 | gevent = git https://github.com/surfly/gevent.git 17 | gevent-websocket = hg https://bitbucket.org/Jeffrey/gevent-websocket 18 | 19 | [python] 20 | recipe = zc.recipe.egg 21 | interpreter = python 22 | eggs = ${buildout:eggs} 23 | 24 | [test] 25 | recipe = zc.recipe.testrunner 26 | eggs = ${buildout:eggs} 27 | pytest 28 | mock 29 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | python-gevent-socketio (0.3.5~rc2) precise; urgency=low 2 | 3 | * New version 4 | 5 | -- Alexandre Bourget Tue, 10 Jul 2012 23:38:22 +0000 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: python-gevent-socketio 2 | Section: python 3 | Priority: extra 4 | Maintainer: Alexandre Bourget 5 | Build-Depends: python-all, python-setuptools, python-gevent, python-greenlet, python-gevent-websocket, debhelper (>= 7) 6 | Standards-Version: 3.9.3 7 | Homepage: http://gevent-socketio.readthedocs.org/en/latest/ 8 | 9 | Package: python-gevent-socketio 10 | Architecture: all 11 | Section: python 12 | Priority: extra 13 | Depends: ${python:Depends} 14 | Homepage: http://gevent-socketio.readthedocs.org/en/latest/ 15 | Description: Python implementation of the Socket.IO server library 16 | Socket.IO is a WebSocket-like abstraction that enables real-time communication 17 | between a browser and a server. gevent-socketio is a Python implementation of 18 | the protocol. 19 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Noppo (Jeffrey Gelens) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this list 8 | of conditions and the following disclaimer. 9 | Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | Neither the name of the Noppo nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without specific 14 | prior written permission. 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | AUTHORS 2 | README.rst 3 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ --buildsystem=python_distutils --with=python2 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/gevent-socketio.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/gevent-socketio.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/gevent-socketio" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/gevent-socketio" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\gevent-socketio.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\gevent-socketio.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/source/handler.rst: -------------------------------------------------------------------------------- 1 | .. _handler_module: 2 | 3 | :mod:`socketio.handler` 4 | ======================= 5 | 6 | This is a lower-level transports handler. It is responsible for calling your WSGI application. 7 | 8 | .. automodule:: socketio.handler 9 | :members: 10 | :undoc-members: 11 | :show-inheritance: 12 | -------------------------------------------------------------------------------- /docs/source/main.rst: -------------------------------------------------------------------------------- 1 | .. _main_module: 2 | 3 | :mod:`socketio` 4 | =============== 5 | 6 | This module holds the main hooking function for your framework of choice. 7 | 8 | Call the `socketio_manage` function from a view in your framework and this 9 | will be the beginning of your Socket.IO journey. 10 | 11 | .. automodule:: socketio 12 | :members: 13 | :undoc-members: 14 | :show-inheritance: 15 | -------------------------------------------------------------------------------- /docs/source/mixins.rst: -------------------------------------------------------------------------------- 1 | .. _mixins_module: 2 | 3 | :mod:`socketio.mixins` 4 | ====================== 5 | 6 | .. automodule:: socketio.mixins 7 | 8 | .. literalinclude:: ../../socketio/mixins.py 9 | :pyobject: BroadcastMixin 10 | 11 | .. literalinclude:: ../../socketio/mixins.py 12 | :pyobject: RoomsMixin 13 | -------------------------------------------------------------------------------- /docs/source/packet.rst: -------------------------------------------------------------------------------- 1 | .. _packet_module: 2 | 3 | :mod:`socketio.packet` 4 | ====================== 5 | 6 | The day to day user doesn't need to use this module directly. 7 | 8 | The packets used internally (that might be exposed if you override the 9 | :meth:`~socketio.namespace.BaseNamespace.process_packet` method of 10 | your Namespace) are dictionaries, and are different from one message 11 | type to another. 12 | 13 | Internal packet types 14 | --------------------- 15 | 16 | Here is a list of message types available in the 17 | Socket.IO protocol: 18 | 19 | The connect packet 20 | ~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | .. code-block:: python 23 | 24 | {"type": "connect", 25 | "qs": "", 26 | "endpoint": "/chat"} 27 | 28 | The ``qs`` parameter is a query string you can add to the io.connect('/chat?a=b'); calls on the client side. 29 | 30 | The **message** packet, equivalent to Socket.IO version 0.6's string message: 31 | 32 | .. code-block:: python 33 | 34 | {"type": "message", 35 | "data": "this is the sent string", 36 | "endpoint": "/chat"} 37 | 38 | {"type": "message", 39 | "data": "some message, but please reply", 40 | "ack": True, 41 | "id": 5, 42 | "endpoint": "/chat"} 43 | 44 | This last message includes a **msg_id**, and asks for an ack, which you can 45 | reply to with ``self.ack()``, so that the client-side callback is fired upon 46 | reception. 47 | 48 | The json packet 49 | ~~~~~~~~~~~~~~~ 50 | 51 | The **json** packet is like a message, with no name (unlike events) but with 52 | structure JSON data attached. It is automatically decoded by gevent-socketio. 53 | 54 | .. code-block:: python 55 | 56 | {"type": "json", 57 | "data": {"this": "is a json object"}, 58 | "endpoint": "/chat"} 59 | 60 | {"type": "json", 61 | "data": {"this": "is a json object", "please": "reply"}, 62 | "ack": True, 63 | "id": 5, 64 | "endpoint": "/chat"} 65 | 66 | The same ``ack`` mechanics also apply for the ``json`` packet. 67 | 68 | The event packet 69 | ~~~~~~~~~~~~~~~~ 70 | 71 | The **event** packet holds a ``name`` and some ``args`` as a list. They are 72 | taken as a list on the browser side (you can ``socket.emit("event", many, 73 | parameters``) in the browser) and passed in as is. 74 | 75 | .. code-block:: python 76 | 77 | {"type": "event", 78 | "endpoint": "/chat", 79 | "name": "my_event", 80 | "args": []} 81 | 82 | {"type": "event", 83 | "endpoint": "/chat", 84 | "name": "my_event", 85 | "ack": True, 86 | "id": 123, 87 | "args": [{"my": "object"}, 2, "mystring"]} 88 | 89 | The same ack semantics apply here as well. 90 | 91 | [INSERT: mark the difference between when YOU create the packet, and when 92 | you receive it, and what you must do with it according to different ack values] 93 | 94 | The heartbeat packet 95 | ~~~~~~~~~~~~~~~~~~~~ 96 | 97 | The **heartbeat** packet just marks the connection as alive for another amount 98 | of time. 99 | 100 | .. code-block:: python 101 | 102 | {"type": "heartbeat", 103 | "endpoint": ""} 104 | 105 | This packet is for the global namespace (or empty namespace). 106 | 107 | Ack mechanics 108 | ------------- 109 | 110 | The client sends a message of the sort: 111 | 112 | .. code-block:: python 113 | 114 | {"type": "message", 115 | "id": 140, 116 | "ack": True, 117 | "endpoint": "/tobi", 118 | "data": ''} 119 | 120 | The 'ack' value is 'true', marking that we want an automatic 'ack' when it 121 | receives the packet. The Node.js version sends the ack itself, without any 122 | server-side code interaction. It dispatches the packet only after sending back 123 | an ack, so the ack isn't really a reply. It's just marking the server received 124 | it, but not if the event/message/json was properly processed. 125 | 126 | The automated reply from such a request is: 127 | 128 | .. code-block:: python 129 | 130 | {"type": "ack", 131 | "ackId": 140, 132 | "endpoint": '', 133 | "args": []} 134 | 135 | Where 'ackId' corresponds to the 'id' of the originating message. Upon 136 | reception of this 'ack' message, the client then looks in an object if there 137 | is a callback function to call associated with this message id (140). If so, 138 | runs it, otherwise, drops the packet. 139 | 140 | There is a second way to ask for an ack, sending a packet like this: 141 | 142 | .. code-block:: python 143 | 144 | {"type": "event", 145 | "id": 1, 146 | "ack": "data", 147 | "endpoint": '', 148 | "name": 'tobi', 149 | "args": []} 150 | 151 | {"type": "json", 152 | "id": 1, 153 | "ack": "data", 154 | "endpoint": '', 155 | "data": {"a": "b"}} 156 | 157 | and the same goes for a 'message' packet, which has the 'ack' equal to 'data'. 158 | When the server receives such a packet, it dispatches the corresponding event 159 | (either the named event specified in an 'event' type packet, or 'message' or 160 | 'json, if the type is so), and *adds* as a parameter, in addition to the 161 | 'args' passed by the event (or 'data' for 'message'/'json'), the ack() function 162 | to call (it encloses the packet 'id' already). Any number of arguments passed 163 | to that 'ack()' function will be passed on to the client-side, and given as 164 | parameter on the client-side function. 165 | 166 | That is the returning 'ack' message, with the data ready to be passed as 167 | arguments to the saved callback on the client side: 168 | 169 | .. code-block:: python 170 | 171 | {"type": "ack", 172 | "ackId": 12, 173 | "endpoint": '', 174 | "args": ['woot', 'wa']} 175 | 176 | To learn more, see the `test_packet.py `_ test cases. It also shows the serialization that happens on the wire. 177 | 178 | 179 | Other module members 180 | -------------------- 181 | 182 | .. automodule:: socketio.packet 183 | :members: 184 | :undoc-members: 185 | :show-inheritance: 186 | -------------------------------------------------------------------------------- /docs/source/server.rst: -------------------------------------------------------------------------------- 1 | .. _server_module: 2 | 3 | :mod:`socketio.server` 4 | ====================== 5 | 6 | This is the component used to hook Gevent and its WSGI server to 7 | the WSGI app to be served, while dispatching any Socket.IO related 8 | activities to the `handler` and the `transports`. 9 | 10 | .. automodule:: socketio.server 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | :special-members: __init__ 15 | -------------------------------------------------------------------------------- /docs/source/server_integration.rst: -------------------------------------------------------------------------------- 1 | .. _server_integration: 2 | 3 | Server integration layers 4 | ========================= 5 | 6 | As gevent-socketio runs on top of Gevent, you need a Gevent-based 7 | server, to yield the control cooperatively to the Greenlets in there. 8 | 9 | gunicorn 10 | -------- 11 | If you have a python file that includes a WSGI application, for gunicorn 12 | integration all you have to do is include the :mod:`socketio.sgunicorn` 13 | 14 | .. code-block:: bash 15 | 16 | gunicorn --worker-class socketio.sgunicorn.GeventSocketIOWorker module:app 17 | 18 | 19 | paster / Pyramid's pserve 20 | ------------------------- 21 | 22 | 23 | Through Gunicorn 24 | ^^^^^^^^^^^^^^^^ 25 | 26 | Gunicorn will handle workers for you and has other features. 27 | 28 | For paster, you just have to define the configuration like this: 29 | 30 | .. code-block:: ini 31 | 32 | [server:main] 33 | use = egg:gunicorn#main 34 | host = 0.0.0.0 35 | port = 6543 36 | workers = 4 37 | worker_class = socketio.sgunicorn.GeventSocketIOWorker 38 | 39 | Directly through gevent 40 | ^^^^^^^^^^^^^^^^^^^^^^^ 41 | 42 | Straight gevent integration is the simplest and has no dependencies. 43 | 44 | In your .ini file: 45 | 46 | .. code-block:: ini 47 | 48 | [server:main] 49 | use = egg:gevent-socketio#paster 50 | host = 0.0.0.0 51 | port = 6543 52 | resource = socket.io 53 | transports = websocket, xhr-polling, xhr-multipart 54 | policy_server = True 55 | policy_listener_host = 0.0.0.0 56 | policy_listener_port = 10843 57 | 58 | ``policy_listener_host`` defaults to ``host``, 59 | ``policy_listener_port`` defaults to ``10843``, ``transports`` 60 | defaults to all transports, ``policy_server`` defaults to ``False`` in 61 | here, ``resource`` defaults to ``socket.io``. 62 | 63 | So you can have a slimmed-down version: 64 | 65 | .. code-block:: ini 66 | 67 | [server:main] 68 | use = egg:gevent-socketio#paster 69 | host = 0.0.0.0 70 | port = 6543 71 | 72 | 73 | 74 | django runserver 75 | ---------------- 76 | You can either define a wsgi app and launch it with gunicorn: 77 | 78 | ``wsgi.py``: 79 | 80 | .. code-block:: python 81 | 82 | import django.core.handlers.wsgi 83 | import os 84 | 85 | os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' 86 | app = django.core.handlers.wsgi.WSGIHandler() 87 | 88 | from commandline: 89 | 90 | .. code-block:: bash 91 | 92 | gunicorn --worker-class socketio.sgunicorn.GeventSocketIOWorker wsgi:app 93 | 94 | 95 | or you can use gevent directly: 96 | 97 | ``run.py`` 98 | 99 | .. code-block:: python 100 | 101 | #!/usr/bin/env python 102 | from gevent import monkey 103 | from socketio.server import SocketIOServer 104 | import django.core.handlers.wsgi 105 | import os 106 | import sys 107 | 108 | monkey.patch_all() 109 | 110 | try: 111 | import settings 112 | except ImportError: 113 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 114 | sys.exit(1) 115 | 116 | PORT = 9000 117 | 118 | os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' 119 | 120 | application = django.core.handlers.wsgi.WSGIHandler() 121 | 122 | sys.path.insert(0, os.path.join(settings.PROJECT_ROOT, "apps")) 123 | 124 | if __name__ == '__main__': 125 | print 'Listening on http://127.0.0.1:%s and on port 10843 (flash policy server)' % PORT 126 | SocketIOServer(('', PORT), application, resource="socket.io").serve_forever() 127 | 128 | 129 | Databases 130 | ========= 131 | 132 | Since gevent is a cooperative concurrency library, no process or 133 | routine or library must block on I/O without yielding control to the 134 | ``gevent`` hub, if you want your application to be fast and efficient. 135 | Making these libraries compatible with such a concurrency model is 136 | often called `greening`, in reference to `Green threads 137 | `_. 138 | 139 | 140 | 141 | You will need `green`_ databases APIs to gevent to work correctly. See: 142 | 143 | * MySQL: 144 | * PyMySQL https://github.com/petehunt/PyMySQL/ 145 | * PostgreSQL: 146 | * psycopg2 http://initd.org/psycopg/docs/advanced.html#index-8 147 | * psycogreen https://bitbucket.org/dvarrazzo/psycogreen/src 148 | 149 | 150 | 151 | Web server front-ends 152 | ===================== 153 | 154 | If your web server does not support websockets, you will not be able 155 | to use this transport, although the other transports may 156 | work. However, this would diminish the value of using real-time 157 | communications. 158 | 159 | The websocket implementation in the different web servers is getting 160 | better every day, but before investing too much too quickly, you might 161 | want to have a look at your web server's status on the subject. 162 | 163 | [INSERT THE STATE OF THE DIFFERENT SERVER IMPLEMENTATIONS SUPPORTING WEBSOCKET 164 | FORWARDING] 165 | 166 | nginx status 167 | ---------------- 168 | 169 | Nginx added the ability to support websockets with version 1.3.13 but it requires a bit of explicit configuration. 170 | 171 | See: http://nginx.org/en/docs/http/websocket.html 172 | 173 | Assuming your config is setup to proxy to your gevent server via something like this: 174 | 175 | .. code-block:: nginx 176 | 177 | location / { 178 | proxy_pass http://127.0.0.1:7000; 179 | proxy_redirect off; 180 | } 181 | 182 | You'll just need to add this additional location section. Note in this example we're using ``/socket.io`` as the entry point (you might have to change it) 183 | 184 | .. code-block:: nginx 185 | 186 | location /socket.io { 187 | proxy_pass http://127.0.0.1:7000/socket.io; 188 | proxy_redirect off; 189 | proxy_http_version 1.1; 190 | proxy_set_header Upgrade $http_upgrade; 191 | proxy_set_header Connection "upgrade"; 192 | } 193 | 194 | Make sure you're running the latest version of Nginx (or atleast >= 1.3.13). Older versions don't support websockets, and the client will have to fallback to long polling. 195 | 196 | Apache 197 | 198 | Using HAProxy to load-balance 199 | 200 | -------------------------------------------------------------------------------- /docs/source/sgunicorn.rst: -------------------------------------------------------------------------------- 1 | .. _gunicorn_module: 2 | 3 | :mod:`socketio.sgunicorn` 4 | ========================= 5 | 6 | .. automodule:: socketio.sgunicorn 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /docs/source/transports.rst: -------------------------------------------------------------------------------- 1 | .. _transports_module: 2 | 3 | :mod:`socketio.transports` 4 | ========================== 5 | 6 | This is largely an internal module, responsible for translating the 7 | different fallback mechanisms to one abstracted Socket, dealing with 8 | payload encoding, multi-message multiplexing and their reverse operation. 9 | 10 | .. automodule:: socketio.transports 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | -------------------------------------------------------------------------------- /docs/source/virtsocket.rst: -------------------------------------------------------------------------------- 1 | .. _virtsocket_module: 2 | 3 | :mod:`socketio.virtsocket` 4 | ========================== 5 | 6 | .. automodule:: socketio.virtsocket 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/cross_origin/README.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | gevent-socketio cross-site example 3 | ================================== 4 | 5 | This example app demonstrates that you can use socket.io (0.9) 6 | connections from hosts other than the origin host with 7 | gevent-socketio. 8 | 9 | To run the example, first install set up your gevent-socketio 10 | development environment, then install the example's requirements 11 | into the same virtualenv by running:: 12 | 13 | pip install -r requirements.txt 14 | 15 | in this directory. Then in two separate shells, start ``web.py`` and 16 | ``sock.py``:: 17 | 18 | python web.py 19 | 20 | Then in shell two:: 21 | 22 | python sock.py 23 | 24 | The two servers run on different ports, simulating a common case where 25 | the main web application is running on one host and the socket.io 26 | server is running on a separate host. 27 | 28 | When both are running, navigate to http://localhost:8080/ and 29 | follow the directions that appear there to see cross-site socket.io 30 | in action. 31 | 32 | socket.io.js 0.8 vs 0.9 33 | ----------------------- 34 | 35 | Note that socket.io.js 0.8 works with gevent-socketio for cross-origin requests 36 | without any special headers in the handshake phase. But socket.io.js 37 | 0.9 makes a change to how it sends the XHR handshake request: it sets 38 | ``withCredentials = true``, which requires that the socket.io server 39 | return an ``Access-Control-Allow-Origin`` header that mentions the 40 | origin server. 41 | -------------------------------------------------------------------------------- /examples/cross_origin/requirements.txt: -------------------------------------------------------------------------------- 1 | bottle>=0.10.6 2 | -------------------------------------------------------------------------------- /examples/cross_origin/sock.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey; monkey.patch_all() 2 | 3 | from bottle import Bottle, request 4 | from socketio import server, namespace, socketio_manage 5 | 6 | app = Bottle() 7 | 8 | class Hello(namespace.BaseNamespace): 9 | 10 | def on_hello(self, data): 11 | print "hello", data 12 | self.emit('greetings', {'from': 'sockets'}) 13 | 14 | 15 | @app.route('/socket.io/') 16 | def socketio(*arg, **kw): 17 | socketio_manage(request.environ, {'': Hello}, request=request) 18 | return "out" 19 | 20 | 21 | if __name__ == '__main__': 22 | server.SocketIOServer( 23 | ('localhost', 9090), app, policy_server=False).serve_forever() 24 | -------------------------------------------------------------------------------- /examples/cross_origin/static/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | 3 | var $sayhi = $('#sayhi'); 4 | var $messages = $('#messages'); 5 | 6 | function message (m) { 7 | $messages.append($('
  • ').html(m)); 8 | } 9 | 10 | var sock = io.connect('http://localhost:9090'); 11 | sock.on('connect', function () { 12 | message('Connected'); 13 | }); 14 | 15 | sock.on('disconnect', function () { 16 | message('Goodbye!'); 17 | }); 18 | 19 | sock.on('greetings', function (data) { 20 | console.log(data); 21 | console.log(arguments); 22 | message('Greetings from ' + data.from); 23 | }); 24 | 25 | $sayhi.click(function (event) { 26 | event.preventDefault(); 27 | sock.emit('hello', {'from': 'the application'}) 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/cross_origin/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | Cross-origin socket.io example 3 | 4 | 9 | 10 | 11 | 12 |

    Hello!

    13 | 14 |

    If cross-origin socket.io connections are working, 15 | messages from socket.io will appear below 16 | when you click this link. 17 |

    18 | 19 |

    Messages

    20 |
      21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/cross_origin/web.py: -------------------------------------------------------------------------------- 1 | import os 2 | from bottle import Bottle, static_file, run 3 | 4 | HERE = os.path.abspath(os.path.dirname(__file__)) 5 | STATIC = os.path.join(HERE, 'static') 6 | 7 | app = Bottle() 8 | 9 | 10 | @app.route('/') 11 | @app.route('/') 12 | def serve(filename='index.html'): 13 | return static_file(filename, root=STATIC) 14 | 15 | 16 | if __name__ == '__main__': 17 | run(app=app, host='localhost', port=8080) 18 | -------------------------------------------------------------------------------- /examples/django_chat/.gitignore: -------------------------------------------------------------------------------- 1 | external 2 | -------------------------------------------------------------------------------- /examples/django_chat/README.rst: -------------------------------------------------------------------------------- 1 | 2 | Installation 3 | ============ 4 | 5 | Steps to setup this example:: 6 | 7 | $ cd examples/django_chat 8 | $ python bootstrap.py 9 | $ ./bin/buildout 10 | $ ./bin/django syncdb 11 | $ ./bin/django runserver_socketio 12 | -------------------------------------------------------------------------------- /examples/django_chat/buildout.cfg: -------------------------------------------------------------------------------- 1 | [buildout] 2 | develop = ../.. 3 | parts = django 4 | 5 | newest = false 6 | versions = versions 7 | eggs = django 8 | gevent 9 | gevent-websocket 10 | gevent-socketio 11 | greenlet 12 | 13 | extensions = mr.developer 14 | auto-checkout = * 15 | sources-dir = external 16 | 17 | [sources] 18 | gevent = hg https://bitbucket.org/denis/gevent 19 | gevent-websocket = hg https://bitbucket.org/Jeffrey/gevent-websocket 20 | 21 | 22 | [versions] 23 | django = 1.4 24 | 25 | [django] 26 | recipe = djangorecipe 27 | settings = development 28 | eggs = ${buildout:eggs} 29 | extra-paths = 30 | project = chatproject 31 | -------------------------------------------------------------------------------- /examples/django_chat/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/django_chat/chat/__init__.py -------------------------------------------------------------------------------- /examples/django_chat/chat/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/django_chat/chat/management/__init__.py -------------------------------------------------------------------------------- /examples/django_chat/chat/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/django_chat/chat/management/commands/__init__.py -------------------------------------------------------------------------------- /examples/django_chat/chat/management/commands/runserver_socketio.py: -------------------------------------------------------------------------------- 1 | 2 | from re import match 3 | from thread import start_new_thread 4 | from time import sleep 5 | from os import getpid, kill, environ 6 | from signal import SIGINT 7 | 8 | from django.conf import settings 9 | from django.core.handlers.wsgi import WSGIHandler 10 | from django.core.management.base import BaseCommand, CommandError 11 | from django.core.management.commands.runserver import naiveip_re, DEFAULT_PORT 12 | from django.utils.autoreload import code_changed, restart_with_reloader 13 | from socketio.server import SocketIOServer 14 | 15 | 16 | RELOAD = False 17 | 18 | def reload_watcher(): 19 | global RELOAD 20 | while True: 21 | RELOAD = code_changed() 22 | if RELOAD: 23 | kill(getpid(), SIGINT) 24 | sleep(1) 25 | 26 | class Command(BaseCommand): 27 | 28 | def handle(self, addrport="", *args, **options): 29 | 30 | if not addrport: 31 | self.addr = '' 32 | self.port = DEFAULT_PORT 33 | else: 34 | m = match(naiveip_re, addrport) 35 | if m is None: 36 | raise CommandError('"%s" is not a valid port number ' 37 | 'or address:port pair.' % addrport) 38 | self.addr, _, _, _, self.port = m.groups() 39 | 40 | # Make the port available here for the path: 41 | # socketio_tags.socketio -> 42 | # socketio_scripts.html -> 43 | # io.Socket JS constructor 44 | # allowing the port to be set as the client-side default there. 45 | environ["DJANGO_SOCKETIO_PORT"] = str(self.port) 46 | 47 | start_new_thread(reload_watcher, ()) 48 | try: 49 | bind = (self.addr, int(self.port)) 50 | print 51 | print "SocketIOServer running on %s:%s" % bind 52 | print 53 | handler = self.get_handler(*args, **options) 54 | server = SocketIOServer(bind, handler, resource="socket.io", policy_server=True) 55 | server.serve_forever() 56 | except KeyboardInterrupt: 57 | if RELOAD: 58 | server.stop() 59 | print "Reloading..." 60 | restart_with_reloader() 61 | else: 62 | raise 63 | 64 | def get_handler(self, *args, **options): 65 | """ 66 | Returns the django.contrib.staticfiles handler. 67 | """ 68 | handler = WSGIHandler() 69 | try: 70 | from django.contrib.staticfiles.handlers import StaticFilesHandler 71 | except ImportError: 72 | return handler 73 | use_static_handler = options.get('use_static_handler', True) 74 | insecure_serving = options.get('insecure_serving', False) 75 | if (settings.DEBUG and use_static_handler or 76 | (use_static_handler and insecure_serving)): 77 | handler = StaticFilesHandler(handler) 78 | return handler 79 | -------------------------------------------------------------------------------- /examples/django_chat/chat/models.py: -------------------------------------------------------------------------------- 1 | 2 | from django.db import models 3 | from django.template.defaultfilters import slugify 4 | 5 | 6 | class ChatRoom(models.Model): 7 | 8 | name = models.CharField(max_length=20) 9 | slug = models.SlugField(blank=True) 10 | 11 | class Meta: 12 | ordering = ("name",) 13 | 14 | def __unicode__(self): 15 | return self.name 16 | 17 | @models.permalink 18 | def get_absolute_url(self): 19 | return ("room", (self.slug,)) 20 | 21 | def save(self, *args, **kwargs): 22 | if not self.slug: 23 | self.slug = slugify(self.name) 24 | super(ChatRoom, self).save(*args, **kwargs) 25 | 26 | class ChatUser(models.Model): 27 | 28 | name = models.CharField(max_length=20) 29 | session = models.CharField(max_length=20) 30 | room = models.ForeignKey("chat.ChatRoom", related_name="users") 31 | 32 | class Meta: 33 | ordering = ("name",) 34 | 35 | def __unicode__(self): 36 | return self.name 37 | -------------------------------------------------------------------------------- /examples/django_chat/chat/sockets.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from socketio.namespace import BaseNamespace 4 | from socketio.mixins import RoomsMixin, BroadcastMixin 5 | from socketio.sdjango import namespace 6 | 7 | @namespace('/chat') 8 | class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin): 9 | nicknames = [] 10 | 11 | def initialize(self): 12 | self.logger = logging.getLogger("socketio.chat") 13 | self.log("Socketio session started") 14 | 15 | def log(self, message): 16 | self.logger.info("[{0}] {1}".format(self.socket.sessid, message)) 17 | 18 | def on_join(self, room): 19 | self.room = room 20 | self.join(room) 21 | return True 22 | 23 | def on_nickname(self, nickname): 24 | self.log('Nickname: {0}'.format(nickname)) 25 | self.nicknames.append(nickname) 26 | self.socket.session['nickname'] = nickname 27 | self.broadcast_event('announcement', '%s has connected' % nickname) 28 | self.broadcast_event('nicknames', self.nicknames) 29 | return True, nickname 30 | 31 | def recv_disconnect(self): 32 | # Remove nickname from the list. 33 | self.log('Disconnected') 34 | nickname = self.socket.session['nickname'] 35 | self.nicknames.remove(nickname) 36 | self.broadcast_event('announcement', '%s has disconnected' % nickname) 37 | self.broadcast_event('nicknames', self.nicknames) 38 | self.disconnect(silent=True) 39 | return True 40 | 41 | def on_user_message(self, msg): 42 | self.log('User message: {0}'.format(msg)) 43 | self.emit_to_room(self.room, 'msg_to_room', 44 | self.socket.session['nickname'], msg) 45 | return True 46 | -------------------------------------------------------------------------------- /examples/django_chat/chat/static/css/chat.css: -------------------------------------------------------------------------------- 1 | #chat, 2 | #nickname, 3 | #messages { 4 | width: 600px; 5 | } 6 | #chat { 7 | position: relative; 8 | border: 1px solid #ccc; 9 | } 10 | #nickname, 11 | #connecting { 12 | position: absolute; 13 | height: 410px; 14 | z-index: 100; 15 | left: 0; 16 | top: 0; 17 | background: #fff; 18 | text-align: center; 19 | width: 600px; 20 | font: 15px Georgia; 21 | color: #666; 22 | display: block; 23 | } 24 | #nickname .wrap, 25 | #connecting .wrap { 26 | padding-top: 150px; 27 | } 28 | #nickname input { 29 | border: 1px solid #ccc; 30 | padding: 10px; 31 | } 32 | #nickname input:focus { 33 | border-color: #999; 34 | outline: 0; 35 | } 36 | #nickname #nickname-err { 37 | color: #8b0000; 38 | font-size: 12px; 39 | visibility: hidden; 40 | } 41 | .connected #connecting { 42 | display: none; 43 | } 44 | .nickname-set #nickname { 45 | display: none; 46 | } 47 | #messages { 48 | height: 380px; 49 | background: #eee; 50 | } 51 | #messages em { 52 | text-shadow: 0 1px 0 #fff; 53 | color: #999; 54 | } 55 | #messages p { 56 | padding: 0; 57 | margin: 0; 58 | font: 12px Helvetica, Arial; 59 | padding: 5px 10px; 60 | } 61 | #messages p b { 62 | display: inline-block; 63 | padding-right: 10px; 64 | } 65 | #messages p:nth-child(even) { 66 | background: #fafafa; 67 | } 68 | #messages #nicknames { 69 | background: #ccc; 70 | padding: 2px 4px 4px; 71 | font: 11px Helvetica; 72 | } 73 | #messages #nicknames span { 74 | color: #000; 75 | } 76 | #messages #nicknames b { 77 | display: inline-block; 78 | color: #fff; 79 | background: #999; 80 | padding: 3px 6px; 81 | margin-right: 5px; 82 | -webkit-border-radius: 10px; 83 | -moz-border-radius: 10px; 84 | border-radius: 10px; 85 | text-shadow: 0 1px 0 #666; 86 | } 87 | #messages #lines { 88 | height: 355px; 89 | overflow: auto; 90 | overflow-x: hidden; 91 | overflow-y: auto; 92 | } 93 | #messages #lines::-webkit-scrollbar { 94 | width: 6px; 95 | height: 6px; 96 | } 97 | #messages #lines::-webkit-scrollbar-button:start:decrement, 98 | #messages #lines ::-webkit-scrollbar-button:end:increment { 99 | display: block; 100 | height: 10px; 101 | } 102 | #messages #lines::-webkit-scrollbar-button:vertical:increment { 103 | background-color: #fff; 104 | } 105 | #messages #lines::-webkit-scrollbar-track-piece { 106 | background-color: #fff; 107 | -webkit-border-radius: 3px; 108 | } 109 | #messages #lines::-webkit-scrollbar-thumb:vertical { 110 | height: 50px; 111 | background-color: #ccc; 112 | -webkit-border-radius: 3px; 113 | } 114 | #messages #lines::-webkit-scrollbar-thumb:horizontal { 115 | width: 50px; 116 | background-color: #fff; 117 | -webkit-border-radius: 3px; 118 | } 119 | #send-message { 120 | background: #fff; 121 | position: relative; 122 | } 123 | #send-message input { 124 | border: none; 125 | height: 30px; 126 | padding: 0 10px; 127 | line-height: 30px; 128 | vertical-align: middle; 129 | width: 580px; 130 | } 131 | #send-message input:focus { 132 | outline: 0; 133 | } 134 | #send-message button { 135 | position: absolute; 136 | top: 5px; 137 | right: 5px; 138 | } 139 | button { 140 | margin: 0; 141 | -webkit-user-select: none; 142 | -moz-user-select: none; 143 | user-select: none; 144 | display: inline-block; 145 | text-decoration: none; 146 | background: #43a1f7; 147 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #377ad0)); 148 | background: -webkit-linear-gradient(top, #43a1f7 0%, #377ad0 100%); 149 | background: -moz-linear-gradient(top, #43a1f7 0%, #377ad0 100%); 150 | background: linear-gradient(top, #43a1f7 0%, #377ad0 100%); 151 | border: 1px solid #2e70c4; 152 | -webkit-border-radius: 16px; 153 | -moz-border-radius: 16px; 154 | border-radius: 16px; 155 | color: #fff; 156 | font-family: "lucida grande", sans-serif; 157 | font-size: 11px; 158 | font-weight: normal; 159 | line-height: 1; 160 | padding: 3px 10px 5px 10px; 161 | text-align: center; 162 | text-shadow: 0 -1px 1px #2d6dc0; 163 | } 164 | button:hover, 165 | button.hover { 166 | background: darker; 167 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #2e70c4)); 168 | background: -webkit-linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 169 | background: -moz-linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 170 | background: linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 171 | border: 1px solid #2e70c4; 172 | cursor: pointer; 173 | text-shadow: 0 -1px 1px #2c6bbb; 174 | } 175 | button:active, 176 | button.active { 177 | background: #2e70c4; 178 | border: 1px solid #2e70c4; 179 | border-bottom: 1px solid #2861aa; 180 | text-shadow: 0 -1px 1px #2b67b5; 181 | } 182 | button:focus, 183 | button.focus { 184 | outline: none; 185 | -webkit-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 186 | -moz-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 187 | box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 188 | } 189 | -------------------------------------------------------------------------------- /examples/django_chat/chat/static/flashsocket/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/django_chat/chat/static/flashsocket/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/django_chat/chat/static/js/chat.js: -------------------------------------------------------------------------------- 1 | // socket.io specific code 2 | var socket = io.connect("/chat"); 3 | 4 | socket.on('connect', function () { 5 | $('#chat').addClass('connected'); 6 | socket.emit('join', window.room); 7 | }); 8 | 9 | socket.on('announcement', function (msg) { 10 | $('#lines').append($('

      ').append($('').text(msg))); 11 | }); 12 | 13 | socket.on('nicknames', function (nicknames) { 14 | $('#nicknames').empty().append($('Online: ')); 15 | for (var i in nicknames) { 16 | $('#nicknames').append($('').text(nicknames[i])); 17 | } 18 | }); 19 | 20 | socket.on('msg_to_room', message); 21 | 22 | socket.on('reconnect', function () { 23 | $('#lines').remove(); 24 | message('System', 'Reconnected to the server'); 25 | }); 26 | 27 | socket.on('reconnecting', function () { 28 | message('System', 'Attempting to re-connect to the server'); 29 | }); 30 | 31 | socket.on('error', function (e) { 32 | message('System', e ? e : 'A unknown error occurred'); 33 | }); 34 | 35 | function message (from, msg) { 36 | $('#lines').append($('

      ').append($('').text(from), msg)); 37 | } 38 | 39 | // DOM manipulation 40 | $(function () { 41 | $('#set-nickname').submit(function (ev) { 42 | socket.emit('nickname', $('#nick').val(), function (set) { 43 | if (set) { 44 | clear(); 45 | return $('#chat').addClass('nickname-set'); 46 | } 47 | $('#nickname-err').css('visibility', 'visible'); 48 | }); 49 | return false; 50 | }); 51 | 52 | $('#send-message').submit(function () { 53 | message('me', $('#message').val()); 54 | socket.emit('user message', $('#message').val()); 55 | clear(); 56 | $('#lines').get(0).scrollTop = 10000000; 57 | return false; 58 | }); 59 | 60 | function clear () { 61 | $('#message').val('').focus(); 62 | }; 63 | }); 64 | -------------------------------------------------------------------------------- /examples/django_chat/chat/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}Chat{% endblock %} 7 | 8 | {% block extra_css %}{% endblock %} 9 | 10 | 17 | {% block extra_js %}{% endblock %} 18 | 19 | 20 | 21 |

      {% block main %}{% endblock %}
      22 | {% block form %}{% endblock %} 23 | 24 | -------------------------------------------------------------------------------- /examples/django_chat/chat/templates/room.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ room }}{% endblock %} 4 | 5 | {% block extra_js %} 6 | 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block main %} 14 |
      15 |
      16 |
      17 |

      Please type in your nickname and press enter.

      18 | 19 |

      Nickname already in use

      20 |
      21 |
      22 |
      23 |
      Connecting to socket.io server
      24 |
      25 |
      26 |
      Online:
      27 |
      28 |
      29 |
      30 | 31 | 32 |
      33 |
      34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /examples/django_chat/chat/templates/rooms.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_js %} 4 | 5 | {% endblock %} 6 | 7 | {% block main %} 8 | {% for room in rooms %} 9 |
      10 |

      {{ room }}

      11 |
        12 | {% for user in room.users.all %} 13 |
      • {{ user }}
      • 14 | {% endfor %} 15 |
      16 |
      17 | {% empty %} 18 |

      There are currently no rooms! Add one below.

      19 | {% endfor %} 20 |
      21 | {% endblock %} 22 | 23 | {% block form %} 24 |
      25 | 26 | 27 | {% csrf_token %} 28 |
      29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /examples/django_chat/chat/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf.urls.defaults import patterns, include, url 3 | import socketio.sdjango 4 | 5 | socketio.sdjango.autodiscover() 6 | 7 | urlpatterns = patterns("chat.views", 8 | url("^socket\.io", include(socketio.sdjango.urls)), 9 | url("^$", "rooms", name="rooms"), 10 | url("^create/$", "create", name="create"), 11 | url("^(?P.*)$", "room", name="room"), 12 | ) 13 | -------------------------------------------------------------------------------- /examples/django_chat/chat/views.py: -------------------------------------------------------------------------------- 1 | from socketio import socketio_manage 2 | 3 | from django.http import HttpResponse 4 | from django.contrib.auth.decorators import user_passes_test 5 | from django.shortcuts import get_object_or_404, render, redirect 6 | 7 | from chat.models import ChatRoom 8 | from chat.sockets import ChatNamespace 9 | 10 | def rooms(request, template="rooms.html"): 11 | """ 12 | Homepage - lists all rooms. 13 | """ 14 | context = {"rooms": ChatRoom.objects.all()} 15 | return render(request, template, context) 16 | 17 | 18 | def room(request, slug, template="room.html"): 19 | """ 20 | Show a room. 21 | """ 22 | context = {"room": get_object_or_404(ChatRoom, slug=slug)} 23 | return render(request, template, context) 24 | 25 | 26 | def create(request): 27 | """ 28 | Handles post from the "Add room" form on the homepage, and 29 | redirects to the new room. 30 | """ 31 | name = request.POST.get("name") 32 | if name: 33 | room, created = ChatRoom.objects.get_or_create(name=name) 34 | return redirect(room) 35 | return redirect(rooms) 36 | -------------------------------------------------------------------------------- /examples/django_chat/chatproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/django_chat/chatproject/__init__.py -------------------------------------------------------------------------------- /examples/django_chat/chatproject/development.py: -------------------------------------------------------------------------------- 1 | from chatproject.settings import * 2 | -------------------------------------------------------------------------------- /examples/django_chat/chatproject/settings.py: -------------------------------------------------------------------------------- 1 | 2 | import os, sys 3 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 4 | if PROJECT_ROOT not in sys.path: 5 | sys.path.insert(0, PROJECT_ROOT) 6 | full_path = lambda *parts: os.path.join(PROJECT_ROOT, *parts) 7 | example_path = full_path("..", "..") 8 | if example_path not in sys.path: 9 | sys.path.append(example_path) 10 | 11 | DEBUG = True 12 | TEMPLATE_DEBUG = DEBUG 13 | 14 | ADMINS = () 15 | MANAGERS = ADMINS 16 | 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', 20 | 'NAME': 'chat.db', 21 | } 22 | } 23 | 24 | SECRET_KEY = 'i_!&$f5@^%y*i_qa$*o&0$3q*1dcv^@_-l2po8-%_$_gwo+i-l' 25 | 26 | TEMPLATE_LOADERS = ( 27 | 'django.template.loaders.filesystem.Loader', 28 | 'django.template.loaders.app_directories.Loader', 29 | ) 30 | 31 | MIDDLEWARE_CLASSES = ( 32 | 'django.middleware.common.CommonMiddleware', 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.middleware.csrf.CsrfViewMiddleware', 35 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 36 | 'django.contrib.messages.middleware.MessageMiddleware', 37 | ) 38 | 39 | STATIC_URL = "/static/" 40 | ROOT_URLCONF = "urls" 41 | TEMPLATE_DIRS = full_path("templates") 42 | LOGIN_URL = "/admin/" 43 | 44 | INSTALLED_APPS = ( 45 | 'django.contrib.admin', 46 | 'django.contrib.auth', 47 | 'django.contrib.contenttypes', 48 | 'django.contrib.sessions', 49 | 'django.contrib.staticfiles', 50 | 'chat', 51 | ) 52 | 53 | LOGGING = { 54 | 'version': 1, 55 | 'disable_existing_loggers': False, 56 | 'formatters': { 57 | 'verbose': { 58 | 'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s' 59 | }, 60 | 'simple': { 61 | 'format': '%(levelname)s %(message)s' 62 | }, 63 | }, 64 | 'handlers': { 65 | 'console':{ 66 | 'level':'DEBUG', 67 | 'class':'logging.StreamHandler', 68 | 'formatter': 'simple' 69 | }, 70 | }, 71 | 'loggers': { 72 | 'django': { 73 | 'handlers':['console'], 74 | 'propagate': True, 75 | 'level':'INFO', 76 | }, 77 | 'socketio': { 78 | 'handlers':['console'], 79 | 'propagate': True, 80 | 'level':'INFO', 81 | }, 82 | }, 83 | } 84 | 85 | -------------------------------------------------------------------------------- /examples/django_chat/chatproject/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf import settings 3 | from django.conf.urls.defaults import patterns, include, url 4 | 5 | from django.contrib import admin 6 | admin.autodiscover() 7 | 8 | urlpatterns = patterns('', 9 | url(r'^admin/', include(admin.site.urls)), 10 | url("", include("chat.urls")), 11 | ) 12 | -------------------------------------------------------------------------------- /examples/flask_chat/README.rst: -------------------------------------------------------------------------------- 1 | Flask Chat 2 | ========== 3 | 4 | Flask implementation of chat from `gevent-socketio `_ examples. 5 | 6 | To Run this example, before you start you need to navigate to this folder and run: 7 | 8 | python init_db.py 9 | 10 | Then to run use: 11 | 12 | python run.py 13 | 14 | Open your browser to: 15 | 16 | localhost:5000 17 | 18 | 19 | And everything should be working now. If you want to reset just stop the server, delete the file: /tmp/chat.db, and re-run init_db.py 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/flask_chat/chat.py: -------------------------------------------------------------------------------- 1 | import re 2 | import unicodedata 3 | from socketio import socketio_manage 4 | from socketio.namespace import BaseNamespace 5 | from socketio.mixins import RoomsMixin, BroadcastMixin 6 | from werkzeug.exceptions import NotFound 7 | from gevent import monkey 8 | 9 | from flask import Flask, Response, request, render_template, url_for, redirect 10 | from flask.ext.sqlalchemy import SQLAlchemy 11 | 12 | monkey.patch_all() 13 | 14 | app = Flask(__name__) 15 | app.debug = True 16 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/chat.db' 17 | db = SQLAlchemy(app) 18 | 19 | 20 | # models 21 | class ChatRoom(db.Model): 22 | __tablename__ = 'chatrooms' 23 | id = db.Column(db.Integer, primary_key=True) 24 | name = db.Column(db.String(20), nullable=False) 25 | slug = db.Column(db.String(50)) 26 | users = db.relationship('ChatUser', backref='chatroom', lazy='dynamic') 27 | 28 | def __unicode__(self): 29 | return self.name 30 | 31 | def get_absolute_url(self): 32 | return url_for('room', slug=self.slug) 33 | 34 | def save(self, *args, **kwargs): 35 | if not self.slug: 36 | self.slug = slugify(self.name) 37 | db.session.add(self) 38 | db.session.commit() 39 | 40 | 41 | class ChatUser(db.Model): 42 | __tablename__ = 'chatusers' 43 | id = db.Column(db.Integer, primary_key=True) 44 | name = db.Column(db.String(20), nullable=False) 45 | session = db.Column(db.String(20), nullable=False) 46 | chatroom_id = db.Column(db.Integer, db.ForeignKey('chatrooms.id')) 47 | 48 | def __unicode__(self): 49 | return self.name 50 | 51 | 52 | # utils 53 | def slugify(value): 54 | value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') 55 | value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) 56 | return re.sub('[-\s]+', '-', value) 57 | 58 | 59 | def get_object_or_404(klass, **query): 60 | instance = klass.query.filter_by(**query).first() 61 | if not instance: 62 | raise NotFound() 63 | return instance 64 | 65 | 66 | def get_or_create(klass, **kwargs): 67 | try: 68 | return get_object_or_404(klass, **kwargs), False 69 | except NotFound: 70 | instance = klass(**kwargs) 71 | instance.save() 72 | return instance, True 73 | 74 | 75 | def init_db(): 76 | db.create_all(app=app) 77 | 78 | 79 | # views 80 | @app.route('/') 81 | def rooms(): 82 | """ 83 | Homepage - lists all rooms. 84 | """ 85 | context = {"rooms": ChatRoom.query.all()} 86 | return render_template('rooms.html', **context) 87 | 88 | 89 | @app.route('/') 90 | def room(slug): 91 | """ 92 | Show a room. 93 | """ 94 | context = {"room": get_object_or_404(ChatRoom, slug=slug)} 95 | return render_template('room.html', **context) 96 | 97 | 98 | @app.route('/create', methods=['POST']) 99 | def create(): 100 | """ 101 | Handles post from the "Add room" form on the homepage, and 102 | redirects to the new room. 103 | """ 104 | name = request.form.get("name") 105 | if name: 106 | room, created = get_or_create(ChatRoom, name=name) 107 | return redirect(url_for('room', slug=room.slug)) 108 | return redirect(url_for('rooms')) 109 | 110 | 111 | class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin): 112 | nicknames = [] 113 | 114 | def initialize(self): 115 | self.logger = app.logger 116 | self.log("Socketio session started") 117 | 118 | def log(self, message): 119 | self.logger.info("[{0}] {1}".format(self.socket.sessid, message)) 120 | 121 | def on_join(self, room): 122 | self.room = room 123 | self.join(room) 124 | return True 125 | 126 | def on_nickname(self, nickname): 127 | self.log('Nickname: {0}'.format(nickname)) 128 | self.nicknames.append(nickname) 129 | self.session['nickname'] = nickname 130 | self.broadcast_event('announcement', '%s has connected' % nickname) 131 | self.broadcast_event('nicknames', self.nicknames) 132 | return True, nickname 133 | 134 | def recv_disconnect(self): 135 | # Remove nickname from the list. 136 | self.log('Disconnected') 137 | nickname = self.session['nickname'] 138 | self.nicknames.remove(nickname) 139 | self.broadcast_event('announcement', '%s has disconnected' % nickname) 140 | self.broadcast_event('nicknames', self.nicknames) 141 | self.disconnect(silent=True) 142 | return True 143 | 144 | def on_user_message(self, msg): 145 | self.log('User message: {0}'.format(msg)) 146 | self.emit_to_room(self.room, 'msg_to_room', 147 | self.session['nickname'], msg) 148 | return True 149 | 150 | 151 | @app.route('/socket.io/') 152 | def socketio(remaining): 153 | try: 154 | socketio_manage(request.environ, {'/chat': ChatNamespace}, request) 155 | except: 156 | app.logger.error("Exception while handling socketio connection", 157 | exc_info=True) 158 | return Response() 159 | 160 | 161 | if __name__ == '__main__': 162 | app.run() 163 | -------------------------------------------------------------------------------- /examples/flask_chat/init_db.py: -------------------------------------------------------------------------------- 1 | import chat 2 | chat.init_db() 3 | 4 | -------------------------------------------------------------------------------- /examples/flask_chat/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==0.9 2 | flask-sqlalchemy==0.7.8 3 | gevent-socketio==0.3.5-rc2 -------------------------------------------------------------------------------- /examples/flask_chat/run.py: -------------------------------------------------------------------------------- 1 | from chat import app 2 | from gevent import monkey 3 | from socketio.server import SocketIOServer 4 | 5 | 6 | monkey.patch_all() 7 | 8 | PORT = 5000 9 | 10 | if __name__ == '__main__': 11 | print 'Listening on http://127.0.0.1:%s and on port 10843 (flash policy server)' % PORT 12 | SocketIOServer(('', PORT), app, resource="socket.io").serve_forever() 13 | -------------------------------------------------------------------------------- /examples/flask_chat/static/css/chat.css: -------------------------------------------------------------------------------- 1 | #chat, 2 | #nickname, 3 | #messages { 4 | width: 600px; 5 | } 6 | #chat { 7 | position: relative; 8 | border: 1px solid #ccc; 9 | } 10 | #nickname, 11 | #connecting { 12 | position: absolute; 13 | height: 410px; 14 | z-index: 100; 15 | left: 0; 16 | top: 0; 17 | background: #fff; 18 | text-align: center; 19 | width: 600px; 20 | font: 15px Georgia; 21 | color: #666; 22 | display: block; 23 | } 24 | #nickname .wrap, 25 | #connecting .wrap { 26 | padding-top: 150px; 27 | } 28 | #nickname input { 29 | border: 1px solid #ccc; 30 | padding: 10px; 31 | } 32 | #nickname input:focus { 33 | border-color: #999; 34 | outline: 0; 35 | } 36 | #nickname #nickname-err { 37 | color: #8b0000; 38 | font-size: 12px; 39 | visibility: hidden; 40 | } 41 | .connected #connecting { 42 | display: none; 43 | } 44 | .nickname-set #nickname { 45 | display: none; 46 | } 47 | #messages { 48 | height: 380px; 49 | background: #eee; 50 | } 51 | #messages em { 52 | text-shadow: 0 1px 0 #fff; 53 | color: #999; 54 | } 55 | #messages p { 56 | padding: 0; 57 | margin: 0; 58 | font: 12px Helvetica, Arial; 59 | padding: 5px 10px; 60 | } 61 | #messages p b { 62 | display: inline-block; 63 | padding-right: 10px; 64 | } 65 | #messages p:nth-child(even) { 66 | background: #fafafa; 67 | } 68 | #messages #nicknames { 69 | background: #ccc; 70 | padding: 2px 4px 4px; 71 | font: 11px Helvetica; 72 | } 73 | #messages #nicknames span { 74 | color: #000; 75 | } 76 | #messages #nicknames b { 77 | display: inline-block; 78 | color: #fff; 79 | background: #999; 80 | padding: 3px 6px; 81 | margin-right: 5px; 82 | -webkit-border-radius: 10px; 83 | -moz-border-radius: 10px; 84 | border-radius: 10px; 85 | text-shadow: 0 1px 0 #666; 86 | } 87 | #messages #lines { 88 | height: 355px; 89 | overflow: auto; 90 | overflow-x: hidden; 91 | overflow-y: auto; 92 | } 93 | #messages #lines::-webkit-scrollbar { 94 | width: 6px; 95 | height: 6px; 96 | } 97 | #messages #lines::-webkit-scrollbar-button:start:decrement, 98 | #messages #lines ::-webkit-scrollbar-button:end:increment { 99 | display: block; 100 | height: 10px; 101 | } 102 | #messages #lines::-webkit-scrollbar-button:vertical:increment { 103 | background-color: #fff; 104 | } 105 | #messages #lines::-webkit-scrollbar-track-piece { 106 | background-color: #fff; 107 | -webkit-border-radius: 3px; 108 | } 109 | #messages #lines::-webkit-scrollbar-thumb:vertical { 110 | height: 50px; 111 | background-color: #ccc; 112 | -webkit-border-radius: 3px; 113 | } 114 | #messages #lines::-webkit-scrollbar-thumb:horizontal { 115 | width: 50px; 116 | background-color: #fff; 117 | -webkit-border-radius: 3px; 118 | } 119 | #send-message { 120 | background: #fff; 121 | position: relative; 122 | } 123 | #send-message input { 124 | border: none; 125 | height: 30px; 126 | padding: 0 10px; 127 | line-height: 30px; 128 | vertical-align: middle; 129 | width: 580px; 130 | } 131 | #send-message input:focus { 132 | outline: 0; 133 | } 134 | #send-message button { 135 | position: absolute; 136 | top: 5px; 137 | right: 5px; 138 | } 139 | button { 140 | margin: 0; 141 | -webkit-user-select: none; 142 | -moz-user-select: none; 143 | user-select: none; 144 | display: inline-block; 145 | text-decoration: none; 146 | background: #43a1f7; 147 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #377ad0)); 148 | background: -webkit-linear-gradient(top, #43a1f7 0%, #377ad0 100%); 149 | background: -moz-linear-gradient(top, #43a1f7 0%, #377ad0 100%); 150 | background: linear-gradient(top, #43a1f7 0%, #377ad0 100%); 151 | border: 1px solid #2e70c4; 152 | -webkit-border-radius: 16px; 153 | -moz-border-radius: 16px; 154 | border-radius: 16px; 155 | color: #fff; 156 | font-family: "lucida grande", sans-serif; 157 | font-size: 11px; 158 | font-weight: normal; 159 | line-height: 1; 160 | padding: 3px 10px 5px 10px; 161 | text-align: center; 162 | text-shadow: 0 -1px 1px #2d6dc0; 163 | } 164 | button:hover, 165 | button.hover { 166 | background: darker; 167 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #2e70c4)); 168 | background: -webkit-linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 169 | background: -moz-linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 170 | background: linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 171 | border: 1px solid #2e70c4; 172 | cursor: pointer; 173 | text-shadow: 0 -1px 1px #2c6bbb; 174 | } 175 | button:active, 176 | button.active { 177 | background: #2e70c4; 178 | border: 1px solid #2e70c4; 179 | border-bottom: 1px solid #2861aa; 180 | text-shadow: 0 -1px 1px #2b67b5; 181 | } 182 | button:focus, 183 | button.focus { 184 | outline: none; 185 | -webkit-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 186 | -moz-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 187 | box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 188 | } 189 | -------------------------------------------------------------------------------- /examples/flask_chat/static/js/chat.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | 3 | var WEB_SOCKET_SWF_LOCATION = '/static/js/socketio/WebSocketMain.swf', 4 | socket = io.connect('/chat'); 5 | 6 | socket.on('connect', function () { 7 | $('#chat').addClass('connected'); 8 | socket.emit('join', window.room); 9 | }); 10 | 11 | socket.on('announcement', function (msg) { 12 | $('#lines').append($('

      ').append($('').text(msg))); 13 | }); 14 | 15 | socket.on('nicknames', function (nicknames) { 16 | $('#nicknames').empty().append($('Online: ')); 17 | for (var i in nicknames) { 18 | $('#nicknames').append($('').text(nicknames[i])); 19 | } 20 | }); 21 | 22 | socket.on('msg_to_room', message); 23 | 24 | socket.on('reconnect', function () { 25 | $('#lines').remove(); 26 | message('System', 'Reconnected to the server'); 27 | }); 28 | 29 | socket.on('reconnecting', function () { 30 | message('System', 'Attempting to re-connect to the server'); 31 | }); 32 | 33 | socket.on('error', function (e) { 34 | message('System', e ? e : 'A unknown error occurred'); 35 | }); 36 | 37 | function message (from, msg) { 38 | $('#lines').append($('

      ').append($('').text(from), msg)); 39 | } 40 | 41 | // DOM manipulation 42 | $(function () { 43 | $('#set-nickname').submit(function (ev) { 44 | socket.emit('nickname', $('#nick').val(), function (set) { 45 | if (set) { 46 | clear(); 47 | return $('#chat').addClass('nickname-set'); 48 | } 49 | $('#nickname-err').css('visibility', 'visible'); 50 | }); 51 | return false; 52 | }); 53 | 54 | $('#send-message').submit(function () { 55 | message('me', $('#message').val()); 56 | socket.emit('user message', $('#message').val()); 57 | clear(); 58 | $('#lines').get(0).scrollTop = 10000000; 59 | return false; 60 | }); 61 | 62 | function clear () { 63 | $('#message').val('').focus(); 64 | } 65 | }); 66 | 67 | }); -------------------------------------------------------------------------------- /examples/flask_chat/static/js/socketio/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/flask_chat/static/js/socketio/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/flask_chat/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Flask | Chat Demo{% endblock title %} 6 | 7 | 14 | 15 | 16 | 17 |

      18 | {% block content %} 19 | {% endblock content %} 20 |
      21 | 22 | {% block form %} 23 | {% endblock form %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% block extra_js %} 31 | {% endblock extra_js %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/flask_chat/templates/room.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ room }}{% endblock %} 4 | 5 | {% block extra_js %} 6 | 9 | 10 | {% endblock %} 11 | 12 | {% block content %} 13 |
      14 |
      15 |
      16 |

      Please type in your nickname and press enter.

      17 | 18 |

      Nickname already in use

      19 |
      20 |
      21 |
      22 |
      Connecting to socket.io server
      23 |
      24 | 25 |
      26 |
      Online:
      27 |
      28 |
      29 | 30 |
      31 | 32 | 33 |
      34 |
      35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /examples/flask_chat/templates/rooms.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extra_js %} 4 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | {% for room in rooms %} 11 |
      12 |

      {{ room }}

      13 |
        14 | {% for user in room.users.all() %} 15 |
      • {{ user }}
      • 16 | {% endfor %} 17 |
      18 |
      19 | {% else %} 20 |

      There are currently no rooms! Add one below.

      21 | {% endfor %} 22 |
      23 | {% endblock content %} 24 | 25 | {% block form %} 26 |
      27 | 28 | 29 |
      30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /examples/json.js: -------------------------------------------------------------------------------- 1 | if(!this.JSON){JSON=function(){function f(n){return n<10?'0'+n:n;} 2 | Date.prototype.toJSON=function(){return this.getUTCFullYear()+'-'+ 3 | f(this.getUTCMonth()+1)+'-'+ 4 | f(this.getUTCDate())+'T'+ 5 | f(this.getUTCHours())+':'+ 6 | f(this.getUTCMinutes())+':'+ 7 | f(this.getUTCSeconds())+'Z';};var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case'string':return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c;} 8 | c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+ 9 | (c%16).toString(16);})+'"':'"'+value+'"';case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';} 10 | if(typeof value.toJSON==='function'){return stringify(value.toJSON());} 11 | a=[];if(typeof value.length==='number'&&!(value.propertyIsEnumerable('length'))){l=value.length;for(i=0;i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 51 | 52 | 53 |

      Live CPU graph

      54 |
      55 |
      This graph shows the live CPU usage as reported 56 | by /proc/stat on Linux-like systems.
      57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/live_cpu_graph/live_cpu_graph/serve.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey; monkey.patch_all() 2 | import gevent 3 | import psutil 4 | 5 | from socketio import socketio_manage 6 | from socketio.server import SocketIOServer 7 | from socketio.namespace import BaseNamespace 8 | from socketio.mixins import BroadcastMixin 9 | 10 | 11 | class CPUNamespace(BaseNamespace, BroadcastMixin): 12 | def recv_connect(self): 13 | def sendcpu(): 14 | prev = None 15 | while True: 16 | vals = psutil.cpu_percent(interval=1, percpu=True) 17 | 18 | if prev: 19 | percent = (sum(vals) - sum(prev)) 20 | self.emit('cpu_data', {'point': percent}) 21 | prev = vals 22 | gevent.sleep(0.1) 23 | self.spawn(sendcpu) 24 | 25 | 26 | class Application(object): 27 | def __init__(self): 28 | self.buffer = [] 29 | 30 | def __call__(self, environ, start_response): 31 | path = environ['PATH_INFO'].strip('/') or 'index.html' 32 | 33 | if path.startswith('static/') or path == 'index.html': 34 | try: 35 | data = open(path).read() 36 | except Exception: 37 | return not_found(start_response) 38 | 39 | if path.endswith(".js"): 40 | content_type = "text/javascript" 41 | elif path.endswith(".css"): 42 | content_type = "text/css" 43 | elif path.endswith(".swf"): 44 | content_type = "application/x-shockwave-flash" 45 | else: 46 | content_type = "text/html" 47 | 48 | start_response('200 OK', [('Content-Type', content_type)]) 49 | return [data] 50 | 51 | if path.startswith("socket.io"): 52 | socketio_manage(environ, {'/cpu': CPUNamespace}) 53 | else: 54 | return not_found(start_response) 55 | 56 | 57 | def not_found(start_response): 58 | start_response('404 Not Found', []) 59 | return ['

      Not Found

      '] 60 | 61 | 62 | if __name__ == '__main__': 63 | print 'Listening on port http://0.0.0.0:8080 and on port 10843 (flash policy server)' 64 | SocketIOServer(('0.0.0.0', 8080), Application(), 65 | resource="socket.io", policy_server=True, 66 | policy_listener=('0.0.0.0', 10843)).serve_forever() 67 | -------------------------------------------------------------------------------- /examples/live_cpu_graph/live_cpu_graph/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/live_cpu_graph/live_cpu_graph/static/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/live_cpu_graph/live_cpu_graph/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 2em; 3 | font-family: sans-serif; 4 | } 5 | 6 | #graph { 7 | margin-top: 1.5em; 8 | } 9 | 10 | #description { 11 | margin-top: 1em; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /examples/live_cpu_graph/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | README = ''#open(os.path.join(here, 'README.md')).read() 7 | CHANGES = ''#open(os.path.join(here, 'CHANGES.txt')).read() 8 | 9 | requires = [ 10 | 'psutil' 11 | ] 12 | 13 | setup(name='live_cpu_graph' 14 | , version='0.1' 15 | , description='An example of HTML5 Canvas + Socket.io.js' 16 | , long_description=README + '\n\n' + CHANGES 17 | , classifiers=[ 18 | 'Intended Audience :: Developers' 19 | , 'License :: OSI Approved :: BSD License' 20 | , 'Operating System :: OS Independent' 21 | , 'Programming Language :: Python' 22 | , 'Topic :: Internet :: WWW/HTTP :: Dynamic Content' 23 | , 'Topic :: Software Development :: Libraries :: Python Modules' 24 | ] 25 | , author='John Anderson' 26 | , author_email='sontek@gmail.com' 27 | , url='https://github.com/eventray/live_cpu_graph' 28 | , keywords='pyramid' 29 | , license='BSD' 30 | , packages=find_packages() 31 | , include_package_data=True 32 | , zip_safe=False 33 | , install_requires=requires 34 | , tests_require=requires 35 | , test_suite='live_cpu_graph' 36 | ) 37 | 38 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/README: -------------------------------------------------------------------------------- 1 | A sample pyramid application using socket.io, backbone.js, and redis for pubsub 2 | 3 | To get the app running: 4 | python setup.py develop 5 | python serve.py 6 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pyramid.config import Configurator 3 | from chatter3.views import socketio_service 4 | from chatter3.views import index 5 | 6 | def simple_route(config, name, url, fn): 7 | config.add_route(name, url) 8 | config.add_view(fn, route_name=name, 9 | renderer="chatter3:templates/%s.mako" % name) 10 | 11 | 12 | def main(global_config, **settings): 13 | config = Configurator() 14 | 15 | simple_route(config, 'index', '/', index) 16 | simple_route(config, 'socket_io', 'socket.io/*remaining', socketio_service) 17 | 18 | config.add_static_view('static', 'static', cache_max_age=3600) 19 | 20 | app = config.make_wsgi_app() 21 | 22 | return app 23 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column 2 | from sqlalchemy.types import UnicodeText 3 | from sqlalchemy.types import Integer 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import scoped_session 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | DBSession = scoped_session(sessionmaker()) 9 | Base = declarative_base() 10 | 11 | 12 | class Chat(Base): 13 | __table_args__ = {'sqlite_autoincrement': True} 14 | __tablename__ = 'chat' 15 | 16 | pk = Column(Integer, primary_key=True, autoincrement=True) 17 | chat_line = Column(UnicodeText, nullable=False) 18 | 19 | 20 | def includeme(config): 21 | config.scan('chatter3.models') 22 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/scripts/populate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pyramid.config import Configurator 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy.orm import sessionmaker 8 | from sqlalchemy.orm import scoped_session 9 | 10 | from chatter3.models import Base 11 | 12 | from pyramid.paster import ( 13 | get_appsettings, 14 | setup_logging, 15 | ) 16 | 17 | DBSession = scoped_session(sessionmaker()) 18 | 19 | here = os.path.dirname(__file__) 20 | 21 | 22 | def usage(argv): # pragma: no cover 23 | cmd = os.path.basename(argv[0]) 24 | print('usage: %s \n' 25 | '(example: "%s development.ini")' % (cmd, cmd)) 26 | sys.exit(1) 27 | 28 | 29 | def main(argv=sys.argv): # pragma: no cover 30 | if len(argv) != 2: 31 | usage(argv) 32 | 33 | config_uri = argv[1] 34 | setup_logging(config_uri) 35 | settings = get_appsettings(config_uri) 36 | 37 | config = Configurator( 38 | settings=settings 39 | ) 40 | 41 | config.include('chatter.models') 42 | 43 | engine = engine_from_config(settings, 'sqlalchemy.') 44 | 45 | Base.metadata.bind = engine 46 | Base.metadata.drop_all(engine) 47 | 48 | Base.metadata.create_all(engine) 49 | 50 | if __name__ == "__main__": # pragma: no cover 51 | main() 52 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/pyramid_backbone_redis_chat/chatter3/static/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/static/chatter.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | WEB_SOCKET_SWF_LOCATION = "/static/WebSocketMain.swf"; 3 | WEB_SOCKET_DEBUG = true; 4 | 5 | // connect to the websocket 6 | var socket = io.connect(); 7 | socket.emit('subscribe') 8 | 9 | // Backbone.js model that will represent our chat log coming in 10 | var ChatModel = Backbone.Model.extend({ 11 | }); 12 | 13 | // The view that reprsents an individual chat line 14 | var ChatItem = Backbone.View.extend({ 15 | render: function(){ 16 | // grab the handlebars.js template we defined for this view 17 | var template = Handlebars.compile($("#chat_item_template").html()); 18 | 19 | // render the template out with the model as a context 20 | this.$el.html(template(this.model.toJSON())); 21 | 22 | // always return this for easy chaining 23 | return this; 24 | }, 25 | }); 26 | 27 | // The view that represents our chat form 28 | var ChatView = Backbone.View.extend({ 29 | 30 | // handle the form submit event and fire the method "send" 31 | events: { 32 | "submit #chat_form": "send" 33 | }, 34 | 35 | send: function(evt) { 36 | evt.preventDefault(); 37 | 38 | var val = $("#chatbox").val(); 39 | 40 | socket.emit("chat", val); 41 | 42 | $("#chatbox").val(""); 43 | }, 44 | 45 | // constructor of the view 46 | initialize: function() { 47 | var me = this; 48 | 49 | // when a new chat event is emitted, add the view item to the DOM 50 | socket.on("chat", function(e) { 51 | 52 | // create the view and pass it the new model for context 53 | var chat_item = new ChatItem({ 54 | model: new ChatModel({ 55 | chat_line: e 56 | }) 57 | }); 58 | 59 | // render it to the DOM 60 | me.$("#chatlog").append(chat_item.render().el); 61 | }); 62 | }, 63 | 64 | render: function(){ 65 | var template = Handlebars.compile($("#chat_template").html()); 66 | $(this.el).html(template()); 67 | }, 68 | 69 | }); 70 | 71 | // Backbone.js router 72 | var Router = Backbone.Router.extend({ 73 | // Match urls with methods 74 | routes: { 75 | "": "index" 76 | }, 77 | 78 | index: function() { 79 | var view = new ChatView({ 80 | el: $("#container"), 81 | }); 82 | 83 | view.render(); 84 | } 85 | 86 | }); 87 | 88 | // start backbone routing 89 | var router = new Router(); 90 | Backbone.history.start({ pushState: true }); 91 | }); 92 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #888; 3 | } 4 | #container { 5 | margin: 0 auto; 6 | width: 550px; 7 | padding: 20px; 8 | background-color: #FFF; 9 | } 10 | 11 | h1 { 12 | margin: 0; 13 | padding: 0; 14 | border-bottom: solid 1px #000; 15 | width: 80%; 16 | } 17 | 18 | #chatlog { 19 | height: 200px; 20 | overflow-y: scroll; 21 | } 22 | 23 | #chatbox { 24 | width: 300px; 25 | border: solid 1px #888; 26 | } 27 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/templates/index.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chatter! 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 23 | 24 | 25 |
      26 |
      27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/chatter3/views.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from json import loads 3 | from json import dumps 4 | 5 | from socketio.namespace import BaseNamespace 6 | from socketio import socketio_manage 7 | 8 | 9 | def index(request): 10 | """ Base view to load our template """ 11 | return {} 12 | 13 | 14 | class ChatNamespace(BaseNamespace): 15 | def listener(self): 16 | r = redis.StrictRedis() 17 | r = r.pubsub() 18 | 19 | r.subscribe('chat') 20 | 21 | for m in r.listen(): 22 | if m['type'] == 'message': 23 | data = loads(m['data']) 24 | self.emit("chat", data) 25 | 26 | def on_subscribe(self, *args, **kwargs): 27 | self.spawn(self.listener) 28 | 29 | def on_chat(self, msg): 30 | r = redis.Redis() 31 | r.publish('chat', dumps(msg)) 32 | 33 | 34 | def socketio_service(request): 35 | retval = socketio_manage(request.environ, 36 | { 37 | '': ChatNamespace, 38 | }, request=request 39 | ) 40 | 41 | return retval 42 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/development.ini: -------------------------------------------------------------------------------- 1 | [app:main] 2 | paste.app_factory = chatter3:main 3 | session.type = file 4 | session.data_dir = /tmp/chatter/data/sessions/data 5 | session.lock_dir = /tmp/chatter/data/sessions/lock 6 | sqlalchemy.url = sqlite:///%(here)s/chatter.db 7 | 8 | [server:main] 9 | use = egg:waitress#main 10 | port = 6543 11 | host = 0.0.0.0 12 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/serve.py: -------------------------------------------------------------------------------- 1 | from socketio.server import SocketIOServer 2 | from pyramid.paster import get_app 3 | from gevent import monkey; monkey.patch_all() 4 | 5 | if __name__ == '__main__': 6 | 7 | app = get_app('development.ini') 8 | 9 | print 'Listening on port http://0.0.0.0:8080 and on port 10843 (flash policy server)' 10 | 11 | SocketIOServer(('0.0.0.0', 8080), app, 12 | resource="socket.io", policy_server=True, 13 | policy_listener=('0.0.0.0', 10843)).serve_forever() 14 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages, Command 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | def _read(path): 10 | with open(path) as f: 11 | data = f.read() 12 | f.close() 13 | 14 | return data 15 | 16 | README = '' 17 | CHANGES = '' 18 | 19 | requires = [ 20 | 'pyramid' 21 | , 'gevent' 22 | , 'gevent-socketio' 23 | , 'pyramid_socketio' 24 | , 'redis' 25 | ] 26 | 27 | if sys.version_info[:3] < (2, 5, 0): 28 | requires.append('pysqlite') 29 | 30 | 31 | class PyTest(Command): 32 | user_options = [] 33 | 34 | def initialize_options(self): 35 | pass 36 | 37 | def finalize_options(self): 38 | pass 39 | 40 | def run(self): 41 | import subprocess 42 | errno = subprocess.call('py.test') 43 | raise SystemExit(errno) 44 | 45 | setup(name='chatter3', 46 | version='0.0', 47 | description='chatter3', 48 | long_description=README + '\n\n' + CHANGES, 49 | classifiers=[ 50 | "Programming Language :: Python", 51 | "Framework :: Pylons", 52 | "Topic :: Internet :: WWW/HTTP", 53 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 54 | ], 55 | author='', 56 | author_email='', 57 | url='', 58 | keywords='web wsgi bfg pylons pyramid', 59 | packages=find_packages(), 60 | include_package_data=True, 61 | zip_safe=False, 62 | test_suite='chatter3', 63 | install_requires=requires, 64 | entry_points="""\ 65 | [paste.app_factory] 66 | main = chatter3:main 67 | """, 68 | paster_plugins=['pyramid'], 69 | ) 70 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/README: -------------------------------------------------------------------------------- 1 | A sample pyramid application using socket.io, backbone.js, persistence, and 2 | redis for pubsub 3 | 4 | To get the app running: 5 | python setup.py develop 6 | python chatter4/scripts/populate.py development.ini 7 | python serve.py 8 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from sqlalchemy import engine_from_config 3 | 4 | from pyramid.config import Configurator 5 | 6 | from chatter4.models import DBSession 7 | from chatter4.views import socketio_service 8 | from chatter4.views import index 9 | from chatter4.views import get_log 10 | 11 | 12 | def simple_route(config, name, url, fn, renderer=None): 13 | if not renderer: 14 | renderer = "chatter4:templates/%s.mako" % name 15 | 16 | config.add_route(name, url) 17 | config.add_view(fn, route_name=name, renderer=renderer) 18 | 19 | 20 | def main(global_config, **settings): 21 | config = Configurator() 22 | 23 | engine = engine_from_config(settings, 'sqlalchemy.') 24 | DBSession.configure(bind=engine) 25 | 26 | simple_route(config, 'index', '/', index) 27 | simple_route(config, 'get_log', '/get_log', get_log, renderer='json') 28 | simple_route(config, 'socket_io', 'socket.io/*remaining', socketio_service) 29 | 30 | config.add_static_view('static', 'static', cache_max_age=3600) 31 | 32 | app = config.make_wsgi_app() 33 | 34 | return app 35 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column 2 | from sqlalchemy.types import UnicodeText 3 | from sqlalchemy.types import Integer 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import scoped_session 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | DBSession = scoped_session(sessionmaker()) 9 | Base = declarative_base() 10 | 11 | 12 | class Chat(Base): 13 | __table_args__ = {'sqlite_autoincrement': True} 14 | __tablename__ = 'chat' 15 | 16 | pk = Column(Integer, primary_key=True, autoincrement=True) 17 | chat_line = Column(UnicodeText, nullable=False) 18 | 19 | def serialize(self): 20 | return { 21 | 'chat_line': self.chat_line 22 | } 23 | 24 | 25 | def includeme(config): 26 | config.scan('chatter4.models') 27 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/scripts/populate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pyramid.config import Configurator 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy.orm import sessionmaker 8 | from sqlalchemy.orm import scoped_session 9 | 10 | from chatter4.models import Base 11 | 12 | from pyramid.paster import ( 13 | get_appsettings, 14 | setup_logging, 15 | ) 16 | 17 | DBSession = scoped_session(sessionmaker()) 18 | 19 | here = os.path.dirname(__file__) 20 | 21 | 22 | def usage(argv): # pragma: no cover 23 | cmd = os.path.basename(argv[0]) 24 | print('usage: %s \n' 25 | '(example: "%s development.ini")' % (cmd, cmd)) 26 | sys.exit(1) 27 | 28 | 29 | def main(argv=sys.argv): # pragma: no cover 30 | if len(argv) != 2: 31 | usage(argv) 32 | 33 | config_uri = argv[1] 34 | setup_logging(config_uri) 35 | settings = get_appsettings(config_uri) 36 | 37 | config = Configurator( 38 | settings=settings 39 | ) 40 | 41 | config.include('chatter4.models') 42 | 43 | engine = engine_from_config(settings, 'sqlalchemy.') 44 | 45 | Base.metadata.bind = engine 46 | Base.metadata.drop_all(engine) 47 | 48 | Base.metadata.create_all(engine) 49 | 50 | if __name__ == "__main__": # pragma: no cover 51 | main() 52 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/static/chatter.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // connect to the websocket 3 | var socket = io.connect(); 4 | socket.emit('subscribe') 5 | 6 | var ChatModel = Backbone.Model.extend({ 7 | }); 8 | 9 | var ChatItem = Backbone.View.extend({ 10 | render: function(){ 11 | var template = Handlebars.compile($("#chat_item_template").html()); 12 | this.$el.html(template(this.model)); 13 | 14 | return this; 15 | }, 16 | }); 17 | 18 | // a collection that will hit a RESTful webservice and pull back all 19 | // results serialized as backbone models 20 | var ChatCollection = Backbone.Collection.extend({ 21 | url: "/get_log" 22 | }); 23 | 24 | 25 | var ChatView = Backbone.View.extend({ 26 | events: { 27 | "submit #chat_form": "send" 28 | }, 29 | 30 | send: function(evt) { 31 | evt.preventDefault(); 32 | var val = $("#chatbox").val(); 33 | 34 | socket.emit("chat", val); 35 | $("#chatbox").val(""); 36 | }, 37 | 38 | initialize: function() { 39 | var me = this; 40 | 41 | socket.on("chat", function(e) { 42 | me.$("#chatlog").append(new ChatItem({ 43 | model: e 44 | }).render().el); 45 | }); 46 | 47 | // re-render the view if the collection fires a reset event 48 | this.collection.on("reset", function() { 49 | me.render(); 50 | }); 51 | }, 52 | 53 | render: function(){ 54 | var template = Handlebars.compile($("#chat_template").html()); 55 | 56 | $(this.el).html(template({ 57 | collection: this.collection.toJSON() 58 | })); 59 | }, 60 | 61 | }); 62 | 63 | var Router = Backbone.Router.extend({ 64 | 65 | routes: { 66 | "": "index" 67 | }, 68 | 69 | index: function() { 70 | var collection = new ChatCollection(); 71 | 72 | var view = new ChatView({ 73 | collection: collection, 74 | el: $("#container") 75 | }); 76 | 77 | // pull the data from the server 78 | collection.fetch(); 79 | 80 | view.render(); 81 | } 82 | 83 | }); 84 | 85 | var router = new Router(); 86 | Backbone.history.start({ pushState: true }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #888; 3 | } 4 | #container { 5 | margin: 0 auto; 6 | width: 550px; 7 | padding: 20px; 8 | background-color: #FFF; 9 | } 10 | 11 | h1 { 12 | margin: 0; 13 | padding: 0; 14 | border-bottom: solid 1px #000; 15 | width: 80%; 16 | } 17 | 18 | #chatlog { 19 | height: 200px; 20 | overflow-y: scroll; 21 | } 22 | 23 | #chatbox { 24 | width: 300px; 25 | border: solid 1px #888; 26 | } 27 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/templates/index.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chatter! 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 27 | 28 | 29 |
      30 |
      31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/chatter4/views.py: -------------------------------------------------------------------------------- 1 | import redis 2 | from json import loads 3 | from json import dumps 4 | 5 | from chatter4.models import DBSession 6 | from chatter4.models import Chat 7 | 8 | from json import loads 9 | from json import dumps 10 | 11 | from socketio.namespace import BaseNamespace 12 | from socketio import socketio_manage 13 | 14 | 15 | def index(request): 16 | """ Base view to load our template """ 17 | return {} 18 | 19 | 20 | def get_log(request): 21 | return [c.serialize() for c in DBSession.query(Chat).all()] 22 | 23 | 24 | class ChatNamespace(BaseNamespace): 25 | def listener(self): 26 | r = redis.StrictRedis() 27 | r = r.pubsub() 28 | 29 | r.subscribe('chat') 30 | 31 | for m in r.listen(): 32 | if m['type'] == 'message': 33 | data = loads(m['data']) 34 | self.emit("chat", data) 35 | 36 | def on_subscribe(self, *args, **kwargs): 37 | self.spawn(self.listener) 38 | 39 | def on_chat(self, msg): 40 | r = redis.Redis() 41 | 42 | # store the data in the database using sqlalchemy 43 | chat = Chat(chat_line=msg) 44 | DBSession.add(chat) 45 | DBSession.commit() 46 | 47 | # we got a new chat event from the client, send it out to 48 | # all the listeners 49 | r.publish('chat', dumps(chat.serialize())) 50 | 51 | 52 | def socketio_service(request): 53 | retval = socketio_manage(request.environ, 54 | { 55 | '': ChatNamespace, 56 | }, request=request 57 | ) 58 | 59 | return retval 60 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/development.ini: -------------------------------------------------------------------------------- 1 | [app:main] 2 | paste.app_factory = chatter4:main 3 | session.type = file 4 | session.data_dir = /tmp/chatter/data/sessions/data 5 | session.lock_dir = /tmp/chatter/data/sessions/lock 6 | sqlalchemy.url = sqlite:///%(here)s/chatter.db 7 | 8 | [server:main] 9 | use = egg:gunicorn#main 10 | host = 0.0.0.0 11 | port = 6543 12 | workers = 4 13 | worker_class = socketio.sgunicorn.GeventSocketIOWorker 14 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/requirements.txt: -------------------------------------------------------------------------------- 1 | pyramid 2 | gevent 3 | gevent-socketio 4 | SQLAlchemy 5 | redis 6 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/serve.py: -------------------------------------------------------------------------------- 1 | from socketio.server import SocketIOServer 2 | from pyramid.paster import get_app 3 | from gevent import monkey; monkey.patch_all() 4 | 5 | if __name__ == '__main__': 6 | 7 | app = get_app('development.ini') 8 | 9 | print 'Listening on port http://0.0.0.0:8080 and on port 10843 (flash policy server)' 10 | 11 | SocketIOServer(('0.0.0.0', 8080), app, 12 | resource="socket.io", policy_server=True, 13 | policy_listener=('0.0.0.0', 10843)).serve_forever() 14 | -------------------------------------------------------------------------------- /examples/pyramid_backbone_redis_chat_persistence/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages, Command 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | def _read(path): 10 | with open(path) as f: 11 | data = f.read() 12 | f.close() 13 | 14 | return data 15 | 16 | README = '' 17 | CHANGES = '' 18 | 19 | requires = ['pyramid', 'gevent', 'gevent-socketio', 'sqlalchemy', 'redis', 'gunicorn'] 20 | 21 | if sys.version_info[:3] < (2, 5, 0): 22 | requires.append('pysqlite') 23 | 24 | 25 | class PyTest(Command): 26 | user_options = [] 27 | 28 | def initialize_options(self): 29 | pass 30 | 31 | def finalize_options(self): 32 | pass 33 | 34 | def run(self): 35 | import subprocess 36 | errno = subprocess.call('py.test') 37 | raise SystemExit(errno) 38 | 39 | setup(name='chatter4', 40 | version='0.0', 41 | description='chatter4', 42 | long_description=README + '\n\n' + CHANGES, 43 | classifiers=[ 44 | "Programming Language :: Python", 45 | "Framework :: Pylons", 46 | "Topic :: Internet :: WWW/HTTP", 47 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 48 | ], 49 | author='', 50 | author_email='', 51 | url='', 52 | keywords='web wsgi bfg pylons pyramid', 53 | packages=find_packages(), 54 | include_package_data=True, 55 | zip_safe=False, 56 | test_suite='chatter4', 57 | install_requires=requires, 58 | entry_points="""\ 59 | [paste.app_factory] 60 | main = chatter4:main 61 | """, 62 | paster_plugins=['pyramid'], 63 | ) 64 | -------------------------------------------------------------------------------- /examples/simple_chat/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 79 | 80 | 81 |
      82 |
      83 |
      84 |

      Please type in your nickname and press enter.

      85 | 86 |

      Nickname already in use

      87 |
      88 |
      89 |
      90 |
      Connecting to socket.io server
      91 |
      92 |
      93 |
      Online:
      94 |
      95 |
      96 |
      97 | 98 | 99 |
      100 |
      101 | 102 | 103 | -------------------------------------------------------------------------------- /examples/simple_chat/chat.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey; monkey.patch_all() 2 | 3 | from socketio import socketio_manage 4 | from socketio.server import SocketIOServer 5 | from socketio.namespace import BaseNamespace 6 | from socketio.mixins import RoomsMixin, BroadcastMixin 7 | 8 | 9 | class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin): 10 | def on_nickname(self, nickname): 11 | self.request['nicknames'].append(nickname) 12 | self.socket.session['nickname'] = nickname 13 | self.broadcast_event('announcement', '%s has connected' % nickname) 14 | self.broadcast_event('nicknames', self.request['nicknames']) 15 | # Just have them join a default-named room 16 | self.join('main_room') 17 | 18 | def recv_disconnect(self): 19 | # Remove nickname from the list. 20 | nickname = self.socket.session['nickname'] 21 | self.request['nicknames'].remove(nickname) 22 | self.broadcast_event('announcement', '%s has disconnected' % nickname) 23 | self.broadcast_event('nicknames', self.request['nicknames']) 24 | 25 | self.disconnect(silent=True) 26 | 27 | def on_user_message(self, msg): 28 | self.emit_to_room('main_room', 'msg_to_room', 29 | self.socket.session['nickname'], msg) 30 | 31 | def recv_message(self, message): 32 | print "PING!!!", message 33 | 34 | class Application(object): 35 | def __init__(self): 36 | self.buffer = [] 37 | # Dummy request object to maintain state between Namespace 38 | # initialization. 39 | self.request = { 40 | 'nicknames': [], 41 | } 42 | 43 | def __call__(self, environ, start_response): 44 | path = environ['PATH_INFO'].strip('/') 45 | 46 | if not path: 47 | start_response('200 OK', [('Content-Type', 'text/html')]) 48 | return ['

      Welcome. ' 49 | 'Try the chat example.

      '] 50 | 51 | if path.startswith('static/') or path == 'chat.html': 52 | try: 53 | data = open(path).read() 54 | except Exception: 55 | return not_found(start_response) 56 | 57 | if path.endswith(".js"): 58 | content_type = "text/javascript" 59 | elif path.endswith(".css"): 60 | content_type = "text/css" 61 | elif path.endswith(".swf"): 62 | content_type = "application/x-shockwave-flash" 63 | else: 64 | content_type = "text/html" 65 | 66 | start_response('200 OK', [('Content-Type', content_type)]) 67 | return [data] 68 | 69 | if path.startswith("socket.io"): 70 | socketio_manage(environ, {'': ChatNamespace}, self.request) 71 | else: 72 | return not_found(start_response) 73 | 74 | 75 | def not_found(start_response): 76 | start_response('404 Not Found', []) 77 | return ['

      Not Found

      '] 78 | 79 | 80 | if __name__ == '__main__': 81 | print 'Listening on port 8080 and on port 843 (flash policy server)' 82 | SocketIOServer(('0.0.0.0', 8080), Application(), 83 | resource="socket.io", policy_server=True, 84 | policy_listener=('0.0.0.0', 10843)).serve_forever() 85 | -------------------------------------------------------------------------------- /examples/simple_chat/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/simple_chat/static/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/simple_chat/static/css/style.css: -------------------------------------------------------------------------------- 1 | #chat, 2 | #nickname, 3 | #messages { 4 | width: 600px; 5 | } 6 | #chat { 7 | position: relative; 8 | border: 1px solid #ccc; 9 | } 10 | #nickname, 11 | #connecting { 12 | position: absolute; 13 | height: 410px; 14 | z-index: 100; 15 | left: 0; 16 | top: 0; 17 | background: #fff; 18 | text-align: center; 19 | width: 600px; 20 | font: 15px Georgia; 21 | color: #666; 22 | display: block; 23 | } 24 | #nickname .wrap, 25 | #connecting .wrap { 26 | padding-top: 150px; 27 | } 28 | #nickname input { 29 | border: 1px solid #ccc; 30 | padding: 10px; 31 | } 32 | #nickname input:focus { 33 | border-color: #999; 34 | outline: 0; 35 | } 36 | #nickname #nickname-err { 37 | color: #8b0000; 38 | font-size: 12px; 39 | visibility: hidden; 40 | } 41 | .connected #connecting { 42 | display: none; 43 | } 44 | .nickname-set #nickname { 45 | display: none; 46 | } 47 | #messages { 48 | height: 380px; 49 | background: #eee; 50 | } 51 | #messages em { 52 | text-shadow: 0 1px 0 #fff; 53 | color: #999; 54 | } 55 | #messages p { 56 | padding: 0; 57 | margin: 0; 58 | font: 12px Helvetica, Arial; 59 | padding: 5px 10px; 60 | } 61 | #messages p b { 62 | display: inline-block; 63 | padding-right: 10px; 64 | } 65 | #messages p:nth-child(even) { 66 | background: #fafafa; 67 | } 68 | #messages #nicknames { 69 | background: #ccc; 70 | padding: 2px 4px 4px; 71 | font: 11px Helvetica; 72 | } 73 | #messages #nicknames span { 74 | color: #000; 75 | } 76 | #messages #nicknames b { 77 | display: inline-block; 78 | color: #fff; 79 | background: #999; 80 | padding: 3px 6px; 81 | margin-right: 5px; 82 | -webkit-border-radius: 10px; 83 | -moz-border-radius: 10px; 84 | border-radius: 10px; 85 | text-shadow: 0 1px 0 #666; 86 | } 87 | #messages #lines { 88 | height: 355px; 89 | overflow: auto; 90 | overflow-x: hidden; 91 | overflow-y: auto; 92 | } 93 | #messages #lines::-webkit-scrollbar { 94 | width: 6px; 95 | height: 6px; 96 | } 97 | #messages #lines::-webkit-scrollbar-button:start:decrement, 98 | #messages #lines ::-webkit-scrollbar-button:end:increment { 99 | display: block; 100 | height: 10px; 101 | } 102 | #messages #lines::-webkit-scrollbar-button:vertical:increment { 103 | background-color: #fff; 104 | } 105 | #messages #lines::-webkit-scrollbar-track-piece { 106 | background-color: #fff; 107 | -webkit-border-radius: 3px; 108 | } 109 | #messages #lines::-webkit-scrollbar-thumb:vertical { 110 | height: 50px; 111 | background-color: #ccc; 112 | -webkit-border-radius: 3px; 113 | } 114 | #messages #lines::-webkit-scrollbar-thumb:horizontal { 115 | width: 50px; 116 | background-color: #fff; 117 | -webkit-border-radius: 3px; 118 | } 119 | #send-message { 120 | background: #fff; 121 | position: relative; 122 | } 123 | #send-message input { 124 | border: none; 125 | height: 30px; 126 | padding: 0 10px; 127 | line-height: 30px; 128 | vertical-align: middle; 129 | width: 580px; 130 | } 131 | #send-message input:focus { 132 | outline: 0; 133 | } 134 | #send-message button { 135 | position: absolute; 136 | top: 5px; 137 | right: 5px; 138 | } 139 | button { 140 | margin: 0; 141 | -webkit-user-select: none; 142 | -moz-user-select: none; 143 | user-select: none; 144 | display: inline-block; 145 | text-decoration: none; 146 | background: #43a1f7; 147 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #377ad0)); 148 | background: -webkit-linear-gradient(top, #43a1f7 0%, #377ad0 100%); 149 | background: -moz-linear-gradient(top, #43a1f7 0%, #377ad0 100%); 150 | background: linear-gradient(top, #43a1f7 0%, #377ad0 100%); 151 | border: 1px solid #2e70c4; 152 | -webkit-border-radius: 16px; 153 | -moz-border-radius: 16px; 154 | border-radius: 16px; 155 | color: #fff; 156 | font-family: "lucida grande", sans-serif; 157 | font-size: 11px; 158 | font-weight: normal; 159 | line-height: 1; 160 | padding: 3px 10px 5px 10px; 161 | text-align: center; 162 | text-shadow: 0 -1px 1px #2d6dc0; 163 | } 164 | button:hover, 165 | button.hover { 166 | background: darker; 167 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #43a1f7), color-stop(1, #2e70c4)); 168 | background: -webkit-linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 169 | background: -moz-linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 170 | background: linear-gradient(top, #43a1f7 0%, #2e70c4 100%); 171 | border: 1px solid #2e70c4; 172 | cursor: pointer; 173 | text-shadow: 0 -1px 1px #2c6bbb; 174 | } 175 | button:active, 176 | button.active { 177 | background: #2e70c4; 178 | border: 1px solid #2e70c4; 179 | border-bottom: 1px solid #2861aa; 180 | text-shadow: 0 -1px 1px #2b67b5; 181 | } 182 | button:focus, 183 | button.focus { 184 | outline: none; 185 | -webkit-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 186 | -moz-box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 187 | box-shadow: 0 1px 0 0 rgba(255,255,255,0.4), 0 0 4px 0 #377ad0; 188 | } 189 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/README: -------------------------------------------------------------------------------- 1 | A sample pyramid application using the minimum required for a real-time chat up without persistance 2 | 3 | To get the app running: 4 | python setup.py develop 5 | python serve.py 6 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from pyramid.config import Configurator 3 | from chatter2.views import socketio_service 4 | from chatter2.views import index 5 | 6 | 7 | def simple_route(config, name, url, fn): 8 | """ 9 | Function to simplify creating routes in pyramid 10 | Takes the pyramid configuration, name of the route, url, and view 11 | function. 12 | """ 13 | config.add_route(name, url) 14 | config.add_view(fn, route_name=name, 15 | renderer="chatter2:templates/%s.mako" % name) 16 | 17 | 18 | def main(global_config, **settings): 19 | config = Configurator() 20 | 21 | simple_route(config, 'index', '/', index) 22 | 23 | # The socketio view configuration 24 | simple_route(config, 'socket_io', 'socket.io/*remaining', socketio_service) 25 | 26 | config.add_static_view('static', 'static', cache_max_age=3600) 27 | 28 | app = config.make_wsgi_app() 29 | 30 | return app 31 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column 2 | from sqlalchemy.types import UnicodeText 3 | from sqlalchemy.types import Integer 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import scoped_session 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | DBSession = scoped_session(sessionmaker()) 9 | Base = declarative_base() 10 | 11 | 12 | class Chat(Base): 13 | """ Base model for storing the chat log """ 14 | __table_args__ = {'sqlite_autoincrement': True} 15 | __tablename__ = 'chat' 16 | 17 | pk = Column(Integer, primary_key=True, autoincrement=True) 18 | chat_line = Column(UnicodeText, nullable=False) 19 | 20 | 21 | def includeme(config): 22 | """ Pyramid configuration """ 23 | config.scan('chatter2.models') 24 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/scripts/populate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pyramid.config import Configurator 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy.orm import sessionmaker 8 | from sqlalchemy.orm import scoped_session 9 | 10 | from chatter2.models import Base 11 | 12 | from pyramid.paster import ( 13 | get_appsettings, 14 | setup_logging, 15 | ) 16 | 17 | DBSession = scoped_session(sessionmaker()) 18 | 19 | here = os.path.dirname(__file__) 20 | 21 | 22 | def usage(argv): # pragma: no cover 23 | cmd = os.path.basename(argv[0]) 24 | print('usage: %s \n' 25 | '(example: "%s development.ini")' % (cmd, cmd)) 26 | sys.exit(1) 27 | 28 | 29 | def main(argv=sys.argv): # pragma: no cover 30 | if len(argv) != 2: 31 | usage(argv) 32 | 33 | config_uri = argv[1] 34 | setup_logging(config_uri) 35 | settings = get_appsettings(config_uri) 36 | 37 | config = Configurator( 38 | settings=settings 39 | ) 40 | 41 | config.include('chatter2.models') 42 | 43 | engine = engine_from_config(settings, 'sqlalchemy.') 44 | 45 | Base.metadata.bind = engine 46 | Base.metadata.drop_all(engine) 47 | 48 | Base.metadata.create_all(engine) 49 | 50 | if __name__ == "__main__": # pragma: no cover 51 | main() 52 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/simple_pyramid_chat/chatter2/static/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/static/chatter.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | WEB_SOCKET_SWF_LOCATION = "/static/WebSocketMain.swf"; 3 | WEB_SOCKET_DEBUG = true; 4 | 5 | // connect to the websocket 6 | var socket = io.connect('/chat'); 7 | 8 | $(window).bind("beforeunload", function() { 9 | socket.disconnect(); 10 | }); 11 | 12 | // Listen for the event "chat" and add the content to the log 13 | socket.on("chat", function(e) { 14 | $("#chatlog").append(e + "
      "); 15 | }); 16 | 17 | socket.on("user_disconnect", function() { 18 | $("#chatlog").append("user disconnected" + "
      "); 19 | }); 20 | 21 | socket.on("user_connect", function() { 22 | $("#chatlog").append("user connected" + "
      "); 23 | }); 24 | 25 | // Execute whenever the form is submitted 26 | $("#chat_form").submit(function(e) { 27 | // don't allow the form to submit 28 | e.preventDefault(); 29 | 30 | var val = $("#chatbox").val(); 31 | 32 | // send out the "chat" event with the textbox as the only argument 33 | socket.emit("chat", val); 34 | 35 | $("#chatbox").val(""); 36 | }); 37 | 38 | $("#join").click(function(e) { 39 | socket.emit('join', 'test') 40 | }) 41 | }); 42 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #888; 3 | } 4 | #container { 5 | margin: 0 auto; 6 | width: 550px; 7 | padding: 20px; 8 | background-color: #FFF; 9 | } 10 | 11 | h1 { 12 | margin: 0; 13 | padding: 0; 14 | border-bottom: solid 1px #000; 15 | width: 80%; 16 | } 17 | 18 | #chatlog { 19 | height: 200px; 20 | overflow-y: scroll; 21 | } 22 | 23 | #chatbox { 24 | width: 300px; 25 | border: solid 1px #888; 26 | } 27 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/templates/index.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chatter! 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
      14 |

      Chat Log

      15 |

      16 |
      17 | 18 | 19 |
      20 | 21 |
      22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/chatter2/views.py: -------------------------------------------------------------------------------- 1 | from socketio.namespace import BaseNamespace 2 | from socketio import socketio_manage 3 | from socketio.mixins import BroadcastMixin 4 | 5 | 6 | def index(request): 7 | """ Base view to load our template """ 8 | return {} 9 | 10 | 11 | class NamedUsersRoomsMixin(BroadcastMixin): 12 | def __init__(self, *args, **kwargs): 13 | super(NamedUsersRoomsMixin, self).__init__(*args, **kwargs) 14 | if 'rooms' not in self.session: 15 | self.session['rooms'] = set() # a set of simple strings 16 | self.session['nickname'] = 'guest123' 17 | 18 | def join(self, room): 19 | """Lets a user join a room on a specific Namespace.""" 20 | self.socket.rooms.add(self._get_room_name(room)) 21 | 22 | def leave(self, room): 23 | """Lets a user leave a room on a specific Namespace.""" 24 | self.socket.rooms.remove(self._get_room_name(room)) 25 | 26 | def _get_room_name(self, room): 27 | return self.ns_name + '_' + room 28 | 29 | def emit_to_room(self, event, args, room): 30 | """This is sent to all in the room (in this particular Namespace)""" 31 | pkt = dict(type="event", 32 | name=event, 33 | args=args, 34 | endpoint=self.ns_name) 35 | room_name = self._get_room_name(room) 36 | for sessid, socket in self.socket.server.sockets.iteritems(): 37 | if not hasattr(socket, 'rooms'): 38 | continue 39 | if room_name in socket.rooms: 40 | socket.send_packet(pkt) 41 | 42 | 43 | class ChatNamespace(BaseNamespace, NamedUsersRoomsMixin): 44 | def on_chat(self, msg): 45 | self.broadcast_event('chat', msg) 46 | 47 | def recv_connect(self): 48 | self.broadcast_event('user_connect') 49 | 50 | def recv_disconnect(self): 51 | self.broadcast_event('user_disconnect') 52 | self.disconnect(silent=True) 53 | 54 | def on_join(self, channel): 55 | self.join(channel) 56 | 57 | 58 | 59 | from pyramid.response import Response 60 | def socketio_service(request): 61 | socketio_manage(request.environ, 62 | {'/chat': ChatNamespace}, 63 | request=request) 64 | 65 | return Response('') 66 | 67 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/development.ini: -------------------------------------------------------------------------------- 1 | [app:main] 2 | paste.app_factory = chatter2:main 3 | session.type = file 4 | session.data_dir = /tmp/chatter/data/sessions/data 5 | session.lock_dir = /tmp/chatter/data/sessions/lock 6 | sqlalchemy.url = sqlite:///%(here)s/chatter.db 7 | 8 | [server:main] 9 | use = egg:gunicorn#main 10 | host = 0.0.0.0 11 | port = 6543 12 | workers = 4 13 | worker_class = socketio.sgunicorn.GeventSocketIOWorker 14 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/serve.py: -------------------------------------------------------------------------------- 1 | from socketio.server import SocketIOServer 2 | from pyramid.paster import get_app 3 | from gevent import monkey; monkey.patch_all() 4 | 5 | if __name__ == '__main__': 6 | 7 | app = get_app('development.ini') 8 | print 'Listening on port http://0.0.0.0:8080 and on port 10843 (flash policy server)' 9 | 10 | SocketIOServer(('0.0.0.0', 8080), app, 11 | resource="socket.io", policy_server=True, 12 | policy_listener=('0.0.0.0', 10843)).serve_forever() 13 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages, Command 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | 9 | def _read(path): 10 | with open(path) as f: 11 | data = f.read() 12 | f.close() 13 | 14 | return data 15 | 16 | README = '' 17 | CHANGES = '' 18 | 19 | requires = [ 20 | 'pyramid' 21 | , 'gevent-socketio' 22 | , 'gevent-websocket' 23 | ] 24 | 25 | if sys.version_info[:3] < (2, 5, 0): 26 | requires.append('pysqlite') 27 | 28 | 29 | class PyTest(Command): 30 | user_options = [] 31 | 32 | def initialize_options(self): 33 | pass 34 | 35 | def finalize_options(self): 36 | pass 37 | 38 | def run(self): 39 | import subprocess 40 | errno = subprocess.call('py.test') 41 | raise SystemExit(errno) 42 | 43 | setup(name='chatter2', 44 | version='0.0', 45 | description='chatter2', 46 | long_description=README + '\n\n' + CHANGES, 47 | classifiers=[ 48 | "Programming Language :: Python", 49 | "Framework :: Pylons", 50 | "Topic :: Internet :: WWW/HTTP", 51 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 52 | ], 53 | author='', 54 | author_email='', 55 | url='', 56 | keywords='web wsgi bfg pylons pyramid', 57 | packages=find_packages(), 58 | include_package_data=True, 59 | zip_safe=False, 60 | test_suite='chatter2', 61 | install_requires=requires, 62 | entry_points="""\ 63 | [paste.app_factory] 64 | main = chatter2:main 65 | """, 66 | paster_plugins=['pyramid'], 67 | ) 68 | -------------------------------------------------------------------------------- /examples/simple_pyramid_chat/wsgi.py: -------------------------------------------------------------------------------- 1 | from pyramid.paster import get_app 2 | 3 | app = get_app('development.ini') 4 | 5 | # Can run with gunicorn using: 6 | #gunicorn -b 0.0.0.0:8080 --workers=4 --worker-class socketio.sgunicorn.GeventSocketIOWorker wsgi:app 7 | 8 | -------------------------------------------------------------------------------- /examples/testapp/development.ini: -------------------------------------------------------------------------------- 1 | [app:main] 2 | paste.app_factory = testapp:main 3 | session.type = file 4 | session.data_dir = /tmp/chatter/data/sessions/data 5 | session.lock_dir = /tmp/chatter/data/sessions/lock 6 | sqlalchemy.url = sqlite:///%(here)s/chatter.db 7 | 8 | [server:main] 9 | use = egg:waitress#main 10 | port = 6543 11 | host = 0.0.0.0 12 | -------------------------------------------------------------------------------- /examples/testapp/serve.py: -------------------------------------------------------------------------------- 1 | from socketio.server import SocketIOServer 2 | from pyramid.paster import get_app 3 | from gevent import monkey; monkey.patch_all() 4 | 5 | if __name__ == '__main__': 6 | 7 | app = get_app('development.ini') 8 | print 'Listening on port http://127.0.0.1:8080 and on port 843 (flash policy server)' 9 | SocketIOServer(('127.0.0.1', 8080), app, policy_server=False, 10 | transports=['websocket']).serve_forever() 11 | -------------------------------------------------------------------------------- /examples/testapp/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup, find_packages, Command 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | def _read(path): 9 | with open(path) as f: 10 | data= f.read() 11 | 12 | f.close() 13 | 14 | return data 15 | 16 | README = '' 17 | CHANGES = '' 18 | 19 | requires = [] 20 | 21 | if sys.version_info[:3] < (2,5,0): 22 | requires.append('pysqlite') 23 | 24 | class PyTest(Command): 25 | user_options = [] 26 | def initialize_options(self): 27 | pass 28 | def finalize_options(self): 29 | pass 30 | def run(self): 31 | import subprocess 32 | errno = subprocess.call('py.test') 33 | raise SystemExit(errno) 34 | 35 | setup(name='testapp', 36 | version='0.0', 37 | description='testapp', 38 | long_description=README + '\n\n' + CHANGES, 39 | classifiers=[ 40 | "Programming Language :: Python", 41 | "Framework :: Pylons", 42 | "Topic :: Internet :: WWW/HTTP", 43 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 44 | ], 45 | author='', 46 | author_email='', 47 | url='', 48 | keywords='web wsgi bfg pylons pyramid', 49 | packages=find_packages(), 50 | include_package_data=True, 51 | zip_safe=False, 52 | test_suite='testapp', 53 | install_requires = requires, 54 | #cmdclass = {'test': PyTest}, 55 | entry_points = """\ 56 | [paste.app_factory] 57 | main = testapp:main 58 | """, 59 | paster_plugins=['pyramid'], 60 | ) 61 | 62 | -------------------------------------------------------------------------------- /examples/testapp/testapp/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from sqlalchemy import engine_from_config 3 | from pyramid.config import Configurator 4 | from testapp.views import socketio_service 5 | from testapp.views import index 6 | from testapp.models import DBSession 7 | 8 | def simple_route(config, name, url, fn): 9 | """ Function to simplify creating routes in pyramid 10 | Takes the pyramid configuration, name of the route, url, and view 11 | function 12 | """ 13 | config.add_route(name, url) 14 | config.add_view(fn, route_name=name, 15 | renderer="testapp:templates/%s.mako" % name) 16 | 17 | def main(global_config, **settings): 18 | config = Configurator() 19 | 20 | engine = engine_from_config(settings, 'sqlalchemy.') 21 | DBSession.configure(bind=engine) 22 | 23 | simple_route(config, 'index', '/', index) 24 | 25 | # The socketio view configuration 26 | config.add_route('socket_io', 'socket.io/*remaining') 27 | 28 | config.add_static_view('static', 'static', cache_max_age=3600) 29 | 30 | config.scan('testapp.views') 31 | 32 | app = config.make_wsgi_app() 33 | 34 | return app 35 | -------------------------------------------------------------------------------- /examples/testapp/testapp/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column 2 | from sqlalchemy.types import UnicodeText 3 | from sqlalchemy.types import Integer 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.orm import scoped_session 6 | from sqlalchemy.orm import sessionmaker 7 | 8 | DBSession = scoped_session(sessionmaker()) 9 | Base = declarative_base() 10 | 11 | class Chat(Base): 12 | """ Base model for storing the chat log """ 13 | __table_args__ = {'sqlite_autoincrement': True} 14 | __tablename__ = 'chat' 15 | 16 | pk = Column(Integer, primary_key=True, autoincrement=True) 17 | chat_line = Column(UnicodeText, nullable=False) 18 | 19 | def includeme(config): 20 | """ Pyramid configuration """ 21 | config.scan('testapp.models') 22 | -------------------------------------------------------------------------------- /examples/testapp/testapp/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | # package 2 | -------------------------------------------------------------------------------- /examples/testapp/testapp/scripts/populate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | from pyramid.config import Configurator 5 | 6 | from sqlalchemy import engine_from_config 7 | from sqlalchemy.orm import sessionmaker 8 | from sqlalchemy.orm import scoped_session 9 | 10 | from testapp.models import Base 11 | 12 | from pyramid.paster import ( 13 | get_appsettings, 14 | setup_logging, 15 | ) 16 | 17 | DBSession = scoped_session(sessionmaker()) 18 | 19 | here = os.path.dirname(__file__) 20 | 21 | def usage(argv):# pragma: no cover 22 | cmd = os.path.basename(argv[0]) 23 | print('usage: %s \n' 24 | '(example: "%s development.ini")' % (cmd, cmd)) 25 | sys.exit(1) 26 | 27 | def main(argv=sys.argv): # pragma: no cover 28 | if len(argv) != 2: 29 | usage(argv) 30 | 31 | config_uri = argv[1] 32 | setup_logging(config_uri) 33 | settings = get_appsettings(config_uri) 34 | 35 | config = Configurator( 36 | settings=settings 37 | ) 38 | 39 | config.include('testapp.models') 40 | 41 | engine = engine_from_config(settings, 'sqlalchemy.') 42 | 43 | Base.metadata.bind = engine 44 | Base.metadata.drop_all(engine) 45 | 46 | Base.metadata.create_all(engine) 47 | 48 | if __name__ == "__main__": # pragma: no cover 49 | main() 50 | -------------------------------------------------------------------------------- /examples/testapp/testapp/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/testapp/testapp/static/favicon.ico -------------------------------------------------------------------------------- /examples/testapp/testapp/static/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #888; 3 | } 4 | #container { 5 | margin: 0 auto; 6 | width: 550px; 7 | padding: 20px; 8 | background-color: #FFF; 9 | } 10 | 11 | h1 { 12 | margin: 0; 13 | padding: 0; 14 | border-bottom: solid 1px #000; 15 | width: 80%; 16 | } 17 | 18 | #chatlog { 19 | height: 200px; 20 | overflow-y: scroll; 21 | } 22 | 23 | #chatbox { 24 | width: 300px; 25 | border: solid 1px #888; 26 | } 27 | -------------------------------------------------------------------------------- /examples/testapp/testapp/templates/index.mako: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chatter! 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
      14 |

      Chat Log

      15 |

      16 |
      17 | 18 | 19 |
      20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
      31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/testapp/testapp/views.py: -------------------------------------------------------------------------------- 1 | from pyramid.view import view_config 2 | import gevent 3 | from socketio import socketio_manage 4 | from socketio.namespace import BaseNamespace 5 | from socketio.mixins import RoomsMixin, BroadcastMixin 6 | from gevent import socket 7 | 8 | def index(request): 9 | """ Base view to load our template """ 10 | return {} 11 | 12 | 13 | 14 | """ 15 | ACK model: 16 | 17 | The client sends a message of the sort: 18 | 19 | {type: 'message', 20 | id: 140, 21 | ack: true, 22 | endpoint: '/tobi', 23 | data: '' 24 | } 25 | 26 | The 'ack' value is 'true', marking that we want an automatic 'ack' when it 27 | receives the packet. The Node.js version sends the ack itself, without any 28 | server-side code interaction. It dispatches the packet only after sending back 29 | an ack, so the ack isn't really a reply. It's just marking the server received 30 | it, but not if the event/message/json was properly processed. 31 | 32 | The automated reply from such a request is: 33 | 34 | {type: 'ack', 35 | ackId: '140', 36 | endpoint: '', 37 | args: [] 38 | } 39 | 40 | Where 'ackId' corresponds to the 'id' of the originating message. Upon 41 | reception of this 'ack' message, the client then looks in an object if there 42 | is a callback function to call associated with this message id (140). If so, 43 | runs it, otherwise, drops the packet. 44 | 45 | There is a second way to ask for an ack, sending a packet like this: 46 | 47 | {type: 'event', 48 | id: 1, 49 | ack: 'data', 50 | endpoint: '', 51 | name: 'chat', 52 | args: ['', ''] 53 | } 54 | 55 | {type: 'json', 56 | id: 1, 57 | ack: 'data', 58 | endpoint: '', 59 | data: {a: 'b'} 60 | } 61 | 62 | .. the same goes for a 'message' packet, which has the 'ack' equal to 'data'. 63 | When the server receives such a packet, it dispatches the corresponding event 64 | (either the named event specified in an 'event' type packet, or 'message' or 65 | 'json, if the type is so), and *adds* as a parameter, in addition to the 66 | 'args' passed by the event (or 'data' for 'message'/'json'), the ack() function 67 | to call (it encloses the packet 'id' already). Any number of arguments passed 68 | to that 'ack()' function will be passed on to the client-side, and given as 69 | parameter on the client-side function. 70 | 71 | That is the returning 'ack' message, with the data ready to be passed as 72 | arguments to the saved callback on the client side: 73 | 74 | {type: 'ack', 75 | ackId: '12', 76 | endpoint: '', 77 | args: ['woot', 'wa'] 78 | } 79 | 80 | """ 81 | 82 | 83 | class GlobalIONamespace(BaseNamespace, BroadcastMixin): 84 | def on_chat(self, *args): 85 | self.emit("bob", {'hello': 'world'}) 86 | print "Received chat message", args 87 | self.broadcast_event_not_me('chat', *args) 88 | 89 | def recv_connect(self): 90 | print "CONNNNNNNN" 91 | self.emit("you_just_connected", {'bravo': 'kid'}) 92 | self.spawn(self.cpu_checker_process) 93 | 94 | def recv_json(self, data): 95 | self.emit("got_some_json", data) 96 | 97 | def on_bob(self, *args): 98 | self.broadcast_event('broadcasted', args) 99 | self.socket['/chat'].emit('bob') 100 | 101 | def cpu_checker_process(self): 102 | """This will be a greenlet""" 103 | ret = os.system("cat /proc/cpu/stuff") 104 | self.emit("cpu_value", ret) 105 | 106 | class ChatIONamespace(BaseNamespace, RoomsMixin): 107 | def on_mymessage(self, msg): 108 | print "In on_mymessage" 109 | self.send("little message back") 110 | self.send({'blah': 'blah'}, json=True) 111 | for x in xrange(2): 112 | self.emit("pack", {'the': 'more', 'you': 'can'}) 113 | 114 | def on_my_callback(self, packet): 115 | return (1, 2) 116 | 117 | def on_trigger_server_callback(self, superbob): 118 | def cb(): 119 | print "OK, WE WERE CALLED BACK BY THE ACK! THANKS :)" 120 | self.emit('callmeback', 'this is a first param', 121 | 'this is the last param', callback=cb) 122 | 123 | def cb2(param1, param2): 124 | print "OK, GOT THOSE VALUES BACK BY CB", param1, param2 125 | self.emit('callmeback', 'this is a first param', 126 | 'this is the last param', callback=cb2) 127 | 128 | def on_rtc_invite(self, sdp): 129 | print "Got an RTC invite, now pushing to others..." 130 | self.emit_to_room('room1', 'rtc_invite', self.session['nickname'], 131 | sdp) 132 | 133 | def recv_connect(self): 134 | self.session['nickname'] = 'guest123' 135 | self.join('room1') 136 | 137 | def recv_message(self, data): 138 | print "Received a 'message' with data:", data 139 | 140 | 141 | def on_disconnect_me(self, data): 142 | print "Disconnecting you buddy", data 143 | self.disconnect() 144 | 145 | 146 | nsmap = {'': GlobalIONamespace, 147 | '/chat': ChatIONamespace} 148 | 149 | @view_config(route_name='socket_io') 150 | def socketio_service(request): 151 | """ The view that will launch the socketio listener """ 152 | 153 | socketio_manage(request.environ, namespaces=nsmap, request=request) 154 | 155 | return {} 156 | 157 | -------------------------------------------------------------------------------- /examples/twitterstream/README: -------------------------------------------------------------------------------- 1 | Streaming twitter updates! 2 | 3 | The server grabs a stream of twitter updates via the twitter API and each connected client gets piped a stream from the server. 4 | 5 | To run: 6 | 7 | python setup.py install 8 | cd twitterstream 9 | python serve.py 10 | 11 | then visit http://127.0.0.1:8080/ 12 | -------------------------------------------------------------------------------- /examples/twitterstream/setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | here = os.path.abspath(os.path.dirname(__file__)) 6 | README = ''#open(os.path.join(here, 'README.md')).read() 7 | CHANGES = ''#open(os.path.join(here, 'CHANGES.txt')).read() 8 | 9 | requires = [ 10 | 'tweetstream' 11 | ] 12 | 13 | setup(name='twitterstream' 14 | , version='0.1' 15 | , description='Streaming twitter updates over Socket.IO' 16 | , long_description=README + '\n\n' + CHANGES 17 | , classifiers=[ 18 | 'Intended Audience :: Developers' 19 | , 'License :: OSI Approved :: BSD License' 20 | , 'Operating System :: OS Independent' 21 | , 'Programming Language :: Python' 22 | , 'Topic :: Internet :: WWW/HTTP :: Dynamic Content' 23 | , 'Topic :: Software Development :: Libraries :: Python Modules' 24 | ] 25 | , author='Philip Neustrom' 26 | , author_email='philipn@gmail.com' 27 | , url='https://github.com/abourget/gevent-socketio' 28 | , license='BSD' 29 | , packages=find_packages() 30 | , include_package_data=True 31 | , zip_safe=False 32 | , install_requires=requires 33 | , tests_require=requires 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /examples/twitterstream/twitterstream/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 | 34 | 35 | 36 |

      Live twitter stream from our server

      37 |
      38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/twitterstream/twitterstream/serve.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey; monkey.patch_all() 2 | import gevent 3 | import tweetstream 4 | import getpass 5 | 6 | from socketio import socketio_manage 7 | from socketio.server import SocketIOServer 8 | from socketio.namespace import BaseNamespace 9 | 10 | 11 | def broadcast_msg(server, ns_name, event, *args): 12 | pkt = dict(type="event", 13 | name=event, 14 | args=args, 15 | endpoint=ns_name) 16 | 17 | for sessid, socket in server.sockets.iteritems(): 18 | socket.send_packet(pkt) 19 | 20 | 21 | def send_tweets(server, user, password): 22 | stream = tweetstream.SampleStream(user, password) 23 | for tweet in stream: 24 | broadcast_msg(server, '/tweets', 'tweet', tweet) 25 | 26 | 27 | def get_credentials(): 28 | user = raw_input("Twitter username: ") 29 | password = getpass.getpass("Twitter password: ") 30 | return (user, password) 31 | 32 | 33 | class Application(object): 34 | def __init__(self): 35 | self.buffer = [] 36 | 37 | def __call__(self, environ, start_response): 38 | path = environ['PATH_INFO'].strip('/') or 'index.html' 39 | 40 | if path.startswith('static/') or path == 'index.html': 41 | try: 42 | data = open(path).read() 43 | except Exception: 44 | return not_found(start_response) 45 | 46 | if path.endswith(".js"): 47 | content_type = "text/javascript" 48 | elif path.endswith(".css"): 49 | content_type = "text/css" 50 | elif path.endswith(".swf"): 51 | content_type = "application/x-shockwave-flash" 52 | else: 53 | content_type = "text/html" 54 | 55 | start_response('200 OK', [('Content-Type', content_type)]) 56 | return [data] 57 | 58 | if path.startswith("socket.io"): 59 | socketio_manage(environ, {'/tweets': BaseNamespace}) 60 | else: 61 | return not_found(start_response) 62 | 63 | 64 | def not_found(start_response): 65 | start_response('404 Not Found', []) 66 | return ['

      Not Found

      '] 67 | 68 | 69 | if __name__ == '__main__': 70 | user, password = get_credentials() 71 | print 'Listening on port http://0.0.0.0:8080 and on port 10843 (flash policy server)' 72 | server = SocketIOServer(('0.0.0.0', 8080), Application(), 73 | resource="socket.io", policy_server=True, 74 | policy_listener=('0.0.0.0', 10843)) 75 | gevent.spawn(send_tweets, server, user, password) 76 | server.serve_forever() 77 | -------------------------------------------------------------------------------- /examples/twitterstream/twitterstream/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/examples/twitterstream/twitterstream/static/WebSocketMain.swf -------------------------------------------------------------------------------- /examples/twitterstream/twitterstream/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 2em; 3 | color: #333333; 4 | font-size: 16px; 5 | line-height: 18px; 6 | font-family: "Helvetica Neue",Arial,sans-serif; 7 | background-color: #e7e7e7; 8 | } 9 | 10 | #tweets { 11 | width: auto; 12 | } 13 | 14 | #tweets p { 15 | margin: 0; 16 | margin-bottom: 0.7em; 17 | background-color: white; 18 | padding: 0.5em; 19 | float: left; 20 | clear: left; 21 | } 22 | 23 | #tweets p a { 24 | color: #838383; 25 | } 26 | -------------------------------------------------------------------------------- /pip-requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | mock -------------------------------------------------------------------------------- /pip-requirements.txt: -------------------------------------------------------------------------------- 1 | gevent>=1.1rc5 2 | gevent-websocket 3 | six -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages 5 | from setuptools.command.test import test as TestCommand 6 | 7 | CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | def get_reqs(*fns): 10 | lst = [] 11 | for fn in fns: 12 | for package in open(os.path.join(CURRENT_DIR, fn)).readlines(): 13 | package = package.strip() 14 | if not package: 15 | continue 16 | lst.append(package.strip()) 17 | return lst 18 | 19 | class PyTest(TestCommand): 20 | def finalize_options(self): 21 | TestCommand.finalize_options(self) 22 | self.test_args = [] 23 | self.test_suite = True 24 | 25 | def run_tests(self): 26 | #import here, cause outside the eggs aren't loaded 27 | import pytest 28 | pytest.main(self.test_args) 29 | 30 | setup( 31 | name="gevent-socketio", 32 | version="0.3.6", 33 | description=( 34 | "SocketIO server based on the Gevent pywsgi server, " 35 | "a Python network library"), 36 | author="Jeffrey Gelens", 37 | author_email="jeffrey@noppo.pro", 38 | maintainer="Alexandre Bourget", 39 | maintainer_email="alex@bourget.cc", 40 | license="BSD", 41 | url="https://github.com/abourget/gevent-socketio", 42 | download_url="https://github.com/abourget/gevent-socketio", 43 | install_requires=get_reqs('pip-requirements.txt'), 44 | setup_requires=('versiontools >= 1.7'), 45 | cmdclass = {'test': PyTest}, 46 | tests_require=get_reqs('pip-requirements-test.txt'), 47 | packages=find_packages(exclude=["examples", "tests"]), 48 | classifiers=[ 49 | "Development Status :: 4 - Beta", 50 | "License :: OSI Approved :: BSD License", 51 | "Programming Language :: Python", 52 | "Operating System :: MacOS :: MacOS X", 53 | "Operating System :: POSIX", 54 | "Topic :: Internet", 55 | "Topic :: Software Development :: Libraries :: Python Modules", 56 | "Intended Audience :: Developers", 57 | ], 58 | entry_points=""" 59 | 60 | [paste.server_runner] 61 | paster = socketio.server:serve_paste 62 | 63 | """, 64 | ) 65 | -------------------------------------------------------------------------------- /socketio/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = (0, 3, 5) 2 | 3 | import logging 4 | import gevent 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | def socketio_manage(environ, namespaces, request=None, error_handler=None, 10 | json_loads=None, json_dumps=None): 11 | """Main SocketIO management function, call from within your Framework of 12 | choice's view. 13 | 14 | The ``environ`` variable is the WSGI ``environ``. It is used to extract 15 | Socket object from the underlying server (as the 'socketio' key), and will 16 | be attached to both the ``Socket`` and ``Namespace`` objects. 17 | 18 | The ``namespaces`` parameter is a dictionary of the namespace string 19 | representation as key, and the BaseNamespace namespace class descendant as 20 | a value. The empty string ('') namespace is the global namespace. You can 21 | use Socket.GLOBAL_NS to be more explicit. So it would look like: 22 | 23 | .. code-block:: python 24 | 25 | namespaces={'': GlobalNamespace, 26 | '/chat': ChatNamespace} 27 | 28 | The ``request`` object is not required, but will probably be useful to pass 29 | framework-specific things into your Socket and Namespace functions. It will 30 | simply be attached to the Socket and Namespace object (accessible through 31 | ``self.request`` in both cases), and it is not accessed in any case by the 32 | ``gevent-socketio`` library. 33 | 34 | Pass in an ``error_handler`` if you want to override the default 35 | error_handler (which is :func:`socketio.virtsocket.default_error_handler`. 36 | The callable you pass in should have the same signature as the default 37 | error handler. 38 | 39 | The ``json_loads`` and ``json_dumps`` are overrides for the default 40 | ``json.loads`` and ``json.dumps`` function calls. Override these at 41 | the top-most level here. This will affect all sockets created by this 42 | socketio manager, and all namespaces inside. 43 | 44 | This function will block the current "view" or "controller" in your 45 | framework to do the recv/send on the socket, and dispatch incoming messages 46 | to your namespaces. 47 | 48 | This is a simple example using Pyramid: 49 | 50 | .. code-block:: python 51 | 52 | def my_view(request): 53 | socketio_manage(request.environ, {'': GlobalNamespace}, request) 54 | 55 | NOTE: You must understand that this function is going to be called 56 | *only once* per socket opening, *even though* you are using a long 57 | polling mechanism. The subsequent calls (for long polling) will 58 | be hooked directly at the server-level, to interact with the 59 | active ``Socket`` instance. This means you will *not* get access 60 | to the future ``request`` or ``environ`` objects. This is of 61 | particular importance regarding sessions (like Beaker). The 62 | session will be opened once at the opening of the Socket, and not 63 | closed until the socket is closed. You are responsible for 64 | opening and closing the cookie-based session yourself if you want 65 | to keep its data in sync with the rest of your GET/POST calls. 66 | """ 67 | socket = environ['socketio'] 68 | socket._set_environ(environ) 69 | socket._set_namespaces(namespaces) 70 | 71 | if request: 72 | socket._set_request(request) 73 | 74 | if error_handler: 75 | socket._set_error_handler(error_handler) 76 | 77 | if json_loads: 78 | socket._set_json_loads(json_loads) 79 | if json_dumps: 80 | socket._set_json_dumps(json_dumps) 81 | 82 | receiver_loop = socket._spawn_receiver_loop() 83 | 84 | gevent.joinall([receiver_loop]) 85 | 86 | # TODO: double check, what happens to the WSGI request here ? it vanishes ? 87 | return 88 | -------------------------------------------------------------------------------- /socketio/defaultjson.py: -------------------------------------------------------------------------------- 1 | ### default json loaders 2 | try: 3 | import simplejson as json 4 | json_decimal_args = {"use_decimal": True} # pragma: no cover 5 | except ImportError: 6 | import json 7 | import decimal 8 | 9 | class DecimalEncoder(json.JSONEncoder): 10 | def default(self, o): 11 | if isinstance(o, decimal.Decimal): 12 | return float(o) 13 | return super(DecimalEncoder, self).default(o) 14 | json_decimal_args = {"cls": DecimalEncoder} 15 | 16 | def default_json_dumps(data): 17 | return json.dumps(data, separators=(',', ':'), 18 | sort_keys=True, 19 | **json_decimal_args) 20 | 21 | def default_json_loads(data): 22 | return json.loads(data) 23 | -------------------------------------------------------------------------------- /socketio/mixins.py: -------------------------------------------------------------------------------- 1 | """ 2 | These are general-purpose Mixins -- for use with Namespaces -- that are 3 | generally useful for most simple projects, e.g. Rooms, Broadcast. 4 | 5 | You'll likely want to create your own Mixins. 6 | """ 7 | 8 | import six 9 | 10 | 11 | class RoomsMixin(object): 12 | def __init__(self, *args, **kwargs): 13 | super(RoomsMixin, self).__init__(*args, **kwargs) 14 | if 'rooms' not in self.session: 15 | self.session['rooms'] = set() # a set of simple strings 16 | 17 | def join(self, room): 18 | """Lets a user join a room on a specific Namespace.""" 19 | self.session['rooms'].add(self._get_room_name(room)) 20 | 21 | def leave(self, room): 22 | """Lets a user leave a room on a specific Namespace.""" 23 | self.session['rooms'].remove(self._get_room_name(room)) 24 | 25 | def _get_room_name(self, room): 26 | return self.ns_name + '_' + room 27 | 28 | def emit_to_room(self, room, event, *args): 29 | """This is sent to all in the room (in this particular Namespace)""" 30 | pkt = dict(type="event", 31 | name=event, 32 | args=args, 33 | endpoint=self.ns_name) 34 | room_name = self._get_room_name(room) 35 | for sessid, socket in six.iteritems(self.socket.server.sockets): 36 | if 'rooms' not in socket.session: 37 | continue 38 | if room_name in socket.session['rooms'] and self.socket != socket: 39 | socket.send_packet(pkt) 40 | 41 | 42 | class BroadcastMixin(object): 43 | """Mix in this class with your Namespace to have a broadcast event method. 44 | 45 | Use it like this: 46 | class MyNamespace(BaseNamespace, BroadcastMixin): 47 | def on_chatmsg(self, event): 48 | self.broadcast_event('chatmsg', event) 49 | """ 50 | def broadcast_event(self, event, *args): 51 | """ 52 | This is sent to all in the sockets in this particular Namespace, 53 | including itself. 54 | """ 55 | pkt = dict(type="event", 56 | name=event, 57 | args=args, 58 | endpoint=self.ns_name) 59 | 60 | for sessid, socket in six.iteritems(self.socket.server.sockets): 61 | socket.send_packet(pkt) 62 | 63 | def broadcast_event_not_me(self, event, *args): 64 | """ 65 | This is sent to all in the sockets in this particular Namespace, 66 | except itself. 67 | """ 68 | pkt = dict(type="event", 69 | name=event, 70 | args=args, 71 | endpoint=self.ns_name) 72 | 73 | for sessid, socket in six.iteritems(self.socket.server.sockets): 74 | if socket is not self.socket: 75 | socket.send_packet(pkt) 76 | -------------------------------------------------------------------------------- /socketio/packet.py: -------------------------------------------------------------------------------- 1 | import six 2 | from socketio.defaultjson import default_json_dumps, default_json_loads 3 | 4 | MSG_TYPES = { 5 | 'disconnect': 0, 6 | 'connect': 1, 7 | 'heartbeat': 2, 8 | 'message': 3, 9 | 'json': 4, 10 | 'event': 5, 11 | 'ack': 6, 12 | 'error': 7, 13 | 'noop': 8, 14 | } 15 | 16 | MSG_VALUES = dict((v, k) for k, v in six.iteritems(MSG_TYPES)) 17 | 18 | ERROR_REASONS = { 19 | 'transport not supported': 0, 20 | 'client not handshaken': 1, 21 | 'unauthorized': 2 22 | } 23 | 24 | REASONS_VALUES = dict((v, k) for k, v in six.iteritems(ERROR_REASONS)) 25 | 26 | ERROR_ADVICES = { 27 | 'reconnect': 0, 28 | } 29 | 30 | ADVICES_VALUES = dict((v, k) for k, v in six.iteritems(ERROR_ADVICES)) 31 | 32 | socketio_packet_attributes = ['type', 'name', 'data', 'endpoint', 'args', 33 | 'ackId', 'reason', 'advice', 'qs', 'id'] 34 | 35 | 36 | def encode(data, json_dumps=default_json_dumps): 37 | """ 38 | Encode an attribute dict into a byte string. 39 | """ 40 | payload = '' 41 | msg = str(MSG_TYPES[data['type']]) 42 | 43 | if msg in ['0', '1']: 44 | # '1::' [path] [query] 45 | msg += '::' + data['endpoint'] 46 | if 'qs' in data and data['qs'] != '': 47 | msg += ':' + data['qs'] 48 | 49 | elif msg == '2': 50 | # heartbeat 51 | msg += '::' 52 | 53 | elif msg in ['3', '4', '5']: 54 | # '3:' [id ('+')] ':' [endpoint] ':' [data] 55 | # '4:' [id ('+')] ':' [endpoint] ':' [json] 56 | # '5:' [id ('+')] ':' [endpoint] ':' [json encoded event] 57 | # The message id is an incremental integer, required for ACKs. 58 | # If the message id is followed by a +, the ACK is not handled by 59 | # socket.io, but by the user instead. 60 | if msg == '3': 61 | payload = data['data'] 62 | if msg == '4': 63 | payload = json_dumps(data['data']) 64 | if msg == '5': 65 | d = {} 66 | d['name'] = data['name'] 67 | if 'args' in data and data['args'] != []: 68 | d['args'] = data['args'] 69 | payload = json_dumps(d) 70 | if 'id' in data: 71 | msg += ':' + str(data['id']) 72 | if data['ack'] == 'data': 73 | msg += '+' 74 | msg += ':' 75 | else: 76 | msg += '::' 77 | if 'endpoint' not in data: 78 | data['endpoint'] = '' 79 | if payload != '': 80 | msg += data['endpoint'] + ':' + payload 81 | else: 82 | msg += data['endpoint'] 83 | 84 | elif msg == '6': 85 | # '6:::' [id] '+' [data] 86 | msg += '::' + data.get('endpoint', '') + ':' + str(data['ackId']) 87 | if 'args' in data and data['args'] != []: 88 | msg += '+' + json_dumps(data['args']) 89 | 90 | elif msg == '7': 91 | # '7::' [endpoint] ':' [reason] '+' [advice] 92 | msg += ':::' 93 | if 'reason' in data and data['reason'] != '': 94 | msg += str(ERROR_REASONS[data['reason']]) 95 | if 'advice' in data and data['advice'] != '': 96 | msg += '+' + str(ERROR_ADVICES[data['advice']]) 97 | msg += data['endpoint'] 98 | 99 | # NoOp, used to close a poll after the polling duration time 100 | elif msg == '8': 101 | msg += '::' 102 | 103 | return msg 104 | 105 | 106 | def decode(rawstr, json_loads=default_json_loads): 107 | """ 108 | Decode a rawstr packet arriving from the socket into a dict. 109 | """ 110 | decoded_msg = {} 111 | try: 112 | # Handle decoding in Python<3. 113 | rawstr = rawstr.decode('utf-8') 114 | except AttributeError: 115 | pass 116 | split_data = rawstr.split(":", 3) 117 | msg_type = split_data[0] 118 | msg_id = split_data[1] 119 | endpoint = split_data[2] 120 | 121 | data = '' 122 | 123 | if msg_id != '': 124 | if "+" in msg_id: 125 | msg_id = msg_id.split('+')[0] 126 | decoded_msg['id'] = int(msg_id) 127 | decoded_msg['ack'] = 'data' 128 | else: 129 | decoded_msg['id'] = int(msg_id) 130 | decoded_msg['ack'] = True 131 | 132 | # common to every message 133 | msg_type_id = int(msg_type) 134 | if msg_type_id in MSG_VALUES: 135 | decoded_msg['type'] = MSG_VALUES[int(msg_type)] 136 | else: 137 | raise Exception("Unknown message type: %s" % msg_type) 138 | 139 | decoded_msg['endpoint'] = endpoint 140 | 141 | if len(split_data) > 3: 142 | data = split_data[3] 143 | 144 | if msg_type == "0": # disconnect 145 | pass 146 | 147 | elif msg_type == "1": # connect 148 | decoded_msg['qs'] = data 149 | 150 | elif msg_type == "2": # heartbeat 151 | pass 152 | 153 | elif msg_type == "3": # message 154 | decoded_msg['data'] = data 155 | 156 | elif msg_type == "4": # json msg 157 | decoded_msg['data'] = json_loads(data) 158 | 159 | elif msg_type == "5": # event 160 | try: 161 | data = json_loads(data) 162 | except ValueError: 163 | print("Invalid JSON event message", data) 164 | decoded_msg['args'] = [] 165 | else: 166 | decoded_msg['name'] = data.pop('name') 167 | if 'args' in data: 168 | decoded_msg['args'] = data['args'] 169 | else: 170 | decoded_msg['args'] = [] 171 | 172 | elif msg_type == "6": # ack 173 | if '+' in data: 174 | ackId, data = data.split('+') 175 | decoded_msg['ackId'] = int(ackId) 176 | decoded_msg['args'] = json_loads(data) 177 | else: 178 | decoded_msg['ackId'] = int(data) 179 | decoded_msg['args'] = [] 180 | 181 | elif msg_type == "7": # error 182 | if '+' in data: 183 | reason, advice = data.split('+') 184 | decoded_msg['reason'] = REASONS_VALUES[int(reason)] 185 | decoded_msg['advice'] = ADVICES_VALUES[int(advice)] 186 | else: 187 | decoded_msg['advice'] = '' 188 | if data != '': 189 | decoded_msg['reason'] = REASONS_VALUES[int(data)] 190 | else: 191 | decoded_msg['reason'] = '' 192 | 193 | elif msg_type == "8": # noop 194 | pass 195 | 196 | return decoded_msg 197 | -------------------------------------------------------------------------------- /socketio/policyserver.py: -------------------------------------------------------------------------------- 1 | from gevent.server import StreamServer 2 | import socket 3 | 4 | __all__ = ['FlashPolicyServer'] 5 | 6 | 7 | class FlashPolicyServer(StreamServer): 8 | policyrequest = "" 9 | policy = """ 10 | """ 11 | 12 | def __init__(self, listener=None, backlog=None): 13 | if listener is None: 14 | listener = ('0.0.0.0', 10843) 15 | StreamServer.__init__(self, listener=listener, backlog=backlog) 16 | 17 | def handle(self, sock, address): 18 | # send and read functions should not wait longer than three seconds 19 | sock.settimeout(3) 20 | try: 21 | # try to receive at most 128 bytes (`POLICYREQUEST` is shorter) 22 | input = sock.recv(128) 23 | if input.startswith(FlashPolicyServer.policyrequest): 24 | sock.sendall(FlashPolicyServer.policy) 25 | except socket.timeout: 26 | pass 27 | sock.close() 28 | -------------------------------------------------------------------------------- /socketio/sdjango.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from socketio import socketio_manage 4 | import django 5 | from django.conf.urls import url 6 | from django.http import HttpResponse 7 | from django.views.decorators.csrf import csrf_exempt 8 | 9 | try: 10 | # Django versions >= 1.9 11 | from django.utils.module_loading import import_module 12 | except ImportError: 13 | # Django versions < 1.9 14 | from django.utils.importlib import import_module 15 | 16 | 17 | SOCKETIO_NS = {} 18 | 19 | 20 | LOADING_SOCKETIO = False 21 | 22 | 23 | def autodiscover(): 24 | """ 25 | Auto-discover INSTALLED_APPS sockets.py modules and fail silently when 26 | not present. NOTE: socketio_autodiscover was inspired/copied from 27 | django.contrib.admin autodiscover 28 | """ 29 | global LOADING_SOCKETIO 30 | if LOADING_SOCKETIO: 31 | return 32 | LOADING_SOCKETIO = True 33 | 34 | import imp 35 | from django.conf import settings 36 | 37 | for app in settings.INSTALLED_APPS: 38 | 39 | try: 40 | app_path = import_module(app).__path__ 41 | except AttributeError: 42 | continue 43 | 44 | try: 45 | imp.find_module('sockets', app_path) 46 | except ImportError: 47 | continue 48 | 49 | import_module("%s.sockets" % app) 50 | 51 | LOADING_SOCKETIO = False 52 | 53 | 54 | class namespace(object): 55 | def __init__(self, name=''): 56 | self.name = name 57 | 58 | def __call__(self, handler): 59 | SOCKETIO_NS[self.name] = handler 60 | return handler 61 | 62 | 63 | @csrf_exempt 64 | def socketio(request): 65 | try: 66 | socketio_manage(request.environ, SOCKETIO_NS, request) 67 | except: 68 | logging.getLogger("socketio").error("Exception while handling socketio connection", exc_info=True) 69 | return HttpResponse("") 70 | 71 | 72 | if django.VERSION >= (1, 8,): 73 | urls = [url(r'', socketio)] 74 | else: 75 | from django.conf.urls import patterns 76 | urls = patterns("", (r'', socketio)) 77 | -------------------------------------------------------------------------------- /socketio/server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | 4 | from socket import error 5 | 6 | from gevent.pywsgi import WSGIServer 7 | 8 | from socketio.handler import SocketIOHandler 9 | from socketio.policyserver import FlashPolicyServer 10 | from socketio.virtsocket import Socket 11 | from geventwebsocket.handler import WebSocketHandler 12 | 13 | __all__ = ['SocketIOServer'] 14 | 15 | 16 | class SocketIOServer(WSGIServer): 17 | """A WSGI Server with a resource that acts like an SocketIO.""" 18 | 19 | def __init__(self, *args, **kwargs): 20 | """This is just like the standard WSGIServer __init__, except with a 21 | few additional ``kwargs``: 22 | 23 | :param resource: The URL which has to be identified as a 24 | socket.io request. Defaults to the /socket.io/ URL. 25 | 26 | :param transports: Optional list of transports to allow. List of 27 | strings, each string should be one of 28 | handler.SocketIOHandler.handler_types. 29 | 30 | :param policy_server: Boolean describing whether or not to use the 31 | Flash policy server. Default True. 32 | 33 | :param policy_listener: A tuple containing (host, port) for the 34 | policy server. This is optional and used only if policy server 35 | is set to true. The default value is 0.0.0.0:843 36 | 37 | :param heartbeat_interval: int The timeout for the server, we 38 | should receive a heartbeat from the client within this 39 | interval. This should be less than the 40 | ``heartbeat_timeout``. 41 | 42 | :param heartbeat_timeout: int The timeout for the client when 43 | it should send a new heartbeat to the server. This value 44 | is sent to the client after a successful handshake. 45 | 46 | :param close_timeout: int The timeout for the client, when it 47 | closes the connection it still X amounts of seconds to do 48 | re open of the connection. This value is sent to the 49 | client after a successful handshake. 50 | 51 | :param log_file: str The file in which you want the PyWSGI 52 | server to write its access log. If not specified, it 53 | is sent to `stderr` (with gevent 0.13). 54 | 55 | """ 56 | self.sockets = {} 57 | if 'namespace' in kwargs: 58 | print("DEPRECATION WARNING: use resource instead of namespace") 59 | self.resource = kwargs.pop('namespace', 'socket.io') 60 | else: 61 | self.resource = kwargs.pop('resource', 'socket.io') 62 | 63 | self.transports = kwargs.pop('transports', None) 64 | 65 | if kwargs.pop('policy_server', True): 66 | try: 67 | address = args[0][0] 68 | except TypeError: 69 | try: 70 | address = args[0].address[0] 71 | except AttributeError: 72 | address = args[0].cfg_addr[0] 73 | policylistener = kwargs.pop('policy_listener', (address, 10843)) 74 | self.policy_server = FlashPolicyServer(policylistener) 75 | else: 76 | self.policy_server = None 77 | 78 | # Extract other config options 79 | self.config = { 80 | 'heartbeat_timeout': 60, 81 | 'close_timeout': 60, 82 | 'heartbeat_interval': 25, 83 | } 84 | for f in ('heartbeat_timeout', 'heartbeat_interval', 'close_timeout'): 85 | if f in kwargs: 86 | self.config[f] = int(kwargs.pop(f)) 87 | 88 | if not 'handler_class' in kwargs: 89 | kwargs['handler_class'] = SocketIOHandler 90 | 91 | 92 | if not 'ws_handler_class' in kwargs: 93 | self.ws_handler_class = WebSocketHandler 94 | else: 95 | self.ws_handler_class = kwargs.pop('ws_handler_class') 96 | 97 | log_file = kwargs.pop('log_file', None) 98 | if log_file: 99 | kwargs['log'] = open(log_file, 'a') 100 | 101 | super(SocketIOServer, self).__init__(*args, **kwargs) 102 | 103 | def start_accepting(self): 104 | if self.policy_server is not None: 105 | try: 106 | if not self.policy_server.started: 107 | self.policy_server.start() 108 | except error as ex: 109 | sys.stderr.write( 110 | 'FAILED to start flash policy server: %s\n' % (ex, )) 111 | except Exception: 112 | traceback.print_exc() 113 | sys.stderr.write('FAILED to start flash policy server.\n\n') 114 | super(SocketIOServer, self).start_accepting() 115 | 116 | def stop(self, timeout=None): 117 | if self.policy_server is not None: 118 | self.policy_server.stop() 119 | super(SocketIOServer, self).stop(timeout=timeout) 120 | 121 | def handle(self, socket, address): 122 | # Pass in the config about timeouts, heartbeats, also... 123 | handler = self.handler_class(self.config, socket, address, self) 124 | handler.handle() 125 | 126 | def get_socket(self, sessid=''): 127 | """Return an existing or new client Socket.""" 128 | 129 | socket = self.sockets.get(sessid) 130 | 131 | if sessid and not socket: 132 | return None # you ask for a session that doesn't exist! 133 | if socket is None: 134 | socket = Socket(self, self.config) 135 | self.sockets[socket.sessid] = socket 136 | else: 137 | socket.incr_hits() 138 | 139 | return socket 140 | 141 | 142 | def serve(app, **kw): 143 | _quiet = kw.pop('_quiet', False) 144 | _resource = kw.pop('resource', 'socket.io') 145 | if not _quiet: # pragma: no cover 146 | # idempotent if logging has already been set up 147 | import logging 148 | logging.basicConfig() 149 | 150 | host = kw.pop('host', '127.0.0.1') 151 | port = int(kw.pop('port', 6543)) 152 | 153 | transports = kw.pop('transports', None) 154 | if transports: 155 | transports = [x.strip() for x in transports.split(',')] 156 | 157 | policy_server = kw.pop('policy_server', False) 158 | if policy_server in (True, 'True', 'true', 'enable', 'yes', 'on', '1'): 159 | policy_server = True 160 | policy_listener_host = kw.pop('policy_listener_host', host) 161 | policy_listener_port = int(kw.pop('policy_listener_port', 10843)) 162 | kw['policy_listener'] = (policy_listener_host, policy_listener_port) 163 | else: 164 | policy_server = False 165 | 166 | server = SocketIOServer((host, port), 167 | app, 168 | resource=_resource, 169 | transports=transports, 170 | policy_server=policy_server, 171 | **kw) 172 | if not _quiet: 173 | print('serving on http://%s:%s' % (host, port)) 174 | server.serve_forever() 175 | 176 | 177 | def serve_paste(app, global_conf, **kw): 178 | """pserve / paster serve / waitress replacement / integration 179 | 180 | You can pass as parameters: 181 | 182 | transports = websockets, xhr-multipart, xhr-longpolling, etc... 183 | policy_server = True 184 | """ 185 | serve(app, **kw) 186 | return 0 187 | -------------------------------------------------------------------------------- /socketio/sgunicorn.py: -------------------------------------------------------------------------------- 1 | import os 2 | import gevent 3 | import time 4 | 5 | from gevent.pool import Pool 6 | from gevent.server import StreamServer 7 | 8 | from gunicorn.workers.ggevent import GeventPyWSGIWorker 9 | from gunicorn.workers.ggevent import PyWSGIHandler 10 | from gunicorn.workers.ggevent import GeventResponse 11 | from gunicorn import version_info as gunicorn_version 12 | from socketio.server import SocketIOServer 13 | from socketio.handler import SocketIOHandler 14 | 15 | from geventwebsocket.handler import WebSocketHandler 16 | 17 | from datetime import datetime 18 | from functools import partial 19 | 20 | 21 | class GunicornWSGIHandler(PyWSGIHandler, SocketIOHandler): 22 | pass 23 | 24 | 25 | class GunicornWebSocketWSGIHandler(WebSocketHandler): 26 | def log_request(self): 27 | start = datetime.fromtimestamp(self.time_start) 28 | finish = datetime.fromtimestamp(self.time_finish) 29 | response_time = finish - start 30 | resp = GeventResponse(self.status, [], 31 | self.response_length) 32 | req_headers = [h.split(":", 1) for h in self.headers.headers] 33 | self.server.log.access( 34 | resp, req_headers, self.environ, response_time) 35 | 36 | 37 | class GeventSocketIOBaseWorker(GeventPyWSGIWorker): 38 | """ The base gunicorn worker class """ 39 | 40 | transports = None 41 | 42 | def __init__(self, age, ppid, socket, app, timeout, cfg, log): 43 | if os.environ.get('POLICY_SERVER', None) is None: 44 | if self.policy_server: 45 | os.environ['POLICY_SERVER'] = 'true' 46 | else: 47 | self.policy_server = False 48 | 49 | super(GeventSocketIOBaseWorker, self).__init__( 50 | age, ppid, socket, app, timeout, cfg, log) 51 | 52 | def run(self): 53 | if gunicorn_version >= (0, 17, 0): 54 | servers = [] 55 | ssl_args = {} 56 | 57 | if self.cfg.is_ssl: 58 | ssl_args = dict( 59 | server_side=True, 60 | do_handshake_on_connect=False, 61 | **self.cfg.ssl_options 62 | ) 63 | 64 | for s in self.sockets: 65 | s.setblocking(1) 66 | pool = Pool(self.worker_connections) 67 | if self.server_class is not None: 68 | self.server_class.base_env['wsgi.multiprocess'] = \ 69 | self.cfg.workers > 1 70 | 71 | server = self.server_class( 72 | s, 73 | application=self.wsgi, 74 | spawn=pool, 75 | resource=self.resource, 76 | log=self.log, 77 | policy_server=self.policy_server, 78 | handler_class=self.wsgi_handler, 79 | ws_handler_class=self.ws_wsgi_handler, 80 | **ssl_args 81 | ) 82 | else: 83 | hfun = partial(self.handle, s) 84 | server = StreamServer( 85 | s, handle=hfun, spawn=pool, **ssl_args) 86 | 87 | server.start() 88 | servers.append(server) 89 | 90 | pid = os.getpid() 91 | try: 92 | while self.alive: 93 | self.notify() 94 | 95 | if pid == os.getpid() and self.ppid != os.getppid(): 96 | self.log.info( 97 | "Parent changed, shutting down: %s", self) 98 | break 99 | 100 | gevent.sleep(1.0) 101 | 102 | except KeyboardInterrupt: 103 | pass 104 | 105 | try: 106 | # Stop accepting requests 107 | [server.stop_accepting() for server in servers] 108 | 109 | # Handle current requests until graceful_timeout 110 | ts = time.time() 111 | while time.time() - ts <= self.cfg.graceful_timeout: 112 | accepting = 0 113 | for server in servers: 114 | if server.pool.free_count() != server.pool.size: 115 | accepting += 1 116 | 117 | if not accepting: 118 | return 119 | 120 | self.notify() 121 | gevent.sleep(1.0) 122 | 123 | # Force kill all active the handlers 124 | self.log.warning("Worker graceful timeout (pid:%s)" % self.pid) 125 | [server.stop(timeout=1) for server in servers] 126 | except: 127 | pass 128 | else: 129 | self.socket.setblocking(1) 130 | pool = Pool(self.worker_connections) 131 | self.server_class.base_env['wsgi.multiprocess'] = \ 132 | self.cfg.workers > 1 133 | 134 | server = self.server_class( 135 | self.socket, 136 | application=self.wsgi, 137 | spawn=pool, 138 | resource=self.resource, 139 | log=self.log, 140 | policy_server=self.policy_server, 141 | handler_class=self.wsgi_handler, 142 | ws_handler_class=self.ws_wsgi_handler, 143 | ) 144 | 145 | server.start() 146 | pid = os.getpid() 147 | 148 | try: 149 | while self.alive: 150 | self.notify() 151 | 152 | if pid == os.getpid() and self.ppid != os.getppid(): 153 | self.log.info( 154 | "Parent changed, shutting down: %s", self) 155 | break 156 | 157 | gevent.sleep(1.0) 158 | 159 | except KeyboardInterrupt: 160 | pass 161 | 162 | try: 163 | # Stop accepting requests 164 | server.kill() 165 | 166 | # Handle current requests until graceful_timeout 167 | ts = time.time() 168 | while time.time() - ts <= self.cfg.graceful_timeout: 169 | if server.pool.free_count() == server.pool.size: 170 | return # all requests was handled 171 | 172 | self.notify() 173 | gevent.sleep(1.0) 174 | 175 | # Force kill all active the handlers 176 | self.log.warning("Worker graceful timeout (pid:%s)" % self.pid) 177 | server.stop(timeout=1) 178 | except: 179 | pass 180 | 181 | 182 | class GeventSocketIOWorker(GeventSocketIOBaseWorker): 183 | """ 184 | Default gunicorn worker utilizing gevent 185 | 186 | Uses the namespace 'socket.io' and defaults to the flash policy server 187 | being disabled. 188 | """ 189 | server_class = SocketIOServer 190 | wsgi_handler = GunicornWSGIHandler 191 | ws_wsgi_handler = GunicornWebSocketWSGIHandler 192 | # We need to define a namespace for the server, it would be nice if this 193 | # was a configuration option, will probably end up how this implemented, 194 | # for now this is just a proof of concept to make sure this will work 195 | resource = 'socket.io' 196 | policy_server = True 197 | 198 | 199 | class NginxGeventSocketIOWorker(GeventSocketIOWorker): 200 | """ 201 | Worker which will not attempt to connect via websocket transport 202 | 203 | Nginx is not compatible with websockets and therefore will not add the 204 | wsgi.websocket key to the wsgi environment. 205 | """ 206 | transports = ['xhr-polling'] 207 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/tests/__init__.py -------------------------------------------------------------------------------- /tests/jstests/jstests.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey; monkey.patch_all() 2 | 3 | from socketio import socketio_manage 4 | from socketio.server import SocketIOServer 5 | from socketio.namespace import BaseNamespace 6 | 7 | 8 | TestHtml = """ 9 | 10 | 11 | 12 | 13 | Gevent-socketio Tests 14 | 15 | 16 | 17 | 18 |
      19 | 20 | 21 | 22 | 23 | 24 | """ 25 | 26 | class TestNamespace(BaseNamespace): 27 | def on_requestack(self, val): 28 | return val, "ack" 29 | 30 | def on_requestackonevalue(self, val): 31 | return val 32 | 33 | class Application(object): 34 | def __init__(self): 35 | self.buffer = [] 36 | 37 | def __call__(self, environ, start_response): 38 | path = environ['PATH_INFO'].strip('/') 39 | 40 | if not path: 41 | start_response('200 OK', [('Content-Type', 'text/html')]) 42 | return [TestHtml] 43 | 44 | if path.startswith('static/') or path.startswith('tests/'): 45 | try: 46 | data = open(path).read() 47 | except Exception: 48 | return not_found(start_response) 49 | 50 | if path.endswith(".js"): 51 | content_type = "text/javascript" 52 | elif path.endswith(".css"): 53 | content_type = "text/css" 54 | elif path.endswith(".swf"): 55 | content_type = "application/x-shockwave-flash" 56 | else: 57 | content_type = "text/html" 58 | 59 | start_response('200 OK', [('Content-Type', content_type)]) 60 | return [data] 61 | 62 | if path.startswith("socket.io"): 63 | socketio_manage(environ, {'/test': TestNamespace}) 64 | else: 65 | return not_found(start_response) 66 | 67 | 68 | def not_found(start_response): 69 | start_response('404 Not Found', []) 70 | return ['

      Not Found

      '] 71 | 72 | 73 | if __name__ == '__main__': 74 | print 'Listening on port 8080 and on port 10843 (flash policy server)' 75 | SocketIOServer(('0.0.0.0', 8080), Application(), resource="socket.io", policy_server=True).serve_forever() 76 | -------------------------------------------------------------------------------- /tests/jstests/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abourget/gevent-socketio/1cdb1594a315326987a17ce0924ea448a82fab01/tests/jstests/static/WebSocketMain.swf -------------------------------------------------------------------------------- /tests/jstests/static/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.10.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | } 71 | 72 | #qunit-userAgent { 73 | padding: 0.5em 0 0.5em 2.5em; 74 | background-color: #2b81af; 75 | color: #fff; 76 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 77 | } 78 | 79 | 80 | /** Tests: Pass/Fail */ 81 | 82 | #qunit-tests { 83 | list-style-position: inside; 84 | } 85 | 86 | #qunit-tests li { 87 | padding: 0.4em 0.5em 0.4em 2.5em; 88 | border-bottom: 1px solid #fff; 89 | list-style-position: inside; 90 | } 91 | 92 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 93 | display: none; 94 | } 95 | 96 | #qunit-tests li strong { 97 | cursor: pointer; 98 | } 99 | 100 | #qunit-tests li a { 101 | padding: 0.5em; 102 | color: #c2ccd1; 103 | text-decoration: none; 104 | } 105 | #qunit-tests li a:hover, 106 | #qunit-tests li a:focus { 107 | color: #000; 108 | } 109 | 110 | #qunit-tests ol { 111 | margin-top: 0.5em; 112 | padding: 0.5em; 113 | 114 | background-color: #fff; 115 | 116 | border-radius: 5px; 117 | -moz-border-radius: 5px; 118 | -webkit-border-radius: 5px; 119 | } 120 | 121 | #qunit-tests table { 122 | border-collapse: collapse; 123 | margin-top: .2em; 124 | } 125 | 126 | #qunit-tests th { 127 | text-align: right; 128 | vertical-align: top; 129 | padding: 0 .5em 0 0; 130 | } 131 | 132 | #qunit-tests td { 133 | vertical-align: top; 134 | } 135 | 136 | #qunit-tests pre { 137 | margin: 0; 138 | white-space: pre-wrap; 139 | word-wrap: break-word; 140 | } 141 | 142 | #qunit-tests del { 143 | background-color: #e0f2be; 144 | color: #374e0c; 145 | text-decoration: none; 146 | } 147 | 148 | #qunit-tests ins { 149 | background-color: #ffcaca; 150 | color: #500; 151 | text-decoration: none; 152 | } 153 | 154 | /*** Test Counts */ 155 | 156 | #qunit-tests b.counts { color: black; } 157 | #qunit-tests b.passed { color: #5E740B; } 158 | #qunit-tests b.failed { color: #710909; } 159 | 160 | #qunit-tests li li { 161 | padding: 5px; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #3c510c; 171 | background-color: #fff; 172 | border-left: 10px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 10px solid #EE5757; 189 | white-space: pre; 190 | } 191 | 192 | #qunit-tests > li:last-child { 193 | border-radius: 0 0 5px 5px; 194 | -moz-border-radius: 0 0 5px 5px; 195 | -webkit-border-bottom-right-radius: 5px; 196 | -webkit-border-bottom-left-radius: 5px; 197 | } 198 | 199 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 200 | #qunit-tests .fail .test-name, 201 | #qunit-tests .fail .module-name { color: #000000; } 202 | 203 | #qunit-tests .fail .test-actual { color: #EE5757; } 204 | #qunit-tests .fail .test-expected { color: green; } 205 | 206 | #qunit-banner.qunit-fail { background-color: #EE5757; } 207 | 208 | 209 | /** Result */ 210 | 211 | #qunit-testresult { 212 | padding: 0.5em 0.5em 0.5em 2.5em; 213 | 214 | color: #2b81af; 215 | background-color: #D2E0E6; 216 | 217 | border-bottom: 1px solid white; 218 | } 219 | #qunit-testresult .module-name { 220 | font-weight: bold; 221 | } 222 | 223 | /** Fixture */ 224 | 225 | #qunit-fixture { 226 | position: absolute; 227 | top: -10000px; 228 | left: -10000px; 229 | width: 1000px; 230 | height: 1000px; 231 | } 232 | -------------------------------------------------------------------------------- /tests/jstests/tests/suite.js: -------------------------------------------------------------------------------- 1 | testTransport = function(transports) 2 | { 3 | var prefix = "socketio - " + transports + ": "; 4 | 5 | connect = function(transports) 6 | { 7 | // Force transport 8 | io.transports = transports; 9 | deepEqual(io.transports, transports, "Force transports"); 10 | var options = { 'force new connection': true } 11 | return io.connect('/test', options); 12 | } 13 | 14 | asyncTest(prefix + "Connect", function() { 15 | expect(4); 16 | test = connect(transports); 17 | test.on('connect', function () { 18 | ok( true, "Connected with transport: " + test.socket.transport.name ); 19 | test.disconnect(); 20 | }); 21 | test.on('disconnect', function (reason) { 22 | ok( true, "Disconnected - " + reason ); 23 | test.socket.disconnect(); 24 | start(); 25 | }); 26 | test.on('connect_failed', function () { 27 | ok( false, "Connection failed"); 28 | start(); 29 | }); 30 | }); 31 | 32 | asyncTest(prefix + "Emit with ack", function() { 33 | expect(3); 34 | test = connect(transports); 35 | test.emit('requestack', 1, function (val1, val2) { 36 | equal(val1, 1); 37 | equal(val2, "ack"); 38 | test.disconnect(); 39 | test.socket.disconnect(); 40 | start(); 41 | }); 42 | }); 43 | 44 | asyncTest(prefix + "Emit with ack one return value", function() { 45 | expect(2); 46 | test = connect(transports); 47 | test.emit('requestackonevalue', 1, function (val1) { 48 | equal(val1, 1); 49 | test.disconnect(); 50 | test.socket.disconnect(); 51 | start(); 52 | }); 53 | }); 54 | } 55 | 56 | transports = [io.transports]; 57 | 58 | // Validate individual transports 59 | for(t in io.transports) 60 | { 61 | if(io.Transport[io.transports[t]].check()) { 62 | transports.push([io.transports[t]]); 63 | } 64 | } 65 | for(t in transports) 66 | { 67 | testTransport(transports[t]) 68 | } 69 | -------------------------------------------------------------------------------- /tests/test_socket.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, main 2 | 3 | from socketio.namespace import BaseNamespace 4 | from socketio.virtsocket import Socket 5 | 6 | 7 | class MockSocketIOServer(object): 8 | """Mock a SocketIO server""" 9 | def __init__(self, *args, **kwargs): 10 | self.sockets = {} 11 | 12 | def get_socket(self, socket_id=''): 13 | return self.sockets.get(socket_id) 14 | 15 | 16 | class MockSocketIOhandler(object): 17 | """Mock a SocketIO handler""" 18 | def __init__(self, *args, **kwargs): 19 | self.server = MockSocketIOServer() 20 | 21 | 22 | class MockNamespace(BaseNamespace): 23 | """Mock a Namespace from the namespace module""" 24 | pass 25 | 26 | 27 | class TestSocketAPI(TestCase): 28 | """Test the virtual Socket object""" 29 | 30 | def setUp(self): 31 | self.server = MockSocketIOServer() 32 | self.virtsocket = Socket(self.server, {}) 33 | 34 | def test__set_namespaces(self): 35 | namespaces = {'/': MockNamespace} 36 | self.virtsocket._set_namespaces(namespaces) 37 | self.assertEqual(self.virtsocket.namespaces, namespaces) 38 | 39 | def test__set_request(self): 40 | request = {'test': 'a'} 41 | self.virtsocket._set_request(request) 42 | self.assertEqual(self.virtsocket.request, request) 43 | 44 | def test__set_environ(self): 45 | environ = [] 46 | self.virtsocket._set_environ(environ) 47 | self.assertEqual(self.virtsocket.environ, environ) 48 | 49 | def test_connected_property(self): 50 | # not connected 51 | self.assertFalse(self.virtsocket.connected) 52 | 53 | # connected 54 | self.virtsocket.state = "CONNECTED" 55 | self.assertTrue(self.virtsocket.connected) 56 | 57 | def test_incr_hist(self): 58 | self.virtsocket.state = "CONNECTED" 59 | 60 | # cause a hit 61 | self.virtsocket.incr_hits() 62 | self.assertEqual(self.virtsocket.hits, 1) 63 | self.assertEqual(self.virtsocket.state, self.virtsocket.STATE_CONNECTED) 64 | 65 | def test_disconnect(self): 66 | # kill connected socket 67 | self.virtsocket.state = "CONNECTED" 68 | self.virtsocket.active_ns = {'test' : MockNamespace({'socketio': self.virtsocket}, 'test')} 69 | self.virtsocket.disconnect() 70 | self.assertEqual(self.virtsocket.state, "DISCONNECTING") 71 | self.assertEqual(self.virtsocket.active_ns, {}) 72 | 73 | def test_kill(self): 74 | # kill connected socket 75 | self.virtsocket.state = "CONNECTED" 76 | self.virtsocket.active_ns = {'test' : MockNamespace({'socketio': self.virtsocket}, 'test')} 77 | self.virtsocket.kill() 78 | self.assertEqual(self.virtsocket.state, "DISCONNECTING") 79 | 80 | def test__receiver_loop(self): 81 | """Test the loop """ 82 | # most of the method is tested by test_packet.TestDecode and 83 | # test_namespace.TestBaseNamespace 84 | pass 85 | # self.virtsocket._receiver_loop() 86 | # self.virtsocket.server_queue.put_nowait_msg('2::') 87 | 88 | 89 | if __name__ == '__main__': 90 | main() 91 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{26,27,33,34} 3 | 4 | recreate = True 5 | 6 | [testenv] 7 | basepython = 8 | py26: python2.6 9 | py27: python2.7 10 | py33: python3.3 11 | py34: python3.4 12 | deps = 13 | -r{toxinidir}/pip-requirements.txt 14 | -r{toxinidir}/pip-requirements-test.txt 15 | commands = py.test 16 | --------------------------------------------------------------------------------