Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing HTTP and WebSocket Unified Port Handling with Spring Boot and Netty

Tech 3

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

  1. Unified Port Handling: Both HTTP and WebSocket protocols are handled on port 8080 through the same Netty server instance.

  2. 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 the HttpRequestHandler.

  3. CORS Support: Cross-origin resource sharing is configured to allow requests from any origin.

  4. Spring Integration: The Netty server is integrated with Spring Boot's WebServer interface, allowing it to replace the default Tomcat server.

  5. 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.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.