Skip to content

Commit f695eac

Browse files
committed
example project: use PubSubTopic instead of broadcast API
1 parent c618562 commit f695eac

File tree

5 files changed

+193
-101
lines changed

5 files changed

+193
-101
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/target
2+
/lib
3+
/classes
4+
/checkouts
5+
pom.xml
6+
pom.xml.asc
7+
*.jar
8+
*.class
9+
/.lein-*
10+
/.nrepl-port
11+
/bin
Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
(defproject clojure-web-example "0.1.0-SNAPSHOT"
1+
(defproject clojure-web-example "0.1.0"
22
:description "FIXME: write description"
3-
:url "http://example.com/FIXME"
3+
:url "https://github.com/nginx-clojure/nginx-clojure/tree/master/example-projects/clojure-web-example"
44
:min-lein-version "2.0.0"
55
:dependencies [[org.clojure/clojure "1.7.0"] ;; v1.5.1+ is OK
66
[compojure "1.4.0"]
77
[ring/ring-defaults "0.1.2"]
88
[ring/ring-anti-forgery "1.0.0"]
99
[org.clojure/tools.logging "0.3.1"]
1010
[ch.qos.logback/logback-classic "1.0.9"]
11-
[nginx-clojure "0.4.2"]]
12-
:plugins [[lein-ring "0.8.13"]]
13-
:ring {:handler clojure-web-example.handler/app}
14-
:profiles
15-
{:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
16-
[ring-mock "0.1.5"]
17-
[ring/ring-devel "1.4.0"]
18-
;;; embeded nginx-clojure is for debug/test usage
19-
[nginx-clojure/nginx-clojure-embed "0.4.2"]]}}
20-
:main ^:skip-aot clojure-web-example.handler)
11+
[org.clojure/tools.reader "0.8.1"]
12+
[nginx-clojure "0.4.3"]
13+
[ring/ring-devel "1.4.0"]
14+
]
15+
:profiles {
16+
:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
17+
[ring-mock "0.1.5"]]}
18+
:embed {:dependencies [
19+
;; embeded nginx-clojure is for debug/test usage
20+
[nginx-clojure/nginx-clojure-embed "0.4.3"]]
21+
:main clojure-web-example.embed-server
22+
}
23+
})

example-projects/clojure-web-example/resources/public/js/chat.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@ var chatroom = {
77
var chatInput = document.getElementById('chat')
88
chatInput.onkeydown = function(event) {
99
if (event.keyCode == 13) {
10-
var message = chatInput.value;
11-
if (message != '') {
12-
chatroom.channel.send(message);
13-
chatInput.value = '';
14-
}
10+
chatroom.send(chatInput);
1511
}
1612
};
13+
var sendBtn = document.getElementById('sendbtn');
14+
sendBtn.onclick = function(event) {
15+
chatroom.send(chatInput);
16+
}
17+
1718
};
1819
this.channel.onclose = function () {
1920
document.getElementById('chat').onkeydown = null;
20-
chatroom.printMsg('Info: WebSocket closed.');
21+
chatroom.printMsg('websocket closed!');
2122
};
2223

2324
this.channel.onmessage = function (msg) {
@@ -26,13 +27,30 @@ var chatroom = {
2627
},
2728
printMsg : function(msg) {
2829
var board = document.getElementById('board');
29-
var p = document.createElement('p');
30-
p.style.wordWrap = 'break-word';
30+
var p = document.createElement('a');
31+
var color = "success";//["success", "info", "warning", "danger"];
32+
if (/\[enter!\]$/.test(msg)) {
33+
color = "warning";
34+
}else if (/\[left!\]$/.test(msg)) {
35+
color = "danger";
36+
}else {
37+
color = "info";
38+
}
39+
p.className = 'list-group-item list-group-item-' + color;
40+
p.href="#";
3141
p.innerHTML = msg;
3242
board.appendChild(p);
3343
board.scrollTop = board.scrollHeight;
3444
},
3545

46+
send : function(chatInput) {
47+
var message = chatInput.value;
48+
if (message != '') {
49+
chatroom.channel.send(message);
50+
chatInput.value = '';
51+
}
52+
},
53+
3654
init : function() {
3755
this.connect('ws://' + window.location.host + "/chat");
3856
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
(ns clojure-web-example.embed-server
2+
(:gen-class)
3+
(:use [clojure-web-example.handler])
4+
(:require [nginx.clojure.embed :as embed]
5+
[clojure.tools.logging :as log]
6+
[ring.middleware.reload :refer [wrap-reload]]))
7+
8+
9+
10+
(defn start-server
11+
"Run an emebed nginx-clojure for debug/test usage."
12+
[dev?]
13+
(embed/run-server
14+
(if dev?
15+
;; Use wrap-reload to enable auto-reload namespaces of modified files
16+
;; DO NOT use wrap-reload in production enviroment
17+
(do
18+
(log/info "enable auto-reloading in dev enviroment")
19+
(wrap-reload #'app))
20+
app)
21+
{:port 8080
22+
;;setup jvm-init-handler
23+
:jvm-init-handler jvm-init-handler
24+
;; define shared map for PubSubTopic
25+
:http-user-defined, "shared_map PubSubTopic tinymap?space=1m&entries=256;\n
26+
shared_map mySessionStore tinymap?space=1m&entries=256;"}))
27+
28+
(defn stop-server
29+
"Stop the embed nginx-clojure"
30+
[]
31+
(embed/stop-server))
32+
33+
(defn -main
34+
[& args]
35+
(let [port (start-server (empty? args))]
36+
(try
37+
(.browse (java.awt.Desktop/getDesktop) (java.net.URI. (str "http://localhost:" port "/")))
38+
(catch java.awt.HeadlessException _))))
39+

example-projects/clojure-web-example/src/clojure_web_example/handler.clj

Lines changed: 102 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
(ns clojure-web-example.handler
2-
(:gen-class)
32
(:require [compojure.core :refer :all]
43
[compojure.route :as route]
54
[hiccup.core :as hiccup]
65
[clojure.tools.logging :as log]
7-
[nginx.clojure.embed :as embed]
86
[nginx.clojure.core :as ncc]
7+
[nginx.clojure.session]
98
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
10-
[ring.util.anti-forgery :refer [anti-forgery-field]]
11-
[ring.middleware.reload :refer [wrap-reload]]))
9+
[ring.util.anti-forgery :refer [anti-forgery-field]]))
1210

1311
(defn handle-login [uid pass session]
1412
"Here we can add server-side auth. In this example we'll just always authenticate
@@ -23,74 +21,116 @@
2321

2422
(def chatroom-users-channels (atom {}))
2523

26-
(def chatroom-event-tag (int (+ 0x80 50)))
24+
(def my-session-store
25+
;; When worker_processes > 1 in nginx.conf, we can not use the default in-memory session store
26+
;; because there're more than one JVM instances and requests from the same session perphaps
27+
;; will be handled by different JVM instances. So here we use cookie store, or nginx shared map store
28+
;; and if we use redis to shared sessions we can try [carmine-store] (https://github.com/ptaoussanis/carmine) or
29+
;; [redis session store] (https://github.com/wuzhe/clj-redis-session)
30+
31+
;; use cookie store
32+
;(ring.middleware.session.cookie/cookie-store {:key "a 16-byte secret"})
33+
34+
;; use nginx shared map store
35+
(nginx.clojure.session/shared-map-store "mySessionStore")
36+
)
37+
38+
(def chatroom-topic)
39+
40+
(def sub-listener-removal-fn)
2741

28-
;; When worker_processes > 1 in nginx.conf, there're more than one JVM instances
29-
;; and requests from the same session perphaps will be handled by different JVM instances.
30-
;; We setup broadcast event listener here to get chatroom messages from other JVM instances.
31-
(def init-broadcast-event-listener
32-
(delay
33-
(ncc/on-broadcast-event-decode!
34-
;;tester
35-
(fn [{tag :tag}]
36-
(= tag chatroom-event-tag))
37-
;;decoder
38-
(fn [{:keys [tag data offset length] :as e}]
39-
(assoc e :data (String. data offset length "utf-8"))))
40-
(ncc/on-broadcast!
41-
(fn [{:keys [tag data]}]
42-
(log/debug "onbroadcast pid=" ncc/process-id tag data @chatroom-users-channels)
43-
(condp = tag
44-
chatroom-event-tag
45-
(doseq [[uid ch] @chatroom-users-channels]
46-
(ncc/send! ch data true false))
47-
nil)))))
42+
;; Because when we use embeded nginx-clojure the nginx-clojure JNI methods
43+
;; won't be registered until the first startup of the nginx server so we need
44+
;; use delayed initialization to make sure some initialization work
45+
;; to be done after nginx-clojure JNI methods being registered.
46+
(defn jvm-init-handler [_]
47+
;; init chatroom topic
48+
;; When worker_processes > 1 in nginx.conf, there're more than one JVM instances
49+
;; and requests from the same session perphaps will be handled by different JVM instances.
50+
;; We need setup subscribing message listener here to get chatroom messages from other JVM instances.
51+
;; The below graph show the message flow in a chatroom
52+
53+
; \-----/ (1)send (js) +-------+
54+
; |User1| -------------------->|WorkerA|
55+
; /-----\ +-------+
56+
; ^ | |
57+
; | (3)send! | |(2)pub!
58+
; '---------------------------' |
59+
; V
60+
; \-----/ (3)send! +-------+
61+
; |User2| <------------------ |WorkerB|
62+
; /-----\ +-------+
63+
(def chatroom-topic (ncc/build-topic! "chatroom-topic"))
64+
;; avoid duplicate adding when auto-reload namespace is enabled in dev enviroments.
65+
(when (bound? #'sub-listener-removal-fn) (sub-listener-removal-fn))
66+
(def sub-listener-removal-fn
67+
(ncc/sub! chatroom-topic nil
68+
(fn [msg _]
69+
(doseq [[uid ch] @chatroom-users-channels]
70+
(ncc/send! ch msg true false)))))
71+
nil)
72+
73+
(def common-html-header
74+
[:head
75+
[:link {:rel "stylesheet" :href "//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"}]
76+
[:link {:rel "stylesheet" :href "//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css"}]
77+
[:script {:src "//ajax.aspnetcdn.com/ajax/jQuery/jquery-1.11.0.min.js"}]
78+
[:script {:src "//netdna.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"}]])
4879

4980
(defroutes app-routes
81+
;; home page
5082
(GET "/" [:as req]
5183
(hiccup/html
52-
[:h1 "Nginx-Clojure Web Example"]
53-
[:hr]
54-
[:h2 (str "Current User: " (get-user req))]
55-
[:a {:href "/hello1"} "HelloWorld"]
56-
[:p]
57-
[:a {:href "/hello2"} "HelloUser"]
58-
[:p]
59-
[:a {:href "/login"} "login"]
60-
[:p]
61-
[:a {:href "/chatroom"} "chatroom"]
84+
common-html-header
85+
[:div.jumbotron
86+
[:h1 "Nginx-Clojure Web Example"]
87+
[:hr]
88+
[:div.alert.alert-success {:role "alert"} (str "Current User: " (get-user req))]
89+
[:p
90+
[:div.btn-toolbar
91+
(for [[href label] {"/hello1" "HelloWorld", "/hello2" "HelloUser",
92+
"/login" "Login", "/chatroom" "ChatRoom"}]
93+
[:a.btn.btn-primary.btn-lg {:href href :role "button"} label])]
94+
]]
6295
))
6396
(GET "/hello1" [] "Hello World!")
6497
(GET "/hello2" [:as req]
6598
(str "Hello " (get-user req) "!"))
6699
;; Websocket based chatroom
100+
;; We can open two browser sessions to test it.
67101
(GET "/chatroom" [:as req]
68102
(hiccup/html
69-
[:h2 (str "Current User: " (get-user req))]
70-
[:hr]
71-
[:input#chat {:type :text :placeholder "type and press ENTER to chat"}]
72-
[:div#container
73-
[:div#board]]
103+
common-html-header
104+
[:div.container
105+
[:div.panel.panel-success
106+
[:div.panel-heading [:h3.panel-title "Chat Room" "@" (get-user req)]]
107+
[:div.input-group.panel-body
108+
[:input#chat.form-control {:type :text :placeholder "type and press ENTER to chat"}]
109+
[:span.input-group-btn
110+
[:button#sendbtn.btn.btn-default {:type :button} "Send!"]]
111+
]
112+
[:div#board.list-group
113+
]
114+
[:div.panel-footer]
115+
]]
74116
[:script {:src "js/chat.js"}]))
117+
;; chatroom Websocket server endpoint
75118
(GET "/chat" [:as req]
76-
@init-broadcast-event-listener
77119
(let [ch (ncc/hijack! req true)
78120
uid (get-user req)]
79121
(when (ncc/websocket-upgrade! ch true)
80-
(ncc/add-aggregated-listener! ch 512
122+
(ncc/add-aggregated-listener! ch 500
81123
{:on-open (fn [ch]
82124
(log/debug "user:" uid " connected!")
83125
(swap! chatroom-users-channels assoc uid ch)
84-
(ncc/broadcast! {:tag chatroom-event-tag :data (str uid ":[enter!]")}))
126+
(ncc/pub! chatroom-topic (str uid ":[enter!]")))
85127
:on-message (fn [ch msg]
86128
(log/debug "user:" uid " msg:" msg)
87-
;; Broadcast message to all nginx worker processes. For more details please
88-
;; see the comments above the definition of `init-broadcast-event-listener`
89-
(ncc/broadcast! {:tag chatroom-event-tag :data (str uid ":" msg)}))
129+
(ncc/pub! chatroom-topic (str uid ":" msg)))
90130
:on-close (fn [ch reason]
91131
(log/debug "user:" uid " left!")
92132
(swap! chatroom-users-channels dissoc uid)
93-
(ncc/broadcast! {:tag chatroom-event-tag :data (str uid ":[left!]")}))})
133+
(ncc/pub! chatroom-topic (str uid ":[left!]")))})
94134
{:status 200 :body ch})))
95135
;; Static files, e.g js/chat.js in dir `public`
96136
;; In production environments it will be overwrited by
@@ -103,43 +143,24 @@
103143
(handle-login uid pass session))
104144
(GET "/login" []
105145
(hiccup/html
106-
[:form {:action "/login" :method "POST"}
107-
(anti-forgery-field)
108-
[:input#user-id {:type :text :name :uid :placeholder "User ID"}]
109-
[:input#user-pass {:type :password :name :pass :placeholder "Password"}]
110-
[:input#submit-btn {:type "submit" :value "Login!"}]
111-
])))
112-
113-
114-
(def my-session-store
115-
;; When worker_processes > 1 in nginx.conf, we can not use the default in-memory session store
116-
;; because there're more than one JVM instances and requests from the same session perphaps
117-
;; will be handled by different JVM instances. So here we use cookie store another choice is
118-
;; [redis session store] (https://github.com/wuzhe/clj-redis-session)
119-
(ring.middleware.session.cookie/cookie-store {:key "a 16-byte secret"}))
146+
common-html-header
147+
[:div.container
148+
[:div.panel.panel-primary
149+
[:div.panel-heading [:h3.panel-title "Login Form"]]
150+
[:div.input-group.panel-body
151+
[:form.form-signin {:action "/login" :method "POST"}
152+
[:h2.form-signin-heading "Please sign in"]
153+
(anti-forgery-field)
154+
[:input#user-id.form-control {:type :text :name :uid :placeholder "User ID"}]
155+
[:input#user-pass.form-control {:type :password :name :pass :placeholder "Password"}]
156+
[:p]
157+
[:input#submit-btn.btn.btn-primary.btn-block {:type "submit" :value "Login!"}]
158+
]]
159+
[:div.panel-footer]
160+
]])))
120161

121162

122163
(def app
123164
(wrap-defaults (routes auth-routes app-routes)
124165
(update-in site-defaults [:session]
125166
assoc :store my-session-store)))
126-
127-
(defn start-server
128-
"Run an emebed nginx-clojure for debug/test usage."
129-
[dev?]
130-
(embed/run-server
131-
(if dev?
132-
;; Use wrap-reload to enable auto-reload namespaces of modified files
133-
;; DO NOT use wrap-reload in production enviroment
134-
(wrap-reload #'app)
135-
app)
136-
{:port 8080}))
137-
138-
(defn stop-server
139-
"Stop the embed nginx-clojure"
140-
[]
141-
(embed/stop-server))
142-
143-
(defn -main
144-
[& args]
145-
(start-server (empty? args)))

0 commit comments

Comments
 (0)