Implementing HTTP and WebSocket Unified Port Handling with Spring Boot and Netty
Overview
This article demonstrates how too implement a unified port handling system for both HTTP and WebSocket protocols using Spring Boot and Netty. The solution allows a single server to process both HTTP requests and WebSocket connections on the same port (e.g., 8080).
Endpoint Examples:
- HTTP:
http://localhost:8080/api - WebSocket:
ws://localhost:8080/ws
The core implementation involves two primary channel handlers: one for HTTP requests and another for WebSocket frames.
WebSocket Handler Implemantation
The WebSocket handler processes encoming WebSocket frames and manages connection handshake events.
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebSocketFrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private static final Logger logger = LoggerFactory.getLogger(WebSocketFrameHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext context, WebSocketFrame frame) {
String messageContent = frame.content().toString(io.netty.util.CharsetUtil.UTF_8);
logger.info("WebSocket message received: {}", messageContent);
}
@Override
public void userEventTriggered(ChannelHandlerContext context, Object event) {
if (event instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
WebSocketServerProtocolHandler.HandshakeComplete handshakeEvent =
(WebSocketServerProtocolHandler.HandshakeComplete) event;
String requestUri = handshakeEvent.requestUri();
HttpHeaders requestHeaders = handshakeEvent.requestHeaders();
logger.info("WebSocket handshake completed - URI: {}", requestUri);
logger.info("WebSocket handshake headers: {}", requestHeaders);
}
}
}
HTTP Handler Implementation
The HTTP handler processes standard HTTP requests and routes them based on the request URI.
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
private static final Logger logger = LoggerFactory.getLogger(HttpRequestHandler.class);
@Override
protected void channelRead0(ChannelHandlerContext context, FullHttpRequest request) {
String requestPath = request.uri();
if (requestPath.endsWith("/api")) {
processApiRequest(context, request);
}
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.wrappedBuffer("OK".getBytes())
);
context.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private void processApiRequest(ChannelHandlerContext context, FullHttpRequest request) {
Channel channel = context.channel();
String requestBody = request.content().toString(CharsetUtil.UTF_8);
String httpMethod = request.method().name();
logger.info("API Request - Body: {}, Method: {}", requestBody, httpMethod);
}
}
Netty Configuration
This configuration class sets up the Netty server with both HTTP and WebSocket handlers.
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
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.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.cors.CorsConfig;
import io.netty.handler.codec.http.cors.CorsConfigBuilder;
import io.netty.handler.codec.http.cors.CorsHandler;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NettyServerConfig {
@Bean(destroyMethod = "shutdownGracefully")
public EventLoopGroup bossEventLoopGroup() {
return new NioEventLoopGroup();
}
@Bean(destroyMethod = "shutdownGracefully")
public EventLoopGroup workerEventLoopGroup() {
return new NioEventLoopGroup();
}
@Bean
public ServerBootstrap nettyServerBootstrap() {
return new ServerBootstrap();
}
@Bean
public Channel nettyServerChannel(EventLoopGroup bossGroup,
EventLoopGroup workerGroup,
ServerBootstrap bootstrap) {
return bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) {
CorsConfig corsConfig = CorsConfigBuilder.forAnyOrigin()
.allowNullOrigin()
.allowCredentials()
.build();
channel.pipeline().addLast(new CorsHandler(corsConfig));
channel.pipeline().addLast(new HttpServerCodec());
channel.pipeline().addLast(new ChunkedWriteHandler());
channel.pipeline().addLast(new HttpObjectAggregator(65536));
channel.pipeline().addLast(new WebSocketServerProtocolHandler("/ws"));
channel.pipeline().addLast(new WebSocketFrameHandler());
channel.pipeline().addLast(new HttpRequestHandler());
}
})
.bind(8080).syncUninterruptibly().channel();
}
}
Netty Web Server Integration
This class integrates the Netty server with Spring Boot's WebServer interface.
import io.netty.channel.Channel;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
public class NettyEmbeddedServer implements WebServer {
private final Channel serverChannel;
public NettyEmbeddedServer(Channel serverChannel) {
this.serverChannel = serverChannel;
}
@Override
public void start() throws WebServerException {
// Server starts immediately after channel binding
}
@Override
public void stop() throws WebServerException {
serverChannel.close().syncUninterruptibly();
}
@Override
public int getPort() {
return 8080;
}
}
Required Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
</dependencies>
Key Implementation Points
-
Unified Port Handling: Both HTTP and WebSocket protocols are handled on port 8080 through the same Netty server instance.
-
Protocol Detection: The pipeline automatically routes requests based on the protocol. WebSocket connections are upgraded via the
WebSocketServerProtocolHandler, while stanadrd HTTP requests are processed by theHttpRequestHandler. -
CORS Support: Cross-origin resource sharing is configured to allow requests from any origin.
-
Spring Integration: The Netty server is integrated with Spring Boot's WebServer interface, allowing it to replace the default Tomcat server.
-
Pipeline Order: The order of handlers in the pipeline is crucial. The WebSocket protocol handler must come before the frame handler, and HTTP handlers must be positioned to handle non-WebSocket traffic.
This implementation provides a foundation for building applications that require both RESTful HTTP APIs and real-time WebSocket communication through a single server port.