Building a Netty-Based Data Forwarding Service in Spring Boot

Environment: Windows 10, JDK 17, Spring Boot 3

Introduction to Netty

Nety is an asynchronous, event-driven network application framework for Java that simplifies the development of high-performance, reliible network servers and clients. It supports multiple protocols including TCP, UDP, and HTTP, and abstracts low-level networking complexities through its pipeline-based architecture.

Implementation Guide

Dependency Configuration

Add the Netty dependency to your pom.xml:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.84.Final</version>
</dependency>

If using Spring Data Redis, exclude its bundled Netty modules to avoid version conflicts:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-transport</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-handler</artifactId>
        </exclusion>
        <exclusion>
            <groupId>io.netty</groupId>
            <artifactId>netty-common</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Netty Server Bootstrap

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;

@Slf4j
@Component
public class NettyServer {

    public void start(InetSocketAddress address) {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                .group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(address)
                .childHandler(new NettyChannelInitializer())
                .option(ChannelOption.SO_BACKLOG, 12800)
                .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture future = bootstrap.bind(address).sync();
            log.info("Netty server listening on port: {}", address.getPort());
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("Server startup failed", e);
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

Channel Pipeline Initialization

import io.netty.channel.ChannelInitializer;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.socket.SocketChannel;

public class NettyChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) {
        ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(2048));
        ch.pipeline()
            .addLast("decoder", new HexMessageDecoder())
            .addLast("encoder", new HexMessageEncoder())
            .addLast(new MessageForwardingHandler());
    }
}

Custom Decoder (Hex String)

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;

public class HexMessageDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        byte[] raw = new byte[in.readableBytes()];
        in.readBytes(raw);
        out.add(bytesToHex(raw));
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b & 0xFF));
        }
        return sb.toString();
    }
}

Custom Encoder (Hex String)

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;

public class HexMessageEncoder extends MessageToByteEncoder<String> {

    @Override
    protected void encode(ChannelHandlerContext ctx, String hexStr, ByteBuf out) {
        out.writeBytes(hexStringToByteArray(hexStr));
    }

    private static byte[] hexStringToByteArray(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                                 + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }
}

Message Forwarding Handler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelId;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.net.InetSocketAddress;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
public class MessageForwardingHandler extends ChannelInboundHandlerAdapter {

    private static MessageForwardingHandler INSTANCE;
    private static final ConcurrentHashMap<ChannelId, ClientInfo> CHANNELS = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        INSTANCE = this;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        InetSocketAddress addr = (InetSocketAddress) ctx.channel().remoteAddress();
        ClientInfo info = new ClientInfo(ctx, addr.getAddress().getHostAddress(), addr.getPort());
        CHANNELS.put(ctx.channel().id(), info);
        log.info("Client connected: {}:{} [{}]", info.ip, info.port, ctx.channel().id());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        ClientInfo removed = CHANNELS.remove(ctx.channel().id());
        if (removed != null) {
            log.info("Client disconnected: {}:{} [{}]", removed.ip, removed.port, ctx.channel().id());
        }
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        String payload = msg.toString();
        log.info("Received from client: {}", payload);

        // Forward logic can be implemented here
        // e.g., forwardViaHttpClient(payload) or forwardViaNettyClient(payload)
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            log.warn("Idle timeout detected, closing connection");
            ctx.close();
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("Unexpected error", cause);
        ctx.close();
    }

    public static class ClientInfo {
        final ChannelHandlerContext ctx;
        final String ip;
        final int port;

        ClientInfo(ChannelHandlerContext ctx, String ip, int port) {
            this.ctx = ctx;
            this.ip = ip;
            this.port = port;
        }
    }
}

Netty-Based Forwarding Client

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ForwardingClient {

    private volatile Channel channel;
    private final EventLoopGroup group = new NioEventLoopGroup();
    private final String targetHost;
    private final int targetPort;

    public ForwardingClient(String host, int port) {
        this.targetHost = host;
        this.targetPort = port;
        connect();
    }

    public void forward(String message) {
        if (channel == null || !channel.isActive()) {
            log.warn("Reconnecting before forwarding");
            connect();
        }
        if (channel != null && channel.isActive()) {
            channel.writeAndFlush(message).addListener(future -> {
                if (future.isSuccess()) {
                    log.debug("Forwarded: {}", message);
                } else {
                    log.error("Forward failed", future.cause());
                }
            });
        }
    }

    private void connect() {
        try {
            Bootstrap bootstrap = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<Channel>() {
                    @Override
                    protected void initChannel(Channel ch) {
                        ch.pipeline()
                            .addLast(new StringEncoder(CharsetUtil.UTF_8))
                            .addLast(new StringDecoder(CharsetUtil.UTF_8));
                    }
                });

            ChannelFuture future = bootstrap.connect(targetHost, targetPort).sync();
            this.channel = future.channel();
            log.info("Forwarding client connected to {}:{}", targetHost, targetPort);
        } catch (Exception e) {
            log.error("Failed to connect forwarding client", e);
            group.shutdownGracefully();
        }
    }

    public void shutdown() {
        group.shutdownGracefully();
    }
}

Auto-Start Netty Server on Application Launch

@Component
public class NettyAutoStarter implements ApplicationRunner {

    @Autowired
    private NettyServer nettyServer;

    @Value("${netty.server.host:0.0.0.0}")
    private String host;

    @Value("${netty.server.port:9999}")
    private int port;

    @Override
    public void run(ApplicationArguments args) {
        nettyServer.start(new InetSocketAddress(host, port));
    }
}

Tags: Netty Spring Boot tcp Hex Encoding Network Programming

Posted on Sun, 10 May 2026 00:41:50 +0000 by gregor63