天天看点

对i/o端口的访问和i/o空间的访问方式是一样的_揭开网络编程的神秘面纱2(BIO、伪异步IO、NIO、AIO)...Channel(通道)Buffer(缓冲区)Selector(多路复用选择器)NIO通信模型AIO通信模型总结

从上文揭开网络编程的神秘面纱1(BIO、伪异步IO、NIO、AIO)中我们知道,不管是BIO模型还是伪异步IO模型,服务端都需要创建一定量的线程来处理客户端Socket的请求,也就是说在任何时候都可能有大量线程处于休眠状态,只是等待输入或者输出数据就绪;而且由于需要为每个线程的调用栈分配内存。这些都是一种资源的浪费。即使jvm在物理上有很大的资源,可以支持非常大数量的线程,但是线程上下文切换所带来的开销也会给我们带来麻烦。

对i/o端口的访问和i/o空间的访问方式是一样的_揭开网络编程的神秘面纱2(BIO、伪异步IO、NIO、AIO)...Channel(通道)Buffer(缓冲区)Selector(多路复用选择器)NIO通信模型AIO通信模型总结

所以BIO模型和伪异步IO模型只适用于中小数量的客户端请求。如果需要支撑高并发的数据,我们需要选择NIO或者AIO模型。

由于NIO涉及到Buffer、Channel、Selector等基础知识,为了便于理解,我们举个例子。小JIA要结婚了,需要给亲朋好友发请柬,这个时候小JIA需要一一拜访,每次拿一张请柬给一个亲朋好友,发送完成后,再回家再取一张请柬发给另外一个亲朋好友,直到所有的好友都拿到请柬。这就是普通的Socket模式,来一个请求ServerSocket就进行处理,处理完再继续接受请求。发到一半的时候小JIA发现还有几百个亲朋好友没发,照这个速度发下去,得到猴年马月,说不定婚礼都过了,请柬还没发完。这个时候小JIA开始动员亲朋好友来帮忙发请柬。小JIA通知了几个亲戚和朋友,让他们过来拿请柬帮忙分发,小JIA的父母帮忙分拣请柬。这就是NioSocket,Buffer就是请柬,而Channel就是帮忙分发请柬的亲戚朋友,而小JIA的父母充当了Selector的职责,负责请柬的分拣。这样的处理方式极大的提高了IO的效率。小JIA再也不用担心亲戚朋友没有准时收到请柬而错过婚礼了。

NioSocket提供ServerSocketChannel和SocketChannel,分别对应ServerSocket和Socket。接下来我们来介绍NIO的几个基础知识。

Channel(通道)

Channel就像水管一样,是一个通道。通道与流的不同之处在于通道是双向的,可以用来读、写或同时读写操作。因为是双向的,所以通道可以比流更好地反应底层操作系统的真实情况。

对i/o端口的访问和i/o空间的访问方式是一样的_揭开网络编程的神秘面纱2(BIO、伪异步IO、NIO、AIO)...Channel(通道)Buffer(缓冲区)Selector(多路复用选择器)NIO通信模型AIO通信模型总结

Channel主要分为两大类:SelectableChannel(用户网络读写)和FileChannel(用于文件操作)。ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

Buffer(缓冲区)

Channel提供从文件、网络读取数据的通道,可是读取或写入数据都必须经过Buffer。Buffer实际上是一个数组,并提供对数据结构化访问以及维护读写位置等信息。

对i/o端口的访问和i/o空间的访问方式是一样的_揭开网络编程的神秘面纱2(BIO、伪异步IO、NIO、AIO)...Channel(通道)Buffer(缓冲区)Selector(多路复用选择器)NIO通信模型AIO通信模型总结

具体的缓存区有:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer。它们实现了共同的接口:Buffer。

Selector(多路复用选择器)

Selector一般称为选择器,当然也可以叫做多路复用器。它是Java NIO核心组件中的一个,用于检查一个或者多个NIO Channel(通道)的状态是否处于可读、可写。

对i/o端口的访问和i/o空间的访问方式是一样的_揭开网络编程的神秘面纱2(BIO、伪异步IO、NIO、AIO)...Channel(通道)Buffer(缓冲区)Selector(多路复用选择器)NIO通信模型AIO通信模型总结

一个Selector可以同时轮询多个Channel,因为jdk使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。也就是一个线程负责Selector的轮询,可以管理成千上万个网络客户端连接。

NIO通信模型

NIO提供了一个全新底层I/O模型。采用面向块的概念,IO操作以大的数据块为单位进行操作,而不是一个个字节或字符进行操作,因此性能有了很大的提高。

//创建选择器selector = Selector.open();//打开监听通道serverChannel = ServerSocketChannel.open();//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式serverChannel.configureBlocking(false);//开启非阻塞模式//绑定端口 backlog设为1024serverChannel.socket().bind(new InetSocketAddress(port),1024);//监听客户端连接请求serverChannel.register(selector, SelectionKey.OP_ACCEPT);
           

1、初始化多路复用器selector、serverSocketChannel通道、设置通道的模式为非阻塞、注册channel到selector上,并监听accept请求;

Set keys = selector.selectedKeys();Iterator it = keys.iterator();
           

2、启动Server服务器,循环selectedKeys,当有channel准备好时就处理,否则一直循环。

3、selectedKey保存了当前请求的Channel和Selector,并提供了不同的操作类型。共有四种:

  • SelectionKey.OP_ACCEPT //请求操作
  • SelectionKey.OP_CONNECT //链接操作
  • SelectionKey.OP_READ //读操作
  • SelectionKey.OP_WRITE //写操作
//处理新接入的请求消息if(key.isAcceptable()){ ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //通过ServerSocketChannel的accept创建SocketChannel实例 //完成该操作意味着完成TCP三次握手,TCP物理链路正式建立 SocketChannel sc = ssc.accept(); //设置为非阻塞的 sc.configureBlocking(false); //注册为读 sc.register(selector, SelectionKey.OP_READ);}
           

3.1 只有在register方法中注册了相应的操作,Selector才会关心相应类型操作的请求。比如我们在上面的代码中注册了SelectionKey.OP_ACCEPT请求操作,那么Seletor就关心新的客户端接入,如果监听到新的客户端接入,就处理新的接入请求,完成TCP三次握手,建立物理链路。并把ServerSocketChannel注册为SelectionKey.OP_READ读操作。

//读消息if(key.isReadable()){ SocketChannel sc = (SocketChannel) key.channel(); //创建ByteBuffer,并开辟一个1M的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); //读取请求码流,返回读取到的字节数 int readBytes = sc.read(buffer); //读取到字节,对字节进行编解码 if(readBytes>0){ }//链路已经关闭,释放资源 else if(readBytes<0){ key.cancel(); sc.close(); }}
           

3.2 当把新接入的客户端连接注册到Selector上,开始监听都操作,读取客户端发送的网络消息,把读取到的消息放到缓冲区,然后进行服务端数据处理操作。

由于jdk的selector在linux等主流操作系统上通过epoll实现,它没有链接句柄数的限制,意味着一个selector可以连接成千上万个客户端,而性能不会随着客户端连接数的增长而线性下降,因此,它适合做高性能、高负载的网络服务器。

AIO通信模型

AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着由这个线程自行进行IO操作,IO操作本身是同步的。

//创建服务端通道channel = AsynchronousServerSocketChannel.open();//绑定端口channel.bind(new InetSocketAddress(port));//用于接收客户端的连接channel.accept(this,new AcceptHandler());
           

1、与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。

public class AcceptHandler implements CompletionHandler{@Overridepublic void completed(AsynchronousSocketChannel channel,AsyncServerHandler serverHandler) {serverHandler.channel.accept(serverHandler, this);//创建新的BufferByteBuffer buffer = ByteBuffer.allocate(1024);//异步读 第三个参数为接收消息回调的业务Handlerchannel.read(buffer, buffer, new ReadHandler(channel));}@Overridepublic void failed(Throwable exc, AsyncServerHandler serverHandler) {exc.printStackTrace();}}
           

2、发出一个事件(accept read write等)之后要指定时间处理类(回调函数),AIO中的事件处理类是CompletionHandler,这个接口定义了两个方法,分别在一部操作和成功时被回调。

  • void completed(V result, A attachment);
  • void failed(Throwable exc, A attachment);

所以对于AIO来说,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再通知线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。

总结

BIO适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高。但程序简单易理解。

NIO适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,编程复杂。

AIO适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,编程比较复杂。