前言
前面的文章讲解了I/O 模型、缓冲区(Buffer)、通道(Channel)、选择器(Selector),这些都是关于NIO的特点,偏于理论一些,这篇文章LZ将通过利用这些知识点来实现NIO的服务器和客户端,当然了,只是一个简单的demo,但是对于NIO的学习来说,足够了,麻雀虽小但五脏俱全。话不多说,开始:
NIO服务端:
1 public class EchoServer { 2 private static int PORT = 8000; 3 4 public static void main(String[] args) throws Exception { 5 // 先确定端口号 6 int port = PORT; 7 if (args != null && args.length > 0) { 8 port = Integer.parseInt(args[0]); 9 } 10 // 打开一个ServerSocketChannel 11 ServerSocketChannel ssc = ServerSocketChannel.open(); 12 // 获取ServerSocketChannel绑定的Socket 13 ServerSocket ss = ssc.socket(); 14 // 设置ServerSocket监听的端口 15 ss.bind(new InetSocketAddress(port)); 16 // 设置ServerSocketChannel为非阻塞模式 17 ssc.configureBlocking(false); 18 // 打开一个选择器 19 Selector selector = Selector.open(); 20 // 将ServerSocketChannel注册到选择器上去并监听accept事件 21 ssc.register(selector, SelectionKey.OP_ACCEPT); 22 while (true) { 23 // 这里会发生阻塞,等待就绪的通道 24 int n = selector.select(); 25 // 没有就绪的通道则什么也不做 26 if (n == 0) { 27 continue; 28 } 29 // 获取SelectionKeys上已经就绪的通道的集合 30 Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); 31 // 遍历每一个Key 32 while (iterator.hasNext()) { 33 SelectionKey sk = iterator.next(); 34 // 通道上是否有可接受的连接 35 if (sk.isAcceptable() && sk.isValid()) { 36 ServerSocketChannel ssc1 = (ServerSocketChannel)sk.channel(); 37 SocketChannel sc = ssc1.accept(); 38 sc.configureBlocking(false); 39 sc.register(selector, SelectionKey.OP_READ); 40 } 41 // 通道上是否有数据可读 42 else if (sk.isReadable() && sk.isValid()) { 43 readDataFromSocket(sk); 44 } 45 iterator.remove(); 46 } 47 } 48 } 49 50 private static ByteBuffer bb = ByteBuffer.allocate(1024); 51 52 // 从通道中读取数据 53 protected static void readDataFromSocket(SelectionKey sk) throws Exception { 54 SocketChannel sc = (SocketChannel)sk.channel(); 55 bb.clear(); 56 while (sc.read(bb) > 0) { 57 bb.flip(); 58 while (bb.hasRemaining()) { 59 System.out.print((char)bb.get()); 60 } 61 System.out.println(); 62 bb.clear(); 63 } 64 } 65 }
代码中的注释其实已经很详细了,再解释一下:
❤ 5~9行:确定要监听的端口号,这里选择的是8000;
❤ 10~17行:这里是服务器的程序,所以选择的通道是ServerSocketChannel,同时获取到它对应的Socket,也就是ServerSocket 因为使用的是NIO,所以将通道设置为非阻塞模式(17行),并绑定端口号8000;
❤ 18~21行:打开一个选择器,注册当前通道感兴趣的事件为Accept,也就是监听来自客户端的Socket数据;
❤ 22~24行:调用选择器的Select()方法,等待来自于客户端的Socket数据。程序会阻塞在这里不会继续让下走,直到客户端有Socket数据到来为止;在这里就可以看出,NIO并不是一种非阻塞IO,因为NIO会阻塞在Selector的select()方法上。
❤ 25~28行:如果select()方法返回值为0的话,表明当前没有准备就绪的通道,所以下面的代码都没有必要执行,所以跳过当前循环,进行下一次的循环;
❤ 29~33行:获取到已经就绪的通道集合,并对其进行迭代循环,集合的泛型是SelectionKey,之前的文章讲过,选择键用于封装特定的通道;
❤ 35~44行:这里是处理数据的核心点,做了两件事:
(1)代码进入36行,表明该通道上已经有数据到来了,接下来做的事情是将对应的SocketChannel注册到选择器上,通过传入OP_READ标记,告诉选择器我们关心新的Socket通道什么时候可以准备好读数据。
(2)代码进入43行,表明该通道已经可以读取数据了,此时调用readDataFromSocket()方法读取通道中的数据。
❤ 45行:将键移除。这样的话才能在通道下一次变为“就绪”时,Selector将再次将其添加到所选的键集合。
NIO客户端:
1 public class EchoClient { 2 3 private static final String STR = "Hello NIO!"; 4 private static final String REMOTE_IP = "127.0.0.1"; 5 private static final int THREAD_COUNT = 5; 6 7 private static class NonBlockingSocketThread extends Thread { 8 public void run() { 9 try { 10 int port = 8000; 11 SocketChannel sc = SocketChannel.open(); 12 sc.configureBlocking(false); 13 sc.connect(new InetSocketAddress(REMOTE_IP, port)); 14 while (!sc.finishConnect()) { 15 System.out.println("同" + REMOTE_IP + "的连接正在建立,请稍等!"); 16 Thread.sleep(10); 17 } 18 System.out.println("连接已建立,待写入内容至指定ip+端口!时间为" + System.currentTimeMillis()); 19 String writeStr = STR + this.getName(); 20 ByteBuffer bb = ByteBuffer.allocate(writeStr.length()); 21 bb.put(writeStr.getBytes()); 22 bb.flip(); // 写缓冲区的数据之前一定要先反转(flip) 23 sc.write(bb); 24 bb.clear(); 25 sc.close(); 26 } 27 catch (IOException e) { 28 e.printStackTrace(); 29 } 30 catch (InterruptedException e) { 31 e.printStackTrace(); 32 } 33 } 34 } 35 36 public static void main(String[] args) throws Exception { 37 NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT]; 38 for (int i = 0; i < THREAD_COUNT; i++) 39 nbsts[i] = new NonBlockingSocketThread(); 40 for (int i = 0; i < THREAD_COUNT; i++) 41 nbsts[i].start(); 42 // 一定要join保证线程代码先于sc.close()运行,否则会有AsynchronousCloseException 43 for (int i = 0; i < THREAD_COUNT; i++) 44 nbsts[i].join(); 45 } 46 }
客户端的代码就是向服务器发送数据就行,使用了NonBlockingSocketThread线程。
运行结果:
先运行服务端:
空白,很正常,因为在监听客户端数据的到来,此时并没有数据。
运行客户端:
看到5个线程的数据已经发送,此时服务端的执行情况是:
数据全部接收到并打印,看到左边的方框还是红色的,说明这5个线程的数据接收、打印完毕之后,再继续等待着客户端的数据的到来。
Selector的关键点:
(1)注册一个ServerSocketChannel到Selector中,这个通道的作用只是为了监听客户端是否有数据到来(数据到来的意思是假如总共有100字节的数据,如果来了一个字节的数据,那么这就算数据到来了),只要有数据到来,就把特定通道注册到Selector中,并指定其感兴趣的事件为读事件;
(2)ServerSocketChannel和SocketChannel(通道里面的是客户端的数据)共同存在Selector中,只要有注册的事件到来,Selector就会取消阻塞状态,遍历SelectionKey集合,继续注册读事件的通道或者从通道中读取数据。
参考:https://www.cnblogs.com/xrq730/p/5186065.html