How to Build a High‑Performance Real‑Time Chat with Netty and Spring Boot 3
This article explains the fundamentals of Java I/O models, introduces Netty’s architecture and thread models, compares real‑time communication techniques, and provides step‑by‑step code for creating a WebSocket‑based instant messaging service using Spring Boot 3 and Netty.
Introduction
Netty is an asynchronous, event‑driven network framework that delivers excellent performance and a clean API for high‑concurrency communication. Combined with Spring Boot 3, it enables the rapid construction of a high‑throughput instant‑messaging system.
IO Model Primer
Blocking vs. Non‑Blocking
Blocking: A thread waits until the resource is ready, unable to perform other tasks.
Non‑Blocking: The thread returns immediately, allowing other work while the resource prepares.
Synchronous vs. Asynchronous
Synchronous: The thread actively waits for the result.
Asynchronous: The thread receives the result via a callback or notification when the resource is ready.
Java’s Three IO Models
BIO (Blocking I/O)
Workflow: thread issues IO request → thread blocks → IO completes → thread resumes.
Resource cost: one thread per connection; does not scale well under high concurrency.
Use case: simple applications with few connections and short request processing time.
NIO (Non‑Blocking I/O)
Workflow: a Selector monitors multiple Channel s; when an event occurs, the corresponding channel is processed.
Resource efficiency: a single thread can manage many connections, suitable for high‑concurrency, low‑latency servers such as chat or game servers.
AIO (Asynchronous I/O)
Workflow: application issues IO request → continues other work → OS notifies via callback when IO completes.
Characteristics: fully OS‑driven, true asynchronous communication.
Limitations: on Linux the performance may not meet expectations; best for extremely high‑throughput scenarios, especially on Windows.
Getting Started with Netty
Netty abstracts the complexities of Java NIO, providing zero‑copy transmission, multi‑protocol support, and a flexible pipeline architecture where each Channel passes through a series of Handler s (e.g., decoder, business logic, encoder).
Thread Models
Single‑Thread Model: All IO operations for a connection are handled by a single NIO thread; simple but limited to low‑connection scenarios.
Multi‑Thread Model: A pool of NIO threads shares the workload across connections, improving throughput while avoiding contention.
Boss‑Worker Model: A boss thread pool accepts incoming connections, while worker threads handle IO and business logic; this is the most common pattern for high‑concurrency servers.
Channel and Handler Lifecycle
Netty defines four channel states—Registered, Active, Inactive, Unregistered—each triggering corresponding methods in ChannelHandler (e.g., channelRegistered(), channelActive()). Handlers also have handlerAdded(), handlerRemoved(), and exceptionCaught() callbacks.
Server Startup Process
Create a ServerBootstrap and configure boss and worker EventLoopGroup s.
Set the channel type (e.g., NioServerSocketChannel).
Attach a ChannelInitializer to build the pipeline for each new connection.
Add handlers such as HttpServerCodec, HttpObjectAggregator, WebSocketServerProtocolHandler, and a custom ChatHandler.
Bind the server to a port (e.g., 875) and start listening.
Choosing a Real‑Time Communication Technique
Ajax Polling: Simple but generates many empty requests and has noticeable latency.
Long Polling: Reduces empty requests but still holds many connections under high load.
WebSocket: Provides a persistent, full‑duplex TCP channel with low overhead; best suited for real‑time chat.
Netty offers native WebSocket support, making it the optimal choice for building a scalable instant‑messaging service.
Code Implementation
Front‑End (WebSocket Client)
// 1. Global configuration
globalData: {
chatServerUrl: "ws://127.0.0.1:875/ws",
CHAT: null,
chatSocketOpen: false
},
// 2. Initialize connection on launch
onLaunch() {
this.doConnect(false);
},
// 3. Connect method
doConnect(isFirst) {
if (isFirst) {
uni.showToast({icon: "loading", title: "Reconnecting...", duration: 2000});
}
if (this.getUserInfoSession()) {
this.globalData.CHAT = uni.connectSocket({url: this.globalData.chatServerUrl});
this.globalData.CHAT.onOpen(() => {
this.globalData.chatSocketOpen = true;
console.log("ws opened, socketOpen = " + this.globalData.chatSocketOpen);
const chatMsg = {senderId: this.getUserInfoSession().id, msgType: 0};
const dataContent = {chatMsg};
this.globalData.CHAT.send({data: JSON.stringify(dataContent)});
});
this.globalData.CHAT.onClose(() => {
this.globalData.chatSocketOpen = false;
console.log("ws closed, socketOpen = " + this.globalData.chatSocketOpen);
});
this.globalData.CHAT.onMessage(res => {
console.log('Received: ' + res.data);
this.dealReceiveLastestMsg(JSON.parse(res.data));
});
this.globalData.CHAT.onError(() => {
this.globalData.chatSocketOpen = false;
console.log('WebSocket connection failed');
});
}
},
// 4. Send message
sendSocketMessage(msg) {
if (this.globalData.chatSocketOpen) {
uni.sendSocketMessage({data: msg});
} else {
uni.showToast({icon: "none", title: "Disconnected from chat server"});
}
},
// 5. Handle incoming message
dealReceiveLastestMsg(msgJSON) {
// ...process and store message, update UI...
},
// 6. Close connection
closeWSConnect() {
this.globalData.CHAT.close();
}Back‑End (Netty Server)
Maven dependency:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.2.0.Final</version>
</dependency>Server bootstrap:
import com.pitayafruits.netty.websocket.WSServerInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class ChatServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WSServerInitializer());
ChannelFuture cf = server.bind(875).sync();
cf.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}Channel initializer (pipeline configuration):
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
public class WSServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpServerCodec());
p.addLast(new ChunkedWriteHandler());
p.addLast(new HttpObjectAggregator(64 * 1024));
p.addLast(new WebSocketServerProtocolHandler("/ws"));
p.addLast(new ChatHandler());
}
}Session manager (maps user IDs to channels, supports multi‑device login):
import io.netty.channel.Channel;
import java.util.*;
public class UserChannelSession {
private static final Map<String, List<Channel>> multiSession = new HashMap<>();
private static final Map<String, String> channelUserMap = new HashMap<>();
public static void putUserChannelIdRelation(String userId, String channelId) {
channelUserMap.put(channelId, userId);
}
public static String getUserIdByChannelId(String channelId) {
return channelUserMap.get(channelId);
}
public static void putMultiChannels(String userId, Channel channel) {
multiSession.computeIfAbsent(userId, k -> new ArrayList<>()).add(channel);
}
public static void removeUserChannels(String userId, String channelId) {
List<Channel> list = multiSession.get(userId);
if (list != null) {
list.removeIf(ch -> ch.id().asLongText().equals(channelId));
if (list.isEmpty()) multiSession.remove(userId);
}
}
public static List<Channel> getMultiChannels(String userId) {
return multiSession.get(userId);
}
}Message handler (core business logic for WebSocket frames):
import com.pitayafruits.enums.MsgTypeEnum;
import com.pitayafruits.pojo.netty.ChatMsg;
import com.pitayafruits.pojo.netty.DataContent;
import com.pitayafruits.utils.JsonUtils;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.time.LocalDateTime;
import java.util.List;
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static final ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame frame) throws Exception {
String content = frame.text();
DataContent data = JsonUtils.jsonToPojo(content, DataContent.class);
ChatMsg msg = data.getChatMsg();
msg.setChatTime(LocalDateTime.now());
int type = msg.getMsgType();
Channel cur = ctx.channel();
String curId = cur.id().asLongText();
if (type == MsgTypeEnum.CONNECT_INIT.type) {
UserChannelSession.putMultiChannels(msg.getSenderId(), cur);
UserChannelSession.putUserChannelIdRelation(curId, msg.getSenderId());
} else if (type == MsgTypeEnum.WORDS.type) {
List<Channel> receivers = UserChannelSession.getMultiChannels(msg.getReceiverId());
if (receivers == null || receivers.isEmpty()) {
msg.setIsReceiverOnLine(false);
} else {
msg.setIsReceiverOnLine(true);
for (Channel rc : receivers) {
Channel target = clients.find(rc.id());
if (target != null) {
data.setChatMsg(msg);
target.writeAndFlush(new TextWebSocketFrame(JsonUtils.objectToJson(data)));
}
}
}
}
cur.writeAndFlush(new TextWebSocketFrame(curId));
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
clients.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
Channel ch = ctx.channel();
String uid = UserChannelSession.getUserIdByChannelId(ch.id().asLongText());
UserChannelSession.removeUserChannels(uid, ch.id().asLongText());
clients.remove(ch);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Channel ch = ctx.channel();
ch.close();
clients.remove(ch);
String uid = UserChannelSession.getUserIdByChannelId(ch.id().asLongText());
UserChannelSession.removeUserChannels(uid, ch.id().asLongText());
}
}Overall Flow Recap
Start the server with ChatServer, which creates boss and worker groups, binds to port 875, and installs WSServerInitializer.
When a client connects, WSServerInitializer builds the pipeline and adds ChatHandler. ChatHandler.handlerAdded() registers the channel in a global ChannelGroup.
The client first sends an initialization frame; the handler records the user‑channel mapping.
Subsequent chat messages are routed to the receiver’s channel(s) and forwarded via WebSocket frames.
On disconnection, handlerRemoved() cleans up mappings and removes the channel from the group.
Conclusion
The guide demonstrates how to assemble a Netty‑based instant‑messaging service integrated with Spring Boot 3, covering I/O fundamentals, thread models, channel lifecycle, server bootstrap, pipeline configuration, session management, and message handling. While the prototype supports basic text chat, it can be extended with offline storage, database persistence, group chat, and multimedia message types.
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.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
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.
