Netty学习笔记

基础概念详解

  • Channel

    管道,其是对 Socket 的封装,其包含了一组 API,大大简化了直接与 Socket 进行操作的复杂性。

  • EventLoopGroup
    EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
    Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。
    一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。(一个线程可以处理多个Channel)

  • ServerBootStrap
    启动类,用于配置整个 Netty 代码,将各个组件关联起来。服务端使用的是 ServerBootStrap,而客户端使用的是则 BootStrap。

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

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

传输类

  • 请求体
package RPC.netty.dto;

import lombok.*;

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class RpcRequest {
    /**
     * 接口名称
     */
   private String interfaceName;

   /**
    * 方法名称
    */
   private String methodName;
}

  • 响应体
package RPC.netty.dto;

import lombok.*;

@Data
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class RpcResponse {

    /**
     * 消息
     */
    private String message;
}

序列化Kryo

  • 接口
package RPC.netty;

/**
 * @author 
 * @date 
 * @description
 */
public interface Serializer {

    /**
     * 序列化
     * @param obj 要序列化的对象
     * @return 字节数组
     */
    byte[] serialize(Object obj);

    /**
     * 反序列化
     * @param bytes 字节数组
     * @param clazz 要反序列化的对象类型
     * @return 反序列化后的对象
     */
    <T> T deserialize(byte[] bytes, Class<T> clazz);
}
  • 接口实现
package RPC.netty.kryo;


import RPC.netty.dto.RpcRequest;
import RPC.netty.dto.RpcResponse;
import RPC.netty.Serializer;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import lombok.extern.slf4j.Slf4j;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

/**
 * @author
 * @date 
 * @description
 */
@Slf4j
public class KryoSerializer implements Serializer {

    /**
     *  kryo是线程不安全的,所以可以通过一下三种方式维护线程安全
     *  1.ThreadLocal、 2.对象池的方式  3.每次编解码实例化
     *  :Kryo使用了一些共享数据结构,比如缓存池,来存储已经序列化的对象实例。这些数据结构如果没有适当的并发控制,可能会导致多个线程同时读写同一份数据,从而引发数据竞争
     */
    private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.register(RpcRequest.class);
        kryo.register(RpcResponse.class);
        //循环引用,Kryo 为了追求高性能,可以关闭循环引用的支持。不过我并不认为关闭它是一件好的选择,大多数情况下,请保持 kryo.setReferences(true)。
        kryo.setReferences(true);
        //Kryo 支持对注册行为,如 kryo.register(SomeClazz.class),这会赋予该 Class 一个从 0 开始的编号,
        // 但 Kryo 使用注册行为最大的问题在于,其不保证同一个 Class 每一次注册的号码相同,这与注册的顺序有关,
        // 也就意味着在不同的机器、同一个机器重启前后都有可能拥有不同的编号,这会导致序列化产生问题,所以在分布式项目中,一般关闭注册行为。
        kryo.setRegistrationRequired(false);
        return kryo;
    });

    @Override
    public byte[] serialize(Object obj) {
        try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
             Output output = new Output(byteArrayOutputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            kryo.writeObject(output, obj);
            kryoThreadLocal.remove();
            return output.toBytes();
        } catch (Exception e) {
            log.error("kryo deserialize error", e);
            throw new RuntimeException("kryo deserialize error");
        }
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
             Input input = new Input(byteArrayInputStream)) {
            Kryo kryo = kryoThreadLocal.get();
            Object object = kryo.readObject(input, clazz);
            kryoThreadLocal.remove();
            return clazz.cast(object);
        } catch (Exception e) {
            log.error("kryo deserialize error", e);
            throw new RuntimeException("kryo deserialize error");
        }
    }
}
  • 编码
package RPC.netty.kryo;

import RPC.netty.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import lombok.AllArgsConstructor;

/**
 * @author 
 * @date 
 * @description 自定义kryo解码器 它负责处理"出站"消息,将消息格式转换为字节数组然后写入到字节数据的容器 ByteBuf 对象中。
 * 网络传输需要通过字节流来实现,ByteBuf 可以看作是 Netty 提供的字节数据的容器,使用它会让我们更加方便地处理字节数据。
 */
@AllArgsConstructor
public class NettyKryoEncoder extends MessageToByteEncoder<Object> {

    private final Serializer serializer;
    private final Class<?> genericClass;

    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) {
        if (genericClass.isInstance(o)) {
            byte[] body = serializer.serialize(o);
            int len = body.length;
            byteBuf.writeInt(len);
            byteBuf.writeBytes(body);
        }
    }
}
  • 解码
package RPC.netty.kryo;

import RPC.netty.Serializer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
 * @author 
 * @date 
 * @description 自定义kryo编码器 负责处理"入站"消息,它会从 ByteBuf 中读取到业务对象对应的字节序列,然后再将字节序列转换为我们的业务对象
 */
@Slf4j
@AllArgsConstructor
public class NettyKryoDecoder extends ByteToMessageDecoder {

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

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

Handler

  • 客户端handler
package RPC.netty.handler;

import RPC.netty.dto.RpcResponse;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.AttributeKey;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * @author 
 * @date 
 * @description 自定义ChannelHandler 处理服务端消息
 */
@Slf4j
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            RpcResponse response = (RpcResponse) msg;
            log.info("服务端返回值:" + response);
            /**
             * NettyClientHandler用于读取服务端发送过来的 RpcResponse 消息对象,并将 RpcResponse 消息对象保存到 AttributeMap 上,AttributeMap 可以看作是一个Channel的共享数据源。
             * 然后通过 channel 和 key 将数据读取出来。
             */
            AttributeKey<RpcResponse> key = AttributeKey.valueOf("response");
            ctx.channel().attr(key).set(response);
            ctx.channel().close();
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("客户端捕捉异常信息:", cause);
        ctx.close();
    }
}

  • 服务端handler
package RPC.netty.handler;

import RPC.netty.dto.RpcRequest;
import RPC.netty.dto.RpcResponse;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * @author
 * @date 
 * @description 自定义ChannelHandler 处理客户端消息
 */
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try {
            RpcRequest request = (RpcRequest) msg;
            log.info("服务端接收请求:" + request.toString());
            RpcResponse response = RpcResponse.builder().message("来自服务器消息").build();
            ChannelFuture channel = ctx.writeAndFlush(response);

            channel.addListener(ChannelFutureListener.CLOSE);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.error("服务端捕捉异常信息:", cause);
        ctx.close();
    }
}

服务器、客户端

  • 服务器
package RPC.netty.host;

import RPC.netty.dto.RpcRequest;
import RPC.netty.dto.RpcResponse;
import RPC.netty.handler.NettyServerHandler;
import RPC.netty.kryo.KryoSerializer;
import RPC.netty.kryo.NettyKryoDecoder;
import RPC.netty.kryo.NettyKryoEncoder;
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.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

/**
 * @author 
 * @date 
 * @description netty服务端
 * (1) Channel
 * 管道,其是对 Socket 的封装,其包含了一组 API,大大简化了直接与 Socket 进行操作的复杂性。
 *
 * (2) EventLoopGroup
 * EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
 * Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,
 * 让该线程处理一个 Channel 的所有 IO 事件。一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。
 * 一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。(一个线程可以处理多个Channel)
 *
 * (3) ServerBootStrap
 * 启动类,用于配置整个 Netty 代码,将各个组件关联起来。服务端使用的是 ServerBootStrap,而客户端使用的是则 BootStrap。
 *
 * (4) ChannelHandler 与 ChannelPipeline
 * ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。
 * 这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
 *
 * (5) ChannelFuture
 * Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。
 * 如果想获取到该异步操作的返回值,可以通过该异步操作对象的 addListener()方法为该异步操作添加监听器,为其注册回调:当结果出来后马上调用执行。
 * Netty 的异步编程模型都是建立在 Future 与回调概念之上的。
 */
@Slf4j
public class NettyServer {

    private final int port;

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

    private void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        KryoSerializer kryoSer = new KryoSerializer();
        try {
            //NETTY服务器启动器
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            设置时间循环对象,前者用来处理accept事件,后者用于处理已经建立的连接的io
            //一个是继承自AbstractBootstrap的group(parentGroup),另一个是自定义的childGroup,其中parentGroup负责处理ServerChannel(接收客户端连接通道)的I/O事件, 而childGroup则是用于处理Channel(客户端连接通道)的I/O事件。
            serverBootstrap.group(bossGroup, workerGroup)
            //用它来建立新accept的连接,用于构造serverSocketChannel的工厂类
            //设置通道类型 内部实际上根据传入的类型参数初始化好了ChannelFactory<? extends C> channelFactory这个Channel工厂对象,后续会使用该工厂对象来生产Channel对象。
            .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))
            //为accept channel的pipeline预添加的inboundHandler
            .childHandler(new ChannelInitializer<SocketChannel>() {
                //当新连接accept的时候,这个方法会调用
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    //为当前的channel的pipeline添加自定义的处理函数
                    socketChannel.pipeline().addLast(new NettyKryoDecoder(kryoSer, RpcRequest.class));
                    socketChannel.pipeline().addLast(new NettyKryoEncoder(kryoSer, RpcResponse.class));
                    socketChannel.pipeline().addLast(new NettyServerHandler());
                }
            });
            //bind方法会创建一个serverChannel,并且会将当前的channel注册到eventLoop上面,然后绑定本地端口,并对其进行初始化,为其的pipeline加一些默认的handler
            ChannelFuture future = serverBootstrap.bind(port).sync();
            //相当于在这里阻塞,直到serverChannel关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("服务器发生异常:" + e);
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) {
        new NettyServer(8989).run();
    }
}
  • 客户端
package RPC.netty.host;

import RPC.netty.dto.RpcRequest;
import RPC.netty.dto.RpcResponse;
import RPC.netty.handler.NettyClientHandler;
import RPC.netty.kryo.KryoSerializer;
import RPC.netty.kryo.NettyKryoDecoder;
import RPC.netty.kryo.NettyKryoEncoder;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;

import java.util.Objects;

/**
 * @author 
 * @date 
 * @description netty客户端
 */
@Slf4j
public class NettyClient {

    private final String host;
    private final int port;
    private static final Bootstrap bootstrap;

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

    static {
        EventLoopGroup eventExecutors = new NioEventLoopGroup();
        //Netty客户端启动器
        bootstrap = new Bootstrap();
        //配置的序列化方式
        KryoSerializer kryo = new KryoSerializer();
        //配置EventLoopGroup,用于处理I/O事件的事件循环组。是异步事件模型的重要实现。
        bootstrap.group(eventExecutors)
        //设置通道类型 内部实际上根据传入的类型参数初始化好了ChannelFactory<? extends C> channelFactory这个Channel工厂对象,后续会使用该工厂对象来生产Channel对象。
        .channel(NioSocketChannel.class)
        //设置事件处理器来处理用户逻辑  一般会设置一个ChannelInitializer的实现类来设置用户的若干个具体ChannelHandler
        .handler(new LoggingHandler(LogLevel.INFO))
        //配置channel的相关选项参数 此处为连接超时时长
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
        //业务逻辑Handler, 可能是HandlerInitializer,也可能是普通Handler
        .handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) {
                ch.pipeline().addLast(new NettyKryoDecoder(kryo, RpcResponse.class));
                ch.pipeline().addLast(new NettyKryoEncoder(kryo, RpcRequest.class));
                ch.pipeline().addLast(new NettyClientHandler());
            }
        });
    }

    /**
     * 1. 首先初始化了一个 Bootstrap
     * 2. 通过 Bootstrap 对象连接服务端
     * 3. 通过 Channel 向服务端发送消息RpcRequest
     * 4. 发送成功后,阻塞等待 ,直到Channel关闭
     * 5. 拿到服务端返回的结果 RpcResponse
     * @param request 请求
     * @return 返回
     */
    public RpcResponse sendMessage(RpcRequest request) {
        try {
            //1. 启动器连接
            ChannelFuture future = bootstrap.connect(host, port).sync();
            log.info("客户端连接到主机:" + host + port);
            Channel channel = future.channel();
            log.info("客户端发送消息:");
            if (Objects.nonNull(channel)) {
                channel.writeAndFlush(request)
                        .addListener(future1 ->  {
                    if (future.isSuccess()) {
                        log.info("客户端发送消息成功:"+ request.toString());
                    } else {
                        log.error("客户端发送消息失败:"+ request.toString());
                    }
                });
                channel.closeFuture().sync();
                AttributeKey<RpcResponse> key = AttributeKey.valueOf("response");
                return channel.attr(key).get();
            }
        } catch (Exception e) {
            log.error("出现异常信息:" , e);
        }
        return null;
    }

    public static void main(String[] args) {
        NettyClient client = new NettyClient("127.0.0.1", 8989);

        for (int i = 0; i < 3; i++) {
            RpcRequest request = RpcRequest.builder().interfaceName("testInterface" + i).methodName("hello" + i).build();
            client.sendMessage(request);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值