Skip to content

Commit 5949740

Browse files
committed
WebSocket: Initial implemenation with netty
1 parent 760f799 commit 5949740

16 files changed

Lines changed: 398 additions & 4 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package examples;
2+
3+
import io.jooby.ExecutionMode;
4+
import io.jooby.Jooby;
5+
6+
import java.nio.file.Paths;
7+
import java.util.concurrent.atomic.AtomicInteger;
8+
9+
public class WebSocketApp extends Jooby {
10+
{
11+
assets("/", Paths.get(System.getProperty("user.dir"), "examples", "www", "websocket"));
12+
13+
ws("/ws", ctx -> {
14+
AtomicInteger counter = new AtomicInteger();
15+
// ws.onConnect(ctx -> {
16+
// System.out.println("connect: " + counter.incrementAndGet());
17+
// });
18+
// ws.onMessage((ctx, msg) -> {
19+
// System.out.println("msg: " + counter.incrementAndGet() + " => " + msg);
20+
// System.out.println(Thread.currentThread());
21+
// });
22+
});
23+
}
24+
25+
public static void main(String[] args) {
26+
runApp(args, ExecutionMode.DEFAULT, WebSocketApp::new);
27+
// runApp(args, ExecutionMode.EVENT_LOOP, WebSocketApp::new);
28+
}
29+
}

examples/www/websocket/index.html

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>WebSocket Starter</title>
5+
</head>
6+
7+
<body>
8+
<div>
9+
<input type="text" id="input"/>
10+
</div>
11+
<div>
12+
<input type="button" id="connectBtn" value="CONNECT" onclick="connect()"/>
13+
<input type="button" id="sendBtn" value="SEND" onclick="send()" disabled="true"/>
14+
</div>
15+
<div id="output">
16+
<p>Output</p>
17+
</div>
18+
<script type="text/javascript" src="/index.js"></script>
19+
</body>
20+
</html>

examples/www/websocket/index.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
var webSocket;
2+
var output = document.getElementById("output");
3+
var connectBtn = document.getElementById("connectBtn");
4+
var sendBtn = document.getElementById("sendBtn");
5+
var protocol = window.location.pathname === "/secure" ? "wss" : "ws";
6+
var port = protocol === "wss" ? 8043 : 8080;
7+
function connect() {
8+
// open the connection if one does not exist
9+
if (webSocket !== undefined
10+
&& webSocket.readyState !== WebSocket.CLOSED) {
11+
return;
12+
}
13+
14+
// Create a websocket
15+
webSocket = new WebSocket(protocol + "://localhost:" + port + "/ws");
16+
17+
webSocket.onopen = function (event) {
18+
updateOutput("Connected!");
19+
connectBtn.disabled = true;
20+
sendBtn.disabled = false;
21+
22+
};
23+
24+
webSocket.onmessage = function (event) {
25+
updateOutput(event.data);
26+
};
27+
28+
webSocket.onclose = function (event) {
29+
updateOutput("Connection Closed");
30+
connectBtn.disabled = false;
31+
sendBtn.disabled = true;
32+
};
33+
}
34+
35+
function send() {
36+
var text = document.getElementById("input").value;
37+
webSocket.send(text);
38+
}
39+
40+
function closeSocket() {
41+
webSocket.close();
42+
}
43+
44+
function updateOutput(text) {
45+
output.innerHTML += "<br/>" + text;
46+
}

jooby/src/main/java/io/jooby/Context.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,10 @@ public interface Context extends Registry {
663663
*/
664664
@Nonnull Context detach(@Nonnull Route.Handler next) throws Exception;
665665

666+
default @Nonnull Context upgrade(@Nonnull WebSocket.Handler handler) {
667+
return null;
668+
}
669+
666670
/*
667671
* **********************************************************************************************
668672
* **** Response methods *************************************************************************

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ public <T> Jooby mvc(@Nonnull Class<T> router, @Nonnull Provider<T> provider) {
292292
}
293293
}
294294

295+
public Route ws(String pattern, WebSocket.Handler handler) {
296+
return router.ws(pattern, handler);
297+
}
298+
295299
@Nonnull @Override public List<Route> getRoutes() {
296300
return router.getRoutes();
297301
}

jooby/src/main/java/io/jooby/Router.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ interface Match {
216216
*/
217217
@Nonnull Router mvc(@Nonnull Object router);
218218

219+
@Nonnull Route ws(@Nonnull String pattern, @Nonnull WebSocket.Handler handler);
220+
219221
/**
220222
* Returns all routes.
221223
*
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.jooby;
2+
3+
public interface WebSocket {
4+
interface Handler {
5+
void apply(Context ctx);
6+
}
7+
8+
interface OnConnect {
9+
void onConnect(WebSocket ws);
10+
}
11+
12+
interface OnMessage {
13+
void onMessage(WebSocket ws);
14+
}
15+
16+
void onConnect(OnConnect listener);
17+
18+
void onMessage(OnMessage listener);
19+
20+
void onError(WebSocket ws, Throwable cause, StatusCode statusCode);
21+
22+
void onClose(WebSocket ws, StatusCode reason);
23+
}

jooby/src/main/java/io/jooby/internal/RouterImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
import io.jooby.BeanConverter;
99
import io.jooby.Context;
10+
import io.jooby.Jooby;
1011
import io.jooby.RegistryException;
1112
import io.jooby.ServiceKey;
1213
import io.jooby.SessionStore;
1314
import io.jooby.StatusCodeException;
1415
import io.jooby.ErrorHandler;
1516
import io.jooby.ExecutionMode;
16-
import io.jooby.Jooby;
1717
import io.jooby.MediaType;
1818
import io.jooby.MessageDecoder;
1919
import io.jooby.MessageEncoder;
@@ -24,8 +24,10 @@
2424
import io.jooby.ServiceRegistry;
2525
import io.jooby.StatusCode;
2626
import io.jooby.TemplateEngine;
27+
import io.jooby.WebSocket;
2728
import io.jooby.internal.asm.ClassSource;
2829
import io.jooby.ValueConverter;
30+
import io.jooby.internal.handler.WebSocketHandler;
2931
import org.slf4j.Logger;
3032
import org.slf4j.LoggerFactory;
3133

@@ -331,6 +333,10 @@ public Router encoder(@Nonnull MediaType contentType, @Nonnull MessageEncoder en
331333
return beanConverters;
332334
}
333335

336+
@Nonnull @Override public Route ws(@Nonnull String pattern, @Nonnull WebSocket.Handler handler) {
337+
return route(GET, pattern, new WebSocketHandler(handler));
338+
}
339+
334340
@Override
335341
public Route route(@Nonnull String method, @Nonnull String pattern,
336342
@Nonnull Route.Handler handler) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.jooby.internal.handler;
2+
3+
import io.jooby.Context;
4+
import io.jooby.Route;
5+
import io.jooby.StatusCode;
6+
import io.jooby.WebSocket;
7+
8+
import javax.annotation.Nonnull;
9+
10+
public class WebSocketHandler implements Route.Handler {
11+
private WebSocket.Handler handler;
12+
13+
public WebSocketHandler(WebSocket.Handler handler) {
14+
this.handler = handler;
15+
}
16+
17+
@Nonnull @Override public Object apply(@Nonnull Context ctx) throws Exception {
18+
boolean webSocket = ctx.header("Upgrade").value("").equalsIgnoreCase("WebSocket");
19+
if (webSocket) {
20+
return ctx.upgrade(handler);
21+
} else {
22+
return ctx.send(StatusCode.NOT_FOUND);
23+
}
24+
}
25+
}

modules/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import io.jooby.StatusCode;
2626
import io.jooby.Value;
2727
import io.jooby.ValueNode;
28+
import io.jooby.WebSocket;
2829
import io.netty.buffer.ByteBuf;
2930
import io.netty.buffer.Unpooled;
3031
import io.netty.channel.ChannelFuture;
3132
import io.netty.channel.ChannelFutureListener;
3233
import io.netty.channel.ChannelHandlerContext;
3334
import io.netty.channel.ChannelPipeline;
3435
import io.netty.channel.DefaultFileRegion;
36+
import io.netty.handler.codec.http.DefaultFullHttpRequest;
3537
import io.netty.handler.codec.http.DefaultFullHttpResponse;
3638
import io.netty.handler.codec.http.DefaultHttpHeaders;
3739
import io.netty.handler.codec.http.DefaultHttpResponse;
@@ -47,10 +49,16 @@
4749
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
4850
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
4951
import io.netty.handler.codec.http.multipart.InterfaceHttpPostRequestDecoder;
52+
import io.netty.handler.codec.http.websocketx.WebSocketDecoderConfig;
53+
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
54+
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
5055
import io.netty.handler.stream.ChunkedNioStream;
5156
import io.netty.handler.stream.ChunkedStream;
5257
import io.netty.handler.stream.ChunkedWriteHandler;
5358
import io.netty.util.ReferenceCounted;
59+
import io.netty.util.concurrent.EventExecutorGroup;
60+
import io.netty.util.concurrent.Future;
61+
import io.netty.util.concurrent.GenericFutureListener;
5462

5563
import javax.annotation.Nonnull;
5664
import java.io.FileInputStream;
@@ -94,7 +102,7 @@ public class NettyContext implements DefaultContext, ChannelFutureListener {
94102
InterfaceHttpPostRequestDecoder decoder;
95103
private Router router;
96104
private Route route;
97-
private ChannelHandlerContext ctx;
105+
ChannelHandlerContext ctx;
98106
private HttpRequest req;
99107
private String path;
100108
private HttpResponseStatus status = HttpResponseStatus.OK;
@@ -112,6 +120,7 @@ public class NettyContext implements DefaultContext, ChannelFutureListener {
112120
private Map<String, String> cookies;
113121
private Map<String, String> responseCookies;
114122
private Boolean resetHeadersOnError;
123+
NettyWebSocket webSocket;
115124

116125
public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, String path,
117126
int bufferSize) {
@@ -261,6 +270,35 @@ public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, S
261270
return this.cookies;
262271
}
263272

273+
@Nonnull @Override public Context upgrade(WebSocket.Handler handler) {
274+
try {
275+
String webSocketURL = getProtocol() + "://" + req.headers().get(HttpHeaderNames.HOST) + path;
276+
WebSocketDecoderConfig config = WebSocketDecoderConfig.newBuilder()
277+
.allowExtensions(true)
278+
.allowMaskMismatch(false)
279+
.withUTF8Validator(false)
280+
.maxFramePayloadLength(131072)
281+
.build();
282+
responseStarted = true;
283+
webSocket = new NettyWebSocket(this);
284+
handler.apply(this);
285+
DefaultFullHttpRequest fullHttpRequest = new DefaultFullHttpRequest(req.protocolVersion(),
286+
req.method(), req.uri(), Unpooled.EMPTY_BUFFER, req.headers(), EmptyHttpHeaders.INSTANCE);
287+
WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(webSocketURL,
288+
null, config);
289+
WebSocketServerHandshaker handshaker = factory.newHandshaker(fullHttpRequest);
290+
handshaker.handshake(ctx.channel(), fullHttpRequest, setHeaders, ctx.newPromise())
291+
.addListener(future -> {
292+
if (future.isSuccess()) {
293+
webSocket.fireConnect(this);
294+
}
295+
});
296+
} catch (Throwable x) {
297+
sendError(x);
298+
}
299+
return this;
300+
}
301+
264302
/* **********************************************************************************************
265303
* Response methods:
266304
* **********************************************************************************************

0 commit comments

Comments
 (0)