Netty实战通信简述(导演剪辑版)以及添加kryo实现自定义序列化编码解码

netty官方文档:https://netty.io/wiki/index.html
netty文章参考:【硬核】肝了一月的Netty知识点强烈推荐!敖丙大佬的超详细源码分析

本文概括了netty的常用类方法以及常见问题,包括使用kryo实现自定义序列化,如果希望了解更多源码知识,可以去看敖丙大佬写的netty文章

Netty执行流程

在这里插入图片描述

Netty 核心组件

Channel

​ Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。

EventLoop 与 EventLoopGroup

EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。

​EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。

Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。

​ 一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。

ServerBootstrap 与 Bootstrap

Bootstarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。

​Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。

​ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。

ChannelHandler 与 ChannelPipeline

​ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。

ChannelFuture

Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册回调:当结果出来后马上调用执行。

Netty - Server 代码

public class NettyServer {
    public static void main(String[] args) throws InterruptedException {
    	// 创建parentGroup和childGroup分别用来监听和操作
        EventLoopGroup parentGroup = new NioEventLoopGroup();
        EventLoopGroup childGroup = new NioEventLoopGroup();
        try {
			// 创建服务启动类
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 绑定两个EventLoopGroup(类似于线程池的线程) 
            bootstrap.group(parentGroup, childGroup)
           			 //告诉Netty启动服务时将使用哪个Channel进行数据处理
                     .channel(NioServerSocketChannel.class)
                     //把childGroup中的一个线程与处理器进行绑定
                     .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
							//ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new SomeSocketServerHandler());
                         }
                    });

            ChannelFuture future = bootstrap.bind(8888).sync();
            System.out.println("服务器已启动。。。");

            future.channel().closeFuture().sync();
        } finally {
            parentGroup.shutdownGracefully();
            childGroup.shutdownGracefully();
        }
    }
}

Server 端 Handler代码

public class DemoSocketServerHandler
                       extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println("Client Address ====== " + ctx.channel().remoteAddress());
        ctx.channel().writeAndFlush("from server:" + UUID.randomUUID());
        ctx.fireChannelActive();
        TimeUnit.MILLISECONDS.sleep(500);
    }
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
                                Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Netty - Client 代码

public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
                            pipeline.addLast(new DemoSocketClientHandler());
                        }
                    });

            ChannelFuture future = bootstrap.connect("localhost", 8888).sync();
            future.channel().closeFuture().sync();
        } finally {
            if(eventLoopGroup != null) {
                eventLoopGroup.shutdownGracefully();
            }
        }
    }
}

Client 端 Handler代码

public class DemoSocketClientHandler
               extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println(msg);
        ctx.channel().writeAndFlush("from client: " + System.currentTimeMillis());
        TimeUnit.MILLISECONDS.sleep(5000);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx)
            throws Exception {
        ctx.channel().writeAndFlush("from client:begin talking");
    }

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

常见问题

netty中ServerBootstrap的bind方法有什么用?sync()方法有用什么用?

Netty的ServerBootstrap类中的bind()方法主要用于绑定服务器到特定的地址和端口。当你调用bind()方法时,Netty会尝试将服务器绑定到指定的地址和端口。如果绑定成功,它将返回一个Channel对象,你可以使用这个对象来处理入站数据。

sync()方法则是用于同步等待服务器绑定操作完成。当调用bind()方法时,Netty实际上会在后台线程上异步地完成绑定操作,但是如果你需要等待这个操作完成,可以使用sync()方法。这将阻塞当前线程,直到绑定操作完成。这对于需要在绑定操作完成后执行某些操作的代码非常有用。

总的来说,bind()方法用于设置服务器的监听地址和端口,而sync()方法用于等待绑定操作完成。在Netty中,这两个方法通常一起使用,以便在服务器绑定到特定地址和端口后执行其他操作。

Handle可以重写哪些方法?这些方法的作用是什么?

	//通道读取事件
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
            throws Exception {
        System.out.println(msg);
        ctx.channel().writeAndFlush("from client: " + System.currentTimeMillis());
        TimeUnit.MILLISECONDS.sleep(5000);
    }

	//当一个新的连接被注册到ChannelPipeline时,这个事件会被触发。在这个方法中,你可以进行一些初始化操作,比如加载配置、启动服务等。
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        super.channelRegistered(ctx);
    }

    //当一个连接被注销(即关闭)时,这个事件会被触发。在这个方法中,你可以进行一些清理操作,比如关闭连接、清理资源等。
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        super.channelUnregistered(ctx);
    }

    //当连接被激活(即,服务器已经成功绑定到指定的地址和端口)后,这个方法会被调用。这是你开始接收入站消息的合适时机。
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
    }

    //连接未激活
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        super.channelInactive(ctx);
    }
    // 当一个入站消息已经被完全处理后,这个方法会被调用。通常,你可以用这个方法来通知你的业务逻辑已经处理完了所有消息。
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        super.channelReadComplete(ctx);
    }

    // 当Netty发出的事件被触发时,这个方法会被调用。一般情况下,你可以在实现自己的handler时在这个方法里面加入自己的逻辑,如心跳检测、业务处理等。
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        super.userEventTriggered(ctx, evt);
    }

    //当channel的写可操作性改变时,这个事件会被触发。一般情况下,写可操作性发生变化表明可能可以写入数据了。在实现自己的handler时,你也可以在这个方法中加入逻辑来响应这种变化,如更新写入进度等。
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        super.channelWritabilityChanged(ctx);
    }

    // 通道发生异常事件
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("client caught exception", cause);
        ctx.close();
    }

大多数可以根据字面意思理解,可以按需求进行重写

netty中ServerBootstrap的channel方法有什么用?

在Netty中,ServerBootstrap是一个框架的核心类,它提供了一个用户编写的ChannelHandler以及线程管理逻辑被执行的方式。而channel()方法是在使用ServerBootstrap启动服务器时的一个非常常用的方法,它的作用是将Channel连接到给定的参数,这里主要可以理解为你需要告诉Netty启动服务时将使用哪个Channel进行数据处理。

至于参数,主要就是传入的Channel实现了哪个ChannelFactory,它会根据网络连接或者已经存在的连接对象生成新的Channel实例。另外这个参数还决定了使用的类型和它的各种参数配置,包括队列长度,并发度等等。

Netty中Handler处理器类型有哪些,分别有什么作用?

ChannelInboundHandler:用于处理 入站 I/O 事件。
ChannelOutboundHandler:用于处理 出站 I/O 操作。
ChannelInboundHandlerAdapter:用于处理 入站 I/O 事件。
ChannelOutboundHandlerAdapter:用于处理 出站 I/O 操作。
ChannelDuplexHandler:可以处理 入站和出站 事件。

使用kryo做我们的自定义序列化器

前文我们也是介绍了kryo的实战使用,那么我们就把netty和kryo一起结合使用一下

NettyKryoDecoder 解码器类

@AllArgsConstructor
@Slf4j
public class NettyKryoDecoder extends ByteToMessageDecoder {

    private final Serializer serializer;

    private final Class<?> genericClass;
    //Netty传输的消息长度也就是对象序列化后对应的字节数组的大小,存储在 ByteBuf 头部
    private static final int BODY_LENGTH = 4;

    /**
     * 解码 ByteBuf 对象
     * @param ctx 解码器关联的 ChannelHandlerContext 对象
     * @param in "入站"数据,也就是 ByteBuf 对象
     * @param out 解码之后的数据对象需要添加到 out 对象里面
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        //1.byteBuf中写入的消息长度所占的字节数已经是4了,所以 byteBuf 的可读字节必须大于 4,
        if (in.readableBytes() >= BODY_LENGTH) {
            //2.标记当前readIndex的位置,以便后面重置readIndex 的时候使用
            in.markReaderIndex();
            //3.读取消息的长度//注意: 消息长度是encode的时候我们自己写入的,参见 NettyKryoEncoder 的encode方法
            int dataLength = in.readInt();
            //4.遇到不合理的情况直接 return
            if (dataLength < 0 || in.readableBytes() < 0) {
                log.error("data length or byteBuf readableBytes is not valid");
                return;
            }
            //5.如果可读字节数小于消息长度的话,说明是不完整的消息,重置readIndex
            if (in.readableBytes() < dataLength) {
                in.resetReaderIndex();
                return;
            }
            // 6.走到这里说明没什么问题了,可以序列化了
            byte[] body = new byte[dataLength];
            in.readBytes(body);
            // 将bytes数组转换为我们需要的对象
            Object obj = serializer.deserialize(body, genericClass);
            out.add(obj);
            log.info("successful decode ByteBuf to Object");
        }
    }
}

NettyKryoEncoder 编码器类

@AllArgsConstructor
public class NettyKryoEncoder extends MessageToByteEncoder<Object> {
    private final Serializer serializer;
    private final Class<?> genericClass;

    //将对象转换为字节码然后写入到 ByteBuf 对象中
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) {
        if (genericClass.isInstance(o)) {
            // 1. 将对象转换为byte
            byte[] body = serializer.serialize(o);
            // 2. 读取消息的长度
            int dataLength = body.length;
            // 3.写入消息对应的字节数组长度,writerIndex 加 4
            byteBuf.writeInt(dataLength);
            //4.将字节数组写入 ByteBuf 对象中
            byteBuf.writeBytes(body);
        }
    }
}

修改之前的netty client以及server代码

ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));

修改后NettyClient代码:

public class NettyClient {
    private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
    private final String host;
    private final int port;
    private static final Bootstrap b;

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

    // 初始化相关资源比如 EventLoopGroup, Bootstrap
    static {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        b = new Bootstrap();
        KryoSerializer kryoSerializer = new KryoSerializer();
        b.group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) {
                        /*自定义序列化编解码器*/
                        ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));
                        ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));
                        ch.pipeline().addLast(new NettyClientHandler());
                    }
                });
    }

    /*** 发送消息到服务端** @param rpcRequest 消息体* @return 服务端返回的数据*/
    public RpcResponse sendMessage(RpcRequest rpcRequest) {
        try {
            ChannelFuture f = b.connect(host, port).sync();
            logger.info("client connect {}", host + ":" + port);
            Channel futureChannel = f.channel();
            logger.info("send message");
            if (futureChannel != null) {
                futureChannel.writeAndFlush(rpcRequest).addListener(future -> {
                    if (future.isSuccess()) {
                        logger.info("client send message: [{}]", rpcRequest.toString());
                    } else {
                        logger.error("Send failed:", future.cause());
                    }
                });
                futureChannel.closeFuture().sync();
                AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse");
                return futureChannel.attr(key).get();
            }
        } catch (InterruptedException e) {
            logger.error("occur exception when connect server:", e);
        }
        return null;
    }
}

修改后NettyServer代码:

public class NettyServer {
    private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);
    private final int port;

    public NettyServer(int port) {
        this.port = port;

    }

    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        KryoSerializer kryoSerializer = new KryoSerializer();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    // TCP默认开启了 Nagle 算法,该算法的作用是尽可能的发送大数据快,减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    // 是否开启 TCP 底层心跳机制
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcRequest.class));
                            ch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcResponse.class));
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            // 绑定端口,同步等待绑定成功
            ChannelFuture f = b.bind(port).sync();
            // 等待服务端监听端口关闭
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            logger.error("occur exception when start server:", e);
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

这样kryo序列化器就会注入到编码以及解码器中,同时会把他转化为RpcResponse以及RpcRequest类

kryo序列化器代码可以看前文:常见的序列化协议以及高性能Kryo实战

然后我们开始测试

添加测试类以及测试代码

@Test
    void test01() {
        new NettyServer(8889).run();
    }

    @Test
    void test02() {
        RpcRequest rpcRequest = RpcRequest.builder().interfaceName("interface").methodName("hello").build();
        NettyClient nettyClient = new NettyClient("127.0.0.1", 8889);
        for (int i = 0; i < 3; i++) {
            nettyClient.sendMessage(rpcRequest);
        }
        RpcResponse rpcResponse = nettyClient.sendMessage(rpcRequest);
        System.out.println(rpcResponse.toString());
    }

客户端控制台输出
在这里插入图片描述

服务端控制台输出
在这里插入图片描述

成功!

  • 18
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty框架提供了很多编码器和解码器,但有时候我们需要自定义编码器和解码器,以满足特定的业务需求。在Netty中,自定义编码器和解码器非常简单,只需要继承ByteToMessageDecoder或MessageToByteEncoder,并实现其中的抽象方法即可。 以下是一个自定义编码器的示例代码: ```java public class MyEncoder extends MessageToByteEncoder<MyMessage> { @Override protected void encode(ChannelHandlerContext ctx, MyMessage msg, ByteBuf out) throws Exception { byte[] data = msg.getData(); out.writeInt(data.length); out.writeBytes(data); } } ``` 在这个示例中,我们定义了一个名为MyEncoder的编码器,它继承自MessageToByteEncoder,并实现了encode方法。在encode方法中,我们首先获取消息的数据,然后将消息的数据长度写入到ByteBuf中,最后将消息的数据写入到ByteBuf中。 以下是一个自定义解码器的示例代码: ```java public class MyDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { if (in.readableBytes() < 4) { return; } int length = in.readInt(); if (in.readableBytes() < length) { in.resetReaderIndex(); return; } byte[] data = new byte[length]; in.readBytes(data); MyMessage message = new MyMessage(length, data); out.add(message); } } ``` 在这个示例中,我们定义了一个名为MyDecoder的解码器,它继承自ByteToMessageDecoder,并实现了decode方法。在decode方法中,我们首先判断ByteBuf中是否有足够的字节可读,如果不够则直接返回。然后从ByteBuf中读取消息的长度和数据,并将它们封装成MyMessage对象,加入到解码结果列表中。 在使用自定义编码器和解码器时,只需要将它们注册到ChannelPipeline中即可。以下是注册的示例代码: ```java ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast(new MyDecoder()); pipeline.addLast(new MyEncoder()); ``` 这样,在ChannelPipeline中的所有ChannelHandler都可以使用MyMessage对象,而无需关心它们与字节流的转换过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值