基础概念详解
-
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);
}
}
}