前言
在分布式系统横行的现在,传统阻塞式的I/O流在系统通信方面显得心有余而力不足,多线程的消耗是服务器支撑不起的,这时候,NIO应运而生,NIO又可以称为非阻塞式的IO,它类似于IO,如下,NIO包里有几个重要的概念:
buffer:NIO是基于缓冲的,buffer是最底层的必要类,这也是IO和NIO的根本不同,虽然stream等有buffer开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而NIO却是直接读到buffer中进行操作。
channel:类似于IO的stream,但是不同的是除了FileChannel,其他的channel都能以非阻塞状态运行。FileChannel执行的是文件的操作,可以直接DMA操作内存而不依赖于CPU。其他比如socketchannel就可以在数据准备好时才进行调用。
selector:用于分发请求到不同的channel,这样才能确保channel不处于阻塞状态就可以收发消息。
接下来,我们从代码中讨论他们各自的优缺点。
传统IO服务器-客户端通信
package OIO; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /* * 单线程,只能有一个socket访问 */ public class IOServer { @SuppressWarnings("resource") public static void main(String[] args) throws Exception { //创建socket服务,监听10101端口 ServerSocket server = new ServerSocket(10101); System.out.print("服务器启动!"); while(true){ //获取一个套接字(阻塞) final Socket socket = server.accept(); System.out.println("来了一个新客户端!"); //业务处理 handler(socket); } } public static void handler(Socket socket){ try { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while(true){ //读取数据(阻塞) int read = inputStream.read(bytes); if(read != -1){ System.out.println(new String(bytes,0,read)); }else{ break; } } } catch (Exception e) { System.out.println("socket关闭!"); try { socket.close(); } catch (IOException e1) { e1.printStackTrace(); } } } }接下来我们启动服务端,测试连接一下,可以看到控制台打印出了“来了一个新客户端!”。
那么,我们试着向服务器发送一条消息,会发现服务器控制台接收并显示了这条消息。
但是,在这种情况下,再次通过telnet连接该服务器,会发现连接不上,原因很简单,我们通过注释可以看到,这段代码中一共有两个阻塞点,一个是在sever.accept()的时候,他在等待客户端连接的请求,如果没有,他会一直阻塞下去直到超时;那么第二个阻塞点就是inputStream.read()的时候,字节流在读取数据的时候是阻塞的,看代码我们不难发现,如果内存中有可读取的字节,那么会读取出来,如果没有,该线程会一直阻塞在这里,直到有数据被读取,或者超时。
那么这段代码结论出来了,只有一个线程去服务一个客户端,就好比说,有一家饭店,只有一个服务生,这个服务生只服务第一个客人。
这样显然是不行的,那么怎么修改这段代码使之可以服务多个客人呢?在IO里,我们采用了多线程的方案,加入 一个线程池(多招聘几个服务生),接下来我们看一下,使用多线程能否解决这个问题。
package OIO; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /* * 加入了线程池,可接受多个socket客户端的消息; * 但是一个线程只能为一个socket服务 */ public class IOServer { @SuppressWarnings("resource") public static void main(String[] args) throws Exception { //创建线程池 ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(); //创建socket服务,监听10101端口 ServerSocket server = new ServerSocket(10101); System.out.print("服务器启动!"); while(true){ //获取一个套接字(阻塞) final Socket socket = server.accept(); System.out.println("来了一个新客户端!"); newCachedThreadPool.execute(new Runnable() { @Override public void run() { //业务处理 handler(socket); } }); } } public static void handler(Socket socket){ try { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); while(true){ //读取数据(阻塞) int read = inputStream.read(bytes); if(read != -1){ System.out.println(new String(bytes,0,read)); }else{ break; } } } catch (Exception e) { System.out.println("socket关闭!"); try { socket.close(); } catch (IOException e1) { e1.printStackTrace(); } } } }我们可以看到,这段代码和之前相比,仅仅是加入了一个线程池,将handler方法加入线程池中去执行。启动后,结果如下:
发现可以执行多个客户端的请求了,虽然解决了这个问题,但这样真的可以吗?我们还是从代码去分析,加入了线程池,当有大规模的访问时,会消耗掉大量的资源,并且会影响新线程。这就好比是一个餐厅有十个服务员(对于一个服务器,线程是有上限的),还是每个线程响应一个客户端(每个服务员只能服务一桌客人,不能离开),如果此时请求连接的客户端过多(吃饭的人太多),服务器还是会挂(没有多余的服务生去服务后来的客人)。那现在怎么办呢?非阻塞式的NIO应运而生。
NIO
NIO作为非阻塞式的IO,它的优点就在于,1、它由一个专门的线程去处理所有的IO事件,并负责分发;2、事件驱动,只有事件到了才会触发,而不是同步的监听这个事件;3、线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。话不多说,看代码。
package NIO; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class NIOServer { //通道管理器 private Selector selector; /** * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 * @param port 绑定的端口号 * @throws IOException */ public void initServer(int port) throws IOException { // 获得一个ServerSocket通道 ServerSocketChannel serverChannel = ServerSocketChannel.open(); // 设置通道为非阻塞 serverChannel.configureBlocking(false); // 将该通道对应的ServerSocket绑定到port端口 serverChannel.socket().bind(new InetSocketAddress(port)); // 获得一个通道管理器 this.selector = Selector.open(); //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后, //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。 serverChannel.register(selector, SelectionKey.OP_ACCEPT); } /** * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 * @throws IOException */ @SuppressWarnings("unchecked") public void listen() throws IOException { System.out.println("服务端启动成功!"); // 轮询访问selector while (true) { //当注册的事件到达时,方法返回;否则,该方法会一直阻塞 selector.select(); // 获得selector中选中的项的迭代器,选中的项为注册的事件 Iterator ite = this.selector.selectedKeys().iterator(); while (ite.hasNext()) { SelectionKey key = (SelectionKey) ite.next(); // 删除已选的key,以防重复处理 ite.remove(); // 客户端请求连接事件 if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key .channel(); // 获得和客户端连接的通道 SocketChannel channel = server.accept(); // 设置成非阻塞 channel.configureBlocking(false); //在这里可以给客户端发送信息 channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes())); //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。 channel.register(this.selector, SelectionKey.OP_READ); // 获得了可读的事件 } else if (key.isReadable()) { read(key); } } } } /** * 处理读取客户端发来的信息 的事件 * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException{ // 服务器可读取消息:得到事件发生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 创建读取的缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); channel.read(buffer); byte[] data = buffer.array(); String msg = new String(data).trim(); System.out.println("服务端收到信息:"+msg); ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes()); channel.write(outBuffer);// 将消息回送给客户端 } /** * 启动服务端测试 * @throws IOException */ public static void main(String[] args) throws IOException { NIOServer server = new NIOServer(); server.initServer(8000); server.listen(); } }这种方式就简单了,我们通过代码可以看到,将通道设置为非阻塞,并将访问事件注册到通道管理器中,同时,listen()方法在同步的监听,并且会一直阻塞在selector.select()方法,一旦注册的事件进入,迭代器会接收到该请求的key,并判断该key是访问accept类型的还是read类型的,进而执行相应的方法,只要是有注册事件的访问,该线程就会一直执行,直到无注册事件访问,线程继续阻塞。
总结
并不是说有了NIO,传统的IO就毫无用处了,当我们在执行持续性的操作(如上传下载)时,IO的方式是要优于 NIO的。分清情况,合理选用。
分布式的网络通信netty就是基于NIO的这种机制,在接下来的博客会说到netty通信的一些总结,欢迎大家斧正。