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