首先是ChannelInboundHandlerAdapter , 这个适配器只用用于实现其接口ChannelInboundHandler 的所有方法,这样我们在编写自己的handler时就不需要实现handler里的每一个方法,而只需要实现我们关心的方法 , 默认情况下 , 对于ChannelInboundHandlerAdapter , 我们比较关心的是他的channelRead()
ChannelInboundHandlerAdapter.java
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
与ChannelinboundHandlerAdapter相似的是ChannelOutboundHandlerAdapter , 他的核心方法是 write() 方法
ChannelOutboundHandlerAdapter.java
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
我们往pipeline中添加的第一个handler中的channelRead方法中 , msg对象其实就是ByteBuf , 服务端在接收数据之后 , 应该首先把这个ByteBuf解码 , 然后把解码之后的结果传递给下一个handler :
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf requestByteBuf = (ByteBuf) msg;
// 解码
Packet packet = PacketCodeC.INSTANCE.decode(requestByteBuf);
// 解码后的对象传递到下一个 handler 处理
ctx.fireChannelRead(packet)
}
在开始解码之前我们来了解另外一个特殊的handler
通常情况下 , 无论是我们在客户端还是在服务端 , 当我们接收到数据之后 , 首先要做的就是事情就是把二进制数据转换成我们所需要的java 对象 , 所以Netty 很贴心的帮我们写了一个父类 ,来专门做这个事情 , 我们来看一下如何使用这个类来实现服务端二进制数据解码:
public class PacketDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) {
out.add(PacketCodeC.INSTANCE.decode(in));
}
}
当我们通过解码器把二进制数据转换到java 对象即指令数据包之后 , 就可以针对每一种指令数据包编写逻辑了 。
回顾一下我们之前处理java 对象的逻辑
if (packet instanceof LoginRequestPacket) {
// ...
} else if (packet instanceof MessageRequestPacket) {
// ...
} else if ...
我们通过if - else 来进行逻辑处理 , 当我们需要处理的指令越来越多的时候 , 代码就会显得越来越臃肿 ,这个时候我们可以通过给pipeline 添加多个handler(集成 ChannelInboundHandlerAdapter) 来解决多if-else 的问题
XXXHandler.java
if (packet instanceof XXXPacket) {
// ...处理
} else {
ctx.fireChannelRead(packet);
}
接下来我们看一看 , 如何使用 SimpleChannelInboundHandler 简化我们的指令处理逻辑
LoginRequestHandler.java
public class LoginRequestHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
// 登录逻辑
}
}
在前面的几个小节 , 我们已经实现了登录和消息处理逻辑 , 处理完请求之后 , 我们都会给客户端一个响应 , 在写响应之前 , 我们需要把响应对象编码成ByteBuf , 结合本小节的内容 , 最后的逻辑框架如下:
public class LoginRequestHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
LoginResponsePacket loginResponsePacket = login(loginRequestPacket);
ByteBuf responseByteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), loginResponsePacket);
ctx.channel().writeAndFlush(responseByteBuf);
}
}
public class MessageRequestHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageRequestPacket messageRequestPacket) {
MessageResponsePacket messageResponsePacket = receiveMessage(messageRequestPacket);
ByteBuf responseByteBuf = PacketCodeC.INSTANCE.encode(ctx.alloc(), messageRequestPacket);
ctx.channel().writeAndFlush(responseByteBuf);
}
}
我们注意到 , 我们处理完每一种指令之后的逻辑都是相似的 , 都需要进行解码 , 然后调用 writeAndFlush() 将数据写到对端 , 这个编码的过程其实也是重复的逻辑 , 而且在编码的过程中国 , 我们还需要手动去创建一个ByteBuf :
PacketCodeC.java
public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet) {
// 1. 创建 ByteBuf 对象
ByteBuf byteBuf = byteBufAllocator.ioBuffer();
// 2. 序列化 java 对象
// 3. 实际编码过程
return byteBuf;
}
而Netty 提供了一个特殊的channelHandler 来专门处理这种逻辑 , 我们不需要每一次将响应写到对端的时候调用一次编码逻辑进行编码 , 也不需要自行创建ByteBuf , 这个类叫做 MessageToByteEncoder , 从字面意思可以看出 , 他的功能就是将对象转化到二进制数据 。
我们来看一下如何使用 MessageToByteEncoder 来实现编码逻辑
public class PacketEncoder extends MessageToByteEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Packet packet, ByteBuf out) {
PacketCodeC.INSTANCE.encode(out, packet);
}
}
PacketEncoder 集成自 MessageToByteEncoder , 泛型参数 Packet 表示这个类的作用是 将Packet 类型对象到二进制的转化 。
这里我们只需要实现encode() 方法 , 我们注意到 , 这个方法的第二个参数是java对象 , 而第三个参数是ByteBuf 对象 , 我们在这个方法里面要做的事情就是把java对象里面的字段写到ByteBuf , 我们不在需要自行去分配ByteBuf , 因此大家注意到 , PacketCodeC 的 encode() 方法 的定义也改了 , 下面是更改前后的对比:
PacketCodeC.java
// 更改前的定义
public ByteBuf encode(ByteBufAllocator byteBufAllocator, Packet packet) {
// 1. 创建 ByteBuf 对象
ByteBuf byteBuf = byteBufAllocator.ioBuffer();
// 2. 序列化 java 对象
// 3. 实际编码过程
return byteBuf;
}
// 更改后的定义
public void encode(ByteBuf byteBuf, Packet packet) {
// 1. 序列化 java 对象
// 2. 实际编码过程
}
我们可以看到 , PacketCodeC 不在需要手动创建ByteBuf对象 , 不在需要把创建完ByteBuf 的进行返回 , 当我们向pipeline 中添加了这个编码器之后 , 我们在指令处理完毕之后就只需要writeAndFlush java 对象即可
public class LoginRequestHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
ctx.channel().writeAndFlush(login(loginRequestPacket));
}
}
public class MessageRequestHandler extends SimpleChannelInboundHandler {
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageResponsePacket messageRequestPacket) {
ctx.channel().writeAndFlush(receiveMessage(messageRequestPacket));
}
}
通过我们前面的分析 , 可以看到 , Netty 为了让我们逻辑更加清晰简洁 , 帮我们做了很多工作 吗能直接用Netty 自带的handler 来解决问题 , 不要重复制造轮子 , 在下面的小节中 , 我们会继续探讨Netty还有哪些开箱即用的handler
分析完服务端的pipeline 与 handler 组成结构 , 相信你们也不难自行分析出客户端handler 的结构了 , 最后我们来看一下服务端和客户端完整的pipeline 与handler结构
对应我们的代码
服务端
serverBootstrap
.childHandler(new ChannelInitializer() {
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new PacketDecoder());
ch.pipeline().addLast(new LoginRequestHandler());
ch.pipeline().addLast(new MessageRequestHandler());
ch.pipeline().addLast(new PacketEncoder());
}
});
客户端
bootstrap
.handler(new ChannelInitializer() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new PacketDecoder());
ch.pipeline().addLast(new LoginResponseHandler());
ch.pipeline().addLast(new MessageResponseHandler());
ch.pipeline().addLast(new PacketEncoder());
}
});
答: channelRead0(ChannelHandlerContext ctx, MessageRequestPacket msg) 的msg是由父类SimpleChannelInboundHandler的channelRead() 方法判断是需要类型后, 强转类型后传递进来的
I imsg = (I) msg;
channelRead0(ctx, imsg);