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 }