• Java NIO的总结


    Java的NIO是非阻塞式的IO

    non-blocking io

    一、理解同步与异步、阻塞与非阻塞

    (1)同步和异步

    ​   同步和异步描述的是一种消息通知的机制,主动等待消息返回还是被动接受消息。同步io指的是调用方通过主动等待获取调用返回的结果来获取消息通知,而异步io指的是被调用方通过某种方式(如,回调函数)来通知调用方获取消息。 

    (2)阻塞非阻塞

    ​   阻塞和非阻塞描述的是调用方在获取消息过程中的状态,阻塞等待还是立刻返回。阻塞io指的是调用方在获取消息的过程中会挂起阻塞,直到获取到消息,而非阻塞io指的是调用方在获取io的过程中会立刻返回而不进行挂起。 

    Java NIO是基于IO多路复用模型,也就是我们经常提到的select,poll,epoll。IO 多路复用本质是同步IO,其需要调用方在读写事件就绪时主动去进行读写。在Java NIO中,通过selector来获取就绪的事件,当selector上监听的channel中没有就绪的读写事件时,其可以直接返回,或者设置一段超时后返回。可以看出Java NIO可以实现非阻塞,而不像传统IO里必须阻塞当前线程直到可读或可写。

    所以理解阻塞与非阻塞其实是理解传统IO区别于NIO的对于线程的阻塞与否。 

      Java NIO 处理连接和 Java socket 处理连接的方式:

     1 //java nio
     2 while(true) {
     3   ......
     4   selector.select(1);
     5   Set<SelectionKey> selectionKeySet= selector.selectedKeys();
     6   ......
     7   //处理selectionKeySet中事件,线程没有阻塞
     8 }
     9 
    10 //java socket处理连接,线程会阻塞
    11 while(true) {
    12   ......
    13   Socket socket = serverSocket.accept();
    14   InputStream in = socket.getInputStream(); 
    15   ......
    16   //处理in中内容
    17 }

    二、NIO的常用操作(对文件的读写)

    (1)写操作

      通过NIO的操作能达到和IO一样的效果,但是读写的操作是通过通道来完成的,但是通道是通过流获取的。(注意标准的步骤

      而且里面的通道比如FileChannel都是从流中获取到的。

      通道中后面要读取数据并存入缓冲区,同时要分配缓冲区以一定的大小。可以认为这个通道是直接搭在数据上的。

      缓冲区里得存放字节数组

     1 package NIOTest;
     2 
     3 import java.io.FileOutputStream;
     4 import java.io.IOException;
     5 import java.nio.ByteBuffer;
     6 import java.nio.channels.FileChannel;
     7 
     8 import org.junit.Test;
     9 
    10 //通过NIO实现文件IO
    11 public class TestNio {
    12     @Test
    13     // 往本地文件中写数据
    14     public void test1() throws IOException {
    15         // 1、创建输出流
    16         FileOutputStream fileOutputStream = new FileOutputStream("C:\Users\Administrator\Desktop\1.txt");
    17         // 2、从流中得到一个通道
    18         FileChannel fileChannel = fileOutputStream.getChannel();
    19         // 3、提供一个缓冲区
    20         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    21         // 4、往缓冲区中存入数据(将字符串转换成字节数组并存储到缓冲区中)
    22         String string = "hello,nio";
    23         byteBuffer.put(string.getBytes());
    24         // 5、反转缓冲区
    25         byteBuffer.flip();
    26         // 6、把缓冲区写到通道中
    27         fileChannel.write(byteBuffer);
    28         // 7、关闭流(关闭流即可关闭通道)
    29         fileOutputStream.close();
    30     }
    31 }

    效果:

    (2)读操作

      输入输出流里面放的一般认为是文件的路径,但是实际上可以看作是一个file。

      其中的file.length()是为了获得文件的数据内容的大小,但是默认返回的是长整型,所以需要通过int转换一下。关闭流就相当于关闭了通道。

     1 @Test
     2     public void test2() throws IOException {
     3 
     4         // 封装成一个文件对象(为了返回文件中的数据内容的长度和大小)
     5         File file = new File("C:\Users\Administrator\Desktop\1.txt");
     6         // 创建输入流
     7         FileInputStream fileInputStream = new FileInputStream(file);
     8         // 创建文件通道
     9         FileChannel fileChannel = fileInputStream.getChannel();
    10         // 创建缓冲区(设置文件有多少数据缓冲区就有多大引入File)
    11         ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
    12         // 从通道中读取数据并存到缓冲区中
    13         fileChannel.read(buffer);
    14         // 把缓冲区中的数据转换成字节数组并转换成String对象
    15         System.out.println(new String(buffer.array()));
    16         fileInputStream.close();
    17     }

    (3)文件的复制操作

      文件从一个文件复制到另一个文件中去,通过通道流的数据的复制来完成。

     1 @Test
     2     public void test3() throws IOException {
     3         // 创建两个流
     4         FileInputStream fileInputStream = new FileInputStream("C:\Users\Administrator\Desktop\1.txt");
     5         FileOutputStream fileOutputStream = new FileOutputStream("C:\Users\Administrator\Desktop\2.txt");
     6         // 得到两个通道
     7         FileChannel fileChannel_read = fileInputStream.getChannel();
     8         FileChannel fileChannel_write = fileOutputStream.getChannel();
     9         // 复制(从读通道流中复制数据到写通道流)
    10         fileChannel_write.transferFrom(fileChannel_read, 0, fileChannel_read.size());
    11         // 关闭
    12         fileChannel_read.close();
    13         fileChannel_write.close();
    14     }

      这就是数据的交换,不需要通过缓冲区把数据取出来单独处理,而是直接通过搭建两个流通道进行复制操作就可以完成数据的转移。

    三、NIO的常用操作(数据的交换)

      Java NIO里面最为重要也是常用的是四大类,选择器,事件,服务端通道,客户端通道。类比处理连接区别于Socket,最大的不同就是通道的概念的引进。

      主要是四大类:

      (1)Selector

      (2)SelectionKey

      (3)ServerSocketChannel

      (4)SocketChannel

    (1)网络客户端程序:

     1 //网络客户端程序
     2 public class NIOClient {
     3     public static void main(String[] args) throws IOException {
     4 
     5         // 1、得到一个网络通道
     6         SocketChannel socketChannel = SocketChannel.open();
     7 
     8         // 2、设置非阻塞的方式
     9         socketChannel.configureBlocking(false);
    10 
    11         // 3、提供服务器端的IP地址和端口号
    12         InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9999);
    13 
    14         // 4、连接服务器端
    15         if (socketChannel.connect(inetSocketAddress) == false) {
    16             while (!socketChannel.finishConnect()) {
    17                 System.out.println("客户端重连......");
    18             }
    19         }
    20 
    21         // 5、得到一个用于读写的缓冲区,并存入数据
    22         String msg = "hello Server";
    23         ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
    24 
    25         // 6、发送数据到通道
    26         socketChannel.write(byteBuffer);
    27         System.in.read();
    28     }
    29 }

      当socketChannel.connect(inetSocketAddress) = false时再次想要连接就得使用socketChannel.finishConnect(),而有可能连接不能顺利的连接上,所以要一直判断,故while (!socketChannel.finishConnect())就可以当连接失败的时候一直处于重连的情况。

    不能立即把socketChannel立即关闭,不然服务器端会报异常,所以用等待控制台系统输入阻断程序完毕。

    (2)服务器端程序

     1 public class NIOServer {
     2     public static void main(String[] args) throws Exception {
     3 
     4         // 1、得到一个ServerSocketChannel对象 老大
     5         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
     6 
     7         // 2、得到一个Selector对象
     8         Selector selector = Selector.open();
     9 
    10         // 3、绑定一个端口号(设置服务器的端口号)
    11         serverSocketChannel.bind(new InetSocketAddress(9999));
    12 
    13         // 4、设置非阻塞的方式
    14         serverSocketChannel.configureBlocking(false);
    15 
    16         // 5、ServerSocketChannel注册的事件就是SelectionKey.OP_ACCEPT看是否有连接,连接到ServerSocketChannel
    17         // 这些注册的通道与事件的关系都是统一由selector调度的
    18         serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    19 
    20         // 6、服务器的业务逻辑
    21         while (true) {
    22             // select当有注册的IO可以进行操作(操作其对应的注册时的方式时)时,将对应的SelectionKey加入到内部集合并返回(非阻塞)
    23             // 监控客户端(看是否有通道的事件被触发)
    24             if (selector.select(2000) == 0) {
    25                 System.out.println("等待连接......");
    26                 continue;
    27             }
    28             // 得到SelectionKey,判断通道里的事件
    29             Iterator<SelectionKey> Iterator = selector.selectedKeys().iterator();
    30             while (Iterator.hasNext()) {
    31                 SelectionKey key = Iterator.next();
    32                 if (key.isAcceptable()) {
    33                     // 客户端连接事件
    34                     System.out.println("OP_READ");
    35                     SocketChannel socketChannel = serverSocketChannel.accept();
    36                     socketChannel.configureBlocking(false);
    37                     socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    38                 }
    39                 if (key.isReadable()) {
    40                     // 读取客户端事件
    41                     SocketChannel socketChannel = (SocketChannel) key.channel();
    42                     ByteBuffer buffer = (ByteBuffer) key.attachment();
    43                     // 从通道里面读取数据然后存放在buffer里面
    44                     socketChannel.read(buffer);
    45                     // 然后将buffer里面的数据转换成字节数组之后通过String包装起来后打印
    46                     System.out.println("客户端发来数据:" + new String(buffer.array()));
    47                 }
    48                 Iterator.remove();
    49                 if (key.isWritable()) {
    50                     // 写到客户端事件
    51 
    52                 }
    53             }
    54         }
    55     }
    56 }

    三、NIO的常用操作(多人聊天室实现多通道数据的连接)

    (1)服务器ChatServer

      接收客户端发来的数据,同时还要把这个数据广播给另外的其他的所有客户端。服务器最重要的代码是进行业务处理,也就是集中在服务器代码中处理selector,通过筛选selector的Keys来判断每一个注册到selector的所有的通道是否发生了事件,发生的事件是什么。

     1 public class ChatServer {
     2     private ServerSocketChannel listenerChannel;// 监听通道
     3     private Selector selector;// 选择器对象
     4     private static final int port = 9999;// 服务器端口
     5 
     6     public ChatServer() {
     7         try {
     8             // 1、得到监听通道
     9             listenerChannel = ServerSocketChannel.open();
    10             // 2、得到选择器
    11             selector = Selector.open();
    12             // 3、绑定端口
    13             listenerChannel.bind(new InetSocketAddress(port));
    14             // 4、设置为非阻塞模式
    15             listenerChannel.configureBlocking(false);
    16             // 5、将选择器绑定到监听通道并监听accept事件
    17             listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
    18             printInfo("Chat Server is ready......");
    19         } catch (IOException e) {
    20             e.printStackTrace();
    21         }
    22     }
    23 
    24     // 6、干活儿
    25     public void start() throws IOException {
    26         // 一直监控
    27         while (true) {
    28             if (selector.select(2000) == 0) {
    29                 System.out.println("等待连接......");
    30                 continue;
    31             }
    32             // 我的所有的key都在selector.selectedKeys()里面,利用key触发监听事件来确定进行怎样的操作
    33             Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
    34             while (iterator.hasNext()) {
    35                 SelectionKey key = iterator.next();
    36                 if (key.isAcceptable()) {
    37                     // 连接请求
    38                     SocketChannel socketChannel = listenerChannel.accept();
    39                     socketChannel.configureBlocking(false);
    40                     // 返回了连接之后注册读取监听事件
    41                     socketChannel.register(selector, SelectionKey.OP_READ);
    42                     System.out.println(socketChannel.getRemoteAddress().toString().substring(1) + "上线了......");
    43                 }
    44                 if (key.isReadable()) {
    45                     // 读取数据请求
    46                     readMsg(key);
    47                 }
    48                 iterator.remove();
    49             }
    50         }
    51     }
    52 
    53     // 读取客户端发来的消息并广播出去
    54     private void readMsg(SelectionKey key) throws IOException {
    55         SocketChannel socketChannel = (SocketChannel) key.channel();
    56         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    57         int count = socketChannel.read(byteBuffer);
    58         if (count > 0) {
    59             String msg = new String(byteBuffer.array());
    60             printInfo(msg);
    61             // 发广播(排除掉当前的通道,自己发的广播再给自己就没有意义了)
    62             // 意思就是说一个客户端发送了消息到服务器之后,经
    63             // 服务器读取之后转发到所有的客户端去,以这种逻辑实现聊天室
    64             broadCast(socketChannel, msg);
    65         }
    66 
    67     }
    68 
    69     // 给所有的客户端发广播
    70     public void broadCast(SocketChannel socketChannel, String msg) throws IOException {
    71         System.out.println("服务器发送了广播......");
    72         // selector.keys()得到所有就绪的通道,即连接上服务器的通道(返回的值是SelectionKey)
    73         for (SelectionKey selectionKey : selector.keys()) {
    74             //通过key得到的通道有可能不是SocketChannel类型的不能直接强转
    75             Channel targetChannel = selectionKey.channel();
    76             if (targetChannel instanceof SocketChannel && targetChannel != socketChannel) {
    77                 SocketChannel destChannel = (SocketChannel) selectionKey.channel();
    78                 ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
    79                 destChannel.write(byteBuffer);
    80             }
    81         }
    82     }
    83 
    84     // 和当前系统的时间进行一个拼接输入到控制台
    85     private void printInfo(String str) {
    86         SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    87         System.out.println("[" + simpleDateFormat.format(new Date()) + "] ->->" + str);
    88     }
    89 
    90     public static void main(String[] args) throws IOException {
    91         new ChatServer().start();
    92     }
    93 }

    (2)客户端Client

     1 public class ChatClient {
     2     private final String HOST = "127.0.0.1";// 服务器地址
     3     private int PORT = 9999;// 服务器端口
     4     private SocketChannel socketChannel;// 网络通道
     5     private String userName;// 聊天用户名
     6 
     7     public ChatClient() throws IOException {
     8         // 得到一个网络通道
     9         socketChannel = SocketChannel.open();
    10         // 设置非阻塞
    11         socketChannel.configureBlocking(false);
    12         // 提供服务器的IP地址和端口号
    13         InetSocketAddress inetSocketAddress = new InetSocketAddress(HOST, PORT);
    14         // 连接服务器端
    15         if (!socketChannel.connect(inetSocketAddress)) {
    16             while (!socketChannel.finishConnect()) {
    17                 System.out.println("Client:连接中...");
    18             }
    19         }
    20         // 得到客户端IP地址和端口信息,作为聊天用户名使用
    21         userName = socketChannel.getLocalAddress().toString().substring(1);
    22         System.out.println("----------Client(" + userName + ") is ready----------");
    23     }
    24 
    25     // 向服务器发送数据
    26     public void sendMsg(String msg) throws IOException {
    27         // 首先进行判断,如果客户端发送的消息是“bye”的话就关闭这个连接通道
    28         if (msg == "bye") {
    29             socketChannel.close();
    30             return;
    31         }
    32         msg = userName + " "+"发送:" + msg;
    33         // 无论是读还是写都得通过ByteBuffer缓冲区来完成
    34         ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
    35         // 调用write方法就完成了写的操作
    36         socketChannel.write(byteBuffer);
    37     }
    38 
    39     // 从服务器读取数据
    40     public void receiveMsg() throws IOException {
    41         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    42         /**
    43          * 通道就会读取数据然后存放到byteBuffer里面 同时read方法会返回一个int类型的整数值
    44          */
    45         int count = socketChannel.read(byteBuffer);
    46         if (count > 0) {
    47             String mString = new String(byteBuffer.array());
    48             System.out.println("您收到一条消息:" + mString.trim());
    49         }
    50     }
    51 }

    (3)测试

     1 public class TestChat {
     2     public static void main(String[] args) throws IOException {
     3         ChatClient chatClient = new ChatClient();
     4         /**
     5          * 发数据还比较好,直接sendMsg就可以了
     6          *
     7          * 但是收数据的话就得一直循环去接收服务器发来的数据,所以考虑开一个线程
     8          */
     9         new Thread() {
    10             public void run() {
    11                 while (true) {
    12                     try {
    13                         chatClient.receiveMsg();
    14                         Thread.sleep(2000);
    15                     } catch (Exception e) {
    16                         // TODO: handle exception
    17                         e.printStackTrace();
    18                     }
    19                 }
    20             }
    21         }.start();
    22         Scanner scanner = new Scanner(System.in);
    23         while (scanner.hasNextLine()) {
    24             String msg = scanner.nextLine();
    25             chatClient.sendMsg(msg);
    26         }
    27     }
    28 }
  • 相关阅读:
    [九度][何海涛] 顺时针打印矩阵
    [何海涛] 求二元查找树的镜像
    [九度][何海涛] 二叉树中和为某一值的路径
    [面试] 水杯题实现
    [九度][何海涛] 最小的K个数
    [九度][何海涛] 字符串的排序
    如何扩展Orchard
    IoC容器Autofac(3) 理解Autofac原理,我实现的部分Autofac功能(附源码)
    使用PrivateObject帮助单元测试
    Nuget如何自动下载依赖DLL引用
  • 原文地址:https://www.cnblogs.com/dashenaichicha/p/12095073.html
Copyright © 2020-2023  润新知