Skip to content

Commit 3d876b4

Browse files
Version of example that doesn't need jquery, fix IOError on resume, fix last message not sent til you send another bug
1 parent 99a14c4 commit 3d876b4

3 files changed

Lines changed: 164 additions & 17 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# -*- coding: utf-8 -*-
2+
import argparse
3+
import random
4+
import os
5+
6+
import cherrypy
7+
8+
from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
9+
from ws4py.websocket import WebSocket
10+
from ws4py.messaging import TextMessage
11+
12+
class ChatWebSocketHandler(WebSocket):
13+
def received_message(self, m):
14+
cherrypy.engine.publish('websocket-broadcast', m)
15+
16+
def closed(self, code, reason="A client left the room without a proper explanation."):
17+
cherrypy.engine.publish('websocket-broadcast', TextMessage(reason))
18+
19+
class Root(object):
20+
def __init__(self, host, port, ssl=False):
21+
self.host = host
22+
self.port = port
23+
self.scheme = 'wss' if ssl else 'ws'
24+
25+
@cherrypy.expose
26+
def index(self):
27+
return """<html>
28+
<head>
29+
</head>
30+
<body>
31+
<p>Note: If viewing this as localhost via SSL and it doesn't work, try using 127.0.0.1 directly</p>
32+
<div>
33+
<textarea id='chat' cols='35' rows='10'></textarea>
34+
<br />
35+
<label for='message'>%(username)s: </label><input id='message' />
36+
<button type="button" id='send' >Send</button>
37+
</div>
38+
<script type='application/javascript'>
39+
websocket = '%(scheme)s://%(host)s:%(port)s/ws';
40+
if (window.WebSocket) {
41+
ws = new WebSocket(websocket);
42+
}
43+
else if (window.MozWebSocket) {
44+
ws = MozWebSocket(websocket);
45+
}
46+
else {
47+
console.log('WebSocket Not Supported');
48+
}
49+
var c = document.getElementById('chat');
50+
51+
window.onbeforeunload = function(e) {
52+
c.value=c.value + 'Bye bye...\\n';
53+
ws.close(1000, '%(username)s left the room');
54+
55+
if(!e) e = window.event;
56+
e.stopPropagation();
57+
e.preventDefault();
58+
};
59+
ws.onmessage = function (evt) {
60+
c.value=c.value + evt.data + '\\n';
61+
};
62+
ws.onopen = function() {
63+
ws.send("%(username)s entered the room");
64+
};
65+
ws.onclose = function(evt) {
66+
c.value=c.value + 'Connection closed by server: ' + evt.code + ' \"' + evt.reason + '\"\\n';
67+
};
68+
69+
document.getElementById('send').onclick = function() {
70+
console.log(document.getElementById('message').value);
71+
ws.send("%(username)s: " +document.getElementById('message').value);
72+
document.getElementById('message').value ="";
73+
return false;
74+
};
75+
76+
77+
</script>
78+
</body>
79+
</html>
80+
""" % {'username': "User%d" % random.randint(0, 100), 'host': self.host, 'port': self.port, 'scheme': self.scheme}
81+
82+
@cherrypy.expose
83+
def ws(self):
84+
cherrypy.log("Handler created: %s" % repr(cherrypy.request.ws_handler))
85+
86+
if __name__ == '__main__':
87+
import logging
88+
from ws4py import configure_logger
89+
configure_logger(level=logging.DEBUG)
90+
91+
parser = argparse.ArgumentParser(description='Echo CherryPy Server')
92+
parser.add_argument('--host', default='127.0.0.1')
93+
parser.add_argument('-p', '--port', default=9000, type=int)
94+
parser.add_argument('--ssl', action='store_true')
95+
args = parser.parse_args()
96+
97+
cherrypy.config.update({'server.socket_host': args.host,
98+
'server.socket_port': args.port,
99+
'tools.staticdir.root': os.path.abspath(os.path.join(os.path.dirname(__file__), 'static'))})
100+
101+
if args.ssl:
102+
cherrypy.config.update({
103+
'server.ssl_module':'builtin',
104+
'server.ssl_certificate': 'server.crt',
105+
'server.ssl_private_key': 'server.key'})
106+
107+
WebSocketPlugin(cherrypy.engine).subscribe()
108+
cherrypy.tools.websocket = WebSocketTool()
109+
110+
cherrypy.quickstart(Root(args.host, args.port, args.ssl), '', config={
111+
'/ws': {
112+
'tools.websocket.on': True,
113+
'tools.websocket.handler_cls': ChatWebSocketHandler
114+
},
115+
'/js': {
116+
'tools.staticdir.on': True,
117+
'tools.staticdir.dir': 'js'
118+
}
119+
}
120+
)

ws4py/manager.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@ def poll(self):
9292
if not self._fds:
9393
time.sleep(self.timeout)
9494
return []
95-
96-
r, w, x = select.select(self._fds, [], [], self.timeout)
95+
try:
96+
r, w, x = select.select(self._fds, [], [], self.timeout)
97+
except IOError as e:
98+
return []
9799
return r
98100

99101
class EPollPoller(object):
@@ -135,7 +137,11 @@ def poll(self):
135137
Polls once and yields each ready-to-be-read
136138
file-descriptor
137139
"""
138-
events = self.poller.poll(timeout=self.timeout)
140+
try:
141+
events = self.poller.poll(timeout=self.timeout)
142+
except IOError:
143+
events = []
144+
139145
for fd, event in events:
140146
if event | select.EPOLLIN | select.EPOLLPRI:
141147
yield fd
@@ -179,7 +185,10 @@ def poll(self):
179185
Polls once and yields each ready-to-be-read
180186
file-descriptor
181187
"""
182-
events = self.poller.poll(timeout=self.timeout)
188+
try:
189+
events = self.poller.poll(timeout=self.timeout)
190+
except IOError:
191+
events = []
183192
for fd, event in events:
184193
if event | select.EPOLLIN | select.EPOLLPRI:
185194
yield fd
@@ -239,7 +248,7 @@ def add(self, websocket):
239248
"""
240249
if websocket in self:
241250
return
242-
251+
243252
logger.info("Managing websocket %s" % format_addresses(websocket))
244253
websocket.opened()
245254
with self.lock:
@@ -257,7 +266,7 @@ def remove(self, websocket):
257266
"""
258267
if websocket not in self:
259268
return
260-
269+
261270
logger.info("Removing websocket %s" % format_addresses(websocket))
262271
with self.lock:
263272
fd = websocket.sock.fileno()
@@ -296,17 +305,22 @@ def run(self):
296305
while self.running:
297306
with self.lock:
298307
polled = self.poller.poll()
299-
300308
if not self.running:
301309
break
302310

303311
for fd in polled:
304312
if not self.running:
305313
break
306-
314+
307315
ws = self.websockets.get(fd)
308-
309316
if ws and not ws.terminated:
317+
#I don't know what kind of errors might spew out of here, but they probably shouldn't crash the entire server.
318+
try:
319+
x = ws.once()
320+
#Treat the error as if once() had returned None
321+
except Exception as e:
322+
x=None
323+
logger.error("Terminating websocket %s due to exception: %s in once method" % (format_addresses(ws), repr(e)) )
310324
if not ws.once():
311325
with self.lock:
312326
fd = ws.sock.fileno()
@@ -317,6 +331,7 @@ def run(self):
317331
logger.info("Terminating websocket %s" % format_addresses(ws))
318332
ws.terminate()
319333

334+
320335
def close_all(self, code=1001, message='Server is shutting down'):
321336
"""
322337
Execute the :meth:`close() <ws4py.websocket.WebSocket.close>`

ws4py/websocket.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def __init__(self, sock, protocols=None, extensions=None, environ=None, heartbea
125125
At which interval the heartbeat will be running.
126126
Set this to `0` or `None` to disable it entirely.
127127
"""
128+
"Internal buffer to get around SSL problems"
129+
self.buf = b''
128130

129131
self._local_address = None
130132
self._peer_address = None
@@ -241,7 +243,7 @@ def unhandled_error(self, error):
241243
"""
242244
Called whenever a socket, or an OS, error is trapped
243245
by ws4py but not managed by it. The given error is
244-
an instance of `socket.error` or `OSError`.
246+
an instance of `socket.error` or `OSError`.
245247
246248
Note however that application exceptions will not go
247249
through this handler. Instead, do make sure you
@@ -306,9 +308,14 @@ def once(self):
306308
Performs the operation of reading from the underlying
307309
connection in order to feed the stream of bytes.
308310
309-
We start with a small size of two bytes to be read
310-
from the connection so that we can quickly parse an
311-
incoming frame header. Then the stream indicates
311+
Because this needs to support SSL sockets, we must always
312+
read as much as might be in the socket at any given time,
313+
however process expects to have itself called with only a certain
314+
number of bytes at a time. That number is found in
315+
self.reading_buffer_size, so we read everything into our own buffer,
316+
and then from there feed self.process.
317+
318+
Then the stream indicates
312319
whatever size must be read from the connection since
313320
it knows the frame payload length.
314321
@@ -321,13 +328,18 @@ def once(self):
321328
return False
322329

323330
try:
324-
b = self.sock.recv(self.reading_buffer_size)
331+
self.buf = self.buf +self.sock.recv(4096)
325332
except (socket.error, OSError) as e:
326333
self.unhandled_error(e)
327334
return False
328335
else:
329-
if not self.process(b):
330-
return False
336+
while len(self.buf)>=self.reading_buffer_size:
337+
#Get the oldest n bytes, and then remove them from the buffer.
338+
b = self.buf[:self.reading_buffer_size]
339+
self.buf = self.buf[self.reading_buffer_size:]
340+
#Process basically only returns false on errors.
341+
if not self.process(b):
342+
return False
331343

332344
return True
333345

@@ -377,7 +389,7 @@ def process(self, bytes):
377389

378390
if not bytes and self.reading_buffer_size > 0:
379391
return False
380-
392+
381393
self.reading_buffer_size = s.parser.send(bytes) or DEFAULT_READING_SIZE
382394

383395
if s.closing is not None:

0 commit comments

Comments
 (0)