NIO组成部分:
从BIO到NIO到AIO解决一个网络通信的应用。
用accept阻塞等待客户端连接,连接建立后,启动一个单独线程负责回复客户端请求。
public class DemoServer extends Thread { private ServerSocket serverSocket; public int getPort() { return serverSocket.getLocalPort(); } public void run() { try { serverSocket = new ServerSocket(0); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); requestHandler.start(); } } catch (IOException e) { e.printStackTrace(); } finally { if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } ; } } } public static void main(String[] args) throws IOException { DemoServer server = new DemoServer(); server.start(); try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream())); bufferedReader.lines().forEach(s -> System.out.println(s)); } } } // 简化实现,不做读取,直接发送字符串 class RequestHandler extends Thread { private Socket socket; RequestHandler(Socket socket) { this.socket = socket; } @Override public void run() { try (PrintWriter out = new PrintWriter(socket.getOutputStream());) { out.println("Hello world!"); out.flush(); } catch (Exception e) { e.printStackTrace(); } } }
不足:启动/销毁一个线程由明显开销,每个线程需要占用明显内存以保存单独的线程栈,每个Client单独启动一个线程比较浪费。
解决方法:引入线程池:
serverSocket = new ServerSocket(0); executor = Executors.newFixedThreadPool(8); while (true) { Socket socket = serverSocket.accept(); RequestHandler requestHandler = new RequestHandler(socket); executor.execute(requestHandler); }
不足:当连接数急剧上升时,线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
解决方法:引入多路复用
通过Selector.open()创建一个Selector作为调度员,创建一个ServerSocketChannel并注册到Selector,接下来Selector阻塞在select操作,当有Channel发生接入请求,就会被唤醒。
public class NIOServer extends Thread { public void run() { try (Selector selector = Selector.open(); ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建 Selector 和 Channel serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888)); serverSocket.configureBlocking(false);//明确配置非阻塞模式 // 注册到 Selector,并说明关注点 serverSocket.register(selector, SelectionKey.OP_ACCEPT); while (true) { selector.select();// 阻塞等待就绪的 Channel,这是关键点之一 Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> iter = selectedKeys.iterator(); while (iter.hasNext()) { SelectionKey key = iter.next(); // 生产系统中一般会额外进行就绪状态检查 sayHelloWorld((ServerSocketChannel) key.channel()); iter.remove(); } } } catch (IOException e) { e.printStackTrace(); } } private void sayHelloWorld(ServerSocketChannel server) throws IOException { try (SocketChannel client = server.accept();) { client.write(Charset.defaultCharset().encode("Hello world!")); } } // 省略了与前面类似的 main }
前面两个样例都是同步阻塞,所以需要多线程处理多任务。而NIO则是单线程轮询事件,高效的定位就绪的Channel,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁切换线程带来的问题。
使用事件和回调,处理Accept、Read等操作,在accept/read/write等关键节点,通过事件机制调用。
AsynchronousServerSocketChannel serverSock = AsynchronousServerSocketChannel.open().bind(sockAddr); serverSock.accept(serverSock, new CompletionHandler<>() { // 为异步操作指定 CompletionHandler 回调函数 @Override public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) { serverSock.accept(serverSock, this); // 另外一个 write(sock,CompletionHandler{}) sayHelloWorld(sockChannel, Charset.defaultCharset().encode ("Hello World!")); } // 省略其他路径处理方法... });