Netty入门——Client开发(二)

上一篇主要介绍了Netty Server端的开发,并且根据源码分析了Server的启动过程。这篇我们主要介绍一下如何编写客户端,并对之前文章中没有提到ChannelPipeline和ChannelHandler,ChannelHandlerContext进行解释。本文中的完整源码可以看这篇文章。亲测可以运行。

PS:本文只是针对初学Netty的同学看,不会特别深入,如果看完本文,你觉得还是不过瘾,强烈推荐闪电侠同学,这位同学对于Netty源代码分析的非常透彻,我看了之后受益匪浅。

客户端代码

客户端代码编写基本和服务端的差不多。只是它的Channel需要设置为NioSocketChannel。下面是一个简单的例子。

public class MyClient {

    public static void main(String[] args) throws Exception {

        // 配置客户端NIO线程组
        //首先创建客户端处理I/O读写的NioEventLoop Group线程组
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {

            //继续创建客户端辅助启动类Bootstrap,随后需要对其进行配置。
            //与服务端不同的是,它的Channel需要设置为NioSocketChannel
            //然后为其添加handler。
            // 在初始化它的时候将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。
            Bootstrap b = new Bootstrap();
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    .handler(new MyClientInitializer());

            // 发起异步连接操作
            //客户端启动辅助类设置完成之后,调用connect方法发起异步连接,
            //然后调用同步方法等待连接成功。
            ChannelFuture channelFuture = bootstrap.connect("localhost", 8899).sync();
            channelFuture.channel().closeFuture().sync();
        }
        finally {
            // 优雅退出,释放NIO线程组
            //在退出之前,释放NIO线程组的资源。
            eventLoopGroup.shutdownGracefully();
        }

    }
}

下面我们重点看一下MyClientInitializer这个类。

public class MyClientInitializer extends ChannelInitializer{
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //(1) 加入拆包器
        pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4));  
        //(2) 加入粘包器
        pipeline.addLast(new LengthFieldPrepender(4));             
        //字符串解码 (3)
        pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
        //字符串编码 (4)
        pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
        //(5)
        pipeline.addLast(new MyClientHandler());
    }
}

可以看到了之前没有遇到过的ChannelPipeline, 下面通过对ChannelPipeline的源码分析看一下他到底是干什么的。

ChannelPipeline简介

通过源代码可以看到,AbstractChannel中包含了一个pipeline,DefaultChannelPipeline继承了ChannelPipeline。他们的关系如下:

Netty入门——Client开发(二)_第1张图片
image.png

看一下DefaultChannelPipeline类的部分源代码:

    .....
    final AbstractChannelHandlerContext head;     //保存handler上下文信息
    final AbstractChannelHandlerContext tail;
    ....
    protected DefaultChannelPipeline(Channel channel) {
           ....
        tail = new TailContext(this);    //实现ChannelOutboundHandler接口
        head = new HeadContext(this);    //实现ChannelInboundHandler接口
 
        head.next = tail;
        tail.prev = head;
    }

在ChannelPipeline中最为重要的是ChannelHandlerContext,ChannelInboundHandler以及ChannelOutboundHandler三个类。ChannelHandlerContext 负责控制事件的流动,将事件从一个ChannelHandler传播到下一个ChannelHandler中处理。
传播顺序(按照MyClientInitializer的例子):
入站:1---->3---->5
出站:5---->4---->2
那么如何控制事件的流转呢?
ChannelHandlerContext代码中可以发现很多以"fire"开头的函数,这些函数的作用就是将事件传播到下一个Handler。

Netty入门——Client开发(二)_第2张图片
image.png

入站事件一般由I/O线程触发,可以归纳为下面几种

ChannelRegistered() // Channel注册到EventLoop
ChannelActive()     // Channel激活
ChannelRead(Object) // Channel读取到数据
ChannelReadComplete()   // Channel读取数据完毕
ExceptionCaught(Throwable)  // 捕获到异常
UserEventTriggered(Object)  // 用户自定义事件
ChannelWritabilityChanged() // Channnel可写性改变,由写高低水位控制
ChannelInactive()   // Channel不再激活
ChannelUnregistered()   // Channel从EventLoop中注销

出站事件一般由用户触发,可以归纳为下面几种

bind(SocketAddress, ChannelPromise) // 绑定到本地地址
connect(SocketAddress, SocketAddress, ChannelPromise)   // 连接一个远端机器
write(Object, ChannelPromise)   // 写数据,实际只加到Netty出站缓冲区
flush() // flush数据,实际执行底层写
read()  // 读数据,实际设置关心OP_READ事件,当数据到来时触发ChannelRead入站事件
disconnect(ChannelPromise)  // 断开连接,NIO Server和Client不支持,实际调用close
close(ChannelPromise)   // 关闭Channel
deregister(ChannelPromise)  // 从EventLoop注销Channel

总结一下:ChannelPipeline 就是一个容器,或者理解成一个双向链表更加形象,用来存放入站和出站事件的Handler。出站入站事件可以通过ChannelHandlerContext传递到下一个Handler中。具体ChannelPipeline如何做到初始化、添加节点、删除节点的操作,大家可以参考这篇文章。
ChannelPipeline、ChannelHandler、ChannelHandlerContext、Channel他们之间的关系可以用下面的图表示:

Netty入门——Client开发(二)_第3张图片
image.png

ChannelHandler简介

我们看到ChannelHandler接口一共才包含三个方法(有一个被废弃)。它还定义了 Sharable 注解,使得Handler在ChannelPipeline中的实例只有一个。


Netty入门——Client开发(二)_第4张图片
image.png

根据代码中的注释我们可以推断出 handlerAdded在handler添加之后被调用。handlerRemoved 在handler被移出的时候被调用。

    /**
     * Gets called after the {@link ChannelHandler} was added to the actual context and it's ready to handle events.
     */
    void handlerAdded(ChannelHandlerContext ctx) throws Exception;

    /**
     * Gets called after the {@link ChannelHandler} was removed from the actual context and it doesn't handle events
     * anymore.
     */
    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;

下面我们逐个看一下ChannelHandler的各个子类。


image.png

ChannelHanderAdapter

ChannelHanderAdapter 只是个框架,里面的方法并没有具体内容。

ChannelOutboundHandler

ChannelOutboundHandler用于拦截出站事件。

    /**
     * Intercepts {@link ChannelHandlerContext#read()}.
     */
    void read(ChannelHandlerContext ctx) throws Exception;

ChannelInboundHandler

ChannelInboundHandler用于拦截入站事件,函数名称大都以channelXXX命名,这里看名字也可以推断,当channel注册时,channel active的时候调用。


Netty入门——Client开发(二)_第5张图片
image.png

其他Handler

回到最开始的客户端代码,我们的最后一个handler 是 MyClientHandler,下面是该类的代买,他继承自SimpleChannelInboundHandler

public class MyClientHandler extends SimpleChannelInboundHandler{

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        //服务端的远程地址
        System.out.println(ctx.channel().remoteAddress());
        System.out.println("client output: "+msg);
        ctx.writeAndFlush("from client: "+ LocalDateTime.now());
    }

    /**
     * 当服务器端与客户端进行建立连接的时候会触发,如果没有触发读写操作,则客户端和客户端之间不会进行数据通信,也就是channelRead0不会执行,
     * 当通道连接的时候,触发channelActive方法向服务端发送数据,触发服务器端的handler的channelRead0回调,然后
     * 服务端向客户端发送数据触发客户端的channelRead0,依次触发。
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush("来自与客户端的问题!");
    }

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

}

SimpleChannelInboundHandler

SimpleChannelInboundHandler的继承关系图如下


Netty入门——Client开发(二)_第6张图片
image.png

SimpleChannelInboundHandler继承自 ChannelInboundHandlerAdapter
。其中channelRead0可以用来处理特定类型的消息。比如在这个例子中的消息类型是String类型。消息的处理我们留到下一篇再讲,这里不再赘述。

protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;

ChannelHandlerContext简介

ChannelHandlerContext 保存的是ChannelHandler 与ChannelPipeline的关系。比如你可以通过channel()方法拿到与ChannelPipeline 绑定的Channel,handler()方法拿到与ChannelHandlerContext绑定的Handler。

    /**
     * The {@link ChannelHandler} that is bound this {@link ChannelHandlerContext}.
     */
    ChannelHandler handler();

ChannelHandlerContext的其他函数都fireXXX命名,上文有介绍过这样方法作用是将事件传递到下一个Handler中。ChannelHandlerContext的继承关系如下图,我们进入AbstractChannelHandlerContext看一下context是如何传递事件的。


Netty入门——Client开发(二)_第7张图片
image.png

我们以注册事件为例。
首先看一下DefaultChannelPipeline 中的fireChannelRegistered 方法。

DefaultChannelPipeline

    @Override
    public final ChannelPipeline fireChannelRegistered() {
        AbstractChannelHandlerContext.invokeChannelRegistered(head);
        return this;
    }

该方法会调用AbstractChannelHandlerContext类中的静态方法invokeChannelRegistered,同时把head作为参数传入。

    static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRegistered();
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRegistered();
                }
            });
        }
    }

为了保证线程安全,Netty首先会确认该操作是在Reactor线程中执行的。invokeChannelRegistered 会直接调用fireChannelRegistered

    private void invokeChannelRegistered() {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelRegistered(this);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelRegistered();
        }
    }

fireChannelRegistered 的源码如下,发现他会去调用findContextInbound方法。

   @Override
    public ChannelHandlerContext fireChannelRegistered() {
        invokeChannelRegistered(findContextInbound());
        return this;
    }

findContextInbound 源码如下,他会线性搜索channelHandlerContext是否是inbound。那么什么是inbound呢?

    private AbstractChannelHandlerContext findContextInbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx = ctx.next;
        } while (!ctx.inbound);
        return ctx;
    }

DefaultChannelHandlerContext

在DefaultChannelHandlerContext类中找到了判断的方法。通过instanceof 方法判断是inboundHandler 还是outboundhandler。

    private static boolean isInbound(ChannelHandler handler) {
        return handler instanceof ChannelInboundHandler;
    }

    private static boolean isOutbound(ChannelHandler handler) {
        return handler instanceof ChannelOutboundHandler;
    }

回到上面的findContextInbound方法,该方法会线性查找到下一个inboundHandler。

至此 客户端这边的 ChannelPipeline和ChannelHandler,ChannelHandlerContext类分析完了,但是MyClientInitializer 类中的LengthFieldBasedFrameDecoder 类我们还没有分析,这里会涉及到Netty的拆包和解码,会在下一篇文章中介绍。

你可能感兴趣的:(Netty入门——Client开发(二))