NIO三大核心
Buffer缓冲区
缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。相比较直接对数组的操作,Buffer API更加容易操作和管理
使用Buffer进行数据写入与读取,需要进行如下四个步骤:
- 将数据写入缓冲区
- 调用buffer.flip(),转换为读取模式,如果不调用,则position还在之前写模式写到的位置,读取数据肯定不全面
- 缓冲区读取数据
- 调用buffer.clear()清除缓冲区,否则写的数据超出的话,则会越界异常
- buffer.compact()仅清除已阅读的数据转为写入模式,否则写的数据超出的话,则会越界异常
Buffer三个重要属性
- capacity容量:作为一个内存块,Buffer具有一定的固定大小,也称为"容量"
- position位置:写入模式时代表写数据的位置。读取模式时代表读取数据的位置
-
limit限制:写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量(可读量)
1.png
补充:
- rewind() 重置position为0
- mark() 标记position的位置
- reset() 重置position为上次mark()标记的位置
ByteBuffer内存类型
ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现。堆外内存获取的方式:ByteBuffer directByteBuffer=ByteBuffer.allocateDirect(noBytes);
-
好处
- 进行网络IO或者文件IO时比heapBuffer少一次拷贝。(file/socker----OS memory ---- jvm heap)。GC会移动对象内存,在写file或者socket的过程中,JVM的实现中,会先把数据复制到堆外,再进行写入。
- GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator
-
解释:为什么JVM需要拷贝一次数据到系统内存,因为jvm本身有gc,所以如果不拷贝,则在移动过程中很可能出现地址变化了的问题(被回收),地址一旦变化,则file/socket操作自然而然不知道哪里是哪里;就类似于交易,如果用股票抵消债务,股票实时变动,很可能走手续过程中价值变化,所以需要先卖出股票然后进行相关操作。
-
建议
- 性能确实可观的时候才去使用,分配给大型、长寿命:(网络传输、文件读写场景)
- 通过虚拟机参数MaxDirectMemorySize限制大小,防止耗尽整个机器的内存;
Channel通道

SocketChannel

ServerSocketChannel

//会导致只能接收一个客户端连接的代码示例
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建网络服务端
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//设置为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8080));//绑定端口
System.out.println("启动成功");
while (true) {
SocketChannel socketChannel=serverSocketChannel.accept();//获取新tcp连接通道
if (socketChannel != null) {
System.out.println("收到新连接"+socketChannel.getLocalAddress());
socketChannel.configureBlocking(false);//默认阻塞的,一定要设置为非阻塞
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());
String response="HTTP/1.1 200OK\r\n"+
"Content-Length: 11\r\n\r\n"+
"Hello World";
ByteBuffer buffer=ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);//非阻塞
}
}
}
}
}
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()) {
//没链接上,则一直等待
Thread.yield();
}
Scanner scanner=new Scanner(System.in);
System.out.println("请输入:");
String msg=scanner.nextLine();
ByteBuffer buffer=ByteBuffer.wrap(msg.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
//读取响应
System.out.println("收到服务端响应");
ByteBuffer requestBuffer=ByteBuffer.allocate(1024);
while (socketChannel.isOpen()&&socketChannel.read(requestBuffer)!=-1) {
if (requestBuffer.position()>0) {
break;
}
}
requestBuffer.flip();
byte[] content=new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
scanner.close();
socketChannel.close();
}
}
Selector选择器
Selector是一个Java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已准备好进行读取或写入。实现单个线程可以管理多个通过,从而管理多个网络连接。如果不使用选择器,则必须自己手动实现while循环类似的代码,才能确保不停的接收连接和正常的数据传输

//client和上面的一样,只是用选择器改进了server端
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//设置为非阻塞模式
//构建一个Selector选择器,并且将channel注册上去
Selector selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);//对serverSocketChannel上面的accept事件感兴趣(serverSocketChannel只能支持accept操作)
serverSocketChannel.socket().bind(new InetSocketAddress(8080));//绑定端口
System.out.println("启动成功");
while (true) {
//不在轮询通道,改用下面轮询事件的方式 select方法有阻塞效果,直到有事件通知才会有返回
selector.select();
//获取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历查询结果
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
//关注Read和Accept两个事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.attachment();
//将拿到的客户端连接通道,注册到selector上面
SocketChannel clientSocketChannel = server.accept();
clientSocketChannel.configureBlocking(false);
//接收到客户端连接之后,注册读取事件,这样下面的key.isReadable()才能进入
clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
System.out.println("收到新连接:" + clientSocketChannel.getRemoteAddress());
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.attachment();
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());
String response = "HTTP/1.1 200OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);//非阻塞
}
}
}
selector.selectNow();
}
}
}
实现一个线程处理多个通道的核心概念理解:事件驱动机制
非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来触发响应的代码执行。(更底层是操作系统的多路复用机制)

BIO和NIO对比

NIO与多线程结合的改进方案
当并发高的时候,单线程处理连接很容易遇到性能瓶颈,则此时可以NIO和多线程结合使用

网友评论