How to Use Spring Boot, Netty, and WebSocket for Server‑to‑Client Push

This article walks through building a Netty‑based WebSocket server integrated with Spring Boot, configuring the channel pipeline, implementing custom handlers, exposing a push‑message service, and testing the end‑to‑end flow that enables the backend to push real‑time messages to web clients.

Architect's Guide
Architect's Guide
Architect's Guide
How to Use Spring Boot, Netty, and WebSocket for Server‑to‑Client Push

Introduction

Netty provides a powerful abstraction over Java NIO with a simple API and a large open‑source community. This guide demonstrates a basic Netty + WebSocket example that enables the backend to push messages to the frontend.

Netty Server

@Component
public class NettyServer {
    static final Logger log = LoggerFactory.getLogger(NettyServer.class);
    @Value("${webSocket.netty.port:8888}")
    int port;
    EventLoopGroup bossGroup;
    EventLoopGroup workGroup;
    @Autowired
    ProjectInitializer nettyInitializer;
    @PostConstruct
    public void start() throws InterruptedException {
        new Thread(() -> {
            bossGroup = new NioEventLoopGroup();
            workGroup = new NioEventLoopGroup();
            ServerBootstrap bootstrap = new ServerBootstrap();
            // bossGroup handles TCP connection requests, workGroup handles read/write with clients
            bootstrap.group(bossGroup, workGroup);
            // Use NIO channel type
            bootstrap.channel(NioServerSocketChannel.class);
            // Set listening port
            bootstrap.localAddress(new InetSocketAddress(port));
            // Set pipeline
            bootstrap.childHandler(nettyInitializer);
            // Bind server and block until successful
            ChannelFuture channelFuture = null;
            try {
                channelFuture = bootstrap.bind().sync();
                log.info("Server started and listen on:{}", channelFuture.channel().localAddress());
                // Listen for channel close
                channelFuture.channel().closeFuture().sync();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
    @PreDestroy
    public void destroy() throws InterruptedException {
        if (bossGroup != null) {
            bossGroup.shutdownGracefully().sync();
        }
        if (workGroup != null) {
            workGroup.shutdownGracefully().sync();
        }
    }
}

Netty Configuration

public class NettyConfig {
    // Global singleton channel group to manage all channels
    private static volatile ChannelGroup channelGroup = null;
    // Map request ID to channel
    private static volatile ConcurrentHashMap<String, Channel> channelMap = null;
    // Two lock objects
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();
    public static ChannelGroup getChannelGroup() {
        if (channelGroup == null) {
            synchronized (lock1) {
                if (channelGroup == null) {
                    channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
                }
            }
        }
        return channelGroup;
    }
    public static ConcurrentHashMap<String, Channel> getChannelMap() {
        if (channelMap == null) {
            synchronized (lock2) {
                if (channelMap == null) {
                    channelMap = new ConcurrentHashMap<>();
                }
            }
        }
        return channelMap;
    }
    public static Channel getChannel(String userId) {
        if (channelMap == null) {
            return getChannelMap().get(userId);
        }
        return channelMap.get(userId);
    }
}

Pipeline Configuration

@Component
public class ProjectInitializer extends ChannelInitializer<SocketChannel> {
    static final String WEBSOCKET_PROTOCOL = "WebSocket";
    @Value("${webSocket.netty.path:/webSocket}")
    String webSocketPath;
    @Autowired
    WebSocketHandler webSocketHandler;
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        // Set up the pipeline
        ChannelPipeline pipeline = socketChannel.pipeline();
        // HTTP codec (WebSocket is based on HTTP)
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new ObjectEncoder());
        // Chunked write handler
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new HttpObjectAggregator(8192));
        pipeline.addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
        // Custom business logic handler
        pipeline.addLast(webSocketHandler);
    }
}

Custom Handler

@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
    /** Executed when a new connection is added */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("New client connected: [{}]", ctx.channel().id().asLongText());
        // Add to global channel group
        NettyConfig.getChannelGroup().add(ctx.channel());
    }
    /** Read incoming data */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        log.info("Server received message: {}", msg.text());
        // Parse user ID and associate with channel
        JSONObject jsonObject = JSONUtil.parseObj(msg.text());
        String uid = jsonObject.getStr("uid");
        NettyConfig.getChannelMap().put(uid, ctx.channel());
        // Store user ID as channel attribute for later retrieval
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        ctx.channel().attr(key).setIfAbsent(uid);
        // Reply to client
        ctx.channel().writeAndFlush(new TextWebSocketFrame("Server received your message"));
    }
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        log.info("User went offline: {}", ctx.channel().id().asLongText());
        // Remove from channel group
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeUserId(ctx);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("Exception: {}", cause.getMessage());
        // Remove channel on error
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeUserId(ctx);
        ctx.close();
    }
    /** Remove user‑channel mapping */
    private void removeUserId(ChannelHandlerContext ctx) {
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        String userId = ctx.channel().attr(key).get();
        NettyConfig.getChannelMap().remove(userId);
    }
}

Push Message Service and Implementation

public interface PushMsgService {
    /** Push to a specific user */
    void pushMsgToOne(String userId, String msg);
    /** Push to all users */
    void pushMsgToAll(String msg);
}
@Service
public class PushMsgServiceImpl implements PushMsgService {
    @Override
    public void pushMsgToOne(String userId, String msg) {
        Channel channel = NettyConfig.getChannel(userId);
        if (Objects.isNull(channel)) {
            throw new RuntimeException("Socket server not connected");
        }
        channel.writeAndFlush(new TextWebSocketFrame(msg));
    }
    @Override
    public void pushMsgToAll(String msg) {
        NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
    }
}

Testing the Flow

The article includes a series of screenshots that show:

Connecting to the server.

Establishing the WebSocket handshake.

Sending a message from the client.

Receiving the server’s acknowledgment.

Calling the push‑message API to broadcast a message to the frontend.

After these steps, the simple Netty example successfully pushes messages from the backend to the web client.

backendJavaNettySpring BootWebSocketServer Push
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.