Skip to content

Commit bbd3494

Browse files
committed
fun demo of websockt + canvas
1 parent 0f95379 commit bbd3494

18 files changed

Lines changed: 1701 additions & 0 deletions

example/websensors/app.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
WebSocket demo with a twist.
4+
5+
The idea of this little demo is to demonstrate
6+
how it can enable interesting functions when coupled with
7+
other HTML5 features, such as Canvas.
8+
9+
In this scenario, we create a canvas that we call a drawing
10+
board. You can draw circles with various colors. A board
11+
has a unique URL, every client connecting to that URL
12+
can see whatever is drawn by other participants. All events
13+
from one client are sent to other clients through
14+
websockets. All other clients then draw on their view
15+
events they receive.
16+
17+
This ought to work well with any HTML5 capable browser such
18+
as Chrome, Firefox, Opera. It even works well on Android using
19+
Chrome. Internet Explorer users... well it may work as well.
20+
21+
This demo uses:
22+
23+
* CherryPy 3.2.5+
24+
* Mako
25+
* jcanvas
26+
* jquery
27+
* HTML5boilerplate (via http://www.initializr.com/)
28+
29+
"""
30+
import json
31+
import os.path
32+
import tempfile
33+
import time
34+
import uuid
35+
36+
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
37+
from ws4py.websocket import WebSocket
38+
39+
import cherrypy
40+
from cherrypy.process import plugins
41+
from mako.lookup import TemplateLookup
42+
from mako.template import Template
43+
44+
cwd_dir = os.path.normpath(os.path.abspath(os.path.dirname(__file__)))
45+
bus = cherrypy.engine
46+
lookup = TemplateLookup(directories=os.path.join(cwd_dir, 'templates'),
47+
module_directory=os.path.join(cwd_dir, 'templates', '.cache'),
48+
input_encoding='utf-8',
49+
output_encoding='utf-8',
50+
collection_size=20)
51+
52+
class DrawingBoardWebSocketHandler(WebSocket):
53+
"""
54+
WebSocket handler that will dispatch drawing events
55+
from this client to all other registered clients
56+
on this board.
57+
58+
An instance of this class is automatically created
59+
when a client is connected. The `board_id` and
60+
`participant_id` attributes are set outside of this
61+
class, when the client is registered to the board.
62+
"""
63+
64+
def closed(self, code, reason):
65+
"""
66+
Called whenever the websocket connection is terminated,
67+
whether abruptly or normally.
68+
"""
69+
bus.websockets.unregister_participant(self.board_id,
70+
self.participant_id)
71+
72+
def received_message(self, m):
73+
"""
74+
Received messages contain events from the
75+
user interface, like coordinates. They are
76+
dispatched to all registered participants on this
77+
board.
78+
"""
79+
bus.websockets.broadcast_state(self.board_id,
80+
self.participant_id,
81+
m)
82+
83+
class DrawingBoardWebSocketPlugin(WebSocketPlugin):
84+
def __init__(self, bus):
85+
"""
86+
This plugin is the board controller. It keeps
87+
track of all boards and their registered participants.
88+
89+
You may access the global instance of this plugin
90+
through the `bus.websockets` attribute.
91+
"""
92+
WebSocketPlugin.__init__(self, bus)
93+
94+
# every 30s, we check if we have dead boards
95+
# and we clean them
96+
plugins.Monitor(bus, self.drop_dead_boards, 30).subscribe()
97+
98+
# board index to quickly retrieve
99+
# clients of a given board
100+
self.boards = {}
101+
102+
def drop_dead_boards(self):
103+
"""
104+
Iterate over all boards and unregister any
105+
that seem to be unused anymore (because it doesn't
106+
have any connected client anymore), or that
107+
have been created for too long (no more than than 5mn
108+
are allowed).
109+
"""
110+
for board_id in self.boards.copy():
111+
board = self.boards[board_id]
112+
if not board['handlers']:
113+
self.unregister_board(board_id)
114+
elif (time.time() - board['created']) > 300:
115+
for ws in board['handlers'].itervalues():
116+
if not ws.terminated:
117+
ws.close(1001, "Board can't exist for more than 5mn")
118+
self.unregister_board(board_id)
119+
120+
def register_board(self, board_id):
121+
"""
122+
Register a board and initialize it
123+
so that it'll accept participants.
124+
"""
125+
if board_id not in self.boards:
126+
self.bus.log("Registering board %s" % board_id)
127+
self.boards[board_id] = {'handlers': {}, 'created': time.time()}
128+
129+
def unregister_board(self, board_id):
130+
"""
131+
Unregister a board and make it unusable.
132+
"""
133+
self.boards.pop(board_id, None)
134+
135+
def register_participant(self, board_id, participant_id, ws_handler):
136+
"""
137+
Register a participant to the given board.
138+
We also set the `board_id` and `participant_id`
139+
attributes on the websocket handler instance.
140+
"""
141+
if board_id in self.boards:
142+
self.bus.log("Registering participant %s to board %s" % (participant_id, board_id))
143+
ws_handler.board_id = board_id
144+
ws_handler.participant_id = participant_id
145+
self.boards[board_id]['handlers'][participant_id] = ws_handler
146+
147+
def unregister_participant(self, board_id, participant_id):
148+
"""
149+
Unregister a participant from this board.
150+
151+
Usually this is called automatically when the client
152+
has closed its connection.
153+
"""
154+
if board_id in self.boards:
155+
board = self.boards[board_id]
156+
if participant_id in self.boards[board_id]:
157+
board['handlers'].pop(participant_id, None)
158+
self.bus.log("Unregistering participant %s from board %s" % (participant_id, board_id))
159+
160+
def broadcast_state(self, board_id, from_participant_id, state):
161+
"""
162+
Dispatch the given `state` to all registered participants
163+
of the board (except the sender itself of course).
164+
"""
165+
if board_id not in self.boards:
166+
return
167+
168+
board = self.boards[board_id]
169+
170+
for (participant_id, ws) in board['handlers'].iteritems():
171+
if from_participant_id != participant_id:
172+
if not ws.terminated:
173+
ws.send(state)
174+
175+
176+
def render_template(template):
177+
"""
178+
Renders a mako template to HTML and
179+
sets the CherryPy response's body with it.
180+
"""
181+
if cherrypy.response.status > 399:
182+
return
183+
184+
data = cherrypy.response.body or {}
185+
template = lookup.get_template(template)
186+
187+
if template and isinstance(data, dict):
188+
cherrypy.response.body = template.render(**data)
189+
190+
# Creating our tool so that they can be
191+
# used below in the CherryPy applications
192+
cherrypy.tools.render = cherrypy.Tool('before_finalize', render_template)
193+
cherrypy.tools.websocket = WebSocketTool()
194+
195+
196+
197+
198+
199+
# Web Application
200+
class SharedDrawingBoardApp(object):
201+
@cherrypy.expose
202+
@cherrypy.tools.render(template='index.html')
203+
def index(self):
204+
return {'boardid': str(uuid.uuid4())[:4]}
205+
206+
@cherrypy.expose
207+
@cherrypy.tools.render(template='board.html')
208+
def board(self, board_id):
209+
bus.websockets.register_board(board_id)
210+
return {'boardid': board_id,
211+
'participantid': str(uuid.uuid4())[:6]}
212+
213+
# WebSocket endpoint
214+
class SharedDrawingBoarWebSocketApp(object):
215+
@cherrypy.expose
216+
@cherrypy.tools.websocket(handler_cls=DrawingBoardWebSocketHandler)
217+
def index(self, board_id, participant_id):
218+
bus.websockets.register_participant(board_id, participant_id,
219+
cherrypy.request.ws_handler)
220+
221+
if __name__ == '__main__':
222+
bus.websockets = DrawingBoardWebSocketPlugin(bus)
223+
bus.websockets.subscribe()
224+
225+
cherrypy.config.update({
226+
'server.socket_host': '0.0.0.0',
227+
'server.thread_pool': 30,
228+
'log.screen': False,
229+
'log.access_file': os.path.join(cwd_dir, 'access.log'),
230+
'log.error_file': os.path.join(cwd_dir, 'error.log'),
231+
'tools.staticfile.root': cwd_dir,
232+
'tools.staticdir.root': cwd_dir,
233+
#'tools.proxy.on': True,
234+
#'tools.proxy.base': 'http://yourhost',
235+
'error_page.404': os.path.join(cwd_dir, "templates", "404.html")
236+
})
237+
238+
app = SharedDrawingBoardApp()
239+
app.ws = SharedDrawingBoarWebSocketApp()
240+
241+
cherrypy.tree.mount(app, '/', {
242+
'/': {
243+
'tools.encode.on': False,
244+
'tools.response_headers.on': True,
245+
},
246+
'/robots.txt': {
247+
'tools.staticfile.on': True,
248+
'tools.staticfile.filename': 'static/robots.txt'
249+
},
250+
'/static': {
251+
'tools.staticdir.on': True,
252+
'tools.staticdir.dir': 'static'
253+
}
254+
})
255+
256+
bus.signals.subscribe()
257+
bus.start()
258+
bus.block()
259+
260+
261+
262+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#board {
2+
background-color: #ccc;
3+
display: block;
4+
}
5+
6+
#tools {
7+
background-color: #000;
8+
float: left;
9+
display: block;
10+
}

0 commit comments

Comments
 (0)