Backend Development 26 min read

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.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Netty Packet Framing: Handling TCP Sticky and Unsticky Packets

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.

JavaNettyTCPDecoderpacket framingsticky packet
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login 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.