├── .gitignore ├── LICENSE ├── README.rst ├── examples ├── chatroom │ ├── README.md │ ├── chatroom.py │ ├── flashpolicy.xml │ └── index.html ├── ping │ ├── flashpolicy.xml │ ├── index.html │ └── ping.py └── transports │ ├── flashpolicy.xml │ ├── index.html │ └── transports.py ├── flashpolicy.xml ├── setup.py ├── tests ├── __init__.py └── proto_test.py └── tornadio ├── __init__.py ├── conn.py ├── flashserver.py ├── periodic.py ├── persistent.py ├── polling.py ├── pollingsession.py ├── proto.py ├── router.py ├── server.py └── session.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.pyc 4 | *.*~ 5 | pyenv 6 | #*# 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Serge. S. Koval. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Tornadio 3 | ======== 4 | 5 | If you're looking for socket.io 0.7+ integration library, check `TornadIO2 `_ 6 | 7 | Contributors 8 | ------------ 9 | 10 | - `Serge S. Koval `_ 11 | 12 | Credits 13 | ------- 14 | 15 | Authors of SocketTornad.IO project: 16 | 17 | - Brendan W. McAdams bwmcadams@evilmonkeylabs.com 18 | - `Matt Swanson `_ 19 | 20 | This is implementation of the `Socket.IO `_ realtime 21 | transport library on top of the `Tornado `_ framework. 22 | 23 | Short Background 24 | ---------------- 25 | 26 | There's a library which already implements Socket.IO integration using Tornado 27 | framework - `SocketTornad.IO `_, but 28 | it was not finished, has several known bugs and not very well structured. 29 | 30 | TornadIO is different from SocketTornad.IO library in following aspects: 31 | 32 | - Simpler internal design, easier to maintain/extend 33 | - No external dependencies (except of the Tornado itself and simplejson on python < 2.6) 34 | - Properly handles on_open/on_close events for polling transports 35 | - Proper Socket.IO protocol parser 36 | - Proper unicode support 37 | - Actively maintained 38 | 39 | Introduction 40 | ------------ 41 | 42 | In order to start working with the TornadIO library, you need to know some basic concepts 43 | on how Tornado works. If you don't, please read Tornado tutorial, which can be found 44 | `here `_. 45 | 46 | If you're familiar with Tornado, do following to add support for Socket.IO to your application: 47 | 48 | 1. Derive from tornadio.SocketConnection class and override on_message method (on_open/on_close are optional): 49 | :: 50 | 51 | class MyConnection(tornadio.SocketConnection): 52 | def on_message(self, message): 53 | pass 54 | 55 | 2. Create handler object that will handle all `socket.io` transport related functionality: 56 | :: 57 | 58 | MyRouter = tornadio.get_router(MyConnection) 59 | 60 | 3. Add your handler routes to the Tornado application: 61 | :: 62 | 63 | application = tornado.web.Application( 64 | [MyRouter.route()], 65 | socket_io_port = 8000) 66 | 67 | 4. Start your application 68 | 5. You have your `socket.io` server running at port 8000. Simple, right? 69 | 70 | Goodies 71 | ------- 72 | 73 | ``SocketConnection`` class provides three overridable methods: 74 | 75 | 1. ``on_open`` called when new client connection was established. 76 | 2. ``on_message`` called when message was received from the client. If client sent JSON object, 77 | it will be automatically decoded into appropriate Python data structures. 78 | 3. ``on_close`` called when client connection was closed (due to network error, timeout or just client-side disconnect) 79 | 80 | 81 | Each ``SocketConnection`` has ``send()`` method which is used to send data to the client. Input parameter 82 | can be one of the: 83 | 84 | 1. String/unicode string - sent as is (though with utf-8 encoding) 85 | 2. Arbitrary python object - encoded as JSON string automatically 86 | 3. List of python objects/strings - encoded as series of the socket.io messages using one of the rules above. 87 | 88 | Configuration 89 | ------------- 90 | 91 | You can configure your handler by passing settings to the ``get_router`` function as a ``dict`` object. 92 | 93 | - **enabled_protocols**: This is a ``list`` of the socket.io protocols the server will respond requests for. 94 | Possibilities are: 95 | - *websocket*: HTML5 WebSocket transport 96 | - *flashsocket*: Flash emulated websocket transport. Requires Flash policy server running on port 843. 97 | - *xhr-multipart*: Works with two connections - long GET connection with multipart transfer encoding to receive 98 | updates from the server and separate POST requests to send data from the client. 99 | - *xhr-polling*: Long polling AJAX request to read data from the server and POST requests to send data to the server. 100 | If message is available, it will be sent through open GET connection (which is then closed) or queued on the 101 | server otherwise. 102 | - *jsonp-polling*: Similar to the *xhr-polling*, but pushes data through the JSONp. 103 | - *htmlfile*: IE only. Creates HTMLFile control which reads data from the server through one persistent connection. 104 | POST requests are used to send data back to the server. 105 | 106 | 107 | - **session_check_interval**: Specifies how often TornadIO will check session container for expired session objects. 108 | In seconds. 109 | - **session_expiry**: Specifies session expiration interval, in seconds. For polling transports it is actually 110 | maximum time allowed between GET requests to consider virtual connection closed. 111 | - **heartbeat_interval**: Heartbeat interval for persistent transports. Specifies how often heartbeat events should 112 | be sent from the server to the clients. 113 | - **xhr_polling_timeout**: Timeout for long running XHR connection for *xhr-polling* transport, in seconds. If no 114 | data was available during this time, connection will be closed on server side to avoid client-side timeouts. 115 | 116 | Resources 117 | ^^^^^^^^^ 118 | 119 | You're not limited with one connection type per server - you can serve different clients in one server instance. 120 | 121 | By default, all socket.io clients use same resource - 'socket.io'. You can change resource by passing `resource` parameter 122 | to the `get_router` function: 123 | :: 124 | 125 | ChatRouter = tornadio.get_router(MyConnection, resource='chat') 126 | 127 | In the client, provide resource you're connecting to, by passing `resource` parameter to `io.Socket` constructor: 128 | :: 129 | 130 | sock = new io.Socket(window.location.hostname, { 131 | port: 8001, 132 | resource: 'chat', 133 | }); 134 | 135 | As it was said before, you can have as many connection types as you want by having unique resources for each connection type: 136 | :: 137 | 138 | ChatRouter = tornadio.get_router(ChatConnection, resource='chat') 139 | PingRouter = tornadio.get_router(PingConnection, resource='ping') 140 | MapRouter = tornadio.get_router(MapConnection, resource='map') 141 | 142 | application = tornado.web.Application( 143 | [ChatRouter.route(), PingRouter.route(), MapRouter.route()], 144 | socket_io_port = 8000) 145 | 146 | Extra parameters 147 | ^^^^^^^^^^^^^^^^ 148 | 149 | If you need some kind of user authentication in your application, you have two choices: 150 | 151 | 1. Send authentication token as a first message from the client 152 | 2. Provide authentication token as part of the `resource` parameter 153 | 154 | TornadIO has support for extra data passed through the `socket.io` resources. 155 | 156 | You can provide regexp in `extra_re` parameter of the `get_router` function and matched data can be accessed 157 | in your `on_open` handler as `kwargs['extra']`. For example: 158 | :: 159 | 160 | class MyConnection(tornadio.SocketConnection): 161 | def on_open(self, *args, **kwargs): 162 | print 'Extra: %s' % kwargs['extra'] 163 | 164 | ChatRouter = tornadio.get_router(MyConnection, resource='chat', extra_re='\d+', extra_sep='/') 165 | 166 | and on the client-side: 167 | :: 168 | 169 | sock = new io.Socket(window.location.hostname, { 170 | port: 8001, 171 | resource: 'chat/123', 172 | }); 173 | 174 | If you will run this example and connect with sample client, you should see 'Extra: 123' printed out. 175 | 176 | Starting Up 177 | ----------- 178 | 179 | Best Way: SocketServer 180 | ^^^^^^^^^^^^^^^^^^^^^^ 181 | 182 | We provide customized version (shamelessly borrowed from the SocketTornad.IO library) of the HttpServer, which 183 | simplifies start of your TornadIO server. 184 | 185 | To start it, do following (assuming you created application object before):: 186 | 187 | if __name__ == "__main__": 188 | socketio_server = SocketServer(application) 189 | 190 | SocketServer will automatically start Flash policy server, if required. 191 | 192 | SocketServer by default will also automatically start ioloop. In order to prevent this behaviour and perform some additional action after socket server is created you can use auto_start param. In this case you should start ioloop manually:: 193 | 194 | if __name__ == "__main__": 195 | socketio_server = SocketServer(application, auto_start=False) 196 | logging.info('You can perform some actions here') 197 | ioloop.IOLoop.instance().start() 198 | 199 | 200 | Going big 201 | --------- 202 | 203 | So, you've finished writting your application and want to share it with rest of the world, so you started 204 | thinking about scalability, deployment options, etc. 205 | 206 | Most of the Tornado servers are deployed behind the nginx, which also used to serve static content. This 207 | won't work very well with TornadIO, as nginx does not support HTTP/1.1, does not support websockets and 208 | XHR-Multipart transport just won't work. 209 | 210 | So, to load balance your TornadIO instances, use alternative solutions like `HAProxy `_. 211 | However, HAProxy does not work on Windows, so if you plan to deploy your solution on Windows platform, 212 | you might want to take look into `MLB `_. 213 | 214 | Scalability is completely different beast. It is up for you, as a developer, to design scalable architecture 215 | of the application. 216 | 217 | For example, if you need to have one large virtual server out of your multiple physical processes (or even servers), 218 | you have to come up with some kind of the synchronization mechanism. This can be either common meeting point 219 | (and also point of failure), like memcached, redis, etc. Or you might want to use some transporting mechanism to 220 | communicate between servers, for example something `AMQP `_ based, `ZeroMQ `_ or 221 | just plain sockets with your protocol. 222 | 223 | For example, with message queues, you can treat TornadIO as a message gateway between your clients and your server backend(s). 224 | 225 | Examples 226 | -------- 227 | 228 | Chatroom Example 229 | ^^^^^^^^^^^^^^^^ 230 | 231 | There is a chatroom example application from the SocketTornad.IO library, contributed by 232 | `swanson `_. It is in the ``examples/chatroom`` directory. 233 | 234 | Ping Example 235 | ^^^^^^^^^^^^ 236 | 237 | Simple ping/pong example to measure network performance. It is in the ``examples/ping`` directory. 238 | 239 | Transports Example 240 | ^^^^^^^^^^^^^^^^^^ 241 | 242 | Simple ping/pong example with chat-like interface with selectable transports. It is in the 243 | ``examples/transports`` directory. 244 | -------------------------------------------------------------------------------- /examples/chatroom/README.md: -------------------------------------------------------------------------------- 1 | To run this example, first start up the Tornado server: 2 | 3 | python chatroom.py 4 | 5 | Open multiple browser windows/tabs and point them to `localhost:8888`. 6 | 7 | You should see a simple chatroom where you can send messages to all of the open connections. 8 | 9 | Example adapted from [mopemope's example for meinheld][1] 10 | 11 | 12 | [1]: http://github.com/mopemope/meinheld/tree/master/example/websocket_chat 13 | -------------------------------------------------------------------------------- /examples/chatroom/chatroom.py: -------------------------------------------------------------------------------- 1 | from os import path as op 2 | 3 | import tornado.web 4 | import tornadio 5 | import tornadio.router 6 | import tornadio.server 7 | 8 | ROOT = op.normpath(op.dirname(__file__)) 9 | 10 | class IndexHandler(tornado.web.RequestHandler): 11 | """Regular HTTP handler to serve the chatroom page""" 12 | def get(self): 13 | self.render("index.html") 14 | 15 | class ChatConnection(tornadio.SocketConnection): 16 | # Class level variable 17 | participants = set() 18 | 19 | def on_open(self, *args, **kwargs): 20 | self.participants.add(self) 21 | self.send("Welcome!") 22 | 23 | def on_message(self, message): 24 | for p in self.participants: 25 | p.send(message) 26 | 27 | def on_close(self): 28 | self.participants.remove(self) 29 | for p in self.participants: 30 | p.send("A user has left.") 31 | 32 | #use the routes classmethod to build the correct resource 33 | ChatRouter = tornadio.get_router(ChatConnection, { 34 | 'enabled_protocols': [ 35 | 'websocket', 36 | 'flashsocket', 37 | 'xhr-multipart', 38 | 'xhr-polling' 39 | ] 40 | }) 41 | 42 | #configure the Tornado application 43 | application = tornado.web.Application( 44 | [(r"/", IndexHandler), ChatRouter.route()], 45 | flash_policy_port = 843, 46 | flash_policy_file = op.join(ROOT, 'flashpolicy.xml'), 47 | socket_io_port = 8001 48 | ) 49 | 50 | if __name__ == "__main__": 51 | import logging 52 | logging.getLogger().setLevel(logging.DEBUG) 53 | 54 | tornadio.server.SocketServer(application) 55 | 56 | -------------------------------------------------------------------------------- /examples/chatroom/flashpolicy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/chatroom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 39 | 40 | 41 |

Chat!

42 |
43 |
44 |
45 | 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/ping/flashpolicy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/ping/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 48 | 49 | 50 | Start ping 51 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/ping/ping.py: -------------------------------------------------------------------------------- 1 | from os import path as op 2 | 3 | from datetime import datetime 4 | 5 | import tornado.web 6 | import tornadio 7 | import tornadio.router 8 | import tornadio.server 9 | 10 | ROOT = op.normpath(op.dirname(__file__)) 11 | 12 | class IndexHandler(tornado.web.RequestHandler): 13 | """Regular HTTP handler to serve the ping page""" 14 | def get(self): 15 | self.render("index.html") 16 | 17 | class PingConnection(tornadio.SocketConnection): 18 | def on_open(self, request, *args, **kwargs): 19 | self.ip = request.remote_ip 20 | 21 | def on_message(self, message): 22 | message['server'] = str(datetime.now()) 23 | message['ip'] = self.ip 24 | self.send(message) 25 | 26 | #use the routes classmethod to build the correct resource 27 | PingRouter = tornadio.get_router(PingConnection) 28 | 29 | #configure the Tornado application 30 | application = tornado.web.Application( 31 | [(r"/", IndexHandler), PingRouter.route()], 32 | socket_io_port = 8001, 33 | flash_policy_port = 843, 34 | flash_policy_file = op.join(ROOT, 'flashpolicy.xml') 35 | ) 36 | 37 | if __name__ == "__main__": 38 | tornadio.server.SocketServer(application) 39 | -------------------------------------------------------------------------------- /examples/transports/flashpolicy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/transports/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 97 | 98 | 99 |

Protocol test!

100 |
101 |
    102 |
  • WebSocket
  • 103 |
  • FlashSocket
  • 104 |
  • XHR-Multipart
  • 105 |
  • XHR-Polling
  • 106 |
  • HtmlFile
  • 107 |
  • JSONP Polling
  • 108 |
109 |
110 |
111 | Connect | Status: disconnected 112 |
113 |
114 |
115 |
116 | 117 | 118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /examples/transports/transports.py: -------------------------------------------------------------------------------- 1 | from os import path as op 2 | 3 | import tornado.web 4 | import tornadio 5 | import tornadio.router 6 | import tornadio.server 7 | 8 | ROOT = op.normpath(op.dirname(__file__)) 9 | 10 | class IndexHandler(tornado.web.RequestHandler): 11 | """Regular HTTP handler to serve the chatroom page""" 12 | def get(self): 13 | self.render("index.html") 14 | 15 | class ChatConnection(tornadio.SocketConnection): 16 | # Class level variable 17 | participants = set() 18 | 19 | def on_open(self, *args, **kwargs): 20 | self.send("Welcome from the server.") 21 | 22 | def on_message(self, message): 23 | # Pong message back 24 | self.send(message) 25 | 26 | #use the routes classmethod to build the correct resource 27 | ChatRouter = tornadio.get_router(ChatConnection) 28 | 29 | #configure the Tornado application 30 | application = tornado.web.Application( 31 | [(r"/", IndexHandler), ChatRouter.route()], 32 | flash_policy_port = 843, 33 | flash_policy_file = op.join(ROOT, 'flashpolicy.xml'), 34 | socket_io_port = 8001 35 | ) 36 | 37 | if __name__ == "__main__": 38 | import logging 39 | logging.getLogger().setLevel(logging.DEBUG) 40 | 41 | tornadio.server.SocketServer(application) 42 | 43 | -------------------------------------------------------------------------------- /flashpolicy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup, find_packages 5 | except ImportError: 6 | from distribute_setup import use_setuptools 7 | use_setuptools() 8 | from setuptools import setup, find_packages 9 | 10 | try: 11 | license = open('LICENSE').read() 12 | except: 13 | license = None 14 | 15 | try: 16 | readme = open('README.rst').read() 17 | except: 18 | readme = None 19 | 20 | setup( 21 | name='TornadIO', 22 | version='0.0.4', 23 | author='Serge S. Koval', 24 | author_email='serge.koval@gmail.com', 25 | packages=['tornadio'], 26 | scripts=[], 27 | url='http://github.com/MrJoes/tornadio/', 28 | license=license, 29 | description='Socket.io server implementation on top of Tornado framework', 30 | long_description=readme, 31 | requires=['simplejson', 'tornado'], 32 | install_requires=[ 33 | 'simplejson >= 2.1.0', 34 | 'tornado >= 1.1.0' 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .proto_test import * 2 | -------------------------------------------------------------------------------- /tests/proto_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.tests.proto_test 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 7 | :license: Apache, see LICENSE for more details. 8 | """ 9 | 10 | from nose.tools import eq_ 11 | 12 | from tornadio import proto 13 | 14 | def test_encode(): 15 | # Test string encode 16 | eq_(proto.encode('abc'), '~m~3~m~abc') 17 | 18 | # Test dict encode 19 | eq_(proto.encode({'a':'b'}), '~m~13~m~~j~{"a": "b"}') 20 | 21 | # Test list encode 22 | eq_(proto.encode(['a','b']), '~m~1~m~a~m~1~m~b') 23 | 24 | # Test unicode 25 | eq_(proto.encode(u'\u0430\u0431\u0432'), 26 | '~m~6~m~' + u'\u0430\u0431\u0432'.encode('utf-8')) 27 | 28 | # Test special characters encoding 29 | eq_(proto.encode('~m~'), '~m~3~m~~m~') 30 | 31 | def test_decode(): 32 | # Test string decode 33 | eq_(proto.decode(proto.encode('abc')), [('~m~', 'abc')]) 34 | 35 | # Test unicode decode 36 | eq_(proto.decode(proto.encode(u'\u0430\u0431\u0432')), 37 | [('~m~', u'\u0430\u0431\u0432'.encode('utf-8'))]) 38 | 39 | # Test JSON decode 40 | eq_(proto.decode(proto.encode({'a':'b'})), 41 | [('~m~', {'a':'b'})]) 42 | 43 | # Test seprate messages decoding 44 | eq_(proto.decode(proto.encode(['a','b'])), 45 | [('~m~', 'a'), ('~m~', 'b')]) 46 | -------------------------------------------------------------------------------- /tornadio/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.router 4 | ~~~~~~~~~~~~~~~ 5 | 6 | For now, just sets debug level. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | 12 | __version__ = (0, 0, 4) 13 | 14 | from tornadio.conn import SocketConnection 15 | from tornadio.router import get_router 16 | -------------------------------------------------------------------------------- /tornadio/conn.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.conn 4 | ~~~~~~~~~~~~~ 5 | 6 | This module implements connection management class. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | import logging, time 12 | 13 | from tornadio import proto, periodic 14 | 15 | class SocketConnection(object): 16 | """This class represents basic connection class that you will derive 17 | from in your application. 18 | 19 | You can override following methods: 20 | 21 | 1. on_open, called on incoming client connection 22 | 2. on_message, called on incoming client message. Required. 23 | 3. on_close, called when connection was closed due to error or timeout 24 | 25 | For example: 26 | 27 | class MyClient(SocketConnection): 28 | def on_open(self, *args, **kwargs): 29 | print 'Incoming client' 30 | 31 | def on_message(self, message): 32 | print 'Incoming message: %s' % message 33 | 34 | def on_close(self): 35 | print 'Client disconnected' 36 | """ 37 | def __init__(self, protocol, io_loop, heartbeat_interval): 38 | """Default constructor. 39 | 40 | `protocol` 41 | Transport protocol implementation object. 42 | `io_loop` 43 | Tornado IOLoop instance 44 | `heartbeat_interval` 45 | Heartbeat interval for this connection, in seconds. 46 | """ 47 | self._protocol = protocol 48 | 49 | self._io_loop = io_loop 50 | 51 | # Initialize heartbeats 52 | self._heartbeat_timer = None 53 | self._heartbeats = 0 54 | self._missed_heartbeats = 0 55 | self._heartbeat_delay = None 56 | self._heartbeat_interval = heartbeat_interval * 1000 57 | 58 | # Connection is not closed right after creation 59 | self.is_closed = False 60 | 61 | def on_open(self, *args, **kwargs): 62 | """Default on_open() handler""" 63 | pass 64 | 65 | def on_message(self, message): 66 | """Default on_message handler. Must be overridden""" 67 | raise NotImplementedError() 68 | 69 | def on_close(self): 70 | """Default on_close handler.""" 71 | pass 72 | 73 | def send(self, message): 74 | """Send message to the client. 75 | 76 | `message` 77 | Message to send. 78 | """ 79 | self._protocol.send(message) 80 | 81 | def close(self): 82 | """Focibly close client connection. 83 | Stop heartbeats as well, as they would cause IOErrors once the connection is closed.""" 84 | self.stop_heartbeat() 85 | self._protocol.close() 86 | 87 | def raw_message(self, message): 88 | """Called when raw message was received by underlying transport protocol 89 | """ 90 | for msg in proto.decode(message): 91 | if msg[0] == proto.FRAME or msg[0] == proto.JSON: 92 | self.on_message(msg[1]) 93 | elif msg[0] == proto.HEARTBEAT: 94 | # TODO: Verify incoming heartbeats 95 | logging.debug('Incoming Heartbeat') 96 | self._missed_heartbeats -= 1 97 | 98 | # Heartbeat management 99 | def reset_heartbeat(self, interval=None): 100 | """Reset (stop/start) heartbeat timeout""" 101 | self.stop_heartbeat() 102 | 103 | # TODO: Configurable heartbeats 104 | if interval is None: 105 | interval = self._heartbeat_interval 106 | 107 | self._heartbeat_timer = periodic.Callback(self._heartbeat, 108 | interval, 109 | self._io_loop) 110 | self._heartbeat_timer.start() 111 | 112 | def stop_heartbeat(self): 113 | """Stop heartbeat""" 114 | if self._heartbeat_timer is not None: 115 | self._heartbeat_timer.stop() 116 | self._heartbeat_timer = None 117 | 118 | def delay_heartbeat(self): 119 | """Delay heartbeat sending""" 120 | if self._heartbeat_timer is not None: 121 | self._heartbeat_delay = self._heartbeat_timer.calculate_next_run() 122 | 123 | def send_heartbeat(self): 124 | """Send heartbeat message to the client""" 125 | self._heartbeats += 1 126 | self._missed_heartbeats += 1 127 | self.send('~h~%d' % self._heartbeats) 128 | 129 | def _heartbeat(self): 130 | """Heartbeat callback. Sends heartbeat to the client.""" 131 | if (self._heartbeat_delay is not None 132 | and time.time() < self._heartbeat_delay): 133 | delay = self._heartbeat_delay 134 | self._heartbeat_delay = None 135 | return delay 136 | 137 | logging.debug('Sending heartbeat') 138 | 139 | if self._missed_heartbeats > 5: 140 | logging.debug('Missed too many heartbeats') 141 | self.close() 142 | else: 143 | self.send_heartbeat() 144 | -------------------------------------------------------------------------------- /tornadio/flashserver.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.flashserver 4 | ~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Flash Socket policy server implementation. Merged with minor modifications 7 | from the SocketTornad.IO project. 8 | 9 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 10 | :license: Apache, see LICENSE for more details. 11 | """ 12 | from __future__ import with_statement 13 | 14 | import socket 15 | import errno 16 | import functools 17 | 18 | from tornado import iostream 19 | 20 | class FlashPolicyServer(object): 21 | """Flash Policy server, listens on port 843 by default (useless otherwise) 22 | """ 23 | def __init__(self, io_loop, port=843, policy_file='flashpolicy.xml'): 24 | self.policy_file = policy_file 25 | self.port = port 26 | 27 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) 28 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 29 | sock.setblocking(0) 30 | sock.bind(('', self.port)) 31 | sock.listen(128) 32 | 33 | self.io_loop = io_loop 34 | callback = functools.partial(self.connection_ready, sock) 35 | self.io_loop.add_handler(sock.fileno(), callback, self.io_loop.READ) 36 | 37 | def connection_ready(self, sock, _fd, _events): 38 | """Connection ready callback""" 39 | while True: 40 | try: 41 | connection, address = sock.accept() 42 | except socket.error, ex: 43 | if ex[0] not in (errno.EWOULDBLOCK, errno.EAGAIN): 44 | raise 45 | return 46 | connection.setblocking(0) 47 | self.stream = iostream.IOStream(connection, self.io_loop) 48 | self.stream.read_bytes(22, self._handle_request) 49 | 50 | def _handle_request(self, request): 51 | """Send policy response""" 52 | if request != '': 53 | self.stream.close() 54 | else: 55 | with open(self.policy_file, 'rb') as file_handle: 56 | self.stream.write(file_handle.read() + '\0') 57 | -------------------------------------------------------------------------------- /tornadio/periodic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.flashserver 4 | ~~~~~~~~~~~~~~~~~~~~ 5 | 6 | This module implements customized PeriodicCallback from tornado with 7 | support of the sliding window. 8 | 9 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 10 | :license: Apache, see LICENSE for more details. 11 | """ 12 | import time, logging 13 | 14 | class Callback(object): 15 | def __init__(self, callback, callback_time, io_loop): 16 | self.callback = callback 17 | self.callback_time = callback_time 18 | self.io_loop = io_loop 19 | self._running = False 20 | 21 | def calculate_next_run(self): 22 | return time.time() + self.callback_time / 1000.0 23 | 24 | def start(self, timeout=None): 25 | self._running = True 26 | 27 | if timeout is None: 28 | timeout = self.calculate_next_run() 29 | 30 | self.io_loop.add_timeout(timeout, self._run) 31 | 32 | def stop(self): 33 | self._running = False 34 | 35 | def _run(self): 36 | if not self._running: 37 | return 38 | 39 | next_call = None 40 | 41 | try: 42 | next_call = self.callback() 43 | except (KeyboardInterrupt, SystemExit): 44 | raise 45 | except: 46 | logging.error("Error in periodic callback", exc_info=True) 47 | 48 | if self._running: 49 | self.start(next_call) 50 | -------------------------------------------------------------------------------- /tornadio/persistent.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.persistent 4 | ~~~~~~~~~~~~~~~~~~~ 5 | 6 | Persistent transport implementations. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | import logging 12 | 13 | import tornado 14 | from tornado.websocket import WebSocketHandler 15 | 16 | from tornadio import proto 17 | 18 | class TornadioWebSocketHandler(WebSocketHandler): 19 | """WebSocket handler. 20 | """ 21 | def __init__(self, router, session_id): 22 | logging.debug('Initializing WebSocket handler...') 23 | 24 | self.router = router 25 | self.connection = None 26 | 27 | super(TornadioWebSocketHandler, self).__init__(router.application, 28 | router.request) 29 | 30 | # HAProxy websocket fix. 31 | # Merged from: 32 | # https://github.com/facebook/tornado/commit/86bd681ff841f272c5205f24cd2a613535ed2e00 33 | def _execute(self, transforms, *args, **kwargs): 34 | # Next Tornado will have the built-in support for HAProxy 35 | if tornado.version_info < (1, 2, 0): 36 | # Write the initial headers before attempting to read the challenge. 37 | # This is necessary when using proxies (such as HAProxy), 38 | # need to see the Upgrade headers before passing through the 39 | # non-HTTP traffic that follows. 40 | self.stream.write( 41 | "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" 42 | "Upgrade: WebSocket\r\n" 43 | "Connection: Upgrade\r\n" 44 | "Server: TornadoServer/%(version)s\r\n" 45 | "Sec-WebSocket-Origin: %(origin)s\r\n" 46 | "Sec-WebSocket-Location: ws://%(host)s%(path)s\r\n\r\n" % (dict( 47 | version=tornado.version, 48 | origin=self.request.headers["Origin"], 49 | host=self.request.host, 50 | path=self.request.path))) 51 | 52 | super(TornadioWebSocketHandler, self)._execute(transforms, *args, 53 | **kwargs) 54 | 55 | 56 | def _write_response(self, challenge): 57 | if tornado.version_info < (1, 2, 0): 58 | self.stream.write("%s" % challenge) 59 | self.async_callback(self.open)(*self.open_args, **self.open_kwargs) 60 | self._receive_message() 61 | else: 62 | super(TornadioWebSocketHandler, self)._write_response(challenge) 63 | 64 | def open(self, *args, **kwargs): 65 | # Create connection instance 66 | heartbeat_interval = self.router.settings['heartbeat_interval'] 67 | self.connection = self.router.connection(self, 68 | self.router.io_loop, 69 | heartbeat_interval) 70 | 71 | # Initialize heartbeats 72 | self.connection.reset_heartbeat() 73 | 74 | # Fix me: websocket is dropping connection if we don't send first 75 | # message 76 | self.send('no_session') 77 | 78 | self.connection.on_open(self.request, *args, **kwargs) 79 | 80 | def on_message(self, message): 81 | self.async_callback(self.connection.raw_message)(message) 82 | 83 | def on_close(self): 84 | if self.connection is not None: 85 | try: 86 | self.connection.on_close() 87 | finally: 88 | self.connection.is_closed = True 89 | self.connection.stop_heartbeat() 90 | 91 | def send(self, message): 92 | self.write_message(proto.encode(message)) 93 | self.connection.delay_heartbeat() 94 | 95 | class TornadioFlashSocketHandler(TornadioWebSocketHandler): 96 | def __init__(self, router, session_id): 97 | logging.debug('Initializing FlashSocket handler...') 98 | 99 | super(TornadioFlashSocketHandler, self).__init__(router, session_id) 100 | -------------------------------------------------------------------------------- /tornadio/polling.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.polling 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | This module implements socket.io polling transports. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | import time 12 | try: 13 | import simplejson as json 14 | except ImportError: 15 | import json 16 | 17 | from urllib import unquote 18 | from tornado.web import RequestHandler, HTTPError, asynchronous 19 | 20 | from tornadio import pollingsession 21 | 22 | class TornadioPollingHandlerBase(RequestHandler): 23 | """All polling transport implementations derive from this class. 24 | 25 | Polling transports have following things in common: 26 | 27 | 1. They use GET to read data from the server 28 | 2. They use POST to send data to the server 29 | 3. They use sessions - first message sent back from the server is session_id 30 | 4. Session is used to create one virtual connection for one or more HTTP 31 | connections 32 | 5. If GET request is not running, data will be cached on server side. On 33 | next GET request, all cached data will be sent to the client in one batch 34 | 6. If there were no GET requests for more than 15 seconds (default), virtual 35 | connection will be closed - session entry will expire 36 | """ 37 | def __init__(self, router, session_id): 38 | """Default constructor. 39 | 40 | Accepts router instance and session_id (if available) and handles 41 | request. 42 | """ 43 | self.router = router 44 | self.session_id = session_id 45 | self.session = None 46 | 47 | super(TornadioPollingHandlerBase, self).__init__(router.application, 48 | router.request) 49 | 50 | def _execute(self, transforms, *args, **kwargs): 51 | # Initialize session either by creating new one or 52 | # getting it from container 53 | if not self.session_id: 54 | session_expiry = self.router.settings['session_expiry'] 55 | 56 | self.session = self.router.sessions.create( 57 | pollingsession.PollingSession, 58 | session_expiry, 59 | router=self.router, 60 | args=args, 61 | kwargs=kwargs) 62 | else: 63 | self.session = self.router.sessions.get(self.session_id) 64 | 65 | if self.session is None or self.session.is_closed: 66 | # TODO: Send back disconnect message? 67 | raise HTTPError(401, 'Invalid session') 68 | 69 | super(TornadioPollingHandlerBase, self)._execute(transforms, 70 | *args, **kwargs) 71 | 72 | @asynchronous 73 | def get(self, *args, **kwargs): 74 | """Default GET handler.""" 75 | raise NotImplementedError() 76 | 77 | @asynchronous 78 | def post(self, *args, **kwargs): 79 | """Default POST handler.""" 80 | raise NotImplementedError() 81 | 82 | def data_available(self, raw_data): 83 | """Called by the session when some data is available""" 84 | raise NotImplementedError() 85 | 86 | @asynchronous 87 | def options(self, *args, **kwargs): 88 | """XHR cross-domain OPTIONS handler""" 89 | self.preflight() 90 | self.finish() 91 | 92 | def preflight(self): 93 | """Handles request authentication""" 94 | if self.request.headers.has_key('Origin'): 95 | if self.verify_origin(): 96 | self.set_header('Access-Control-Allow-Origin', 97 | self.request.headers['Origin']) 98 | 99 | self.set_header('Access-Control-Allow-Credentials', 'true') 100 | 101 | return True 102 | else: 103 | return False 104 | else: 105 | return True 106 | 107 | def verify_origin(self): 108 | """Verify if request can be served""" 109 | # TODO: Verify origin 110 | return True 111 | 112 | class TornadioXHRPollingSocketHandler(TornadioPollingHandlerBase): 113 | """XHR polling transport implementation. 114 | 115 | Polling mechanism uses long-polling AJAX GET to read data from the server 116 | and POST to send data to the server. 117 | 118 | Properties of the XHR polling transport: 119 | 120 | 1. If there was no data for more than 20 seconds (by default) from the 121 | server, GET connection will be closed to avoid HTTP timeouts. In this case 122 | socket.io client-side will just make another GET request. 123 | 2. When new data is available on server-side, it will be sent through the 124 | open GET connection or cached otherwise. 125 | """ 126 | def __init__(self, router, session_id): 127 | self._timeout = None 128 | 129 | self._timeout_interval = router.settings['xhr_polling_timeout'] 130 | 131 | super(TornadioXHRPollingSocketHandler, self).__init__(router, 132 | session_id) 133 | 134 | @asynchronous 135 | def get(self, *args, **kwargs): 136 | if not self.session.set_handler(self): 137 | # Check to avoid double connections 138 | # TODO: Error logging 139 | raise HTTPError(401, 'Forbidden') 140 | 141 | if not self.session.send_queue: 142 | self._timeout = self.router.io_loop.add_timeout( 143 | time.time() + self._timeout_interval, 144 | self._polling_timeout) 145 | else: 146 | self.session.flush() 147 | 148 | def _polling_timeout(self): 149 | # TODO: Fix me 150 | if self.session: 151 | self.data_available('') 152 | 153 | @asynchronous 154 | def post(self, *args, **kwargs): 155 | if not self.preflight(): 156 | raise HTTPError(401, 'unauthorized') 157 | 158 | # Special case for IE XDomainRequest 159 | ctype = self.request.headers.get("Content-Type", "").split(";")[0] 160 | if ctype == '': 161 | data = None 162 | body = self.request.body 163 | 164 | if body.startswith('data='): 165 | data = unquote(body[5:]) 166 | else: 167 | data = self.get_argument('data', None) 168 | 169 | self.async_callback(self.session.raw_message)(data) 170 | 171 | self.set_header('Content-Type', 'text/plain; charset=UTF-8') 172 | self.write('ok') 173 | self.finish() 174 | 175 | def _detach(self): 176 | if self.session: 177 | self.session.remove_handler(self) 178 | self.session = None 179 | 180 | def on_connection_close(self): 181 | self._detach() 182 | 183 | def data_available(self, raw_data): 184 | self.preflight() 185 | self.set_header('Content-Type', 'text/plain; charset=UTF-8') 186 | self.set_header('Content-Length', len(raw_data)) 187 | self.write(raw_data) 188 | self.finish() 189 | 190 | # Detach connection 191 | self._detach() 192 | 193 | class TornadioXHRMultipartSocketHandler(TornadioPollingHandlerBase): 194 | """XHR Multipart transport implementation. 195 | 196 | Transport properties: 197 | 1. One persistent GET connection used to receive data from the server 198 | 2. Sends heartbeat messages to keep connection alive each 12 seconds 199 | (by default) 200 | """ 201 | @asynchronous 202 | def get(self, *args, **kwargs): 203 | if not self.session.set_handler(self): 204 | # TODO: Error logging 205 | raise HTTPError(401, 'Forbidden') 206 | 207 | self.set_header('Content-Type', 208 | 'multipart/x-mixed-replace;boundary="socketio; charset=UTF-8"') 209 | self.set_header('Connection', 'keep-alive') 210 | self.write('--socketio\n') 211 | 212 | # Dump any queued messages 213 | self.session.flush() 214 | 215 | # We need heartbeats 216 | self.session.reset_heartbeat() 217 | 218 | @asynchronous 219 | def post(self, *args, **kwargs): 220 | if not self.preflight(): 221 | raise HTTPError(401, 'unauthorized') 222 | 223 | data = self.get_argument('data') 224 | self.async_callback(self.session.raw_message)(data) 225 | 226 | self.set_header('Content-Type', 'text/plain; charset=UTF-8') 227 | self.write('ok') 228 | self.finish() 229 | 230 | def on_connection_close(self): 231 | if self.session: 232 | self.session.stop_heartbeat() 233 | self.session.remove_handler(self) 234 | 235 | def data_available(self, raw_data): 236 | self.preflight() 237 | self.write("Content-Type: text/plain; charset=UTF-8\n\n") 238 | self.write(raw_data + '\n') 239 | self.write('--socketio\n') 240 | self.flush() 241 | 242 | self.session.delay_heartbeat() 243 | 244 | class TornadioHtmlFileSocketHandler(TornadioPollingHandlerBase): 245 | """IE HtmlFile protocol implementation. 246 | 247 | Uses hidden frame to stream data from the server in one connection. 248 | 249 | Unfortunately, it is unknown if this transport works, as socket.io 250 | client-side fails in IE7/8. 251 | """ 252 | @asynchronous 253 | def get(self, *args, **kwargs): 254 | if not self.session.set_handler(self): 255 | raise HTTPError(401, 'Forbidden') 256 | 257 | self.set_header('Content-Type', 'text/html; charset=UTF-8') 258 | self.set_header('Connection', 'keep-alive') 259 | self.set_header('Transfer-Encoding', 'chunked') 260 | self.write('%s' % (' ' * 244)) 261 | 262 | # Dump any queued messages 263 | self.session.flush() 264 | 265 | # We need heartbeats 266 | self.session.reset_heartbeat() 267 | 268 | @asynchronous 269 | def post(self, *args, **kwargs): 270 | if not self.preflight(): 271 | raise HTTPError(401, 'unauthorized') 272 | 273 | data = self.get_argument('data') 274 | self.async_callback(self.session.raw_message)(data) 275 | 276 | self.set_header('Content-Type', 'text/plain; charset=UTF-8') 277 | self.write('ok') 278 | self.finish() 279 | 280 | def on_connection_close(self): 281 | if self.session: 282 | self.session.stop_heartbeat() 283 | self.session.remove_handler(self) 284 | 285 | def data_available(self, raw_data): 286 | self.write( 287 | '' % json.dumps(raw_data) 288 | ) 289 | self.flush() 290 | 291 | self.session.delay_heartbeat() 292 | 293 | class TornadioJSONPSocketHandler(TornadioXHRPollingSocketHandler): 294 | """JSONP protocol implementation. 295 | """ 296 | def __init__(self, router, session_id): 297 | self._index = None 298 | 299 | super(TornadioJSONPSocketHandler, self).__init__(router, session_id) 300 | 301 | @asynchronous 302 | def get(self, *args, **kwargs): 303 | self._index = kwargs.get('jsonp_index', None) 304 | super(TornadioJSONPSocketHandler, self).get(*args, **kwargs) 305 | 306 | @asynchronous 307 | def post(self, *args, **kwargs): 308 | self._index = kwargs.get('jsonp_index', None) 309 | super(TornadioJSONPSocketHandler, self).post(*args, **kwargs) 310 | 311 | def data_available(self, raw_data): 312 | if not self._index: 313 | raise HTTPError(401, 'unauthorized') 314 | 315 | message = 'io.JSONP[%s]._(%s);' % ( 316 | self._index, 317 | json.dumps(raw_data) 318 | ) 319 | 320 | self.preflight() 321 | self.set_header("Content-Type", "text/javascript; charset=UTF-8") 322 | self.set_header("Content-Length", len(message)) 323 | self.write(message) 324 | self.finish() 325 | 326 | # Detach connection 327 | self._detach() 328 | -------------------------------------------------------------------------------- /tornadio/pollingsession.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.pollingsession 4 | ~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | This module implements polling session class. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | from tornadio import proto, session 12 | 13 | class PollingSession(session.Session): 14 | """This class represents virtual protocol connection for polling transports. 15 | 16 | For disconnected protocols, like XHR-Polling, it will cache outgoing 17 | messages, if there is on going GET connection - will pass cached/current 18 | messages to the actual transport protocol implementation. 19 | """ 20 | def __init__(self, session_id, expiry, router, 21 | args, kwargs): 22 | # Initialize session 23 | super(PollingSession, self).__init__(session_id, expiry) 24 | 25 | # Set connection 26 | self.connection = router.connection(self, 27 | router.io_loop, 28 | router.settings['heartbeat_interval']) 29 | 30 | self.handler = None 31 | self.send_queue = [] 32 | 33 | # Forward some methods to connection 34 | self.on_open = self.connection.on_open 35 | self.raw_message = self.connection.raw_message 36 | self.on_close = self.connection.on_close 37 | 38 | self.reset_heartbeat = self.connection.reset_heartbeat 39 | self.stop_heartbeat = self.connection.stop_heartbeat 40 | self.delay_heartbeat = self.connection.delay_heartbeat 41 | 42 | # Send session_id 43 | self.send(session_id) 44 | 45 | # Notify that channel was opened 46 | self.on_open(router.request, *args, **kwargs) 47 | 48 | def on_delete(self, forced): 49 | """Called by the session management class when item is 50 | about to get deleted/expired. If item is getting expired, 51 | there is possibility to force rescheduling of the item 52 | somewhere in the future, so it won't be deleted. 53 | 54 | Rescheduling is used in case when there is on-going GET 55 | connection. 56 | """ 57 | if not forced and self.handler is not None and not self.is_closed: 58 | self.promote() 59 | else: 60 | self.close() 61 | 62 | def set_handler(self, handler): 63 | """Associate request handler with this virtual connection. 64 | 65 | If there is already handler associated, it won't be changed. 66 | """ 67 | if self.handler is not None: 68 | return False 69 | 70 | self.handler = handler 71 | 72 | # Promote session item 73 | self.promote() 74 | 75 | return True 76 | 77 | def remove_handler(self, handler): 78 | """Remove associated Tornado handler. 79 | 80 | Promotes session in the cache, so time between two calls can't 81 | be greater than 15 seconds (by default) 82 | """ 83 | if self.handler != handler: 84 | # TODO: Assert 85 | return False 86 | 87 | self.handler = None 88 | 89 | # Promote session so session item will live a bit longer 90 | # after disconnection 91 | self.promote() 92 | 93 | def flush(self): 94 | """Send all pending messages to the associated request handler (if any) 95 | """ 96 | if self.handler is None: 97 | return 98 | 99 | if not self.send_queue: 100 | return 101 | 102 | self.handler.data_available(proto.encode(self.send_queue)) 103 | self.send_queue = [] 104 | 105 | def send(self, message): 106 | """Append message to the queue and send it right away, if there's 107 | connection available. 108 | """ 109 | self.send_queue.append(message) 110 | 111 | self.flush() 112 | 113 | def close(self): 114 | """Forcibly close connection and notify connection object about that. 115 | """ 116 | if not self.connection.is_closed: 117 | try: 118 | # Notify that connection was closed 119 | self.connection.on_close() 120 | finally: 121 | self.connection.is_closed = True 122 | 123 | @property 124 | def is_closed(self): 125 | """Check if connection was closed or not""" 126 | return self.connection.is_closed 127 | -------------------------------------------------------------------------------- /tornadio/proto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.proto 4 | ~~~~~~~~~~~~~~ 5 | 6 | Socket.IO 0.6.x protocol codec. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | try: 12 | import simplejson as json 13 | json_decimal_args = {"use_decimal":True} 14 | except ImportError: 15 | import json 16 | import decimal 17 | class DecimalEncoder(json.JSONEncoder): 18 | def default(self, o): 19 | if isinstance(o, decimal.Decimal): 20 | return float(o) 21 | return super(DecimalEncoder, self).default(o) 22 | json_decimal_args = {"cls":DecimalEncoder} 23 | 24 | FRAME = '~m~' 25 | HEARTBEAT = '~h~' 26 | JSON = '~j~' 27 | 28 | def encode(message): 29 | """Encode message to the socket.io wire format. 30 | 31 | 1. If message is list, it will encode each separate list item as a message 32 | 2. If message is a unicode or ascii string, it will be encoded as is 33 | 3. If message some arbitrary python object or a dict, it will be JSON 34 | encoded 35 | """ 36 | encoded = '' 37 | if isinstance(message, list): 38 | for msg in message: 39 | encoded += encode(msg) 40 | elif (not isinstance(message, (unicode, str)) 41 | and isinstance(message, (object, dict))): 42 | if message is not None: 43 | encoded += encode('~j~' + json.dumps(message, **json_decimal_args)) 44 | else: 45 | msg = message.encode('utf-8') 46 | encoded += "%s%d%s%s" % (FRAME, len(msg), FRAME, msg) 47 | 48 | return encoded 49 | 50 | def decode(data): 51 | """Decode socket.io messages 52 | 53 | Returns message tuples, first item in a tuple is message type (see 54 | message declarations in the beginning of the file) and second item 55 | is decoded message. 56 | """ 57 | messages = [] 58 | 59 | idx = 0 60 | 61 | while data[idx:idx+3] == FRAME: 62 | # Skip frame 63 | idx += 3 64 | 65 | len_start = idx 66 | while data[idx].isdigit(): 67 | idx += 1 68 | 69 | msg_len = int(data[len_start:idx]) 70 | 71 | msg_type = data[idx:idx + 3] 72 | 73 | # Skip message type 74 | idx += 3 75 | 76 | msg_data = data[idx:idx + msg_len] 77 | 78 | if msg_data.startswith(JSON): 79 | msg_data = json.loads(msg_data[3:]) 80 | elif msg_data.startswith(HEARTBEAT): 81 | msg_type = HEARTBEAT 82 | msg_data = msg_data[3:] 83 | 84 | messages.append((msg_type, msg_data)) 85 | 86 | idx += msg_len 87 | 88 | return messages 89 | -------------------------------------------------------------------------------- /tornadio/router.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.router 4 | ~~~~~~~~~~~~~~~ 5 | 6 | Transport protocol router and main entry point for all socket.io clients. 7 | 8 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 9 | :license: Apache, see LICENSE for more details. 10 | """ 11 | import logging 12 | 13 | from tornado import ioloop 14 | from tornado.web import RequestHandler, HTTPError 15 | 16 | from tornadio import persistent, polling, session 17 | 18 | PROTOCOLS = { 19 | 'websocket': persistent.TornadioWebSocketHandler, 20 | 'flashsocket': persistent.TornadioFlashSocketHandler, 21 | 'xhr-polling': polling.TornadioXHRPollingSocketHandler, 22 | 'xhr-multipart': polling.TornadioXHRMultipartSocketHandler, 23 | 'htmlfile': polling.TornadioHtmlFileSocketHandler, 24 | 'jsonp-polling': polling.TornadioJSONPSocketHandler, 25 | } 26 | 27 | DEFAULT_SETTINGS = { 28 | # Sessions check interval in seconds 29 | 'session_check_interval': 15, 30 | # Session expiration in seconds 31 | 'session_expiry': 30, 32 | # Heartbeat time in seconds. Do not change this value unless 33 | # you absolutely sure that new value will work. 34 | 'heartbeat_interval': 12, 35 | # Enabled protocols 36 | 'enabled_protocols': ['websocket', 'flashsocket', 'xhr-multipart', 37 | 'xhr-polling', 'jsonp-polling', 'htmlfile'], 38 | # XHR-Polling request timeout, in seconds 39 | 'xhr_polling_timeout': 20, 40 | } 41 | 42 | 43 | class SocketRouterBase(RequestHandler): 44 | """Main request handler. 45 | 46 | Manages creation of appropriate transport protocol implementations and 47 | passing control to them. 48 | """ 49 | _connection = None 50 | _route = None 51 | _sessions = None 52 | _sessions_cleanup = None 53 | settings = None 54 | 55 | def _execute(self, transforms, *args, **kwargs): 56 | try: 57 | extra = kwargs['extra'] 58 | proto_name = kwargs['protocol'] 59 | proto_init = kwargs['protocol_init'] 60 | session_id = kwargs['session_id'] 61 | 62 | logging.debug('Incoming session %s(%s) Session ID: %s Extra: %s' % ( 63 | proto_name, 64 | proto_init, 65 | session_id, 66 | extra 67 | )) 68 | 69 | # If protocol is disabled, raise HTTPError 70 | if proto_name not in self.settings['enabled_protocols']: 71 | raise HTTPError(403, 'Forbidden') 72 | 73 | protocol = PROTOCOLS.get(proto_name, None) 74 | 75 | if protocol: 76 | handler = protocol(self, session_id) 77 | handler._execute(transforms, *extra, **kwargs) 78 | else: 79 | raise Exception('Handler for protocol "%s" is not available' % 80 | proto_name) 81 | except ValueError: 82 | # TODO: Debugging 83 | raise HTTPError(403, 'Forbidden') 84 | 85 | @property 86 | def connection(self): 87 | """Return associated connection class.""" 88 | return self._connection 89 | 90 | @property 91 | def sessions(self): 92 | return self._sessions 93 | 94 | @classmethod 95 | def route(cls): 96 | """Returns prepared Tornado routes""" 97 | return cls._route 98 | 99 | @classmethod 100 | def tornadio_initialize(cls, connection, user_settings, resource, 101 | io_loop=None, extra_re=None, extra_sep=None): 102 | """Initialize class with the connection and resource. 103 | 104 | Does all behind the scenes work to setup routes, etc. Partially 105 | copied from SocketTornad.IO implementation. 106 | """ 107 | 108 | 109 | # Associate connection object 110 | cls._connection = connection 111 | 112 | # Initialize io_loop 113 | cls.io_loop = io_loop or ioloop.IOLoop.instance() 114 | 115 | # Associate settings 116 | settings = DEFAULT_SETTINGS.copy() 117 | 118 | if user_settings is not None: 119 | settings.update(user_settings) 120 | 121 | cls.settings = settings 122 | 123 | # Initialize sessions 124 | cls._sessions = session.SessionContainer() 125 | 126 | check_interval = settings['session_check_interval'] * 1000 127 | cls._sessions_cleanup = ioloop.PeriodicCallback(cls._sessions.expire, 128 | check_interval, 129 | cls.io_loop).start() 130 | 131 | # Copied from SocketTornad.IO with minor formatting 132 | if extra_re: 133 | if not extra_re.startswith('(?P'): 134 | extra_re = r'(?P%s)' % extra_re 135 | if extra_sep: 136 | extra_re = extra_sep + extra_re 137 | else: 138 | extra_re = "(?P)" 139 | 140 | proto_re = "|".join(PROTOCOLS.keys()) 141 | 142 | cls._route = (r"/(?P%s)%s/" 143 | "(?P%s)/?" 144 | "(?P[0-9a-zA-Z]*)/?" 145 | "(?P\d*?)|(?P\w*?)/?" 146 | "(?P\d*?)" % (resource, 147 | extra_re, 148 | proto_re), 149 | cls) 150 | 151 | def get_router(handler, settings=None, resource='socket.io/*', 152 | io_loop=None, extra_re=None, extra_sep=None): 153 | """Create new router class with desired properties. 154 | 155 | Use this function to create new socket.io server. For example: 156 | 157 | class PongConnection(SocketConnection): 158 | def on_message(self, message): 159 | self.send(message) 160 | 161 | PongRouter = get_router(PongConnection) 162 | 163 | application = tornado.web.Application([PongRouter.route()]) 164 | """ 165 | router = type('SocketRouter', (SocketRouterBase,), {}) 166 | router.tornadio_initialize(handler, settings, resource, 167 | io_loop, extra_re, extra_sep) 168 | return router 169 | -------------------------------------------------------------------------------- /tornadio/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.router 4 | ~~~~~~~~~~~~~~~ 5 | 6 | Implements handy wrapper to start FlashSocket server (if FlashSocket 7 | protocol is enabled). Shamesly borrowed from the SocketTornad.IO project. 8 | 9 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 10 | :license: Apache, see LICENSE for more details. 11 | """ 12 | import logging 13 | 14 | from tornado import ioloop 15 | from tornado.httpserver import HTTPServer 16 | 17 | from tornadio.flashserver import FlashPolicyServer 18 | 19 | class SocketServer(HTTPServer): 20 | """HTTP Server which does some configuration and automatic setup 21 | of Socket.IO based on configuration. 22 | Starts the IOLoop and listening automatically 23 | in contrast to the Tornado default behavior. 24 | If FlashSocket is enabled, starts up the policy server also.""" 25 | 26 | def __init__(self, application, 27 | no_keep_alive=False, io_loop=None, 28 | xheaders=False, ssl_options=None, 29 | auto_start=True 30 | ): 31 | """Initializes the server with the given request callback. 32 | 33 | If you use pre-forking/start() instead of the listen() method to 34 | start your server, you should not pass an IOLoop instance to this 35 | constructor. Each pre-forked child process will create its own 36 | IOLoop instance after the forking process. 37 | """ 38 | settings = application.settings 39 | 40 | flash_policy_file = settings.get('flash_policy_file', None) 41 | flash_policy_port = settings.get('flash_policy_port', None) 42 | socket_io_port = settings.get('socket_io_port', 8001) 43 | socket_io_address = settings.get('socket_io_address', '') 44 | 45 | io_loop = io_loop or ioloop.IOLoop.instance() 46 | 47 | HTTPServer.__init__(self, 48 | application, 49 | no_keep_alive, 50 | io_loop, 51 | xheaders, 52 | ssl_options) 53 | 54 | logging.info('Starting up tornadio server on port \'%s\'', 55 | socket_io_port) 56 | 57 | self.listen(socket_io_port, socket_io_address) 58 | 59 | if flash_policy_file is not None and flash_policy_port is not None: 60 | try: 61 | logging.info('Starting Flash policy server on port \'%d\'', 62 | flash_policy_port) 63 | 64 | FlashPolicyServer( 65 | io_loop = io_loop, 66 | port=flash_policy_port, 67 | policy_file=flash_policy_file) 68 | except Exception, ex: 69 | logging.error('Failed to start Flash policy server: %s', ex) 70 | 71 | # Set auto_start to False in order to have opportunities 72 | # to work with server object and/or perform some actions 73 | # after server is already created but before ioloop will start. 74 | # Attention: if you use auto_start param set to False 75 | # you should start ioloop manually 76 | if auto_start: 77 | logging.info('Entering IOLoop...') 78 | io_loop.start() 79 | -------------------------------------------------------------------------------- /tornadio/session.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | tornadio.session 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | Simple heapq-based session implementation with sliding expiration window 7 | support. 8 | 9 | :copyright: (c) 2011 by the Serge S. Koval, see AUTHORS for more details. 10 | :license: Apache, see LICENSE for more details. 11 | """ 12 | 13 | from heapq import heappush, heappop 14 | from time import time 15 | from hashlib import md5 16 | from random import random 17 | 18 | class Session(object): 19 | """Represents one session object stored in the session container. 20 | Derive from this object to store additional data. 21 | """ 22 | 23 | def __init__(self, session_id, expiry=None): 24 | self.session_id = session_id 25 | self.promoted = None 26 | self.expiry = expiry 27 | 28 | if self.expiry is not None: 29 | self.expiry_date = time() + self.expiry 30 | 31 | def promote(self): 32 | """Mark object is living, so it won't be collected during next 33 | run of the session garbage collector. 34 | """ 35 | if self.expiry is not None: 36 | self.promoted = time() + self.expiry 37 | 38 | def on_delete(self, forced): 39 | """Triggered when object was expired or deleted.""" 40 | pass 41 | 42 | def __cmp__(self, other): 43 | return cmp(self.expiry_date, other.expiry_date) 44 | 45 | def __repr__(self): 46 | return '%f %s %d' % (getattr(self, 'expiry_date', -1), 47 | self.session_id, 48 | self.promoted or 0) 49 | 50 | def _random_key(): 51 | """Return random session key""" 52 | i = md5() 53 | i.update('%s%s' % (random(), time())) 54 | return i.hexdigest() 55 | 56 | class SessionContainer(object): 57 | def __init__(self): 58 | self._items = dict() 59 | self._queue = [] 60 | 61 | def create(self, session, expiry=None, **kwargs): 62 | """Create new session object.""" 63 | kwargs['session_id'] = _random_key() 64 | kwargs['expiry'] = expiry 65 | 66 | session = session(**kwargs) 67 | 68 | self._items[session.session_id] = session 69 | 70 | if expiry is not None: 71 | heappush(self._queue, session) 72 | 73 | return session 74 | 75 | def get(self, session_id): 76 | """Return session object or None if it is not available""" 77 | return self._items.get(session_id, None) 78 | 79 | def remove(self, session_id): 80 | """Remove session object from the container""" 81 | session = self._items.get(session_id, None) 82 | 83 | if session is not None: 84 | session.promoted = -1 85 | session.on_delete(True) 86 | return True 87 | 88 | return False 89 | 90 | def expire(self, current_time=None): 91 | """Expire any old entries""" 92 | if not self._queue: 93 | return 94 | 95 | if current_time is None: 96 | current_time = time() 97 | 98 | while self._queue: 99 | # Top most item is not expired yet 100 | top = self._queue[0] 101 | 102 | # Early exit if item was not promoted and its expiration time 103 | # is greater than now. 104 | if top.promoted is None and top.expiry_date > current_time: 105 | break 106 | 107 | # Pop item from the stack 108 | top = heappop(self._queue) 109 | 110 | need_reschedule = (top.promoted is not None 111 | and top.promoted > current_time) 112 | 113 | # Give chance to reschedule 114 | if not need_reschedule: 115 | top.promoted = None 116 | top.on_delete(False) 117 | 118 | need_reschedule = (top.promoted is not None 119 | and top.promoted > current_time) 120 | 121 | # If item is promoted and expiration time somewhere in future 122 | # just reschedule it 123 | if need_reschedule: 124 | top.expiry_date = top.promoted 125 | top.promoted = None 126 | heappush(self._queue, top) 127 | else: 128 | del self._items[top.session_id] 129 | --------------------------------------------------------------------------------