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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
