Netty构建NIO的httpClient

先简单的了解一下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事件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容