NIO的介绍
NIO(New IO)同步非阻塞IO模型,采用了事件驱动的思想来实现一个复用器,来解决大并发的问题。读、写、可连接、可接受等操作在BIO中是阻塞处理,在NIO中,将其设置为非阻塞,并将这些操作视为事件,当关注某个事件时,将其注册到复用器Selector(本质上使用底层操作系统提供的IO复用器:select、poll,epoll)。由系统来监听事件是否准备就绪, 当socket有读或者可写操作时,系统会通知相应的用户程序来处理,将流读取到缓冲区或者写入到系统中。
一个复用器上是可以注册多个用户的连接,有效的用户请求才会来通知用户程序,用户程序才进行介入处理:
复用器(Selector)也叫做多路选择器,作用是检查一个或者多个NIO的channel(Channel),可以实现单线程管理多个channel,也可以管理多个网络请求.
channel:通道
用户IO操作的连接,对原有的IO补充,不能直接访问数据需要和缓冲区Buffer结合使用,通道主要有:
- SocketChannel:主要是用户连接服务端,一般在客户端实现
- ServerSocketChannel:监听新进来的TCP连接,对于每一个新用户连接创建一个SocketChannel实例,一般在服务端实现
Buffer:缓冲区
IO流中的数据需要经过缓冲区交给channel
NIO编码
服务端编程
1、实例ServerSocketChannel实例【ServerSocketChannel.open()】
2、服务端绑定端口【bind】
3、将serversocketchannel设置为非阻塞【serverSocketChannel.configureBlocking(false)】
4、实例话selector实例【Selector.open()】
5、将serversocketchannel注册到selector上,并关注可接受事件【serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT)】
6、selector阻塞等待至少一个事件发生【selector.select()】
7、有感兴趣事件发生,遍历感兴趣事件集合
8、如果是可接受事件、通过serversocketchannel调用accept获取socketchannel
9、将socketchannel设置为非阻塞
10、将socketchannel注册到selector复用上,并关注可读、可写事件
11、循环等待感兴趣事件集合,即跳转至第6步
12、如果当前是可读事件、通过socketchannel调用read来进行读数据操作
13、关闭资源
public class Server {
public static void main(String[] args) {
ServerSocketChannel serverSocketChannel = null;
try {
//创建ServerSocketchannel实例
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(7777));
System.out.println("服务端启动啦");
//设置ServerSocketchannel为非阻塞 true:表示则塞 false:非阻塞
serverSocketChannel.configureBlocking(false);
//创建复用器selector实例
Selector selector = Selector.open();
//将serverSocketChannel注册到selector中,并关注的是可接受事件
/**
* register(Selector sel, int ops)
* 第一个参数:复用器实例
* 第二个参数:表述关注的事件
* 存在4种事件
* 1、可读事件 read
* 2、可写事件 write
* 3、可接收事件 accept
* 4、可连接事件 connect
*
*/
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT | SelectionKey.OP_WRITE);
//selector阻塞等待事件发生
while (selector.select() >= 0) {
//获取已发生事件集合
Iterator <SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
//删除已完成事件
iterator.remove();
if (selectionKey.isValid() && selectionKey.isAcceptable()) {
System.out.println("有接收事件发生");
//有可接受事件发生
//SelectableChannel 所有通道的父类,ServerSocketChannel、SocketChannel
ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) selectionKey.channel();
//通过调用accept获取SocketChannel实例
SocketChannel socketChannel = serverSocketChannel1.accept();
System.out.println("有新用户连接啦"+socketChannel.getRemoteAddress());
//设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到selector复用器上,并关注读事件
socketChannel.register(selector, SelectionKey.OP_READ);
}
if (selectionKey.isValid() && selectionKey.isReadable()) {
System.out.println("有可读事件发生");
//有可读事件发生
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
//读取数据
//创建一个缓存实例
ByteBuffer buffer = ByteBuffer.allocate(100);
//将数据从channel写入缓存
int read = socketChannel.read(buffer);
//读写模式切换
buffer.flip();
//创建有效大小的byte数组
byte[] bytes = new byte[read];
buffer.get(bytes);
String msg = new String(bytes);
System.out.println("数据为:"+msg);
}
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
System.out.println("服务端关闭啦");
if (serverSocketChannel != null) {
try {
serverSocketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端编码流程
1、创建socketchannel实例
2、将socketchannel设置非阻塞
3、创建Selector复用器实例
4、将socketchannel实例注册到Selector实例中,并关注可连接事件
5、socketchannel主动调用connect连接服务端,会立即返回结果
6、如果结果为false:当前连接还未完成,通过selector.select由系统监听事件发生,
7、进行读写事件
8、关闭资源
public class Client {
public static void main(String[] args) {
SocketChannel socketChannel = null;
try {
//创建socketChannel实例
socketChannel = SocketChannel.open();
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//创建Selector复用器实例
Selector selector = Selector.open();
//将socketChannel注册到Selector实例中,并关注可连接事件
socketChannel.register(selector,SelectionKey.OP_CONNECT);
//主动连接服务端,该方法不会阻塞,会立即返回结果,要么成功,要么未连接成功,则交由os等待连接过程
if (!socketChannel.connect(new InetSocketAddress("127.0.0.1", 7777))) {
//返回为false,表示连接还未连接成功
System.out.println("connect还未完成!");
//等待系统返回就绪事件
selector.select();
//进行完成连接操作
socketChannel.finishConnect();
}
System.out.println("客户端连接服务端成功");
//进行读写事件
byte[] bytes = "Hello Tulun".getBytes();
//创建buffer
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
//往Buffer写数据
buffer.put(bytes);
//读写模式切换
buffer.flip();
//往socketchannel通道中写数据
socketChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if (socketChannel != null) {
System.out.println("客户端关闭啦");
try {
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
练习:
通过NIO实现Echo命令,要求:客户端可多次进行消息发送。
问题:
问题1:客户端为什么主动connect连接?
在BIO中connect操作是可阻塞的方法,在NIO中设置为非阻塞,交给复用器来监听事件是否完成(connect可连接事件),内核帮助监听事件是否完成,必须先触发事件,触发之后内核才能监听事件是否完成,客户端来主动连接服务端,在NIO中connect是非阻塞,当前connect操作立即放回(true:连接已完成 false:当前事件已经触发了,但连接还没完成,就需要等待内核来关注事件是否完成)。
问题2:为什么客户端断开连接服务端一致循环接收到可读事件为空?
客户端断开连接,服务端会接收到-1,占用空间,服务端认为有数据接收,就会一直认为有可读事件服务端需要处理。
判断通道接收为-1表示结束接收
问题3:为什么写操作没有注册到复用器?
写操作在NIO中也是一个事件,但是写事件需要主动发起写操作,一般写完会立即write操作不会进行阻塞,即通常写操作不需要注册。
NIO重要组件
channel:通道
Java中NIO的channel类似流,但和流有不同,通道既可以从通道中读取数据,又可以写数据到通道,但流的读写通常是单向的;通道中的数据需要先读到一个buffer,或者要从一个buffer写入。
channel示例:
channel主要实现类
- SocketChannel:通过TCP来读写网络中的数据,一般是客户端的实现
- ServerSocketChannel:监听新进来的TCP连接,对每一个新连接创建一个SocketChannel,一般是服务端的实现
- DatagramChannel:通过UDP读写网络中的数据通道
- FileChannel:用于读取、写入等操作文件的通道
FileChanle使用示例:
RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/gongdezhe/Desktop/download/test", "rw");
//可以进行读写操作
FileChannel fileChannel = randomAccessFile.getChannel();
//写数据
byte[] bytes = "hello".getBytes();
//将数据交给buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(40);
byteBuffer.put(bytes); //将数据写入到buffer
byteBuffer.flip();
//写入通道
fileChannel.write(byteBuffer);
//关闭通道
fileChannel.close();
Buffer:缓存
Java中的NIO的Buffer用与和Channel交互。Java堆NIO中的内存缓存包装成BUffer对象,提供了一系列方法,方便开始使用。
基本使用:
以读数据为例:
- 从channel将数据写入buffer
- 调用buffer.flip()进行读写切换
- 从buffer中读取数据
- 对 buffer清空clear()和compact()
Buffer 实现
Buffer底层是通过特定类型(long,double...)的数组存储数据,借助4个指针来操作的。
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
capacity:buffer是有固定大小的,即capacity表示底层数组的大小
position:取决于读写模式
写操作:将数据写入buffer,每写入数据position会移动一位,初始值为0,最大值为capacity
读操作:会从某个特定位置读,当读写模式切换后,position会被重置为0,读取数据时,position移动表示下一个读取的位置
limit:写模式下,表示最多王buffer中写入的数据,最大写入的位置capacity,此时limit=capacity;读模式下,limit表示最多能读取到的数据。
mark:读写过程中指针变化
Buffer 类型:
Buffer是一个抽象类,其实现的子类有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
buffer缓冲区的分配:
Buffer分配空间分两种,一种是在堆上开辟的空间,一种是在堆外开辟的空间(零拷贝技术)
Buffer对象的创建:
以ByteBuffer为例介绍:
ByteBuffer allocate(int capacity):在堆上创建指定大小的缓冲区
ByteBuffer wrap(byte[] array):通过byte数组创建一个缓冲区
ByteBuffer wrap(byte[] array,int offset, int length),截取bytebuffer部分内容创建缓冲区
ByteBuffer allocateDirect(int capacity) 在堆外内存创建一个指定大小的缓冲区
Buffer中的方法:
向buffer中写数据:
1、从channel写到buffer:channel.read(buffer);
2、通过buffer的put方法:buffer.put()
flip()读写模式切换:通过flip操作将limit置为position,然后将position置为0
从buffer中读数据:
有两种方式:
1、从buffer中读取到channel:channel.write(buffer)
2、通过buffer的get操作处理:buffer.get()
selector:复用器
作用是检查一个或者多个NIO Channel的状态是否是可读、可写,可以实现单线程管理多个channel,也可以管理多个网络请求。
Selector的使用:
1.创建selector的实例:Selector selector = Selector.open();
2.将通道注册到复用器上
//将socketChannel设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel注册到Selector实例中,并关注可连接事件
socketChannel.register(selector,SelectionKey.OP_CONNECT*);
注册的channel是非阻塞的,SelectableChannel抽象类中提供了configureBlocking设置当前的通道是阻塞的还是非阻塞的。
3.使用复用器来监听事件是否完成
//等待系统返回就绪事件
selector.select();
select方法是会阻塞的,直至内核监听到注册的感兴趣事件发生才返回。
4.遍历感兴趣事件集合
selector.selectedKeys().iterator()
通过selectedKeys返回的是SelectionKey的set集合,存放的是感兴趣事件的已准备就绪的。
5.如果还有关注的事件,就跳转至第3步继续循环等待
6.最终关闭复用器selector
SelectableChannel抽象类
SelectableChannel抽象类称之为可选择的通道,SelectableChannel是具有阻塞的和非阻塞的两种模式,在非阻塞模式下,永远不会阻塞IO操作,将会使用selector作为异步支持,即read和write操作都不会阻塞,可能立即返回,新创建的SelectableChannel总是处于阻塞的,需要手动调换用configureBlocking设置为false,如果选用selector多路复用器必须是channel为非阻塞的,将channel设置为非阻塞之后,其是不能改变,直至将SelectionKey注销掉。
SocketChannel、ServerSocketChannel、DatagramChannel都是SelectableChannel的子类,都是可以来设置为非阻塞的。FileChannel是不能设置非阻塞的,其不是SelectableChannel子类实现。
SelectionKey介绍
SelectionKey表示的是一个特定的通道对象和一个特定的复用器对象直接的注册关系。SelectionKey中对4个事件用4个常量表示:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
SelectionKey中提供的方法:
public abstract SelectableChannel channel();
//返回该SelectionKey对应通道
public abstract Selector selector();
//返回该SelectionKey注册的选择器
public abstract boolean isValid();
//判断该SelectionKey是否有效
public abstract void cancel();
//撤销该SelectionKey
public abstract int interestOps();
//返回SelectionKey的关注操作符
//设置该SelectionKey的关注键,返回更改后新的SelectionKey
public abstract SelectionKey interestOps(int ops);
public abstract int readyOps();
//返回SelectionKey的预备操作符
//这里readyOps()方法返回的是该SelectionKey的预备操作符
//判断该SelectionKey的预备操作符是否是OP_READ
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_WRITE
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_CONNECT
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_ACCEPT
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
//设置SelectionKey的附件
public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}
//返回SelectionKey的附件
public final Object attachment() {
return attachment;
}
interestOps():对应的是该SelectionKey上注册的关注事件,通过register来完成感兴趣事件的集合
readyOps():通道上实际准备就绪的事件。
复用器的选择过程:
Selector抽象类中提供的方法:
Set<SelectionKey> keys()方法返回所有的注册的通道实例,一个selector上是可以注册多个通道,即返回所有注册的通道SelectionKey。
Set<SelectionKey> selectedKeys():返回的是所有准备就绪的通道实例。
选择过程:
select获取准备就绪的事件,返回就是已经有就是事件发生。
int select():阻塞到至少有一个通道上注册的事件就绪了;
int select(long timeout);和select()一样,会则塞等待事件就绪,最长阻塞时间为timeout;
int selectNow():非阻塞,调用会立即返回。
返回结果为int类型表示有多少通道已经就绪,是自上次调用select后已经处于就绪的通道数量。上次未处理的通道是不计入当前数量。
停止选择过程:
wakeup():通过调用wakeup()方法让调用select()处于阻塞的方法立即返回;
close():关闭selector实例,使得任意一个处于select则塞的线程都会被唤醒,会将selector上所有的channel都被注销(cancel)。
选择过程(图示):
在程序执行过程中,可以调用key.cancel()方法,该方法不会立即注销channel通道等操作,而是将SelectionKey加入到已取消的集合中,在调用select()等方法时才会进行通道注销等操作
Java中Selector复用器本质是对操作系统提供的IO复用模型(select()、poll()、epoll())封装