|
1 | 1 | (ns clojure-web-example.handler
|
2 |
| - (:gen-class) |
3 | 2 | (:require [compojure.core :refer :all]
|
4 | 3 | [compojure.route :as route]
|
5 | 4 | [hiccup.core :as hiccup]
|
6 | 5 | [clojure.tools.logging :as log]
|
7 |
| - [nginx.clojure.embed :as embed] |
8 | 6 | [nginx.clojure.core :as ncc]
|
| 7 | + [nginx.clojure.session] |
9 | 8 | [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]])) |
12 | 10 |
|
13 | 11 | (defn handle-login [uid pass session]
|
14 | 12 | "Here we can add server-side auth. In this example we'll just always authenticate
|
|
23 | 21 |
|
24 | 22 | (def chatroom-users-channels (atom {}))
|
25 | 23 |
|
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) |
27 | 41 |
|
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"}]]) |
48 | 79 |
|
49 | 80 | (defroutes app-routes
|
| 81 | + ;; home page |
50 | 82 | (GET "/" [:as req]
|
51 | 83 | (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 | + ]] |
62 | 95 | ))
|
63 | 96 | (GET "/hello1" [] "Hello World!")
|
64 | 97 | (GET "/hello2" [:as req]
|
65 | 98 | (str "Hello " (get-user req) "!"))
|
66 | 99 | ;; Websocket based chatroom
|
| 100 | + ;; We can open two browser sessions to test it. |
67 | 101 | (GET "/chatroom" [:as req]
|
68 | 102 | (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 | + ]] |
74 | 116 | [:script {:src "js/chat.js"}]))
|
| 117 | + ;; chatroom Websocket server endpoint |
75 | 118 | (GET "/chat" [:as req]
|
76 |
| - @init-broadcast-event-listener |
77 | 119 | (let [ch (ncc/hijack! req true)
|
78 | 120 | uid (get-user req)]
|
79 | 121 | (when (ncc/websocket-upgrade! ch true)
|
80 |
| - (ncc/add-aggregated-listener! ch 512 |
| 122 | + (ncc/add-aggregated-listener! ch 500 |
81 | 123 | {:on-open (fn [ch]
|
82 | 124 | (log/debug "user:" uid " connected!")
|
83 | 125 | (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!]"))) |
85 | 127 | :on-message (fn [ch msg]
|
86 | 128 | (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))) |
90 | 130 | :on-close (fn [ch reason]
|
91 | 131 | (log/debug "user:" uid " left!")
|
92 | 132 | (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!]")))}) |
94 | 134 | {:status 200 :body ch})))
|
95 | 135 | ;; Static files, e.g js/chat.js in dir `public`
|
96 | 136 | ;; In production environments it will be overwrited by
|
|
103 | 143 | (handle-login uid pass session))
|
104 | 144 | (GET "/login" []
|
105 | 145 | (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 | + ]]))) |
120 | 161 |
|
121 | 162 |
|
122 | 163 | (def app
|
123 | 164 | (wrap-defaults (routes auth-routes app-routes)
|
124 | 165 | (update-in site-defaults [:session]
|
125 | 166 | 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