美文网首页
Netty构建NIO的httpClient

Netty构建NIO的httpClient

作者: 可乐爱上咖啡 | 来源:发表于2017-02-07 15:53 被阅读5968次

    先简单的了解一下BIO与NIO

    下图是几种常见I/O模型的对比:


    图片.png

    传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。

    对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

    最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。

    经典的BIO模式,每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更多的事情。多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。
      不过,这个模型最本质的问题在于,严重依赖于线程。线程的创建和销毁成本很高,线程本身占用较大内存,线程的切换成本是很高的。

    NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的。
      NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机会:如果一个连接不能读写(socket.read()返回0或者socket.write()返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注册标记位,然后切换到其它就绪的连接(channel)继续进行读写。

    结合事件模型使用NIO同步非阻塞特性

    NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
    首先需要注册当这几个事件到来的时候所对应的处理器。
    其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。

    interface ChannelHandler{ 
      void channelReadable(Channel channel); 
      void channelWritable(Channel channel); 
    }
    class Channel{ 
      Socket socket;
      Event event;//读,写或者连接
     }
    //IO线程主循环: 
    class IoThread extends Thread{ 
      public void run(){ 
        Channel channel; 
        while(channel=Selector.select()){
          //选择就绪的事件和对应的连接 
          if(channel.event==accept){ 
            registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器 
          } 
          if(channel.event==write){
           getChannelHandler(channel).channelWritable(channel);//如果可以写,则执行写事件 
          } 
          if(channel.event==read){
           getChannelHandler(channel).channelReadable(channel);//如果可以读,则执行读事件
          }
        }
      } 
      Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器 
    }
    

    这个程序很简短,也是最简单的Reactor模式:注册所有感兴趣的事件处理器,单线程轮询选择就绪事件,执行事件处理器。注意select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的轮询(select,poll),这个函数是阻塞的。所以你可以放心大胆地在一个while(true)里面调用这个函数而不用担心CPU空转。

    为解决高并发时线程数过多的问题,这里我们使用成熟的NIO框架Netty编写httpClient。

    Bootstrap是Socket客户端创建工具类,用户通过Bootstrap可以方便的创建netty的客户端并发起异步TCP连接操作。

    创建客户端连接辅助类Bootstrap
            Bootstrap b = new Bootstrap();
            b.group(workerGroup);//NioEventLoopGroup
            b.channel(NioSocketChannel.class);
            b.option(ChannelOption.SO_KEEPALIVE, false);
            b.option(ChannelOption.SO_TIMEOUT, this.timeout);
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    // 客户端接收到的是httpResponse响应,所以要使用HttpResponseDecoder进行解码
                    ch.pipeline().addLast(new HttpResponseDecoder());
                    // 客户端发送的是httprequest,所以要使用HttpRequestEncoder进行编码
                    ch.pipeline().addLast(new HttpRequestEncoder());
                    ch.pipeline().addLast(handler);
                }
            });
    

    上述代码就是客户端创建的流程:
    1.用户线程创建Bootstrap
    Bootstrap是Socket客户端创建工具类,通过API设置创建客户端相关的参数,异步发起客户端连接。
    2.指定处理客户端连接、IO读写的Reactor线程组NioEventLoopGroup。可以通过构造函数指定I/O线程的个数,默认为CPU内核数的2倍。
    3.通过Bootstrap的ChannelFactory和用户指定的Channel类型创建用于客户端连接的NioSocketChannel。此处的NioSocketChannel类似于Java NIO提供的SocketChannel。
    4.设置TCP参数

    主要TCP参数如下:
    (1) SO_TIMEOUT: 控制读取操作将阻塞多少毫秒,如果返回值为0,计时器就被禁止了,该线程将被无限期阻塞。
    (2) SO_SNDBUF: 套接字使用的发送缓冲区大小
    (3) SO_RCVBUF: 套接字使用的接收缓冲区大小
    (4) SO_REUSEADDR : 是否允许重用端口
    (5) CONNECT_TIMEOUT_MILLIS: 客户端连接超时时间,原生NIO不提供该功能,Netty使用的是自定义连接超时定时器检测和超时控制
    (6) TCP_NODELAY : 是否使用Nagle算法

    5.创建默认的channel Handler pipeline,用于调度和执行网络事件。
    Bootstrap为了简化Handler的编排,提供了ChannelInitializer,当TCP链路注册成功后,调用initChannel接口。
    pipeline维护着一个或者多个handler 用于异步的处理I/O数据,如HttpResponseDecoder是一个客户端接收到的是httpResponse响应,所以要使用HttpResponseDecoder进行解码的handle。

    客户端连接操作

    以下内容转自netty4源码分析-connect
    http://xw-z1985.iteye.com/blog/1937999

            // Start the client.
            ChannelFuture f = b.connect(host, port);
            f.channel().closeFuture();
    

    1、异步发起TCP连接 b.connect();
    在doConnect方法中调用initAndRegister方法,创建和初始化NioSocketChannel,并注册channel对应的网络监听状态位到多路复用器。
    coonect方法并没有返回一个Channel 而是一个 ChannelFuture。 ChannelFutre提供添加一个addListener的方法,使得这个channel真正被打开的时候调用用户设置的回调。
    2、由多路复用器在I/O中轮询个Channel,处理连接结果
    3、如果连接成功,设置Future结果,发送连接成功事件,触发ChannelPipeline执行
    4、由ChannelPipeline调度执行系统和用户的ChannelHandler,执行业务逻辑

    这里主要看一下cennect操作

    Channel创建完成后,连接操作会异步执行,最终调用到HeadContext的connect方法.

    doConnect三种可能结果

    1.连接成功,然会true;
     2.暂时没有连接上,服务器端没有返回ACK应答,连接结果不确定,返回false。此种结果下,需要将NioSocketChannel中的selectionKey设置为OP_CONNECT,监听连接结果;
     3.接连失败,直接抛出I/O异常
      异步返回之后,需要判断连接结果,如果成功,则触发ChannelActive事件。最终会将NioSocketChannel中的selectionKey设置为SelectionKey.OP_READ,用于监听网络读操作。

    异步连接结果通知

    NioEventLoop的Selector轮询客户端连接Channel,当服务端返回应答后,进行判断。依旧是NioEventLoop中的processSelectedKey方法。
    doFinishConnect方法通过调用SocketChannel的finishConnect方法完成连接的建立,在NioSocketChannel中实现。此时,isActive()返回true,所以触发ChannelActive事件,该事件是一个inbound事件,所以Inbound的处理器可以通过实现channelActive方法来进行相应的操作。

    总结:从发起connect请求到请求建立先后共经历了以下几件事情:
    1、创建套接字SocketChannel
    2、设置套接字为非阻塞
    3、设置channel当前感兴趣的事件为SelectionKey.OP_READ
    4、创建作用于SocketChannel的管道Pipeline,该管道中此时的处理器链表为:Head(outbound)->tail(inbound)。
    5、设置SocketChannel的options和attrs。
    6、为管道增加一个Inbound处理器ChannelInitializer。经过此步骤后,管道中的处理器链表为:head(outbound)->ChannelInitializer(inbound)->tail(inbound)。注意ChannelInitializer的实现方法initChannel,里面会当channelRegisgered事件发生时将EchoClientHandler加入到管道中。
    7、启动客户端线程,并将register0任务加入到线程的任务队列中。而register0任务做的事情为:将SocketChannel、0、注册到selector中并得到对应的selectionkey。然后通过回调,将doConnect0任务加入到线程的任务队列中。线程从启动到现在这段时间内,任务队列的变化如下:register0任务->register0任务,doConnect0任务-> doConnect0任务
    8、通过channelRegistered事件,将EchoClientHandler加入到管道中,并移除ChannelInitializer,经过此步骤后,管道中的处理器链表为:head(outbound)-> EchoClientHandler (inbound)->tail(inbound)。管道从创建到现在这段时间内,处理器链表的变化历史为:head->tail,head->ChannelInitializer(inbound)->tail,head-> EchoClientHandler (inbound)->tail
    9、doConnect0任务会触发connect事件,connect是一个Outbound事件,headHandler通过调用AbstractNioUnsafe的方法向服务端发起connect请求,并设置ops为SelectionKey.OP_CONNECT
    10、客户端线程NioEventLoop中的select接收到connect事件后,将SelectionKey.OP_CONNECT从ops中移除,然后调用finishConnect方法完成连接的建立。到此,connect就正式建立了。
    11、最后触发ChannelActive事件。

    相关文章

      网友评论

          本文标题:Netty构建NIO的httpClient

          本文链接:https://www.haomeiwen.com/subject/fwuyittx.html