基于BIO的服务器,服务端可能要同时保持几百万个HTTP连接,而这些连接并不是每时每刻都在传输数据,所以这种情况不适合使用BIO的服务器;而且需要保证共享资源的同步与安全,这个实现起来相对复杂。这时候就是基于NIO的服务器出场的时候了。
先来比较下两种服务器的代码
1.NIO实现
1 public class SelectorServer 2 { 3 private static int PORT = 1234; 4 5 public static void main(String[] args) throws Exception 6 { 7 // 先确定端口号 8 int port = PORT; 9 if (args != null && args.length > 0) 10 { 11 port = Integer.parseInt(args[0]); 12 } 13 // 打开一个ServerSocketChannel 14 ServerSocketChannel ssc = ServerSocketChannel.open(); 15 // 获取ServerSocketChannel绑定的Socket 16 ServerSocket ss = ssc.socket(); 17 // 设置ServerSocket监听的端口 18 ss.bind(new InetSocketAddress(port)); 19 // 设置ServerSocketChannel为非阻塞模式 20 ssc.configureBlocking(false); 21 // 打开一个选择器 22 Selector selector = Selector.open(); 23 // 将ServerSocketChannel注册到选择器上去并监听accept事件 24 ssc.register(selector, SelectionKey.OP_ACCEPT); 25 while (true) 26 { 27 // 这里会发生阻塞,等待就绪的通道 28 int n = selector.select(); 29 // 没有就绪的通道则什么也不做 30 if (n == 0) 31 { 32 continue; 33 } 34 // 获取SelectionKeys上已经就绪的通道的集合 35 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); 36 // 遍历每一个Key 37 while (iterator.hasNext()) 38 { 39 SelectionKey sk = iterator.next(); 40 // 通道上是否有可接受的连接 41 if (sk.isAcceptable()) 42 { 43 ServerSocketChannel ssc1 = (ServerSocketChannel)sk.channel(); 44 SocketChannel sc = ssc1.accept(); 45 sc.configureBlocking(false); 46 sc.register(selector, SelectionKey.OP_READ); 47 } 48 // 通道上是否有数据可读 49 else if (sk.isReadable()) 50 { 51 readDataFromSocket(sk); 52 } 53 iterator.remove(); 54 } 55 } 56 } 57 58 private static ByteBuffer bb = ByteBuffer.allocate(1024); 59 60 // 从通道中读取数据 61 protected static void readDataFromSocket(SelectionKey sk) throws Exception 62 { 63 SocketChannel sc = (SocketChannel)sk.channel(); 64 bb.clear(); 65 while (sc.read(bb) > 0) 66 { 67 bb.flip(); 68 while (bb.hasRemaining()) 69 { 70 System.out.print((char)bb.get()); 71 } 72 System.out.println(); 73 bb.clear(); 74 } 75 } 76 }
2.BIO 多线程实现
1 package com.defonds.socket.begin; 2 3 import java.io.BufferedReader; 4 import java.io.DataInputStream; 5 import java.io.DataOutputStream; 6 import java.io.InputStreamReader; 7 import java.net.ServerSocket; 8 import java.net.Socket; 9 10 public class Server { 11 public static final int PORT = 12345;//监听的端口号 12 13 public static void main(String[] args) { 14 System.out.println("服务器启动... "); 15 Server server = new Server(); 16 server.init(); 17 } 18 19 public void init() { 20 try { 21 ServerSocket serverSocket = new ServerSocket(PORT); 22 while (true) { 23 // 一旦有堵塞, 则表示服务器与客户端获得了连接 24 Socket client = serverSocket.accept(); 25 // 处理这次连接 26 new HandlerThread(client); 27 } 28 } catch (Exception e) { 29 System.out.println("服务器异常: " + e.getMessage()); 30 } 31 } 32 33 private class HandlerThread implements Runnable { 34 private Socket socket; 35 public HandlerThread(Socket client) { 36 socket = client; 37 new Thread(this).start(); 38 } 39 40 public void run() { 41 try { 42 // 读取客户端数据 43 DataInputStream input = new DataInputStream(socket.getInputStream()); 44 String clientInputStr = input.readUTF();//这里要注意和客户端输出流的写方法对应,否则会抛 EOFException 45 // 处理客户端数据 46 System.out.println("客户端发过来的内容:" + clientInputStr); 47 48 // 向客户端回复信息 49 DataOutputStream out = new DataOutputStream(socket.getOutputStream()); 50 System.out.print("请输入: "); 51 // 发送键盘输入的一行 52 String s = new BufferedReader(new InputStreamReader(System.in)).readLine(); 53 out.writeUTF(s); 54 55 out.close(); 56 input.close(); 57 } catch (Exception e) { 58 System.out.println("服务器 run 异常: " + e.getMessage()); 59 } finally { 60 if (socket != null) { 61 try { 62 socket.close(); 63 } catch (Exception e) { 64 socket = null; 65 System.out.println("服务端 finally 异常:" + e.getMessage()); 66 } 67 } 68 } 69 } 70 } 71 }
基于NIO的服务器相当于把老版本多线程服务器中的read和accept的阻塞时间,都统一集中到了selector.select()里面。当有socket数据过来的时候,selector会选择与当前socket数据对应的一个且已经在其内部注册的channel来执行对应的方法。相当于在老版本多线程服务器中抽象出了一层,这一层就是select方法,这个方法负责接收"任务"以及分发"任务"。
第一种,开多线程可以去接多个请求,防止一个线程的read卡住,无法接收其他客户端的请求。
第二种,把服务提供者全部托管给selector,当有消费任务到来的时候,选择与当前任务适配的服务提供者去提供(而真正的读写操作的阻塞是使用CPU的,真正在"干活",而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时)