• NIO网络编程


    前言

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

    作者:Joe
    努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
  • 相关阅读:
    ssh登陆报错“WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!”的解决方法
    python错误:SyntaxError: invalid character in identifier
    Python3中出现UnicodeEncodeError: 'ascii' codec can't encode characters in ordinal not in range(128)的解决方法
    Jmeter在Mac下安装教程
    TensorFlow | win10下使用docker安装tensorflow
    Docker | 删除 image 失败的一种情况
    基础技能 | Git
    Leetcode-探索 | 两数之和
    Leetcode-探索 | 移动零
    基础复习-算法设计基础 | 复杂度计算
  • 原文地址:https://www.cnblogs.com/Joe-Go/p/9993664.html
Copyright © 2020-2023  润新知