Skip to content

Commit

Permalink
add websocket multiprotocol support (http1, http2) (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
mostroverkhov authored Jul 25, 2024
1 parent 70998e9 commit f1dce3d
Show file tree
Hide file tree
Showing 19 changed files with 1,629 additions and 226 deletions.
54 changes: 47 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,30 @@
[![Build](https://github.com/jauntsdn/netty-websocket-http2/actions/workflows/ci-build.yml/badge.svg)](https://github.com/jauntsdn/netty-websocket-http2/actions/workflows/ci-build.yml)
# netty-websocket-http2

Netty based implementation of [rfc8441](https://tools.ietf.org/html/rfc8441) - bootstrapping websockets with http/2
Netty based implementation of [rfc8441](https://tools.ietf.org/html/rfc8441) - bootstrapping websockets with http/2, and multiprotocol support (websocket-over-http1, websocket-over-http2).

Library addresses 2 use cases: for application servers and clients,
It is transparent use of existing http1 websocket handlers on top of http2 streams; for gateways/proxies,
It is websockets-over-http2 support with no http1 dependencies and minimal overhead.
### use cases

* Websocket channel API

for application servers and clients, It provides transparent use of existing http1 websocket handlers on top of http2 streams. Compatible with
callbacks codec (described below).

* Websocket handshake only API

for gateways/proxies, It provides websockets-over-http2 support with no http1 dependencies and minimal overhead.

* Websocket multiprotocol

for application servers, It provides transparent use of existing http1 websocket handlers to process both http1 and http2 websockets.
Compatible with callbacks codec (described below).

[https://jauntsdn.com/post/netty-websocket-http2/](https://jauntsdn.com/post/netty-websocket-http2/)

### much faster http1 codec
Integration with [jauntsdn/netty-websocket-http1](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-callbacks-codec) codec for websocket-http1
Integration with [jauntsdn/netty-websocket-http1](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-callbacks-codec) codec (callbacks codec) for websocket-http1
frames processing [improves](https://github.com/jauntsdn/netty-websocket-http2/tree/develop/netty-websocket-http2-perftest/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/perftest/callbackscodec)
throughput 1.4x - 1.7x for small messages.
throughput 1.4x - 1.7x for small messages compared to one provided by netty (default codec).

### websocket channel API
Intended for application servers and clients.
Expand All @@ -25,6 +37,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler();
Http2WebSocketServerHandler http2webSocketHandler =
Http2WebSocketServerBuilder.create()
.codec(Http1WebSocketCodec.DEFAULT)
.acceptor(
(ctx, path, subprotocols, request, response) -> {
switch (path) {
Expand Down Expand Up @@ -70,6 +83,7 @@ EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler();
Http2WebSocketClientHandler http2WebSocketClientHandler =
Http2WebSocketClientBuilder.create()
.codec(Http1WebSocketCodec.DEFAULT)
.handshakeTimeoutMillis(15_000)
.build();
Expand Down Expand Up @@ -127,6 +141,26 @@ Runnable demo is available in `netty-websocket-http2-example` module -
[handshakeserver](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/handshakeserver/Main.java),
[channelclient](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/channelclient/Main.java).

### websocket multiprotocol
Provides transparent use of existing http1 websocket handlers to process both http1 and http2 websockets.

* Server
```groovy
MultiProtocolWebSocketServerHandler multiprotocolHandler =
MultiprotocolWebSocketServerBuilder.create()
.path("/echo")
.subprotocols("echo.jauntsdn.com")
.defaultCodec()
.handler(new DefaultEchoWebSocketHandler())
.build();
ch.pipeline().addLast(sslHandler, multiprotocolHandler);
```

Runnable demo is available in `netty-websocket-http2-example` module -
[multiprotocol.server.defaultcodec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/defaultcodec/Main.java),
[multiprotocol.server.callbackscodec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/server/callbackscodec/Main.java),
[multiprotocol.client.defaultcodec](https://github.com/jauntsdn/netty-websocket-http2/blob/develop/netty-websocket-http2-example/src/main/java/com/jauntsdn/netty/handler/codec/http2/websocketx/example/multiprotocol/client/Main.java),

### configuration
Initial settings of server http2 codecs (`Http2ConnectionHandler` or `Http2FrameCodec`) should contain [SETTINGS_ENABLE_CONNECT_PROTOCOL=1](https://tools.ietf.org/html/rfc8441#section-9.1)
parameter to advertise websocket-over-http2 support.
Expand Down Expand Up @@ -181,6 +215,12 @@ Events are fired on parent channel, also on websocket channel if one gets create
* `Http2WebSocketHandshakeErrorEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders, error)`
* `Http2WebSocketHandshakeSuccessEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders)`

These events are accompanied by transport agnostic variants

* `WebSocketHandshakeStartEvent(websocketId, path, subprotocols, timestampNanos, requestHeaders)`
* `WebSocketHandshakeErrorEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders, error)`
* `WebSocketHandshakeSuccessEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders)`

#### close events

Outbound `Http2WebSocketLocalCloseEvent` on websocket channel pipeline closes
Expand Down Expand Up @@ -265,7 +305,7 @@ the results are as follows (measured over time spans of 5 seconds):

* `channelserver, channelclient` packages for websocket subchannel API demos.
* `handshakeserver, channelclient` packages for handshake only API demo.
* `multiprotocolserver, multiprotocolclient` packages for demo of server handling htt1/http2 websockets on the same port.
* `multiprotocol` packages for demo of server handling htt1/http2 websockets on the same port.
* `lwsclient` package for client demo that runs against [https://libwebsockets.org/testserver/](https://libwebsockets.org/testserver/) which hosts websocket-over-http2
server implemented with [libwebsockets](https://github.com/warmcat/libwebsockets) - popular C-based networking library.

Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ subprojects {

repositories {
mavenCentral()
mavenLocal()
}

plugins.withType(JavaPlugin) {
Expand Down
3 changes: 3 additions & 0 deletions multiprotocol_callbacks_server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

./gradlew netty-websocket-http2-example:runMultiProtocolCallbacksServer
3 changes: 3 additions & 0 deletions multiprotocol_default_server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh

./gradlew netty-websocket-http2-example:runMultiProtocolDefaultServer
3 changes: 0 additions & 3 deletions multiprotocol_server.sh

This file was deleted.

13 changes: 10 additions & 3 deletions netty-websocket-http2-example/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ description = "Netty based implementation of rfc8441 - bootstrapping websockets

dependencies {
implementation project(":netty-websocket-http2")
implementation project(":netty-websocket-multiprotocol")
implementation project(":netty-websocket-http2-callbacks-codec")
implementation "org.slf4j:slf4j-api"
runtimeOnly "io.netty:netty-tcnative-boringssl-static::${osdetector.classifier}"
runtimeOnly "ch.qos.logback:logback-classic"
Expand All @@ -37,9 +39,14 @@ task runChannelServer(type: JavaExec) {
mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.channelserver.Main"
}

task runMultiProtocolServer(type: JavaExec) {
task runMultiProtocolDefaultServer(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolserver.Main"
mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.defaultcodec.Main"
}

task runMultiProtocolCallbacksServer(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.callbackscodec.Main"
}

task runChannelClient(type: JavaExec) {
Expand All @@ -54,6 +61,6 @@ task runLwsClient(type: JavaExec) {

task runMultiProtocolClient(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolclient.Main"
mainClass = "com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.client.Main"
}

Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocolclient;
package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.client;

import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientBuilder;
import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketClientHandler;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.callbackscodec;

import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketCallbacksHandler;
import com.jauntsdn.netty.handler.codec.http.websocketx.WebSocketFrameListener;
import com.jauntsdn.netty.handler.codec.http2.websocketx.Http2WebSocketEvent.Http2WebSocketHandshakeSuccessEvent;
import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security;
import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiProtocolWebSocketServerHandler;
import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiprotocolWebSocketServerBuilder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler.HandshakeComplete;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.ReferenceCountUtil;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
static final Logger logger = LoggerFactory.getLogger(Main.class);

public static void main(String[] args) throws Exception {
String host = "localhost";
int port = 8099;
SslContext sslContext = Security.serverSslContext("localhost.p12", "localhost");

Channel server =
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(
new ChannelInitializer<SocketChannel>() {

@Override
protected void initChannel(SocketChannel ch) {
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
MultiProtocolWebSocketServerHandler multiprotocolHandler =
MultiprotocolWebSocketServerBuilder.create()
.path("/echo")
.subprotocols("echo.jauntsdn.com")
.callbacksCodec()
.handler(
new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new CallbacksServerHandler());
}
})
.build();
ch.pipeline().addLast(sslHandler, multiprotocolHandler);
}
})
.bind(host, port)
.sync()
.channel();
logger.info("\n==> Websocket server (callbacks codec) is listening on {}:{}", host, port);
server.closeFuture().await();
}

private static class CallbacksServerHandler extends ChannelInboundHandlerAdapter {

@Override
public void userEventTriggered(ChannelHandlerContext c, Object evt) throws Exception {
if (evt instanceof HandshakeComplete || evt instanceof Http2WebSocketHandshakeSuccessEvent) {
WebSocketCallbacksHandler.exchange(
c,
(ctx, webSocketFrameFactory) ->
new WebSocketFrameListener() {
@Override
public void onChannelRead(
ChannelHandlerContext context,
boolean finalFragment,
int rsv,
int opcode,
ByteBuf payload) {
ByteBuf textFrame =
webSocketFrameFactory.mask(
webSocketFrameFactory.createTextFrame(
ctx.alloc(), payload.readableBytes()));
textFrame.writeBytes(payload);
payload.release();
ctx.write(textFrame);
}

@Override
public void onChannelReadComplete(ChannelHandlerContext ctx1) {
ctx1.flush();
}

@Override
public void onExceptionCaught(ChannelHandlerContext ctx1, Throwable cause) {
if (cause instanceof IOException) {
return;
}
logger.error("Unexpected websocket error", cause);
ctx1.close();
}
});
}
super.userEventTriggered(c, evt);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ReferenceCountUtil.safeRelease(msg);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.jauntsdn.netty.handler.codec.http2.websocketx.example.multiprotocol.server.defaultcodec;

import com.jauntsdn.netty.handler.codec.http2.websocketx.example.Security;
import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiProtocolWebSocketServerHandler;
import com.jauntsdn.netty.handler.codec.websocketx.multiprotocol.MultiprotocolWebSocketServerBuilder;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Main {
static final Logger logger = LoggerFactory.getLogger(Main.class);

public static void main(String[] args) throws Exception {
String host = "localhost";
int port = 8099;
SslContext sslContext = Security.serverSslContext("localhost.p12", "localhost");

Channel server =
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(
new ChannelInitializer<SocketChannel>() {

@Override
protected void initChannel(SocketChannel ch) {
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
MultiProtocolWebSocketServerHandler multiprotocolHandler =
MultiprotocolWebSocketServerBuilder.create()
.path("/echo")
.subprotocols("echo.jauntsdn.com")
.defaultCodec()
.handler(new DefaultEchoWebSocketHandler())
.build();
ch.pipeline().addLast(sslHandler, multiprotocolHandler);
}
})
.bind(host, port)
.sync()
.channel();
logger.info("\n==> Websocket server (default codec) is listening on {}:{}", host, port);
server.closeFuture().await();
}

@ChannelHandler.Sharable
private static class DefaultEchoWebSocketHandler
extends SimpleChannelInboundHandler<TextWebSocketFrame> {

@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame webSocketFrame) {
ctx.write(webSocketFrame.retain());
}

@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
super.channelReadComplete(ctx);
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof IOException) {
return;
}
logger.error("Unexpected websocket error", cause);
ctx.close();
}
}
}
Loading

0 comments on commit f1dce3d

Please sign in to comment.