11 KiB
netty-websocket-http2
Netty based implementation of rfc8441 - bootstrapping websockets with http/2
Library addresses two 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.
https://jauntsdn.com/post/netty-websocket-http2/
websocket channel API
Intended for application servers and clients.
Allows transparent application of existing http1 websocket handlers on top of http2 stream.
- Server
EchoWebSocketHandler http1WebSocketHandler = new EchoWebSocketHandler();
Http2WebSocketServerHandler http2webSocketHandler =
Http2WebSocketServerBuilder.create()
.acceptor(
(ctx, path, subprotocols, request, response) -> {
switch (path) {
case "/echo":
if (subprotocols.contains("echo.jauntsdn.com")
&& acceptUserAgent(request, response)) {
/*selecting subprotocol for accepted requests is mandatory*/
Http2WebSocketAcceptor.Subprotocol
.accept("echo.jauntsdn.com", response);
return ctx.executor()
.newSucceededFuture(http1WebSocketHandler);
}
break;
case "/echo_all":
if (subprotocols.isEmpty()
&& acceptUserAgent(request, response)) {
return ctx.executor()
.newSucceededFuture(http1WebSocketHandler);
}
break;
}
return ctx.executor()
.newFailedFuture(
new WebSocketHandshakeException(
"websocket rejected, path: " + path));
})
.build();
ch.pipeline()
.addLast(sslHandler,
http2frameCodec,
http2webSocketHandler);
- Client
Channel channel =
new Bootstrap()
.handler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
Http2WebSocketClientHandler http2WebSocketClientHandler =
Http2WebSocketClientBuilder.create()
.handshakeTimeoutMillis(15_000)
.build();
ch.pipeline()
.addLast(
sslHandler,
http2FrameCodec,
http2WebSocketClientHandler);
}
})
.connect(address)
.sync()
.channel();
Http2WebSocketClientHandshaker handShaker = Http2WebSocketClientHandshaker.create(channel);
Http2Headers headers =
new DefaultHttp2Headers().set("user-agent", "jauntsdn-websocket-http2-client/1.1.2");
ChannelFuture handshakeFuture =
/*http1 websocket handler*/
handShaker.handshake("/echo", headers, new EchoWebSocketHandler());
handshakeFuture.channel().writeAndFlush(new TextWebSocketFrame("hello http2 websocket"));
Successfully handshaked http2 stream spawns websocket subchannel, with provided
http1 websocket handlers on its pipeline.
Runnable demo is available in netty-websocket-http2-example
module -
channelserver,
channelclient.
websocket handshake only API
Intended for intermediaries/proxies.
Only verifies whether http2 stream is valid websocket, then passes it down the pipeline as POST
request with x-protocol=websocket
header.
Http2WebSocketServerHandler http2webSocketHandler =
Http2WebSocketServerBuilder.buildHandshakeOnly();
Http2StreamsHandler http2StreamsHandler = new Http2StreamsHandler();
ch.pipeline()
.addLast(sslHandler,
frameCodec,
http2webSocketHandler,
http2StreamsHandler);
Works with both callbacks-style Http2ConnectionHandler
and frames based Http2FrameCodec
.
Http2WebSocketServerBuilder.buildHandshakeOnly();
Runnable demo is available in netty-websocket-http2-example
module -
handshakeserver,
channelclient.
configuration
Initial settings of server http2 codecs (Http2ConnectionHandler
or Http2FrameCodec
) should contain SETTINGS_ENABLE_CONNECT_PROTOCOL=1
parameter to advertise websocket-over-http2 support.
Also server http2 codecs must disable built-in headers validation because It is not compatible
with rfc8441 due to newly introduced :protocol
pseudo-header. All websocket handlers provided by this library
do headers validation on their own - both for websocket and non-websocket requests.
Above configuration may be done with utility methods of Http2WebSocketServerBuilder
public static Http2FrameCodecBuilder configureHttp2Server(
Http2FrameCodecBuilder http2Builder);
public static Http2ConnectionHandlerBuilder configureHttp2Server(
Http2ConnectionHandlerBuilder http2Builder)
compression & subprotocols
Client and server permessage-deflate
compression configuration is shared by all streams
Http2WebSocketServerBuilder.compression(enabled);
or
Http2WebSocketServerBuilder.compression(
compressionLevel,
allowServerWindowSize,
preferredClientWindowSize,
allowServerNoContext,
preferredClientNoContext);
Client subprotocols are configured on per-path basis
EchoWebSocketHandler http1WebsocketHandler = new EchoWebSocketHandler();
ChannelFuture handshake =
handShaker.handshake("/echo", "subprotocol", headers, http1WebsocketHandler);
On a server It is responsibility of Http2WebSocketAcceptor
to select supported subprotocol with
Http2WebSocketAcceptor.Subprotocol.accept(subprotocol, response);
lifecycle
Handshake events and several shutdown options are available when
using Websocket channel
style APIs.
handshake events
Events are fired on parent channel, also on websocket channel if one gets created
Http2WebSocketHandshakeStartEvent(websocketId, path, subprotocols, timestampNanos, requestHeaders)
Http2WebSocketHandshakeErrorEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders, error)
Http2WebSocketHandshakeSuccessEvent(webSocketId, path, subprotocols, timestampNanos, responseHeaders)
close events
Outbound Http2WebSocketLocalCloseEvent
on websocket channel pipeline closes
http2 stream by sending empty DATA
frame with END_STREAM
flag set.
Graceful and RST
stream shutdown by remote endpoint is represented with inbound Http2WebSocketRemoteCloseEvent
(with type CLOSE_REMOTE_ENDSTREAM
and CLOSE_REMOTE_RESET
respectively) on websocket channel pipeline.
Graceful connection shutdown by remote with GO_AWAY
frame is represented by inbound Http2WebSocketRemoteGoAwayEvent
on websocket channel pipeline.
shutdown
Closing websocket channel terminates its http2 stream by sending RST
frame.
validation & write error events
Both API style handlers send Http2WebSocketHandshakeErrorEvent
for invalid websocket-over-http2 and http requests.
For http2 frame write errors Http2WebSocketWriteErrorEvent
is sent on parent channel if auto-close is not enabled;
otherwise exception is delivered with ChannelPipeline.fireExceptionCaught
followed by immediate close.
flow control
Inbound flow control is done automatically as soon as DATA
frames are received.
Library relies on netty's DefaultHttp2LocalFlowController
for refilling receive window.
Outbound flow control is expressed as websocket channels writability change on send window
exhaust/refill, provided by DefaultHttp2RemoteFlowController
.
websocket stream weight
Initial stream weight is configured with
Http2WebSocketClientBuilder.streamWeight(weight);
it can be updated by firing Http2WebSocketStreamWeightUpdateEvent
on websocket channel pipeline.
performance
Library relies on capabilities provided by netty's Http2ConnectionHandler
so performance characteristics should be similar.
netty-websocket-http2-perftest
module contains application that gives rough throughput/latency estimate. The application is started with perf_server.sh
, perf_client.sh
.
On modern box one can expect following results for single websocket:
To evaluate performance with multiple connections we compose an application comprised with simple echo server, and client sending batches of messages periodically over single websocket per connection (approximately models chat application)
With 25k active connections each sending batches of 5-10 messages of 0.2-0.5 KBytes over single websocket every 15-30seconds, the results are as follows (measured over time spans of 5 seconds):
examples
netty-websocket-http2-example
module contains demos showcasing both API styles, with this library/browser as clients.
channelserver, channelclient
packages for websocket subchannel API demos.handshakeserver, channelclient
packages for handshake only API demo.lwsclient
package for client demo that runs against https://libwebsockets.org/testserver/ which hosts websocket-over-http2 server implemented with libwebsockets - popular C-based networking library.
browser example
Channelserver
example serves web page at https://www.localhost:8099
that sends pings to /echo
endpoint.
Currently only Mozilla Firefox
and latest Google Chrome
support websockets-over-http2.
build & binaries
./gradlew
Releases are published on MavenCentral
repositories {
mavenCentral()
}
dependencies {
implementation 'com.jauntsdn.netty:netty-websocket-http2:1.1.2'
}