NIO网络编程 I/O 模型

前言

  前面的文章讲解了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

猜你喜欢

转载自www.cnblogs.com/Joe-Go/p/9993664.html