一、Buffer缓冲区
package com.itbac.net.NIO.BIO; import java.nio.ByteBuffer; /** * Buffer缓冲区 */ public class BufferDemo { public static void main(String[] args) { //构建一个byte 字节缓冲区,容量是4 // allocate分配内存(在堆中)堆中内存要复制到堆外才能用于网络通信,因为堆中有GC管理 // allocateDirect 分配内存(在堆外),减少一次内存复制操作,提高性能,但是要自己回收内存 ByteBuffer byteBuffer = ByteBuffer.allocate(4); System.out.println(String.format("初始化:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); //写入3字节的数据 byteBuffer.put((byte) 1); byteBuffer.put((byte) 2); byteBuffer.put((byte) 3); System.out.println(String.format("写入3字节后:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); System.out.println("开始读取——"); byteBuffer.flip(); System.out.println(String.format("切换成读模式:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); byte a = byteBuffer.get(); System.out.println("读第1个:"+a); System.out.println(String.format("读取1字节后:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); byte b = byteBuffer.get(); System.out.println("读第2个:"+b); System.out.println(String.format("读取2字节后:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); /** * 继续写入3字节,此时读模式下,limit=3,position=2,继续写入只能写入一条数据,再多就会溢出。java.nio.BufferOverflowException * clear()方法清除整个缓存去。 compact()方法仅清除已阅读的数据。==> 两个方法都会转为写入模式。 */ byteBuffer.compact(); System.out.println(String.format("compact()后:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); byteBuffer.put((byte) 4); byteBuffer.put((byte) 5); byteBuffer.put((byte) 6); System.out.println(String.format("最终的情况:capacity容量:%s,position位置:%s,limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // byteBuffer.rewind(); 重置position为0 // byteBuffer.mark(); 标记position的位置 // byteBuffer.reset(); 重置position为上次 mark()标记的位置 } }
二、Channel通道
客户端
package com.itbac.net.NIO.BIO; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.util.Scanner; //客户端 public class NIOClient { public static void main(String[] args) throws IOException { //创建客户端通道 SocketChannel socketChannel = SocketChannel.open(); //设置非阻塞 socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080)); while (!socketChannel.finishConnect()) { //没连接上,则让出CPU ,一直等待 Thread.yield(); } Scanner scanner = new Scanner(System.in); System.out.println("请输入:"); //发送内容 String msg = scanner.nextLine(); //包装要发出的数据 ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); while (byteBuffer.hasRemaining()) { //如果Buffer缓冲区有剩余,循环写入通道中。 socketChannel.write(byteBuffer); } //读取响应 System.out.println("收到服务端响应:"); //开辟接收数据内存空间,堆内 ByteBuffer requestBuffer = ByteBuffer.allocate(1024); //通道开启状态 & 从通道读数据,写入到 buffer缓存区 while (socketChannel.isOpen()&& socketChannel.read(requestBuffer) !=-1) { //长连接情况下,需要手动判断数据有没有读取结束。 if (requestBuffer.position()==requestBuffer.limit()) { //buffer缓冲区,写入模式切换成读取模式 requestBuffer.flip(); byte[] content = new byte[requestBuffer.limit()]; requestBuffer.get(content); System.out.println(new String(content)); //清空已读,转成写模式 requestBuffer.compact(); } } //大于0 ,有写入的数据 if (requestBuffer.position()>0) { //buffer缓冲区,写入模式切换成读取模式 requestBuffer.flip(); byte[] content = new byte[requestBuffer.limit()]; requestBuffer.get(content); System.out.println(new String(content)); //清空已读,转成写模式 requestBuffer.compact(); } //关闭键盘录入 scanner.close(); //关闭通道 socketChannel.close(); } }
服务端
package com.itbac.net.NIO.BIO; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; public class NIOServer { public static void main(String[] args) throws IOException { //创建网络服务端 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //设置非阻塞IO serverSocketChannel.configureBlocking(false); //获取服务端套接字 ServerSocket serverSocket = serverSocketChannel.socket(); //绑定地址 serverSocket.bind(new InetSocketAddress("127.0.0.1",8080)); while (true) { SocketChannel socketChannel = serverSocketChannel.accept(); // 获取新tcp连接通道 // tcp请求 读取/响应 if (socketChannel != null) { System.out.println("收到新连接 : " + socketChannel.getRemoteAddress()); socketChannel.configureBlocking(false); // 默认是阻塞的,一定要设置为非阻塞 try { ByteBuffer requestBuffer = ByteBuffer.allocate(1024); while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) { // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了) if (requestBuffer.position() > 0) { break; } } if(requestBuffer.position() == 0) { continue; // 如果没数据了, 则不继续后面的处理 } requestBuffer.flip(); byte[] content = new byte[requestBuffer.limit()]; requestBuffer.get(content); System.out.println(new String(content)); System.out.println("收到数据,来自:"+ socketChannel.getRemoteAddress()); // 响应结果 200 String response = "HTTP/1.1 200 OK " + "Content-Length: 11 " + "Hello World"; ByteBuffer buffer = ByteBuffer.wrap(response.getBytes()); while (buffer.hasRemaining()) { socketChannel.write(buffer);// 非阻塞 } } catch (IOException e) { e.printStackTrace(); } } } // 用到了非阻塞的API, 在设计上,和BIO可以有很大的不同.继续改进 } }
用集合存储多个连接通道,单线程轮询处理的服务器。
package com.itbac.net.NIO.BIO; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * 直接基于非阻塞的写法,一个线程处理轮询所有请求 */ public class NIOServer1 { /** * 已经建立连接的集合 */ private static List<SocketChannel> channels = new ArrayList<>(); public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //设置非阻塞 serverSocketChannel.configureBlocking(false); //绑定ip , 端口 serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1",8080)); System.out.println("启动成功"); while (true) { SocketChannel socketChannel = serverSocketChannel.accept(); //TCP 请求 读取/响应 if (null != socketChannel) { System.out.println("收到新连接:" + socketChannel.getRemoteAddress()); socketChannel.configureBlocking(false); channels.add(socketChannel); }else { //没有新连接的情况下,就去处理现有的连接的数据,处理完的就删除掉 Iterator<SocketChannel> iterator = channels.iterator(); while (iterator.hasNext()) { SocketChannel channel = iterator.next(); try { ByteBuffer byteBuffer = ByteBuffer.allocate(1024); if (channel.read(byteBuffer) == 0) { //等于0 ,代表这个通道没有数据需要处理,那就待会再处理。 continue; } //通道是打开的,从通道读数据到缓冲区 while (channel.isOpen() && channel.read(byteBuffer) != -1) { //长连接情况下,需要手动判断数据有没有读取结束。(此处做一个简单的判断:超过0字节就认为请求结束了) if (byteBuffer.position() > 0) { break; } } if (0 == byteBuffer.position()) { continue; } //读写反转 byteBuffer.flip(); byte[] content = new byte[byteBuffer.limit()]; byteBuffer.get(content); System.out.println(new String(content)); System.out.println("收到数据,来自:" + channel.getRemoteAddress()); //响应结果 String response = "HTTP/1.1 200 OK " + "Content-Length: 11 " + "Hello World"; ByteBuffer buffer = ByteBuffer.wrap(response.getBytes()); //缓冲区还有剩余数据,就写到通道中 while (buffer.hasRemaining()) { //缓冲区数据写到通道中 channel.write(buffer); } iterator.remove(); } catch (IOException e) { e.printStackTrace(); } finally { iterator.remove(); } } } } //用到了非阻塞的API,在设计上,和BIO可以有很大的不同 //问题:轮询通道的方式,低效,浪费CPU } }
结合Selector 实现的非阻塞服务端
package com.itbac.net.NIO.BIO; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.Set; /** * 结合Selector 实现的非阻塞服务端(放弃对 channel 的轮询,借助消息通知机制) */ public class NIOServerV2 { public static void main(String[] args) throws IOException { //1.创建网络服务端 serverSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //设置非阻塞模式 serverSocketChannel.configureBlocking(false); //2.构建一个Selector 选择器,并且将 channel 注册上去 Selector selector = Selector.open(); //将 serverSocketChannel 注册到 selector SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel); //对serverSocketChannel 上面的accept 事件感兴趣 (serverSocketChannel只能支持 accept操作) selectionKey.interestOps(SelectionKey.OP_ACCEPT); //3.绑定端口 serverSocketChannel.socket().bind(new InetSocketAddress(8080)); System.out.println("启动成功"); while (true) { //不再轮询通道,改用下面轮询事件的方式。 //select()方法有阻塞效果,直到有事件通知才会有返回。 selector.select(); //获取事件 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //遍历查询结果 e Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { //被封装的查询结果 SelectionKey key = iterator.next(); iterator.remove(); //关注 Read 和 Accept 两个事件 //接收事件 if (key.isAcceptable()) { //获取拿到的服务端通道 ServerSocketChannel server = (ServerSocketChannel)key.attachment(); //mainReactor 轮询 accept SocketChannel clientSocketChannel = server.accept(); //设置非阻塞 clientSocketChannel.configureBlocking(false); //将拿到的客户端连接通道,注册到selector上面 clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel); System.out.println("收到新连接:" + clientSocketChannel.getRemoteAddress()); } //读事件 if (key.isReadable()) { //客户端通道 SocketChannel socketChannel = (SocketChannel)key.attachment(); try { //创建缓冲区 ByteBuffer requestBuffer = ByteBuffer.allocate(1024); //通道是开放的 , 读数据 while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) { //长连接情况下,需要手动判断数据有没有读取结束(此处做一个简单的判断:超过0字节就认为请求结束了) if (requestBuffer.position() > 0) { break; } } if (requestBuffer.position() == 0) { //没有数据了,则不继续后面的处理 continue; } //缓冲区,读写翻转。 requestBuffer.flip(); byte[] content = new byte[requestBuffer.limit()]; //从缓冲区拿出数据 requestBuffer.get(content); System.out.println(new String(content)); System.out.println("收到数据,来自:" + socketChannel.getRemoteAddress()); //TODO 业务操作 数据库 接口调用 等等 //响应结果 200 String response = "HTTP/1.1 200 OK " + "Content-Length: 11 " + "Hello World"; ByteBuffer buffer = ByteBuffer.wrap(response.getBytes()); //缓冲区有内容 while (buffer.hasRemaining()) { //向客户端通道,写数据 socketChannel.write(buffer); } } catch (IOException e) { e.printStackTrace(); //取消事件订阅 key.cancel(); } } } selector.selectNow(); } //问题:此处一个 selector 监听所有事件,一个线程处理所有请求事件,会成为瓶颈! 要有多线程的运用 } }