Netty中使用MessagePack時的TCP粘包問題與解決方案

longmanma發表於2021-09-09

[toc]


Netty中使用MessagePack時的TCP粘包問題與解決方案

透過下面的例項程式碼來演示在Netty中使用MessagPack時會出現的TCP粘包問題,為了學習的連貫性,參考了《Netty權威指南》第7章中的程式碼,但是需要注意的是,書中並沒有提供完整程式碼,提供的程式碼都是片段性的,所以我根據自己的理解把服務端的程式碼和客戶端的程式碼寫了出來,可以作為參考。

仍然需要注意的是,我使用的是Netty 4.x的版本。

另外我在程式程式碼中寫了非常詳細的註釋,所以這裡不再進行更多的說明。

在使用MessagePack時的TCP粘包問題

編碼器與解碼器

MsgpackEncoder.java
package cn.xpleaf.msgpack;import org.msgpack.MessagePack;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.MessageToByteEncoder;/** * MsgpackEncoder繼承自Netty中的MessageToByteEncoder類, * 並重寫抽象方法encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) * 它負責將Object型別的POJO物件編碼為byte陣列,然後寫入到ByteBuf中 * @author yeyonghao * */public class MsgpackEncoder extends MessageToByteEncoder {    @Override    protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {        // 建立MessagePack物件        MessagePack msgpack = new MessagePack();        // 將物件編碼為MessagePack格式的位元組陣列        byte[] raw = msgpack.write(msg);        // 將位元組陣列寫入到ByteBuf中        out.writeBytes(raw);    }}
MsgpackDecoder.java
package cn.xpleaf.msgpack;import java.util.List;import org.msgpack.MessagePack;import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.handler.codec.ByteToMessageDecoder;import io.netty.handler.codec.MessageToMessageDecoder;/** * MsgpackDecoder繼承自Netty中的MessageToMessageDecoder類, * 並重寫抽象方法decode(ChannelHandlerContext ctx, ByteBuf msg, List out) * 首先從資料包msg(資料型別取決於繼承MessageToMessageDecoder時填寫的泛型型別)中獲取需要解碼的byte陣列 * 然後呼叫MessagePack的read方法將其反序列化(解碼)為Object物件 * 將解碼後的物件加入到解碼列表out中,這樣就完成了MessagePack的解碼操作 * @author yeyonghao * */public class MsgpackDecoder extends MessageToMessageDecoder {    @Override    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List out) throws Exception {        // 從資料包msg中(這裡的資料型別為ByteBuf,因為Netty的通訊基於ByteBuf物件)        final byte[] array;        final int length = msg.readableBytes();        array = new byte[length];        /**         * 這裡使用的是ByteBuf的getBytes方法來將ByteBuf物件轉換為位元組陣列,前面是使用readBytes,直接傳入一個接收的位元組陣列引數即可         * 這裡的引數比較多,第一個引數是index,關於readerIndex,說明如下:         * ByteBuf是透過readerIndex跟writerIndex兩個位置指標來協助緩衝區的讀寫操作的,具體原理等到Netty原始碼分析時再詳細學習一下         * 第二個引數是接收的位元組陣列         * 第三個引數是dstIndex the first index of the destination         * 第四個引數是length   the number of bytes to transfer         */        msg.getBytes(msg.readerIndex(), array, 0, length);        // 建立一個MessagePack物件        MessagePack msgpack = new MessagePack();        // 解碼並新增到解碼列表out中        out.add(msgpack.read(array));    }}

服務端

EchoServer.java
package cn.xpleaf.echo;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;public class EchoServer {    public void bind(int port) throws Exception {        // 配置服務端的NIO執行緒組        EventLoopGroup bossGroup = new NioEventLoopGroup();        EventLoopGroup workerGroup = new NioEventLoopGroup();        try {            ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)                .channel(NioServerSocketChannel.class)                .option(ChannelOption.SO_BACKLOG, 1024)                .childHandler(new ChannelInitializer() {                    @Override                    protected void initChannel(SocketChannel ch) throws Exception {                        // 新增MesspagePack解碼器                        ch.pipeline().addLast("msgpack decoder", new MsgPackDecode());                        // 新增MessagePack編碼器                        ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder());                        // 新增業務處理handler                        ch.pipeline().addLast(new EchoServerHandler());                    }                });            // 繫結埠,同步等待成功            ChannelFuture f = b.bind(port).sync();            // 等待服務端監聽埠關閉            f.channel().closeFuture().sync();        } finally {            // 優雅退出,釋放執行緒池資源            bossGroup.shutdownGracefully();            workerGroup.shutdownGracefully();        }    }    public static void main(String[] args) throws Exception {        int port = 8080;        if(args != null && args.length > 0) {            try {                port = Integer.valueOf(port);            } catch (NumberFormatException e) {                // TODO: handle exception            }        }        new EchoServer().bind(port);    }}
EchoServerHandler.java
package cn.xpleaf.echo;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class EchoServerHandler extends ChannelInboundHandlerAdapter {    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        System.out.println("Server receive the msgpack message : " + msg);        ctx.write(msg);    }    @Override    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {        ctx.flush();    }    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {        // 發生異常,關閉鏈路        ctx.close();    }}

客戶端

EchoClient.java
package cn.xpleaf.echo;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;public class EchoClient {    public void connect(String host, int port, int sendNumber) throws Exception {        // 配置客戶端NIO執行緒組        EventLoopGroup group = new NioEventLoopGroup();        try {            Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                .option(ChannelOption.TCP_NODELAY, true)                // 設定TCP連線超時時間                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)                .handler(new ChannelInitializer() {                    @Override                    protected void initChannel(SocketChannel ch) throws Exception {                        // 新增MesspagePack解碼器                        ch.pipeline().addLast("msgpack decoder", new MsgPackDecode());                        // 新增MessagePack編碼器                        ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder());                        // 新增業務處理handler                        ch.pipeline().addLast(new EchoClientHandler(sendNumber));                    }                });            // 發起非同步連線操作            ChannelFuture f = b.connect(host, port).sync();            // 等待客戶端鏈路關閉            f.channel().closeFuture().sync();        } finally {            // 優雅退出,釋放NIO執行緒組            group.shutdownGracefully();        }    }    public static void main(String[] args) throws Exception {        int port = 8080;        if(args != null && args.length > 0) {            try {                port = Integer.valueOf(port);            } catch (NumberFormatException e) {                // 採用預設值            }        }        int sendNumber = 1000;        new EchoClient().connect("localhost", port, sendNumber);    }}
EchoClientHander.java
package cn.xpleaf.echo;import cn.xpleaf.pojo.User;import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerAdapter;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;public class EchoClientHandler extends ChannelInboundHandlerAdapter {    // sendNumber為寫入傳送緩衝區的物件數量    private int sendNumber;    public EchoClientHandler(int sendNumber) {        this.sendNumber = sendNumber;    }    /**     * 構建長度為userNum的User物件陣列     * @param userNum     * @return     */    private User[] getUserArray(int userNum) {        User[] users = new User[userNum];        User user = null;        for(int i = 0; i " + i);            user.setAge(i);            users[i] = user;        }        return users;    }    @Override    public void channelActive(ChannelHandlerContext ctx) {        User[] users = getUserArray(sendNumber);        for (User user : users) {            ctx.writeAndFlush(user);        }    }    @Override    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {        System.out.println("Client receive the msgpack message : " + msg);    }    @Override    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {        ctx.flush();    }    @Override    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {        ctx.close();    }}

POJO

User.java
package cn.xpleaf.pojo;import org.msgpack.annotation.Message;@Messagepublic class User {    private String name;    private int age;    public String getName() {        return name;    }    public void setName(String name) {        this.name = name;    }    public int getAge() {        return age;    }    public void setAge(int age) {        this.age = age;    }    @Override    public String toString() {        return "User [name=" + name + ", age=" + age + "]";    }}

測試

當EchoClient.java中的sendNumber為1時,服務端和客戶端都是正常工作的,此時,服務端和客戶端的輸出分別如下:

服務端:

Server receive the msgpack message : ["ABCDEFG --->0",0]

客戶端:

Client receive the msgpack message : ["ABCDEFG --->0",0]

但是當sendNumber數字很大時,就不能正常工作了,比如可以設定為1000,此時輸出結果如下:

服務端:

Server receive the msgpack message : ["ABCDEFG --->0",0]Server receive the msgpack message : ["ABCDEFG --->1",1]Server receive the msgpack message : ["ABCDEFG --->3",3]...省略輸出...Server receive the msgpack message : ["ABCDEFG --->146",146]Server receive the msgpack message : 70Server receive the msgpack message : ["ABCDEFG --->156",156]Server receive the msgpack message : ["ABCDEFG --->157",157]...省略輸出...

客戶端:

Client receive the msgpack message : ["ABCDEFG --->0",0]Client receive the msgpack message : 62Client receive the msgpack message : 68

顯然執行結果跟預期的不太一樣,這是因為出現了TCP粘包問題。

粘包問題解決方案

在前面程式碼的基礎上,只需要對EchoServer.javaEchoClient.java中的程式碼進行修改即可。

EchoServer.java

package cn.xpleaf.echo02;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.handler.codec.LengthFieldBasedFrameDecoder;import io.netty.handler.codec.LengthFieldPrepender;public class EchoServer {    public void bind(int port) throws Exception {        // 配置服務端的NIO執行緒組        EventLoopGroup bossGroup = new NioEventLoopGroup();        EventLoopGroup workerGroup = new NioEventLoopGroup();        try {            ServerBootstrap b = new ServerBootstrap();            b.group(bossGroup, workerGroup)                .channel(NioServerSocketChannel.class)                .option(ChannelOption.SO_BACKLOG, 1024)                .childHandler(new ChannelInitializer() {                    @Override                    protected void initChannel(SocketChannel ch) throws Exception {                        // 新增長度欄位解碼器                        // 在MessagePack解碼器之前增加LengthFieldBasedFrameDecoder,用於處理半包訊息                        // 它會解析訊息頭部的長度欄位資訊,這樣後面的MsgpackDecoder接收到的永遠是整包訊息                        ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));                        // 新增MesspagePack解碼器                        ch.pipeline().addLast("msgpack decoder", new MsgPackDecode());                        // 新增長度欄位編碼器                        // 在MessagePack編碼器之前增加LengthFieldPrepender,它將在ByteBuf之前增加2個位元組的訊息長度欄位                        ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));                        // 新增MessagePack編碼器                        ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder());                        // 新增業務處理handler                        ch.pipeline().addLast(new EchoServerHandler());                    }                });            // 繫結埠,同步等待成功            ChannelFuture f = b.bind(port).sync();            // 等待服務端監聽埠關閉            f.channel().closeFuture().sync();        } finally {            // 優雅退出,釋放執行緒池資源            bossGroup.shutdownGracefully();            workerGroup.shutdownGracefully();        }    }    public static void main(String[] args) throws Exception {        int port = 8080;        if(args != null && args.length > 0) {            try {                port = Integer.valueOf(port);            } catch (NumberFormatException e) {                // TODO: handle exception            }        }        new EchoServer().bind(port);    }}

EchoClient.java

package cn.xpleaf.echo02;import cn.demo.simple.MsgPackDecode;import cn.xpleaf.msgpack.MsgpackDecoder;import cn.xpleaf.msgpack.MsgpackEncoder;import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelOption;import io.netty.channel.EventLoopGroup;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import io.netty.handler.codec.LengthFieldBasedFrameDecoder;import io.netty.handler.codec.LengthFieldPrepender;public class EchoClient {    public void connect(String host, int port, int sendNumber) throws Exception {        // 配置客戶端NIO執行緒組        EventLoopGroup group = new NioEventLoopGroup();        try {            Bootstrap b = new Bootstrap();            b.group(group).channel(NioSocketChannel.class)                .option(ChannelOption.TCP_NODELAY, true)                // 設定TCP連線超時時間                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)                .handler(new ChannelInitializer() {                    @Override                    protected void initChannel(SocketChannel ch) throws Exception {                        // 新增長度欄位解碼器                        // 在MessagePack解碼器之前增加LengthFieldBasedFrameDecoder,用於處理半包訊息                        // 它會解析訊息頭部的長度欄位資訊,這樣後面的MsgpackDecoder接收到的永遠是整包訊息                        ch.pipeline().addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));                        // 新增MesspagePack解碼器                        ch.pipeline().addLast("msgpack decoder", new MsgPackDecode());                        // 新增長度欄位編碼器                        // 在MessagePack編碼器之前增加LengthFieldPrepender,它將在ByteBuf之前增加2個位元組的訊息長度欄位                        ch.pipeline().addLast("frameEncoder", new LengthFieldPrepender(2));                        // 新增MessagePack編碼器                        ch.pipeline().addLast("msgpack encoder", new MsgpackEncoder());                        // 新增業務處理handler                        ch.pipeline().addLast(new EchoClientHandler(sendNumber));                    }                });            // 發起非同步連線操作            ChannelFuture f = b.connect(host, port).sync();            // 等待客戶端鏈路關閉            f.channel().closeFuture().sync();        } finally {            // 優雅退出,釋放NIO執行緒組            group.shutdownGracefully();        }    }    public static void main(String[] args) throws Exception {        int port = 8080;        if(args != null && args.length > 0) {            try {                port = Integer.valueOf(port);            } catch (NumberFormatException e) {                // 採用預設值            }        }        int sendNumber = 1000;        new EchoClient().connect("localhost", port, sendNumber);    }}

測試

可以將EchoClient.javasendNumber設定為1000或更大,此時服務端和客戶端的輸出結果跟預期的都是一樣的。

測試結果為,服務端和客戶端都會列印1000行的資訊(假設sendNumber為1000),這裡不再給出執行結果。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2813877/,如需轉載,請註明出處,否則將追究法律責任。

相關文章