Netty Packet Framing: Handling TCP Sticky and Unsticky Packets
This article explains TCP packet fragmentation and aggregation (sticky/unsticky packets), demonstrates the phenomena with examples, and provides detailed solutions using Netty's built‑in decoders such as FixedLengthFrameDecoder, LineBasedFrameDecoder, DelimiterBasedFrameDecoder, and LengthFieldBasedFrameDecoder, including code samples and configuration tips.
Netty Packet Framing: Handling TCP Sticky and Unsticky Packets
This article introduces the concepts of TCP packet fragmentation (unpacking) and aggregation (sticky packets), explains why they occur, and shows how to solve them using Netty's decoders.
Sticky and Unsticky Packet Phenomena
When a client sends two data packets (package1 and package2) to a server, the server may receive the data in four possible ways: normal reception of both packets, sticky packet where both are combined into one, unpacked packet where one packet is split into multiple parts, and a mixed case where packets are both split and combined.
Sticky Packet Example
Server code (only a LoggingHandler to print logs):
public class StickyServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
}
})
.bind(8081);
}
}Client code that sends ten 16‑byte messages:
public class StickyClient {
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("客户端连接成功,开始发送数据");
for (int i = 0 ; i < 10 ; i++) {
ByteBuf byteBuf = ctx.alloc().buffer(16);
byteBuf.writeBytes(new byte[]{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15});
ctx.channel().writeAndFlush(byteBuf);
}
log.info("数据已发送完成");
}
});
}
}).connect("127.0.0.1",8081);
}
}The server log shows that although the client sent ten messages, the server received a single 160‑byte chunk, demonstrating a sticky packet.
Unsticky Packet Example
Server code that decodes strings and prints them:
public class UnpackServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("服务端接收的内容:" + msg.toString());
}
});
}
})
.bind(8081);
}
}Client code that sends 500 short messages:
public class UnpackClient {
public static void main(String[] args) {
new Bootstrap()
.group(new NioEventLoopGroup())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0 ; i < 500 ; i++) {
ctx.channel().writeAndFlush("大家好,我是大明哥,一个专注[死磕 Java] 的男人!!!\r\n");
}
}
});
}
}).connect("127.0.0.1",8081);
}
}The resulting server output contains garbled and incomplete messages, indicating an unpacked packet situation.
Why Sticky/Unsticky Packets Occur
TCP is a byte‑stream protocol that does not understand application‑level message boundaries; it may split or merge data based on network conditions, MTU, MSS, sliding windows, and the Nagle algorithm.
MTU (Maximum Transmission Unit)
MTU is the largest packet size that can be transmitted over a link layer, typically 1500 bytes.
When a packet exceeds the MTU, it must be fragmented, which can lead to unpacked packets.
MSS (Maximum Segment Size)
MSS is the maximum amount of TCP payload data, negotiated during the three‑way handshake.
MSS is derived from MTU (MTU − IP header − TCP header). Both sides agree on the smaller MSS value.
Nagle Algorithm
The Nagle algorithm coalesces small TCP packets to reduce congestion.
Disabling Nagle (via ChannelOption.TCP_NODELAY ) can reduce latency for time‑critical applications, and Netty disables it by default.
Netty Decoders for Packet Framing
Netty provides several ready‑made decoders that implement common framing strategies.
FixedLengthFrameDecoder
Decodes fixed‑size frames; e.g., new FixedLengthFrameDecoder(8) reads 8 bytes at a time.
public class FixedLengthFrameServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(),new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收内容:" + ((ByteBuf)msg).toString(Charset.defaultCharset()));
}
});
}
})
.bind(8081);
}
}Advantages: simple to use; drawbacks: wasteful padding when messages are shorter than the fixed length.
LineBasedFrameDecoder
Splits incoming data on line delimiters (\n or \r\n). It also enforces a maximum frame length to protect against excessively long lines.
public class LineBasedFrameDecoderServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(12));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收内容:" + ((ByteBuf) msg).toString(Charset.defaultCharset()));
}
});
}
})
.bind(8081);
}
}DelimiterBasedFrameDecoder
Works like LineBasedFrameDecoder but allows any custom delimiter.
public class DelimiterBasedFrameDecoderServer {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer
() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("|".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(10, false, false, delimiter));
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("接收内容:" + ((ByteBuf) msg).toString(Charset.defaultCharset()));
}
});
}
})
.bind(8081);
}
}LengthFieldBasedFrameDecoder
Handles frames where the length of the payload is encoded in a length field. It requires four parameters: lengthFieldOffset , lengthFieldLength , lengthAdjustment , and initialBytesToStrip . The article describes seven typical scenarios illustrating how to configure these parameters for various protocol designs.
Key scenarios include:
Basic length + content (offset 0, lengthFieldLength 2, adjustment 0, strip 0).
Strip length field (strip 2).
Length includes header bytes (negative adjustment).
Length field after a custom header (offset > 0).
Length field not adjacent to content (adjustment > 0).
Complex cases with multiple headers and length adjustments.
Understanding these configurations enables developers to correctly decode messages and avoid sticky/unsticky packet problems.
Conclusion
TCP’s nature leads to packet fragmentation and aggregation, but Netty offers a suite of decoders—FixedLengthFrameDecoder, LineBasedFrameDecoder, DelimiterBasedFrameDecoder, and LengthFieldBasedFrameDecoder—to address these issues. Selecting the appropriate decoder and configuring its parameters according to the protocol’s framing rules ensures reliable message processing in Java backend applications.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.