学习Redis和Netty等可以高效处理Socket请求的框架或工具都提到了IO多路复用模型,那么IO多路复用模型究竟是什么,本文将会对其进行一个简单的介绍。主要涉及到如下几方面知识:
所谓阻塞IO是指调用方从发起IO请到收到被调用方返回的数据之间的这段时间,调用方线程一直处于阻塞状态,若是是UI线程则意味着界面不响应,假死。借用网上的一张经典阻塞IO模型以下:
用Socket编程代码演示以下:java
public class Server { public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("服务器启动完成...监听启动!"); //开启监听,等待客户端的访问 while(true) { Socket socket = serverSocket.accept(); // 获取输入流,由于是客户端向服务器端发送了数据 InputStream inputStream = socket.getInputStream(); // 建立一个缓冲流 BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); String info = null; while ((info = br.readLine()) != null) { System.out.println("这里是服务端 客户端是:" + info); } //向客户端作出响应 OutputStream outputStream = socket.getOutputStream(); info = "这里是服务器端,咱们接受到了你的请求信息,正在处理...处理完成!"; outputStream.write(info.getBytes()); outputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } }
public class Client { public static void main(String[] args) throws IOException, InterruptedException { try { Socket socket = new Socket("localhost",8080); OutputStream outputStream = socket.getOutputStream(); String info = "你好啊!"; //输出! Thread.sleep(1000); outputStream.write(info.getBytes()); socket.shutdownOutput(); //接收服务器端的响应 InputStream inputStream = socket.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(inputStream)); while ((info = br.readLine())!=null){ System.out.println("接收到了服务端的响应!" + info); } //刷新缓冲区 outputStream.flush(); outputStream.close(); inputStream.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } }
上面的服务端程序有个很严重的问题:若是有多个客户端访问的话,则客户端必须按顺序访问,若是第一个链接连上来以后,无论发不发消息,后面的链接只能等待。这样会致使大量的客户端阻塞。 为了解决这个问题,通常会在一个客户端创建链接以后,服务端启动一个线程来处理与之的通信。可是若是客户端数太多,就会致使服务端建立大量的线程,线程的建立、上下文切换也会致使服务器的负载大幅度升高。所以也就产生了下面要说的非阻塞IO。
Linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:
当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而用户就能够在本次到下次再发起read询问的时间间隔内作其余事情,或者直接再次发送read操做。
对应到Socket服务器的代码以下:编程
public class SelectorServer { public static void main(String[] args) throws IOException { Selector selector = Selector.open(); ServerSocketChannel server = ServerSocketChannel.open(); server.socket().bind(new InetSocketAddress(8080)); // 将其注册到 Selector 中,监听 OP_ACCEPT 事件 server.configureBlocking(false);//非阻塞IO的设置 server.register(selector, SelectionKey.OP_ACCEPT); while (true) { int readyChannels = selector.select(); if (readyChannels == 0) { continue; } Set<SelectionKey> readyKeys = selector.selectedKeys(); // 遍历 Iterator<SelectionKey> iterator = readyKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isAcceptable()) { // 有已经接受的新的到服务端的链接 SocketChannel socketChannel = server.accept(); // 有新的链接并不表明这个通道就有数据, // 这里将这个新的 SocketChannel 注册到 Selector,监听 OP_READ 事件,等待数据 socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { // 有数据可读 // 上面一个 if 分支中注册了监听 OP_READ 事件的 SocketChannel SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); int num = socketChannel.read(readBuffer); if (num > 0) { // 处理进来的数据... System.out.println("收到数据:" + new String(readBuffer.array()).trim()); ByteBuffer buffer = ByteBuffer.wrap("返回给客户端的数据...".getBytes()); socketChannel.write(buffer); } else if (num == -1) { // -1 表明链接已经关闭 socketChannel.close(); } } } }
Client端能够继续沿用上面的。服务器
该种实现方式的好处是,不用建立多个线程。在一个主线程里即把全部客户端链接都处理了。由于是非阻塞链接,服务端在收到链接后直接将该链接对应的socketChannel注册到selector中,并监控该链接的Read操做。当有数据到来时在对Socket进行数据的传输处理。 可是这种方案仍然有个问题,就是主程序要不断的轮询,无论有没有链接,链接是否可用。而且收到selector的事件后也不知道究竟是什么操做准备好了,只能逐个判断。这样催生了后来的异步IO。异步IO的问题在合理先不讲了,后面再说。
写到这里对IO多路复用也有了个大体的了解,其实应该把“IO多路复用”拆解来看。框架
这里在举一个现实生活中的例子。一我的开饭馆(服务器),来了10个客人(客户端链接),这时老板是选择雇佣10个服务员(线程)分别为每一个客人服务仍是用一个服务员来监听全部客人的动做等着为各个客人服务呢,相信你们都会作出正确的选择。异步
参考资料:
https://www.zhihu.com/questio...
https://www.javadoop.com/post...socket