Java-第十七部分-NIO和Netty-Handler链调用机制和TCP粘包拆包

NIO和Netty全文

Handler链调用机制

  • ChannelHandler处理入站和出站数据的逻辑容器
  1. ChannelInboundHandler处理入站数据,读取,从头到尾
  2. ChannelOutboundHandler处理出站数据,发送,从尾到头

image.png

  • 入站数据被解码inbound,出站数据被编码outbound,根据添加的顺序独立运行,通过inbound/outbound来判断是否执行该handler
  • ProtobufEncoder编码器

image.png

  • ProtobufDecoder解码器

image.png

  • ctx调用writeAndFlush交给当前handler的下一个outHandlerctx.channel.writeAndFlushpipeline中最后一个outHandler开始调用

解码器

  • ByteToMessageDecoder继承关系,对入站数据进行缓冲,直到数据准备好被处理,读取全部信息

image.png

  • 由于不知道远程节点是否会一次性发送一个完整的信息,tcp可能会出现沾包拆包的问题
  • 解码int的例子

image.png

自定义编解码器

  • 双向传递long类型数据

image.png

LongToByteEncoder

public class LongToByteEncoder extends MessageToByteEncoder<Long> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
        System.out.println("LongToByteEncoder - encode");
        System.out.println("msg: " + msg);
        out.writeLong(msg);
    }
}
复制代码

ByteToLongDecoder

public class ByteToLongDecoder extends ByteToMessageDecoder {
    /**
     * decode会根据接受的数据,被调用多次,知道没有新的元素被添加到list,或者buf没有更多可读的字节
     * 如果out不为空,就会将list的内容传递给下一个inboundhandler,那么该处理器的方法也会被调用多次
     * @param ctx 上下文
     * @param in 入站buf
     * @param out list集合,将解码后的数据传递给下一个handler
     * @throws Exception
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //long 8个字节,至少有8个字节才能读取
        System.out.println("ByteToLongDecoder - decode");
        //满8个字节读取一次
        if (in.readableBytes() >= 8) {
            out.add(in.readLong());
        }
    }
}
复制代码

ClientInitializer

public class ClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        //出站handler,进行编码
        ch.pipeline().addLast(new LongToByteEncoder());
        //解码器,inbound
        ch.pipeline().addLast(new ByteToLongDecoder());
        //业务handler
        ch.pipeline().addLast(new ClientHandler());
    }
}
复制代码

ServerInitializer

public class ServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //入站handler
        pipeline.addLast(new ByteToLongDecoder());
        //出站handler
        pipeline.addLast(new LongToByteEncoder());
        pipeline.addLast(new ServerHandler());
    }
}
复制代码

public class ClientHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println("server: " + ctx.channel().remoteAddress() + " long: "+ msg);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("ClientHandler - channelActive");
        ctx.writeAndFlush(12345L);
        //该处理器的前一个处理器为 `LongToByteEncoder`,只能处理long类型的数据
        //ctx.writeAndFlush(Unpooled.copiedBuffer("abcdabcdabcdabce", StandardCharsets.UTF_8));
    }
}
复制代码
  • 当写String时,服务器收到的结果,为分开两次读
ByteToLongDecoder - decode
[12345]
client: /127.0.0.1:64208 long: 12345
ByteToLongDecoder - decode
[7017280452178371428] abcdabcd
client: /127.0.0.1:64208 long: 7017280452178371428
ByteToLongDecoder - decode
[7017280452178371429] abcdabce
client: /127.0.0.1:64208 long: 7017280452178371429
复制代码

ServerHandler

public class ServerHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        System.out.println("client: " + ctx.channel().remoteAddress() + " long: " + msg);
        //发送long
        ctx.writeAndFlush(98773L);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
复制代码

解析

  • MessageToByteEncoderwrite方法,判断该类型是否能处理,如果不能处理,直接写出去ctx.write(msg, promise);,符合责任链模式

image.png image.png

  • 调用本地方法isInstance

image.png

  • 编码器和解码器,接受消息类型与待处理消息类型不一致,就不会执行该handler
  • 解码器进行解码,需要判断buf中的数据是否符合数据类型,否则接收到的结果与期望不一致

ReplayingDecoder

  • 继承MessageToByteDecoder,不需要判断readablelBytes() 是否有可读的区域,数据是否足够读取,内部进行管理
  • 参数类型用Void
public class ReplayByteToLongDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("ReplayByteToLongDecoder - decode");
        //数据量不足,会直接跳过,不传给下一个handler
        out.add(in.readLong());
    }
}
复制代码
  • 并不是所有ByteBuf的操作都支持,调用不支持的,会报异常
  • 在某些情况下,慢于MessageToByteDecoder,网络缓慢并且消息格式负责,消息会被拆成多个碎片

其他编解码器

  • LineBasedFrameDecoder 用行尾控制字符\n或者\r\n,作为分隔符来解析数据
  • DelimiteBasedFrameDecoder 自定义的特殊字符作为消息的分隔符
  • HttpObjectDecoder 解析http数据
  • LengthFieldBasedFrameDecoder 指定长度来标识整包消息,可以自动地处理黏包和半包消息

image.png image.png

TCP粘包和拆包

  • TCP面向连接,面向流,收发两端都要有一一成对的socket,发送段为了将多个发给接收端的包,更有效的发给对方,使用了优化算法 Nagle,将多次间隔较小且数据量小的数据,合并成一个数据块,进行封包

提高了效率,但是接收端难分辨出完整的数据包,面向流的通信无消息保护边界

  • 客户端分别发送了两个数据包D1和D2给服务端,服务端一次读取到的字节数是不确定的,有四种情况
  1. 服务端两次读取到了两个独立的数据包
  2. 服务端一次接收到了两个数据包,D1和D2粘合在一起,粘包
  3. 服务端分两次读取到了数据包,第一次读取到了完整的D1包和部分的D2包,第二次读取到了D2包的剩余内容,拆包
  4. 服务端分两次读取到了数据包,第一次读取到D1包的部分内容,第二次读取到部分的D1包和完整的D2包

image.png

解决粘包和拆包

  • 自定义协议+编解码器,解决服务端每次读取数据长度

协议包类

//协议包
public class MessageProtocol {
    private int len;
    private byte[] content;
    public int getLen() {
        return len;
    }
    public void setLen(int len) {
        this.len = len;
    }
    public byte[] getContent() {
        return content;
    }
    public void setContent(byte[] content) {
        this.content = content;
    }
    public MessageProtocol(int len, byte[] content) {
        this.len = len;
        this.content = content;
    }
}
复制代码

解码器

public class MessageDecoder extends ReplayingDecoder<Void> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        System.out.println("MessageDecoder - decode");
        int len = in.readInt(); //先读长度
        byte[] content = new byte[len];
        in.readBytes(content); //在读内容
        MessageProtocol mp = new MessageProtocol(len, content);
        out.add(mp);
    }
}
复制代码

编码器

public class MessageEncoder extends MessageToByteEncoder<MessageProtocol> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
        System.out.println("MessageEncoder - encode");
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}
复制代码

ClientHandler

public class ClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //发送数据
        for (int i = 0; i < 10; i++) {
            String message = "hello server - " + i;
            byte[] content = message.getBytes(StandardCharsets.UTF_8);
            int len = content.length;
            MessageProtocol mp = new MessageProtocol(len, content);
            ctx.writeAndFlush(mp);
        }
    }
    private int count = 0;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
        int len =  msg.getLen();
        byte[] content = msg.getContent();
        System.out.println("client len: " + len + " content: " + new String(content, StandardCharsets.UTF_8));
        System.out.println("client receive count: " + count++);
    }
复制代码

ServerHandler

public class ServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
    private int count = 0;
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
        int len = msg.getLen();
        byte[] content = msg.getContent();
        System.out.println("server: len - " + len + " - " + new String(content, StandardCharsets.UTF_8));
        System.out.println("receive count: " + count++);
        //会写
        String response = UUID.randomUUID().toString();
        int resLen = response.getBytes(StandardCharsets.UTF_8).length;
        MessageProtocol resMp = new MessageProtocol(resLen, response.getBytes(StandardCharsets.UTF_8));
        ctx.writeAndFlush(resMp);
    }
复制代码

Guess you like

Origin juejin.im/post/7068284060434432008