Skip to content

Commit 8683736

Browse files
author
user
committed
RR
1 parent f3c5b17 commit 8683736

File tree

7 files changed

+186
-47
lines changed

7 files changed

+186
-47
lines changed

Diff for: lua/pusher.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ while true do
133133
wb:send_close()
134134
else
135135
-- normal traffic on the channel; copy to websocket
136-
local ok, err = wb:send_text(cjson.encode({src=chan, msg=msg}))
136+
local ok, err = wb:send_text(msg)
137137
if not ok then
138138
STATE:die("Couldn't write: " .. err)
139139
end

Diff for: nginx.conf

+2-2
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ http {
6363
alias static/$1$2;
6464
}
6565

66-
location = /___WS {
67-
# Websockets come in here; may have arg: channel name
66+
location = /__W__ {
67+
# Websockets come in here
6868
access_by_lua_file 'lua/before.lua';
6969
content_by_lua_file 'lua/pusher.lua';
7070
}

Diff for: python/cfcapp.py

+62-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
import time
3-
from flask import Flask, json
3+
from flask import Flask, json, request
44
from flask.app import setupmethod
55
from threading import Thread
66

@@ -10,24 +10,35 @@ def start(self):
1010
super(DaemonThread, self).start()
1111

1212
class WSConnection(object):
13-
def __init__(self, app, fid, wsid):
13+
def __init__(self, app, wsid, fid):
1414
self.app = app
1515
self.fid = fid
1616
self.wsid = wsid
1717

1818
def __repr__(self):
1919
return '<WSConnection %s fid=..%s>' % (self.wsid, self.fid[-4:])
2020

21-
def open(self):
21+
def created(self):
2222
# do something when first connected
23+
print "%r: created" % self
2324
pass
2425

2526
def closed(self):
26-
# do something after closed; cannot send messages at this point
27+
# do something after being closed, but cannot send messages at this point
28+
print "%r: closed" % self
2729
pass
2830

2931
def tx(self, msg):
30-
self.app.tx(msg, wsid=self.wsid)
32+
self.app.tx(msg, conn=self)
33+
34+
class CFCContextVars(object):
35+
# This class is put into the context of all templates as "CFC"
36+
#
37+
@property
38+
def WEBSOCKET_URL(self):
39+
" Provide a URL for the websocket to be used "
40+
scheme = request.environ['wsgi.url_scheme']
41+
return '%s://%s/__W__' % ('ws' if scheme == 'http' else 'wss', request.host)
3142

3243
class CFCFlask(Flask):
3344
''' Extensions to Flask() object to support app needs for CFC frontend '''
@@ -45,20 +56,33 @@ class CFCFlask(Flask):
4556
ping_rate = 15 # seconds
4657

4758
def __init__(self, *a, **kws):
48-
# list of functions that want to receive data from websocket clients
59+
60+
# List of functions that want to receive data from websocket clients
61+
# Extend this using the decorator app.ws_rx_handler
4962
self.ws_rx_handlers = []
5063

5164
# map of all current connections
5265
self.ws_connections = {}
5366

67+
# Domains we are implementing today; lowercase, canonical names only.
68+
# you can still redirect www. variations and such, but don't include them
69+
# in this list.
5470
self.my_vhosts = kws.pop('vhosts', ['lh', 'none'])
71+
72+
# We need some threads. You can add yours too, by decorating with
73+
# app.background_task
74+
self.ws_background_tasks = [ self.pinger, self.rxer ]
5575

5676
super(CFCFlask, self).__init__(*a, **kws)
5777

78+
@self.context_processor
79+
def extra_ctx():
80+
return dict(CFC = CFCContextVars())
81+
5882
def pinger(self):
5983
# Keep all connections alive with some minimal traffic
6084
RDB = self.redis
61-
RDB.publish('bcast', 'server restart')
85+
#RDB.publish('bcast', 'RESTART')
6286

6387
while 1:
6488
RDB.publish('bcast', 'PING')
@@ -79,8 +103,7 @@ def rxer(self):
79103
vhost = vhost[3:]
80104
assert vhost in self.my_vhosts, "Unexpended hostname: %s" % vhost
81105

82-
# Data from WS is wrapped in some json by LUA code. Trustable.
83-
self.logger.debug('before = %r' % here)
106+
# This data from WS is already wrapped as JSON by LUA code. Trustable.
84107
try:
85108
here = json.loads(here)
86109
except:
@@ -109,24 +132,32 @@ def rxer(self):
109132
conn = self.ws_connections.get(wsid, None)
110133
if not conn:
111134
# this will happen if you restart python while the nginx/lua stays up
112-
self.logger.warn('New WSID')
135+
self.logger.warn('Existing/unexpected WSID')
113136
conn = self.ws_new_connection(wsid, fid)
114137

115138
# Important: do not trust "msg" here as it comes
116139
# unverified from browser-side code. Could be nasty junk.
117-
self.logger.debug('here = %r' % here)
118140
msg = here.get('msg', None)
119141

142+
if msg[0] == '{' and msg[-1] == '}':
143+
# looks like json
144+
try:
145+
msg = json.loads(msg)
146+
except:
147+
self.logger.debug('RX[%s] got bad JSON: %r' % (vhost, msg))
148+
149+
120150
for handler in self.ws_rx_handlers:
121151
handler(vhost, conn, msg)
122152

123-
self.logger.debug('RX[%s] %r' % (vhost, msg))
153+
if not self.ws_rx_handlers:
154+
self.logger.debug('RX[%s] %r' % (vhost, msg))
124155

125156
def ws_new_connection(self, wsid, fid):
126157
''' New WS connection, track it.
127158
'''
128159
self.ws_connections[wsid] = rv = WSConnection(self, wsid, fid)
129-
rv.open()
160+
rv.created()
130161
return rv
131162

132163
def tx(self, msg, conn = None, fid=None, wsid=None, bcast=False):
@@ -146,27 +177,40 @@ def tx(self, msg, conn = None, fid=None, wsid=None, bcast=False):
146177
elif bcast:
147178
chan = 'bcast'
148179

180+
if not isinstance(msg, basestring):
181+
# convert into json, if not already
182+
msg = json.dumps(msg)
183+
149184
self.redis.publish(chan, msg)
150185

151-
def ws_close(self, wsid):
186+
def ws_close(self, wsid_or_conn):
152187
'''
153188
Close a specific web socket from server side; perhaps because it mis-behaved.
154189
155190
LUA code detects this message and kills it's connection.
156191
'''
157-
self.tx('CLOSE', wsid=wsid)
192+
self.tx('CLOSE', wsid=getattr(wsid_or_conn, 'wsid', wsid_or_conn))
158193

159194
@setupmethod
160195
def ws_rx_handler(self, f):
161-
"""Registers a function to be called when traffic is received via web sockets
196+
"""
197+
Registers a function to be called when traffic is received via web sockets
162198
163199
"""
164200
self.ws_rx_handlers.append(f)
165201
return f
202+
203+
@setupmethod
204+
def background_task(self, f):
205+
"""
206+
Registers a function to be run as a background thread
207+
"""
208+
self.ws_background_tasks.append(f)
209+
return f
166210

167211

168212
def start_bg_tasks(self):
169213
''' start long-lived background threads '''
170-
DaemonThread(name="pinger", target=self.pinger, args=[]).start()
171-
DaemonThread(name="rxer", target=self.rxer, args=[]).start()
214+
for fn in self.ws_background_tasks:
215+
DaemonThread(name=fn.__name__, target=fn, args=[]).start()
172216

Diff for: python/example_app.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
1+
import time
12
from flask import request, Response, render_template, json
23
from cfcapp import CFCFlask
34

45
app = CFCFlask(__name__)
56

67
@app.ws_rx_handler
78
def rx_data(vhost, conn, msg):
8-
say = json.loads(msg)['say']
9-
m = json.dumps({'from': conn.fid[-8:].upper(),
10-
'content': say})
9+
say = msg['say']
10+
11+
m = {'from': conn.fid[-8:].upper(), 'content': say}
1112
app.tx(m, bcast=True)
1213

14+
class Robot(object):
15+
def __init__(self):
16+
self.heard = set()
17+
18+
@app.background_task
19+
def robot1():
20+
while 1:
21+
say = "I'm a robot and the time is %s" % time.strftime('%T')
22+
m = {'from': 'Robot1', 'content': say}
23+
app.tx(m, bcast=True)
24+
time.sleep(15)
25+
26+
@app.ws_rx_handler
27+
def rx_data(vhost, conn, msg):
28+
said = msg['say']
29+
if 'robot' in said: return
30+
31+
user = conn.fid[-8:].upper()
32+
if user not in self.heard:
33+
m = {'from': 'Robot1', 'content': "Hello %s" % user}
34+
app.tx(m, bcast=True)
35+
self.heard.add(user)
36+
37+
Robot()
38+
39+
1340
@app.route('/')
1441
def ws_test():
1542
return render_template("chat.html", vhost=request.host)

Diff for: python/fserver.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def start_server(ip, port, debug, redis_url):
6060

6161

6262
if debug:
63-
app.run(host="0.0.0.0", port=port)
63+
app.run(host="0.0.0.0", port=port, debug=True)
6464
else:
6565
print "Running as FastCGI at %s:%d" % (ip, port)
6666
MyWSGIServer(app, bindAddress=(ip, port), multiplexed=True, umask=0).run()

Diff for: python/templates/chat.html

+82-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,88 @@
11
<!DOCTYPE html>
2-
<meta charset="utf-8" />
3-
<title>CFC Chat Demo</title>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<title>Bootstrap 101 Template</title>
48

5-
<h2>CFC Chat Demo</h2>
9+
<!-- Bootstrap -->
10+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
11+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
612

7-
<button onclick="doSend('Hello!')">Say: Hello</button>
8-
<button onclick="doSend('Bye.')">Say: Bye</button>
9-
<button onclick="doSend('Testing 1, 2, 3...')">Say: Testing</button>
13+
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
14+
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
15+
<!--[if lt IE 9]>
16+
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
17+
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
18+
<![endif]-->
19+
20+
<style>
21+
body {
22+
padding-top: 50px;
23+
}
24+
.demo-template {
25+
padding: 40px 15px;
26+
}
27+
28+
#output {
29+
max-height: 320px;
30+
overflow-y: scroll;
31+
}
32+
33+
#output pre {
34+
padding: 4px;
35+
margin: 0;
36+
}
37+
</style>
38+
39+
40+
41+
</head>
42+
<body>
43+
<nav class="navbar navbar-inverse navbar-fixed-top">
44+
<div class="container">
45+
<div class="navbar-header">
46+
<a class="navbar-brand" href="#">CFC Demo</a>
47+
</div>
48+
</div>
49+
</nav>
50+
51+
<div class="container">
52+
<div class="row">
53+
<div class="col-md-7">
54+
<h3>About</h3>
55+
<p>
56+
This is just a very simple demo of websocket-based chat.
57+
Much more is possible, including dynamic page content loading or
58+
calling JS methods from the server side. The ultimate would be
59+
to load only a single HTML page, and then tell the browser to
60+
fetch other resources only as needed.
61+
</p>
62+
</div>
63+
<div class="col-md-5">
64+
<div class="demo-template">
65+
<h3>Chat Box</h3>
66+
67+
<button class="btn btn-default" onclick="doSend('Hello!')">Say: Hello</button>
68+
<button class="btn btn-default" onclick="doSend('Bye.')">Say: Bye</button>
69+
<button class="btn btn-default" onclick="doSend('Testing 1, 2, 3...')">Say: Testing</button>
70+
71+
<hr>
72+
<div id="output"></div>
73+
</div>
74+
</div>
75+
</div>
76+
77+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
78+
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
79+
80+
<script language="javascript" type="text/javascript">
81+
{% include "websocket.js" with context %}
82+
</script>
83+
84+
</body>
85+
</html>
1086

11-
<hr>
12-
<div id="output"></div>
1387

1488

15-
<script language="javascript" type="text/javascript">
16-
{% include "websocket.js" with context %}
17-
</script>

Diff for: python/templates/websocket.js

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
var wsUri = "ws://{{request.host}}/___WS";
1+
var wsUri = "{{CFC.WEBSOCKET_URL}}";
22

33
var output;
44
function init() {
@@ -22,31 +22,28 @@ function onClose(evt) {
2222

2323
function onMessage(evt) {
2424

25-
var frame = JSON.parse(evt.data);
26-
console.log("rx=", frame);
25+
var msg = JSON.parse(evt.data);
26+
console.log("msg=", msg);
2727

28-
if(!frame.msg) return;
29-
30-
var app = JSON.parse(frame.msg);
31-
console.log("app=", app);
32-
33-
if(app) {
34-
writeToScreen('<span style="color: blue;">' + app.from + ':</span> ' + app.content);
28+
if(msg.from) {
29+
writeToScreen('<span style="color: blue;">' + msg.from + ':</span> ' + msg.content);
3530
}
3631
}
3732
function onError(evt) {
3833
writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
3934
}
4035
function doSend(message) {
4136
message = JSON.stringify({'say': message});
42-
//writeToScreen("TX: " + message);
4337
websocket.send(message);
4438
}
4539
function writeToScreen(message) {
4640
var pre = document.createElement("pre");
4741
pre.style.wordWrap = "break-word";
4842
pre.innerHTML = message;
4943
output.appendChild(pre);
44+
45+
$(output).scrollTop($(output)[0].scrollHeight);
46+
5047
}
5148

5249
window.addEventListener("load", init, false);

0 commit comments

Comments
 (0)