一、NIO基本概念
1、NIO介绍
- NIO(New IO)是一个可以替代标准Java IO API的IO API(从Java 1.4开始),Java NIO提供了与标准IO不同的IO工作方式。
- NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
2、NIO 核心部分组成
- Channels:负责连接
- Buffers: 负责数据的存取
- Selectors
3、与IO的区别
- IO面向流(输入流、输出流,单向的),NIO面向缓冲区;
- IO是阻塞式的,NIO是非阻塞式的;
- IO无通道,NIO有通道(Channel),双向流通,既可读,也可写,Channel相当于铁路(不能运输东西),必须借助火车运输(缓冲区);
- IO无选择器,NIO有选择器(Selector)。
二、NIO缓冲区Buffer
1、在java里为除boolean外的基本数据类型都提供了一个缓冲区
ByteBuffer
ShortBuffer
IntBuffer
LongBuffer
DoubleBuffer
FloatBuffer
2、Buffer读写数据步骤
(1)写入数据到Buffer;
(2)调用flip()方法;
(3)从Buffer中读取数据;
(4)调用clear()方法或者compact()方法。
3、Buffer四个属性
(1)private int mark = -1;标记
(2)private int position = 0;位置
初始值为0,最大可为capacity - 1,位置会根据get()和put()发生改变。
当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0。
(3)private int limit;上限
缓冲区第一个不能被读写的元素的位置
当在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时,limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。
(4)private int capacity;容量
缓冲区能够容纳的最大数量,一旦被创建就不可改变。
一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
4、Buffer基本方法
(1)获取缓冲区:allocate()
ByteBuffer allocate = ByteBuffer.allocate(1024);
(2)存入元素: put()
allocate.put("abcdef".getBytes())
(3)取出元素:get()
使用get方法前,必须将缓冲区切换成读取模式,使用flip()方法,切换后,limit会变成已经存入元素的个数,position变为0。
(4)切换模式:allocate.flip()
取出元素 byte[] bytes = new byte[10]; byte[]里面的数字不能超过limit
allocate.get(bytes);
(5)重写读,做标记: mark() 恢复到标记的位置: reset()
allocate.mark();
allocate.reset();
(6)重复读取:rewind()
allocate.rewind();
(7)重置缓冲区:clear()
回到最初的状态,但里面的数据还在,处于被遗忘的状态
allocate.clear();
5、注意
(1)当向buffer写入数据时,buffer会记录下写了多少数据。
一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。
在读模式下,可以读取之前写入到buffer的所有数据。
(2)一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。
有两种方式能清空缓冲区:调用clear()或compact()方法。
clear()方法会清空整个缓冲,
compact()方法只会清除已经读过的数据。
任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
6、直接缓冲区和非直接缓冲区
(1)直接缓冲区将缓冲区建立在物理内存中,可以提高效率。不安全
(2)非直接缓冲区将缓冲区建立在JVM中
创建直接缓冲区: allocateDirect()
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(1024);
7、Demo
public class NIODemo {
public static void main(String[] args) {
//获取缓冲区 allocate()
ByteBuffer allocate = ByteBuffer.allocate(1024);
//存入元素 put()
allocate.put("abcdef".getBytes());
//取出元素 get(),使用get方法前,必须将缓冲区切换成读取模式
allocate.flip(); //切换模式
byte[] bytes = new byte[3];
allocate.get(bytes); //get(byte[] dst)
System.out.println(new String(bytes));
System.out.println((char)allocate.get(2)); //get(int index)
//重新读 做标记mark()
allocate.mark();
重复读取
allocate.rewind();
//恢复到标记的位置
allocate.reset();
//重置缓冲区 clear(),里面的数据还在,处于被遗忘的状态
allocate.clear();
byte[] bytes1 = new byte[3];
allocate.get(bytes1);
}
}
三、NIO通道Channel
1、NIO通道Channel
NIO通道Channel是负责连接的,IO源与目标打开的连接,与传统IO流类似,只是Channel本身不能直接访问,必须借助缓冲区。
2、Java提供的通道
FileChannel
SocketChannel
ServerSocketChannel
DatagramChannel
3、获取通道:
(1)getChannel()
(2)open()
public class NIOChannelDemo {
public static void main(String[] args) throws IOException {
//getChannelDemo();
//openDemo();
}
//获取通道方法1:getChannel()方法示例
public static void getChannelDemo() throws IOException {
FileInputStream fis = new FileInputStream("D:\\test\\aa.jpg");
FileOutputStream fos = new FileOutputStream("D:\\test1\\aa.jpg");
//获取通道
FileChannel inchannel = fis.getChannel();
FileChannel outchannel = fos.getChannel();
//创建缓冲区
ByteBuffer bb = ByteBuffer.allocate(1024);
//循环读取
while(inchannel.read(bb) != -1) {
//切换成读模式
bb.flip();
outchannel.write(bb);
//重置缓冲区
bb.clear();
}
inchannel.close();
outchannel.close();
fis.close();
fos.close();
}
//获取通道方法2:open()方法
public static void openDemo() throws IOException {
FileChannel inChannel = FileChannel.open(Paths.get("D:\\test\\aa.zip"), StandardOpenOption.READ); //读的操作
FileChannel outChannel = FileChannel.open(Paths.get("D:\\test1\\aa.zip"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); //操作
inChannel.transferTo(0, inChannel.size(), outChannel); //position 位置
inChannel.close();
outChannel.close();
}
}
4、Demo1:NIOChannel文件复制
(1)服务器端
public class SelectorServer {
public static void main(String[] args) throws IOException {
ServerSocketChannel open = ServerSocketChannel.open();
//绑定端口
open.bind(new InetSocketAddress(12306));
FileChannel open2 = FileChannel.open(Paths.get("D:\\test1\\aa.zip"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
ByteBuffer allocate = ByteBuffer.allocate(1024);
SocketChannel accept = open.accept();
while(accept.read(allocate) != -1) {
allocate.flip();
open2.write(allocate);
allocate.clear();
}
//关流
accept.close();
open2.close();
open.close();
}
}
(2)客户端
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",12306));
//文件的通道
FileChannel open2 = FileChannel.open(Paths.get("D:\\test\\aa.zip"), StandardOpenOption.READ);
//创建缓冲区
ByteBuffer bb = ByteBuffer.allocate(1024);
while(open2.read(bb) != -1) {
bb.flip();
open.write(bb);
bb.clear();
}
//关闭连接
open.close();
open2.close();
}
}
5、Demo2:NIOChannel聊天
(1)聊天室服务器端
public class ChatServer {
public static void main(String[] args) throws IOException {
//建立连接,打开通道
ServerSocketChannel open = ServerSocketChannel.open();
//绑定端口号
open.bind(new InetSocketAddress(12306));
//创建缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
System.out.println("服务器已就绪。。。");
Scanner scanner = new Scanner(System.in);
//接收客户端的连接
SocketChannel sc = open.accept();
//监听连接
while(true) {
//读取
sc.read(allocate);
////切换模式
allocate.flip();
System.out.println(new String(allocate.array(), 0, allocate.limit()));
allocate.clear();
//写回去
String next = "服务器说:" + scanner.next();
//存入缓冲区
allocate.put(next.getBytes());
//切换成写模式
allocate.flip();
sc.write(allocate);
allocate.clear();
}
}
}
(2)聊天室客户端
public class ChatClient {
public static void main(String[] args) throws IOException, InterruptedException {
//与服务端建立连接,打开通道
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",12306));
//创建缓冲区
ByteBuffer bb = ByteBuffer.allocate(1024);
//输入信息
Scanner sc = new Scanner(System.in);
System.out.println("已连接到服务器。。");
//向服务端发送信息
while(true) {
String message = "客户端说" + sc.next();
bb.put(message.getBytes());
//切换成读模式
bb.flip();
open.write(bb);
bb.clear();
//接收服务器返回的信息
open.read(bb);
//切换
bb.flip();
System.out.println(new String(bb.array(), 0, bb.limit()));
bb.clear();
}
}
}
四、NIO选择器Selector
1、NIO选择器Selector
Selector是NIO的核心,解决阻塞问题,是Channel的多路复用器,用于检测通道
2、Selector的创建 open()
Selector selector = Selector.open();
3、Demo:NIOSelector聊天室
(1)服务器端
public class SelectorClient {
public static void main(String[] args) throws IOException {
//建立连接,打开通道
ServerSocketChannel open = ServerSocketChannel.open();
//绑定端口号
open.bind(new InetSocketAddress(12306));
//将服务器的通道切换成非阻塞模式
open.configureBlocking(false);
//创建选择器
Selector select = Selector.open();
//服务端的通道是用来接收客户端的连接,应该把连接事件注册到选择器
open.register(select, SelectionKey.OP_ACCEPT); //sel表示选择器,ops表示注册的事件
//轮询选择器上所注册的事件
while(select.select() > 0) {
//取出选择器上的所有事件,对事件进行判断
Set<SelectionKey> selectedKeys = select.selectedKeys();
//遍历Set集合
Iterator<SelectionKey> it = selectedKeys.iterator();
while(it.hasNext()) {
//取出给key
SelectionKey key = it.next();
it.remove();
if(key.isAcceptable()) { //如果该事件是一个接收客户端连接的事件,就接收客户端的连接
connect(open, select);
}
if(key.isReadable()) { //如果该事件是一个可读的事件,则读取客户端的数据
read(select, key);
}
}
}
}
//接收连接事件
public static void connect(ServerSocketChannel open,Selector select ) throws IOException {
SocketChannel accept = open.accept();
//将接收到的通道切换成非阻塞模式
accept.configureBlocking(false);
//将该通道的读取事件注册到选择器
accept.register(select, SelectionKey.OP_READ);
}
//读取数据事件
public static void read(Selector select,SelectionKey key) throws IOException {
//首先取出注册该事件的通道
SocketChannel channel = (SocketChannel)key.channel(); //SelectableChannel是SocketChannel的父类,所以必须向下转型
//创建缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
while(channel.read(allocate) > 0) {
allocate.flip();
//拿到数据后,发给所以客户端,所以要拿到所有客户端的通道
Set<SelectionKey> keys = select.keys();
//判断是否是SocketChannel的通道
for(SelectionKey ch: keys) {
SelectableChannel channel2 = ch.channel();
//对这个通道进行判断 channel2是否是SocketChannel的实例化对象
if(channel2 instanceof SocketChannel) {
//如果是的话要强转
SocketChannel schannel = (SocketChannel)channel2;
schannel.write(allocate);
//重复写
allocate.rewind();
}
}
//将数据转发给所有客户端后,重置缓冲区
allocate.clear();
}
}
}
(2)客户端
public class SelectorClient {
public static void main(String[] args) throws IOException {
//与服务端建立连接,打开通道
SocketChannel open = SocketChannel.open(new InetSocketAddress("127.0.0.1",12306));
//将客户端通道切换成非阻塞模式
open.configureBlocking(false);
//创建缓冲区
ByteBuffer allocate = ByteBuffer.allocate(1024);
//创建Scanner对象
Scanner sc = new Scanner(System.in);
getMessage(open);
while(true) {
String next = sc.next();
//将数据存入缓冲区
allocate.put(next.getBytes());
//切换成读模式
allocate.flip();
open.write(allocate);
allocate.clear();
}
}
//创建一个定时器,不断接收服务端发过来的信息
public static void getMessage(SocketChannel open) {
Timer time = new Timer();
ByteBuffer allocate = ByteBuffer.allocate(1024);
time.schedule(new TimerTask() {
@Override
public void run() {
try {
int read = open.read(allocate);
if(read > 0) {
allocate.flip();
System.out.println(new String(allocate.array(), 0, allocate.limit()));
allocate.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}, 50,50);
}
}