JS Bin

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 }
posted @ 2019-12-25 09:52  左五六  阅读(580)  评论(0编辑  收藏  举报