最近在学NIO,感觉目前对NIO的了解有了进一步的认识,不再像第一次接触它,面对,Buffer,Channel,Selector等新概念一头雾水。话不多少,开始记录下。
下面的概念性的东西,我参考了一下别人的博客。(说的挺好)真正理解NIO
IO是面向缓冲区的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性。Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。
Buffer,Channel,Selector
缓冲区本质上是一个可以写入数据的内存块,然后可以再次读取,该对象提供了一组方法,可以更轻松地使用内存块,使用缓冲区读取和写入数据通常遵循以下四个步骤:
写数据到缓冲区;
2. 调用buffer.flip()方法;
3. 从缓冲区中读取数据;
4. 调用buffer.clear()
当向buffer写入数据时,buffer会记录下写了多少数据,一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式,在读模式下可以读取之前写入到buffer的所有数据,一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
Buffer在与Channel交互时,需要一些标志:
buffer的大小/容量 - Capacity
作为一个内存块,Buffer有一个固定的大小值,用参数capacity表示。
当前读/写的位置 - Position
当写数据到缓冲时,position表示当前待写入的位置,position最大可为capacity – 1;当从缓冲读取数据时,position表示从当前位置读取。
信息末尾的位置 - limit
在写模式下,缓冲区的limit表示你最多能往Buffer里写多少数据;
一个组件,可以检测多个NIO channel,看看读或者写事件是否就绪。
多个Channel以事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
下面开始手写聊天室,直接开干。
Sever.java
//服务端 public class Server{ //定义Selector选择器。用于监听某时刻客户端连接 static Selector selector; public static void main(String[] args) { try { //静态方法,直接创建一个Selector selector = Selector.open(); //创建Server端通道,NIO中的数据通信全部基于通道,可以把它看做IO中的流 ServerSocketChannel server = ServerSocketChannel.open(); //绑定端口 server.bind(new InetSocketAddress(90)); //设置服务端通道为非阻塞。这是必须滴,如果不设置,整个NIO通信,就毫无意义 server.configureBlocking(false); //把未来的服务端通道中的所有连接请求注册到Selector中,请求key设置为OP_ACCEPT server.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务已在90端口启动..."); //下面是死循环 while (true) { //这是NIO的重大创新,它会每隔十秒钟,从Selector中获取此刻所有连接。 //然后放到Selector选择器中 //当然你也可以不设置时间,那它就会一直获取,然后放到Selector选择器中 //如果没有获取到连接,那它就会一直阻塞 selector.select(10000); //获取此刻的所有连接key //包含服务端和客户端连接 //注意:我说的是此刻,不包括Selector.select()之前的。 final Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey next = iterator.next(); //判断key类型是不是连接key,就是客户端向服务端的发送的连接请求 //此时key代表服务端,key类型就是上面服务端通道注册到Selector的key类型 if (next.isAcceptable()) { //客户端通道 SocketChannel accept = server.accept(); //设置客户端通道为非阻塞 accept.configureBlocking(false); //把客户端通道注册到Selector选择器中,key类型为OP_READ accept.register(selector, SelectionKey.OP_READ); //打印客户端连接信息 //每有一个连接,就会打印,类比聊天室中xxx进入聊天室 System.out.println(accept.getRemoteAddress() + "进入聊天室.."); } //下面判断key是不是读类型 if (next.isReadable()) { read(next); } //这句话很重要,一个连接处理完,要把它从当前选择key中去掉 //要不然下次selector.selectedKeys()的时候还会获取到。会进行多次注册 //会出现各种问题。 iterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } //服务端读取客户端通道发送的信息,然后发送给其他客服端通道 public static void read(SelectionKey selectionKey) throws IOException { SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); try { ByteBuffer byteBuffer = ByteBuffer.allocate(1024); String prefx = "收到来自"+channel.getRemoteAddress()+"的消息:"; byteBuffer.put(prefx.getBytes()); //读取客户端通道发送的数据,放到buffer缓存中 int read = channel.read(byteBuffer); //因为通道是双向的,此时要把Buffer的方向反转一下 byteBuffer.flip(); if (read > 0) { //再把buffer里的数据写到其他通道中 byte[] bytes = byteBuffer.array(); write(channel, bytes); write(channel, byteBuffer); } } catch (Exception e) { e.printStackTrace(); selectionKey.cancel(); if (channel != null) { System.out.println(channel.getRemoteAddress() + "下线了"); } } } public static void write(SocketChannel channel, byte[] bytes) throws IOException { Set<SelectionKey> selectionKeys = selector.keys(); for (SelectionKey key : selectionKeys) { if (key.isValid()) { SelectableChannel selectableChannel = key.channel(); //判断是不是客户端,要过滤掉服务端 if (selectableChannel instanceof SocketChannel) { //在判断是不是自己,自己也不需要发送消息 SocketChannel selcte = (SocketChannel) selectableChannel; if (selcte != channel) { selcte.write(ByteBuffer.wrap(bytes)); } } } } } }至此,服务端,已准备完毕。下面我们来编写客户端。由于NIO是解决服务端通信中的问题,所以客户端,我们可以用NIO,也可以用传统io。下面我用一个传统IO,连个NIO编写客户端。
Clent1.java(IO)
public class Client1 { public static void main(String[] args) throws IOException { try { Socket client = new Socket("127.0.0.1", 90); OutputStream outputStream = client.getOutputStream(); InputStream inputStream = client.getInputStream(); Scanner scanner = new Scanner(System.in); System.out.println("输入信息:"); //发送数据线程 new Thread(() -> { while (true) { String s = scanner.nextLine(); try { outputStream.write(s.getBytes()); outputStream.flush(); } catch (IOException e) { e.printStackTrace(); } } }).start(); //读取线程 new Thread(() -> { while (true) { try { byte[] bytes = new byte[1024]; int count = 0; while ((count = inputStream.read(bytes)) != -1) { String msg = new String(bytes,0,count); System.out.println(msg); } } catch (Exception e) { } } }).start(); } catch (IOException e) { e.printStackTrace(); } } }Client2.java(NIO)
public class Client2 { public static void main(String[] args) throws IOException { try { SocketChannel clientChannel = SocketChannel.open(); clientChannel.connect(new InetSocketAddress(90)); Scanner scanner = new Scanner(System.in); System.out.println("输入信息:"); //写线程 new Thread(() -> { while (true) { String s = scanner.nextLine(); try { clientChannel.write(ByteBuffer.wrap(s.getBytes())); } catch (IOException e) { e.printStackTrace(); } } }).start(); //读线程 new Thread(() -> { while (true) { try { ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int read = clientChannel.read(byteBuffer); byteBuffer.flip(); if(read > 0){ String msg = new String(byteBuffer.array()); System.out.println(msg); } } catch (Exception e) { } } }).start(); } catch ( IOException e) { e.printStackTrace(); } } }Client3.java(NIO)
启动服务端
启动3个客户端
在Client1发送消息,
client2和client3就会收到消息。
Client2
Client3
以上是基于NIO手写的简易聊天室。虽然很简易,但总体上算是对NIO有了比较清晰的认识。
