《netty入门与实战》笔记-05:netty内置的channelHandler

Netty 内置了不少开箱即用的 ChannelHandler。下面,咱们经过学习 Netty 内置的 ChannelHandler 来逐步构建咱们的 pipeline。java

ChannelInboundHandlerAdapter 与 ChannelOutboundHandlerAdapter

首先是 ChannelInboundHandlerAdapter ,这个适配器主要用于实现其接口 ChannelInboundHandler 的全部方法,这样咱们在编写本身的 handler 的时候就不须要实现 handler 里面的每一种方法,而只须要实现咱们所关心的方法,默认状况下,对于 ChannelInboundHandlerAdapter,咱们比较关心的是他的以下方法promise

ChannelInboundHandlerAdapter.java微信

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ctx.fireChannelRead(msg);
}

他的做用就是接收上一个 handler 的输出,这里的 msg 就是上一个 handler 的输出。你们也能够看到,默认状况下 adapter 会经过 fireChannelRead() 方法直接把上一个 handler 的输出结果传递到下一个 handler框架

ChannelInboundHandlerAdapter 相似的类是 ChannelOutboundHandlerAdapter,它的核心方法以下ide

ChannelOutboundHandlerAdapter.java工具

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ctx.write(msg, promise);
}

默认状况下,这个 adapter 也会把对象传递到下一个 outBound 节点,它的传播顺序与 inboundHandler 相反,这里就再也不对这个类展开了。学习

咱们往 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)
}

不过在开始解码以前,咱们来了解一下另一个特殊的 handlernetty

ByteToMessageDecoder

一般状况下,不管咱们是在客户端仍是服务端,当咱们收到数据以后,首先要作的事情就是把二进制数据转换到咱们的一个 Java 对象,因此 Netty 很贴心地写了一个父类,来专门作这个事情,下面咱们来看一下,如何使用这个类来实现服务端的解码code

public class PacketDecoder extends ByteToMessageDecoder {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) {
        out.add(PacketCodeC.INSTANCE.decode(in));
    }
}

当咱们继承了 ByteToMessageDecoder 这个类以后,咱们只须要实现一下 decode() 方法,这里的 in 你们能够看到,传递进来的时候就已是 ByteBuf 类型,因此咱们再也不须要强转,第三个参数是 List 类型,咱们经过往这个 List 里面添加解码后的结果对象,就能够自动实现结果往下一个 handler 进行传递,这样,咱们就实现了解码的逻辑 handler

另外,值得注意的一点,对于 Netty 里面的 ByteBuf,咱们使用 4.1.6.Final 版本,默认状况下用的是堆外内存,在 ByteBuf 这一小节中咱们提到,堆外内存咱们须要自行释放,在咱们前面小节的解码的例子中,其实咱们已经漏掉了这个操做,这一点是很是致命的,随着程序运行愈来愈久,内存泄露的问题就慢慢暴露出来了, 而这里咱们使用 ByteToMessageDecoderNetty 会自动进行内存的释放,咱们不用操心太多的内存管理方面的逻辑。

当咱们经过解码工具把二进制数据转换到 Java 对象即指令数据包以后,就能够针对每一种指令数据包编写逻辑了。

SimpleChannelInboundHandler

回顾一下咱们前面处理 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); 
}

这样一个好处就是,每次添加一个指令处理器,逻辑处理的框架都是一致的.

可是,你们应该也注意到了,这里咱们编写指令处理 handler 的时候,依然编写了一段咱们其实能够不用关心的 if else 判断,而后还要手动传递没法处理的对象 (XXXPacket) 至下一个指令处理器,这也是一段重复度极高的代码,所以,Netty 基于这种考虑抽象出了一个 SimpleChannelInboundHandler 对象,类型判断和对象传递的活都自动帮咱们实现了,而咱们能够专一于处理咱们所关心的指令便可。

下面,咱们来看一下如何使用 SimpleChannelInboundHandler 来简化咱们的指令处理逻辑

LoginRequestHandler.java

public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
        // 登陆逻辑
    }
}

SimpleChannelInboundHandler 从字面意思也能够看到,使用它很是简单,咱们在继承这个类的时候,给他传递一个泛型参数,而后在 channelRead0() 方法里面,咱们不用再经过 if 逻辑来判断当前对象是不是本 handler 能够处理的对象,也不用强转,不用往下传递本 handler 处理不了的对象,这一切都已经交给父类 SimpleChannelInboundHandler 来实现了,咱们只须要专一于咱们要处理的业务逻辑便可。

上面的 LoginRequestHandler 是用来处理登陆的逻辑,同理,咱们能够很轻松地编写一个消息处理逻辑处理器

MessageRequestHandler.java

public class MessageRequestHandler extends SimpleChannelInboundHandler<MessageRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageRequestPacket messageRequestPacket) {

    }
}

MessageToByteEncoder

在前面几个小节,咱们已经实现了登陆和消息处理逻辑,处理完请求以后,咱们都会给客户端一个响应,在写响应以前,咱们须要把响应对象编码成 ByteBuf,结合咱们本小节的内容,最后的逻辑框架以下

public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @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<MessageRequestPacket> {
    @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,从字面意思也能够看出,它的功能就是将对象转换到二进制数据。

下面,咱们来看一下,咱们如何来实现编码逻辑

public class PacketEncoder extends MessageToByteEncoder<Packet> {

    @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,所以,你们注意到,PacketCodeCencode() 方法的定义也改了,下面是更改先后的对比

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 进行返回。当咱们向 pipeline 中添加了这个编码器以后,咱们在指令处理完毕以后就只须要 writeAndFlush java 对象便可,像这样

public class LoginRequestHandler extends SimpleChannelInboundHandler<LoginRequestPacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, LoginRequestPacket loginRequestPacket) {
        ctx.channel().writeAndFlush(login(loginRequestPacket));
    }
}

public class MessageRequestHandler extends SimpleChannelInboundHandler<MessageResponsePacket> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageResponsePacket messageRequestPacket) {
        ctx.channel().writeAndFlush(receiveMessage(messageRequestPacket));
    }
}

经过咱们前面的分析,能够看到, Netty 为了让咱们逻辑更为清晰简洁,帮咱们作了不少工做,能直接用 Netty 自带的 handler 来解决的问题,不要重复造轮子。在接下里的小节,咱们会继续探讨 Netty 还有哪些开箱即用的 handler

总结

本小节,咱们经过学习 netty 内置的 channelHandler 来逐步构建咱们的服务端 pipeline,经过内置的 channelHandler 能够减小不少重复逻辑。

  1. 基于 ByteToMessageDecoder,咱们能够实现自定义解码,而不用关心 ByteBuf 的强转和 解码结果的传递。
  2. 基于 SimpleChannelInboundHandler,咱们能够实现每一种指令的处理,再也不须要强转,再也不有冗长乏味的 if else 逻辑,不须要手动传递对象。
  3. 基于 MessageToByteEncoder,咱们能够实现自定义编码,而不用关心 ByteBuf 的建立,不用每次向对端写 Java 对象都进行一次编码。

以上内容来源于掘金小册《Netty 入门与实战:仿写微信 IM 即时通信系统》,若想得到更多,更详细的内容,请用微信扫码订阅:

相关文章
相关标签/搜索