Skip to content

Commit 087886e

Browse files
authored
Merge pull request #484 from ring-clojure/websockets
Add experimental websocket support
2 parents c62702a + 7169e59 commit 087886e

File tree

4 files changed

+333
-32
lines changed

4 files changed

+333
-32
lines changed

ring-core/src/ring/websocket.clj

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
(ns ring.websocket
2+
(:refer-clojure :exclude [send])
3+
(:import [java.nio ByteBuffer]))
4+
5+
(defprotocol Listener
6+
(on-open [listener socket])
7+
(on-message [listener socket message])
8+
(on-pong [listener socket data])
9+
(on-error [listener socket throwable])
10+
(on-close [listener socket code reason]))
11+
12+
(extend-protocol Listener
13+
clojure.lang.IPersistentMap
14+
(on-open [m socket]
15+
(when-let [kv (find m :on-open)] ((val kv) socket)))
16+
(on-message [m socket message]
17+
(when-let [kv (find m :on-message)] ((val kv) socket message)))
18+
(on-pong [m socket data]
19+
(when-let [kv (find m :on-pong)] ((val kv) socket data)))
20+
(on-error [m socket throwable]
21+
(when-let [kv (find m :on-error)] ((val kv) socket throwable)))
22+
(on-close [m socket code reason]
23+
(when-let [kv (find m :on-close)] ((val kv) socket code reason))))
24+
25+
(defprotocol Socket
26+
(-send [socket message])
27+
(-ping [socket data])
28+
(-pong [socket data])
29+
(-close [socket status reason]))
30+
31+
(defprotocol AsyncSocket
32+
(-send-async [socket message succeed fail]))
33+
34+
(defprotocol TextData
35+
(->string [data]))
36+
37+
(defprotocol BinaryData
38+
(->byte-buffer [data]))
39+
40+
(extend-protocol TextData
41+
String
42+
(->string [s] s))
43+
44+
(extend-protocol BinaryData
45+
(Class/forName "[B")
46+
(->byte-buffer [bs] (ByteBuffer/wrap bs))
47+
ByteBuffer
48+
(->byte-buffer [bb] bb))
49+
50+
(defn- encode-message [message]
51+
(cond
52+
(satisfies? TextData message) (->string message)
53+
(satisfies? BinaryData message) (->byte-buffer message)
54+
:else (throw (ex-info "message is not a valid text or binary data type"
55+
{:message message}))))
56+
57+
(defn send
58+
([socket message]
59+
(-send socket (encode-message message)))
60+
([socket message succeed fail]
61+
(-send-async socket (encode-message message) succeed fail)))
62+
63+
(defn ping
64+
([socket]
65+
(-ping socket (ByteBuffer/allocate 0)))
66+
([socket data]
67+
(-ping socket (->byte-buffer data))))
68+
69+
(defn pong
70+
([socket]
71+
(-pong socket (ByteBuffer/allocate 0)))
72+
([socket data]
73+
(-pong socket (->byte-buffer data))))
74+
75+
(defn close
76+
([socket]
77+
(-close socket 1000 "Normal Closure"))
78+
([socket code reason]
79+
(-close socket code reason)))
80+
81+
(defn websocket-request? [request]
82+
(let [headers (:headers request)]
83+
(and (.equalsIgnoreCase "upgrade" (get headers "connection"))
84+
(.equalsIgnoreCase "websocket" (get headers "upgrade")))))
85+
86+
(defn websocket-response? [response]
87+
(contains? response ::listener))

ring-jetty-adapter/project.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
:dependencies [[org.clojure/clojure "1.7.0"]
88
[ring/ring-core "1.10.0"]
99
[ring/ring-jakarta-servlet "1.10.0"]
10-
[org.eclipse.jetty/jetty-server "11.0.15"]]
10+
[org.eclipse.jetty/jetty-server "11.0.15"]
11+
[org.eclipse.jetty.websocket/websocket-jetty-server "11.0.15"]]
1112
:aliases {"test-all" ["with-profile" "default:+1.8:+1.9:+1.10:+1.11" "test"]}
1213
:profiles
1314
{:dev {:dependencies [[clj-http "3.12.3"]
14-
[less-awful-ssl "1.0.6"]]
15+
[less-awful-ssl "1.0.6"]
16+
[hato "0.9.0"]
17+
[org.slf4j/slf4j-simple "2.0.7"]]
1518
:jvm-opts ["-Dorg.eclipse.jetty.server.HttpChannelState.DEFAULT_TIMEOUT=500"]}
1619
:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
1720
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}

ring-jetty-adapter/src/ring/adapter/jetty.clj

Lines changed: 111 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
"A Ring adapter that uses the Jetty 9 embedded web server.
33
44
Adapters are used to convert Ring handlers into running web servers."
5-
(:require [ring.util.jakarta.servlet :as servlet])
6-
(:import [org.eclipse.jetty.server
5+
(:require [ring.util.jakarta.servlet :as servlet]
6+
[ring.websocket :as ws])
7+
(:import [java.nio ByteBuffer]
8+
[org.eclipse.jetty.server
79
Request
810
Server
911
ServerConnector
@@ -12,55 +14,136 @@
1214
HttpConnectionFactory
1315
SslConnectionFactory
1416
SecureRequestCustomizer]
17+
[org.eclipse.jetty.servlet ServletContextHandler ServletHandler]
1518
[org.eclipse.jetty.server.handler AbstractHandler]
1619
[org.eclipse.jetty.util BlockingArrayQueue]
1720
[org.eclipse.jetty.util.thread ThreadPool QueuedThreadPool]
1821
[org.eclipse.jetty.util.ssl SslContextFactory$Server KeyStoreScanner]
22+
[org.eclipse.jetty.websocket.server
23+
JettyWebSocketServerContainer
24+
JettyWebSocketCreator]
25+
[org.eclipse.jetty.websocket.api
26+
Session
27+
WebSocketConnectionListener
28+
WebSocketListener
29+
WebSocketPingPongListener
30+
WriteCallback]
31+
[org.eclipse.jetty.websocket.server.config
32+
JettyWebSocketServletContainerInitializer]
1933
[jakarta.servlet AsyncContext DispatcherType AsyncEvent AsyncListener]
2034
[jakarta.servlet.http HttpServletRequest HttpServletResponse]))
2135

22-
(defn- ^AbstractHandler proxy-handler [handler]
23-
(proxy [AbstractHandler] []
24-
(handle [_ ^Request base-request ^HttpServletRequest request response]
25-
(when-not (= (.getDispatcherType request) DispatcherType/ERROR)
26-
(let [request-map (servlet/build-request-map request)
27-
response-map (handler request-map)]
28-
(servlet/update-servlet-response response response-map)
29-
(.setHandled base-request true))))))
36+
(defn- websocket-socket [^Session session]
37+
(let [remote (.getRemote session)]
38+
(reify
39+
ws/Socket
40+
(-send [_ message]
41+
(if (string? message)
42+
(.sendString remote message)
43+
(.sendBytes remote message)))
44+
(-ping [_ data]
45+
(.sendPing remote data))
46+
(-pong [_ data]
47+
(.sendPong remote data))
48+
(-close [_ status reason]
49+
(.close session status reason))
50+
ws/AsyncSocket
51+
(-send-async [_ message succeed fail]
52+
(let [callback (reify WriteCallback
53+
(writeSuccess [_] (succeed))
54+
(writeFailed [_ ex] (fail ex)))]
55+
(if (string? message)
56+
(.sendString remote message callback)
57+
(.sendBytes remote message callback)))))))
58+
59+
(defn- websocket-listener [listener]
60+
(let [socket (volatile! nil)]
61+
(reify
62+
WebSocketConnectionListener
63+
(onWebSocketConnect [_ session]
64+
(vreset! socket (websocket-socket session))
65+
(ws/on-open listener @socket))
66+
(onWebSocketClose [_ status reason]
67+
(ws/on-close listener @socket status reason))
68+
(onWebSocketError [_ throwable]
69+
(ws/on-error listener @socket throwable))
70+
WebSocketListener
71+
(onWebSocketText [_ message]
72+
(ws/on-message listener @socket message))
73+
(onWebSocketBinary [_ payload offset length]
74+
(let [buffer (ByteBuffer/wrap payload offset length)]
75+
(ws/on-message listener @socket buffer)))
76+
WebSocketPingPongListener
77+
(onWebSocketPing [_ _])
78+
(onWebSocketPong [_ payload]
79+
(ws/on-pong listener @socket payload)))))
80+
81+
(defn- websocket-creator [{listener ::ws/listener}]
82+
(reify JettyWebSocketCreator
83+
(createWebSocket [_ _ _]
84+
(websocket-listener listener))))
85+
86+
(defn- upgrade-to-websocket [^HttpServletRequest request response response-map]
87+
(let [context (.getServletContext request)
88+
container (JettyWebSocketServerContainer/getContainer context)
89+
creator (websocket-creator response-map)]
90+
(.upgrade container creator request response)))
91+
92+
(defn- ^ServletHandler proxy-handler [handler]
93+
(proxy [ServletHandler] []
94+
(doHandle [_ ^Request base-request request response]
95+
(let [request-map (servlet/build-request-map request)
96+
response-map (handler request-map)]
97+
(try
98+
(if (ws/websocket-response? response-map)
99+
(upgrade-to-websocket request response response-map)
100+
(servlet/update-servlet-response response response-map))
101+
(finally
102+
(.setHandled base-request true)))))))
30103

31104
(defn- async-jetty-raise [^AsyncContext context ^HttpServletResponse response]
32105
(fn [^Throwable exception]
33106
(.sendError response 500 (.getMessage exception))
34107
(.complete context)))
35108

36-
(defn- async-jetty-respond [context response]
109+
(defn- async-jetty-respond [context request response]
37110
(fn [response-map]
38-
(servlet/update-servlet-response response context response-map)))
111+
(if (ws/websocket-response? response-map)
112+
(upgrade-to-websocket request response response-map)
113+
(servlet/update-servlet-response response context response-map))))
39114

40115
(defn- async-timeout-listener [request context response handler]
41116
(proxy [AsyncListener] []
42117
(onTimeout [^AsyncEvent _]
43118
(handler (servlet/build-request-map request)
44-
(async-jetty-respond context response)
119+
(async-jetty-respond context request response)
45120
(async-jetty-raise context response)))
46121
(onComplete [^AsyncEvent _])
47122
(onError [^AsyncEvent _])
48123
(onStartAsync [^AsyncEvent _])))
49124

50-
(defn- ^AbstractHandler async-proxy-handler [handler timeout timeout-handler]
51-
(proxy [AbstractHandler] []
52-
(handle [_ ^Request base-request ^HttpServletRequest request ^HttpServletResponse response]
125+
(defn- ^ServletHandler async-proxy-handler [handler timeout timeout-handler]
126+
(proxy [ServletHandler] []
127+
(doHandle [_ ^Request base-request request response]
53128
(let [^AsyncContext context (.startAsync request)]
54129
(.setTimeout context timeout)
55130
(when timeout-handler
56131
(.addListener
57132
context
58133
(async-timeout-listener request context response timeout-handler)))
59-
(handler
60-
(servlet/build-request-map request)
61-
(async-jetty-respond context response)
62-
(async-jetty-raise context response))
63-
(.setHandled base-request true)))))
134+
(try
135+
(handler
136+
(servlet/build-request-map request)
137+
(async-jetty-respond context request response)
138+
(async-jetty-raise context response))
139+
(finally
140+
(.setHandled base-request true)))))))
141+
142+
(defn- ^ServletContextHandler context-handler [proxy-handler]
143+
(doto (ServletContextHandler.)
144+
(.setServletHandler proxy-handler)
145+
(.setAllowNullPathInfo true)
146+
(JettyWebSocketServletContainerInitializer/configure nil)))
64147

65148
(defn- ^ServerConnector server-connector [^Server server & factories]
66149
(ServerConnector. server #^"[Lorg.eclipse.jetty.server.ConnectionFactory;" (into-array ConnectionFactory factories)))
@@ -213,13 +296,13 @@
213296
:response-header-size - the maximum size of a response header (default 8192)
214297
:send-server-version? - add Server header to HTTP response (default true)"
215298
[handler options]
216-
(let [server (create-server (dissoc options :configurator))]
217-
(if (:async? options)
218-
(.setHandler server
219-
(async-proxy-handler handler
220-
(:async-timeout options 0)
221-
(:async-timeout-handler options)))
222-
(.setHandler server (proxy-handler handler)))
299+
(let [server (create-server (dissoc options :configurator))
300+
proxy (if (:async? options)
301+
(async-proxy-handler handler
302+
(:async-timeout options 0)
303+
(:async-timeout-handler options))
304+
(proxy-handler handler))]
305+
(.setHandler server (context-handler proxy))
223306
(when-let [configurator (:configurator options)]
224307
(configurator server))
225308
(try

0 commit comments

Comments
 (0)