Netty实战练习——tcp拆包和粘包

目录

一、发现问题

二、自定义协议

三、编解码链条

四、运行结果

源码地址:


一、发现问题

继续上篇帖子的内容,https://blog.csdn.net/weixin_43599368/article/details/84206351

利用java原生序列化方式来编码解码,其余代码如下:

RpcClient:

package rpcserver.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
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 rpcserver.common.InputParam;
import rpcserver.common.OriginJava.ClientMessageDecoder;
import rpcserver.common.OriginJava.ClientMessageEncoder;


public class RpcClient {
    int port;
    String host;

    public RpcClient(int port, String host) {
        this.port = port;
        this.host = host;
    }

    public void run() {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try {
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
//                                    第一种java原生的序列化方式
                                    .addLast("decoder", new ClientMessageDecoder())
                                    .addLast("encoder", new ClientMessageEncoder())
                                    .addLast(new RpcClientHandler());

                        }
                    });

            Channel channel = bootstrap.connect(host, port).sync().channel();
            for(int i = 0; i < 100; i ++) {
                InputParam inputParam = new InputParam();
                inputParam.setNum1(i);
                inputParam.setNum2(i * 2);
                channel.writeAndFlush(inputParam);
                System.out.println("client 发送出去的信息是" + inputParam.toString());
            }

            //从键盘读出一个字符,然后返回它的Unicode码;目的是等待client接收完消息再退出
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new RpcClient(8080, "127.0.0.1").run();
    }
}

RpcClientHandler:

public class RpcClientHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object outputParam) throws Exception {
        System.out.println("client接受到的数据是:" + outputParam.toString());
    }
}

RpcServer:

package rpcserver.server;

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 rpcserver.common.OriginJava.ServerMessageDecoder;
import rpcserver.common.OriginJava.ServerMessageEncoder;

public class RpcServer {
    private int port;

    public RpcServer(int port) {
        this.port = port;
    }

    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
//                                    第一种java原生的序列化方式
                                    .addLast("decoder", new ServerMessageDecoder())
                                    .addLast("encoder", new ServerMessageEncoder())
                                    .addLast(new RpcServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture f = serverBootstrap.bind(port).sync();
            f.channel().closeFuture().sync();
        } catch (Exception e) {
            System.out.println("RpcServer error:" + e);
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("RpcServer 关闭了");
        }

    }

    public static void main(String[] args) {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new RpcServer(port).run();
    }

}

RpcServerHandler:

public class RpcServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        InputParam inputParam = (InputParam) msg;
        System.out.println("server 接受到的数据是 " + inputParam.toString());

        OutputParam outputParam = new OutputParam();
        outputParam.setStr1("第一个数是:"+String.valueOf(inputParam.getNum1()));
        outputParam.setStr2("第二个数是:"+String.valueOf(inputParam.getNum2()));
        ctx.writeAndFlush(outputParam);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }

}

运行

client连续发送2条数据,运行结果如下:

而client发送100条数据,却出现问题:

server端接收到13条就报错,怀疑发生了tcp粘包与拆包。

二、自定义协议

我们通过自定义协议来解决tcp粘包拆包问题

首先定义一个常量,用来标记开始位

public class TcpPackageProtocolHead {
    public static int head = 0X76;
}

然后定义协议类

public class TcpPackageProtocol {
    private int head = TcpPackageProtocolHead.head;
    private int messageLength;
    private byte[] message;

    public TcpPackageProtocol() {
    }

    public TcpPackageProtocol(int messageLength, byte[] message) {
        this.messageLength = messageLength;
        this.message = message;
    }

    public int getHead() {
        return head;
    }

    public void setHead(int head) {
        this.head = head;
    }

    public int getMessageLength() {
        return messageLength;
    }

    public void setMessageLength(int messageLength) {
        this.messageLength = messageLength;
    }

    public byte[] getMessage() {
        return message;
    }

    public void setMessage(byte[] message) {
        this.message = message;
    }
}

三、编解码链条

协议解析作为一个单独的encoder和decoder,所以整个消息编解码流程变为如下:

原本:

  1. messageDecoder:  byte -> message
  2. messageEncoder: message -> byte

 

添加了protocol之后:

  1. protocolDecoder:byte -> protocol
  2. messageDecoder:  protocol -> message
  3. messageEncoder: message -> protocol
  4. protocolEncoder: protocol -> byte

1.ProtocolDecoder:

public class ProtocolDecoder extends ByteToMessageDecoder {
    public final int BASE_LENGTH = 4 + 4;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        // 可读长度必须大于基本长度
        if (in.readableBytes() >= BASE_LENGTH) {

            // 记录包头开始的index
            int beginReader;

            while (true) {
                // 获取包头开始的index
                beginReader = in.readerIndex();
                // 标记包头开始的index
                in.markReaderIndex();
                // 读到了协议的开始标志,结束while循环
                if (in.readInt() == TcpPackageProtocolHead.head) {
                    break;
                }

                // 未读到包头,略过一个字节,每次略过,一个字节,去读取,包头信息的开始标记
                in.resetReaderIndex();
                in.readByte();

                // 当略过,一个字节之后,数据包的长度,又变得不满足
                // 此时,应该结束。等待后面的数据到达
                if (in.readableBytes() < BASE_LENGTH) {
                    return;
                }
            }

            // 消息的长度

            int length = in.readInt();
            // 判断请求数据包数据是否到齐
            if (in.readableBytes() < length) {
                // 还原读指针
                in.readerIndex(beginReader);
                return;
            }

            // 读取data数据
            byte[] data = new byte[length];
            in.readBytes(data);

            TcpPackageProtocol protocol = new TcpPackageProtocol(data.length, data);
            out.add(protocol);
        }
    }
}

2.ClientMessageDecoder:

protocol -> message,protocol中有效的是byte[],相当于byte[]转成OutputParam,也就是OutputParam的反序列化,借助ObjectInputStream来实现。

public class ClientMessageDecoder extends MessageToMessageDecoder<TcpPackageProtocol> {

    @Override
    protected void decode(ChannelHandlerContext ctx, TcpPackageProtocol msg, List<Object> out) throws Exception {
        byte[] bytes = msg.getMessage();
        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        OutputParam outputParam = (OutputParam)inputStream.readObject();
        out.add(outputParam);
    }
}

3.ClientMessageEncoder:

message->protocol,相当于InputParam的序列化.此处想了半天,因为发现ObjectOutputStream.writeObject(aa)返回值是void,而我们需要把序列化的结果设置给protocol的byte[],后来才知道返回值包含在aa中了,所以借助bytebuf来读取序列化后的结果。

public class ClientMessageEncoder extends MessageToMessageEncoder<InputParam> {
    @Override
    protected void encode(ChannelHandlerContext ctx, InputParam msg, List<Object> out) throws Exception {
        TcpPackageProtocol protocol = new TcpPackageProtocol();
        protocol.setHead(TcpPackageProtocolHead.head);

        ByteBuf byteBuf = Unpooled.buffer();
        ObjectOutputStream o = new ObjectOutputStream(new ByteBufOutputStream(byteBuf));
        o.writeObject(msg);

        int size = byteBuf.readableBytes();
        byte[] req = new byte[size];
        byteBuf.readBytes(req);
        protocol.setMessageLength(size);
        protocol.setMessage(req);
        out.add(protocol);
    }
}

参考原本encoder的写法:

就是将返回值写到了bytebuf out中。

public class ClientMessageEncoder extends MessageToByteEncoder<InputParam> {

    @Override
    protected void encode(ChannelHandlerContext ctx, InputParam msg, ByteBuf out) throws Exception {
        ObjectOutputStream o = new ObjectOutputStream(new ByteBufOutputStream(out));
        o.writeObject(msg);
    }
}

4.ProtocolEncoder:

public class ProtocolEncoder extends MessageToByteEncoder<TcpPackageProtocol> {

    @Override
    protected void encode(ChannelHandlerContext ctx, TcpPackageProtocol msg, ByteBuf out) throws Exception {
        // 1.写入消息的开头的信息标志(int类型)
        out.writeInt(TcpPackageProtocolHead.head);
        // 2.写入消息的长度(int 类型)
        out.writeInt(msg.getMessageLength());
        // 3.写入消息的内容(byte[]类型)
        out.writeBytes(msg.getMessage());
    }
}

ServerDecoder和Encoder类似,不解释直接贴代码:

public class ServerMessageDecoder extends MessageToMessageDecoder<TcpPackageProtocol> {
    @Override
    protected void decode(ChannelHandlerContext ctx, TcpPackageProtocol msg, List<Object> out) throws Exception {
        byte[] bytes = msg.getMessage();
        ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
        InputParam inputParam = (InputParam)inputStream.readObject();
        out.add(inputParam);
    }
}
public class ServerMessageEncoder extends MessageToMessageEncoder<OutputParam> {
    @Override
    protected void encode(ChannelHandlerContext ctx, OutputParam msg, List<Object> out) throws Exception {
        TcpPackageProtocol protocol = new TcpPackageProtocol();
        protocol.setHead(TcpPackageProtocolHead.head);

        ByteBuf byteBuf = Unpooled.buffer();
        ObjectOutputStream o = new ObjectOutputStream(new ByteBufOutputStream(byteBuf));
        o.writeObject(msg);

        int size = byteBuf.readableBytes();
        byte[] req = new byte[size];
        byteBuf.readBytes(req);
        protocol.setMessageLength(size);
        protocol.setMessage(req);
        out.add(protocol);
    }
}

 

 

RpcClient:

public class RpcClient {
    int port;
    String host;

    public RpcClient(int port, String host) {
        this.port = port;
        this.host = host;
    }

    public void run() {
        EventLoopGroup group = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try {
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline()
                                    .addLast(new ClientMessageDecoder())
                                    .addLast(new ProtocolEncoder())
                                    .addLast(new ClientMessageEncoder())
                                    .addLast(new RpcClientHandler());

                        }
                    });

            Channel channel = bootstrap.connect(host, port).sync().channel();
            for(int i = 0; i < 100; i ++) {
                InputParam inputParam = new InputParam();
                inputParam.setNum1(i);
                inputParam.setNum2(i * 2);
                channel.writeAndFlush(inputParam);
                System.out.println("client 发送出去的信息是" + inputParam.toString());
            }

            //从键盘读出一个字符,然后返回它的Unicode码;目的是等待client接收完消息再退出
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new RpcClient(8080, "127.0.0.1").run();
    }
}

注意netty handler执行顺序问题,从其他博客贴过来的图比较直观一些:

Decoder顺序执行,Encoder逆序执行。

RpcServer类似,此处不贴代码,感兴趣的同学可以到帖子最后github中下载相关源码。

四、运行结果

运行结果如下,server可以收到client发过来的100条数据,并且client可以收到server回传的另外100条。

源码地址:

https://github.com/tianyaning/code.git

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值