Netty编解码&粘包拆包&心跳机制&断线自动重连

一、编解码

通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。转换的原因:网络数据总是一系列的字节。

入站:消息会被解码。也就是说,从字节转换为另一种格式,通常是一个 Java 对象
出站:相反方向转换,当前格式转换成字节

ChannelHandler:充当了处理入站和出站数据的应用程序逻辑容器

ChannelInboundHandler——处理入站数据以及各种状态变化;

ChannelOutboundHandler——处理出站数据并且允许拦截所有的操作。

ChannelPipeline:提供了ChannelHandler链的容器。

如果事件是出站,调用实现ChannelOutboundHandler的handler,从tail到head方向
如果事件是入站,调用实现ChannelInboundHandler的handler,从head到tail方向

Google Protobuf编解码:

Protobuf的优点:

(1)在谷歌内部长期使用,产品成熟度高;

(2)跨语言、支持多种语言;

(3)编码后的消息更小,更加有利于存储和传输;

(4)编解码的性能非常高;

Protobuf开发环境搭建:

  1. Protobuf下载地址

2、现在的文件本地解压

3、设置环境变量

输入protoc --version,出现以下信息则安装成功

C:\Users\kbp1234>protoc --version
libprotoc 22.0

4.编写proto文件,生成对应java文件

SubScribeReq.proto:

package netty;
option java_package="com.netty.codec.protobuf";
option java_outer_classname="SubScribeReqDTO";

message SubScribeReq{

    required int32 subReqID = 1;
    required string userName = 2;
    required string productName = 3;
    required string address = 4;

}
窗口中输入以下命令:
PS D:\soft\taaaa> protoc --java_out . SubScribeReq.proto

5、netty protobuf 编解码实例:

引入maven依赖:

     <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.21.7</version>
        </dependency>

NettyServer:

package com.netty.codec.protobuf;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;

public class NettyServer {
    public static void main(String[] args) throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        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 ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("encode", new ProtobufEncoder());
                            pipeline.addLast("decoder",new ProtobufDecoder(SubScribeReqDTO.SubScribeReq.getDefaultInstance()));
                            pipeline.addLast(new NettyServerHandler());
                        }
                    });

            System.out.println("netty server start。。");
            ChannelFuture channelFuture = serverBootstrap.bind(9000).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

NettyServerHandler:

package com.netty.codec.protobuf;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        SubScribeReqDTO.SubScribeReq.Builder builder = new SubScribeReqDTO.SubScribeReq().newBuilderForType();
        builder.setSubReqID(1)
                .setAddress("上海")
                .setUserName("嘿嘿")
                .setProductName("手机");
        ctx.writeAndFlush(builder);;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        SubScribeReqDTO.SubScribeReq subScribeReq = (SubScribeReqDTO.SubScribeReq)msg;
        System.out.println("addresss:" + subScribeReq.getAddress() +
                " productName:"+ subScribeReq.getProductName() +
                " userName:"+ subScribeReq.getUserName());




    }

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

NettyClient:

package com.netty.codec.protobuf;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;

public class NettyClient {
    public static void main(String[] args) throws Exception {

        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group).channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("encode", new ProtobufEncoder());
                            pipeline.addLast("decoder", new ProtobufDecoder(SubScribeReqDTO.SubScribeReq.getDefaultInstance()));
                            pipeline.addLast(new NettyClientHandler());
                        }
                    });

            System.out.println("netty client start。。");
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9000).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

NettyClientHandler:

package com.netty.codec.protobuf;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        SubScribeReqDTO.SubScribeReq req = (SubScribeReqDTO.SubScribeReq)msg;

        System.out.println("收到服务器消息:" + req.getAddress());
    }

    //通道就绪触发该方法
    @Override
    public void  channelActive(ChannelHandlerContext ctx) throws Exception {
        SubScribeReqDTO.SubScribeReq.Builder builder = new SubScribeReqDTO.SubScribeReq().newBuilderForType();
        builder.setSubReqID(1)
                .setAddress("地址")
                .setUserName("张三")
                .setProductName("产品名字");
        ctx.writeAndFlush(builder);;


    }
}

二、TCP粘包拆包

假设客户端发送2个数据包D1和D2给服务端,由于服务端一次读取到不确定,存在一下5种情况

粘包的解决策略:

  1. 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格

  1. 在包尾增加回车换行符进行分隔符

  1. 将消息分为消息头和消息体,消息头中包含表示消息总长度

三、Netty心跳检测机制

心跳:TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性。

在 Netty 中, 实现心跳机制的关键是 IdleStateHandler:

    public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }

readerIdleTimeSeconds: 指定的时间间隔内读超时 会触发一个 READER_IDLE 的IdleStateEvent 事件.

writerIdleTimeSeconds: 指定的时间间隔内写超时.,会触发一个 WRITER_IDLE 的IdleStateEvent 事件.

allIdleTimeSeconds: 读/写超时 即当在指定的时间间隔内没有读或写操作时, 会触发一个 ALL_IDLE 的 IdleStateEvent 事件.

添加IdleStateHandler:

package com.netty.codec.protobuf;

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;


public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //添加空闲状态处理器,当超过 5s 没有收到远端连接的信息时,该处理器会传播一个 IdleStateHandler
        pipeline.addLast(new IdleStateHandler(0, 0, 5, TimeUnit.SECONDS));
        //添加 IdleStateHandler 传播的 IdleStateEvent 的处理器
        pipeline.addLast(new HeartbeatHandler());
        //添加普通入站处理器
    }
}

添加HeartbeatHandler处理IdleStateEvent事件:


//IdleEvent 事件处理器
public final class HeartbeatHandler extends ChannelInboundHandlerAdapter {
    private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(
            Unpooled.copiedBuffer("HEARTBEAT", Charset.forName("UTF-8")));

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            //当捕获到 IdleStateEvent,发送心跳到远端,并添加一个监听器,如果发送失败就关闭服务端连接
            ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
                    .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
        } else {
            //不是IdleStateEvent,就将该事件传递到下一个处理器
            super.userEventTriggered(ctx, evt);
        }
    }
}

四、Netty断线自动重连实现

4.1 如何监听到断线重连

1、void channelInactive(ChannelHandlerContext ctx);在客户端关闭时被调用,表示客户端断开连接

2、void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception 入栈时发生异常调用

3、心跳检测断开链接

4.2 客户端向服务端发起重连,仅需加上connect()调用即可:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值