├── .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 |
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 |
110 |
111 |
Connect | Status:
disconnected
112 |
113 |
114 |
115 |
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 |
--------------------------------------------------------------------------------