您的位置:首页 > 编程语言 > Java开发

JavaNIO全解

2017-08-29 18:28 127 查看
java.io包主要是面向流的,而NIO是面向块的。意味着在尽可能的情况下,I/O操作以大的数据块为单位进行的,而不是一次一个字节或字符进行。采用这样的方式可以提高I/O性能,当然也牺牲了操作的简单性。

新特性:

多路选择的非封锁式I/O设施;

支持文件锁和内存映射;

支持正则表达式的模式匹配设施;

字符集编码器和译码器。

1、缓冲区和Buffer

在基本IO操作中所有操作都是直接以流的形式完成的;而在NIO中,所有的操作要使用缓冲区,且所有读写操作都是在缓存区完成的。缓冲区是一个线性的,有序的数据集,只能容纳某种特定数据类型。

1、基本操作:

public static void main(String[] args) throws Exception{
//开辟缓冲区
IntBuffer buf = IntBuffer.allocate(10);
System.out.println("写入数据前:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());

int temp[] = {1,2,3};
buf.put(4);
buf.put(temp);
System.out.println("写入数据后:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());
//重设缓冲区,从写模式变到读模式
buf.flip();
System.out.println("重设后:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());
//清空已读取
buf.compact();
System.out.println("清空已读取后:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());
//清空
buf.clear();
System.out.println("清空后:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());
}


写入数据前:position,limit,capacity
0 10 10
写入数据后:position,limit,capacity
4 10 10
重设后,写变读:position,limit,capacity
0 4 10
清空已读取后:position,limit,capacity
4 10 10
清空后:position,limit,capacity
0 10 10




写模式下,写入数据后,position有变化,表示指向了下一个缓冲区读取或写入的操作指针,例如上面写入了四个数,那position指向第5个位置。limit表示我们所能写入的最大数据量。它等同于buffer的容量。

调用flip重设后,读模式下,position重置到了0,每次读取后,position向后移动。limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。

capacity始终表示buffer容量。

compact()是清空已读取的数据,clear是清空buffer。

上面的例子是IntBuffer,还有一些其他的缓冲区:

ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。

2、创建子缓冲区

可以用slice()方法从缓冲区创建一个子缓冲区,子缓冲区和原缓冲区中的部分数据可以共享。

public static void main(String[] args) throws Exception{
IntBuffer buf = IntBuffer.allocate(10);
IntBuffer sub = null;
int b[] = {1,2,3,4,5,6,7,8,9,10};
buf.put(b);
buf.flip();
System.out.println("写入数据后:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());
while (buf.hasRemaining()) {
System.out.print(buf.get()+"、");
}

buf.position(2);//修改指针
buf.limit(6);//子缓冲最大位置
sub = buf.slice();

//修改子缓冲数据
for (int i=0;i<sub.capacity();i++) {
sub.put(0);
}
buf.flip();
buf.limit(buf.capacity());
System.out.println("\n修改后子缓冲区数据:position,limit,capacity");
System.out.println(buf.position()+" "+buf.limit()+" "+buf.capacity());
while (buf.hasRemaining()) {
System.out.print(buf.get()+"、");
}
}


写入数据后:position,limit,capacity
0 10 10
1、2、3、4、5、6、7、8、9、10、
修改后子缓冲区数据:position,limit,capacity
0 10 10
1、2、0、0、0、0、7、8、9、10、>


可以看到,修改子缓冲区,主缓冲区内容也改变了。

3、只读缓冲区和直接缓冲区

asReadOnlyBuffer()创建只读缓冲区;

只有Bytebuffer有直接缓冲区,调用allocateDirect(int capacity)

2、通道Channel

通道可以读取和写入数据,类似输入输出流,但是程序不会直接操作通道,所有内容都是先读或者写到缓冲区,再通过缓冲区取得和写入。传统流操作分为输入输出,但通道是双向操作的。



Channel是一个接口,close()方法关闭通道,isOpen判断此通道是否是打开的。

下面列出Java NIO中最重要的集中Channel的实现:

FileChannel

DatagramChannel

SocketChannel

ServerSocketChannel

FileChannel用于文件的数据读写。 DatagramChannel用于UDP的数据读写。 SocketChannel用于TCP的数据读写。 ServerSocketChannel允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel.

1、FileChannel

FileChannel是Channel子类,可以进行文件读写操作。

//将内容读到缓冲区
public int read(ByteBuffer dst) throws IOException;
//将内容从缓冲区写入通道
public int write(ByteBuffer src) throws IOException;
//将通道文件区域内容映射到内存
public abstract MappedByteBuffer map(MapMode mode,long position, long size)
throws IOException;


(1)使用输出通道输出内容到txt:

public static void main(String[] args) throws Exception{
String info = "hello world";
File file = new File("d:\\demo4.txt");
FileOutputStream out = new FileOutputStream(file);
//得到一个File通道
FileChannel fout = out.getChannel();
//向缓冲区写入数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put(info.getBytes());
byteBuffer.flip();
fout.write(byteBuffer);
//关闭通道
fout.close();
out.close();
}


(2)利用FileChannel读取数据到Buffer的例子:

public static void main(String[] args) throws Exception{
File file = new File("d:\\demo4.txt");
FileInputStream input = new FileInputStream(file);
//得到一个File通道
FileChannel finput = input.getChannel();
//向缓冲区写入数据
ByteBuffer byteBuffer = ByteBuffer.allocate(5);
//读到缓冲区
int bytesRead = finput.read(byteBuffer);
while (bytesRead != -1) {
System.out.println("\nRead " + bytesRead);
byteBuffer.flip();

while(byteBuffer.hasRemaining()){
System.out.print((char) byteBuffer.get());
}

byteBuffer.clear();
bytesRead = finput.read(byteBuffer);
}
finput.close();
input.close();
}


Read 5
hello
Read 5
worl
Read 1


(3)通道之间的数据传输

transferFrom():把数据从通道源传输到FileChannel,

transferTo():FileChannel数据传输到另一个channel

例子:

我有两个文件,demo2.txt内容为:“hello world”,demo4.txt内容为:“你好世界”





下面用transferFrom()举例

代码:

public static void main(String[] args) throws Exception{
File fromFile = new File ("d:\\demo4.txt");//内容为“你好世界”
FileChannel fromChannel = new FileInputStream(fromFile).getChannel();

File toFile = new File ("d:\\demo2.txt");//内容为“hello world”
FileChannel toChannel = new FileOutputStream(toFile,true).getChannel();
//复制demo4的内容到demo2中
long postion = toFile.length();
long count = fromChannel.size();
try {
toChannel.transferFrom(fromChannel, postion, count);
}
catch (IOException e) {
throw e;
}
finally {
if (fromChannel != null) fromChannel.close();
if (toChannel != null) toChannel.close();
}
}


结果:



transferTo()举例:

public static void main(String[] args) throws Exception{
File fromFile = new File ("d:\\demo4.txt");//内容为“你好世界”
FileChannel fromChannel = new FileInputStream(fromFile).getChannel();

File toFile = new File ("d:\\demo2.txt");//内容为“hello world”
FileChannel toChannel = new FileOutputStream(toFile,true).getChannel();
//复制demo4的内容到demo2中,从第二个字开始
long postion = 2;
long count = fromChannel.size();
try {
fromChannel.transferTo(postion, count, toChannel);
}
catch (IOException e) {
throw e;
}
finally {
if (fromChannel != null) fromChannel.close();
if (toChannel != null) toChannel.close();
}
}


结果:



由上面的例子可见,两个方法都能实现内容复制,区别只在于调用方法的是哪个FileChannel,postion始终指的是fromChannel的位置,count指要复制内容的大小。

2、内存映射

内存映射可以把文件映射到内存中,这样内存中的数据就可以用内存读写指令来访问,不用InputStream/OutputStream这样的IO操作类,采用这种方式是最快的。可以使用FileChannel的map方法:

public abstract MappedByteBuffer map(MapMode mode,long position, long size)
throws IOException;


此方法在使用时要指定映射模式,3种模式可选择:

/**
* Mode for a read-only mapping.只读
*/
public static final MapMode READ_ONLY
= new MapMode("READ_ONLY");

/**
* Mode for a read/write mapping.读写
*/
public static final MapMode READ_WRITE
= new MapMode("READ_WRITE");

/**
* Mode for a private (copy-on-write) mapping.写入时复制
*/
public static final MapMode PRIVATE
= new MapMode("PRIVATE");


需要注意,使用MappedByteBuffer写入数据是很危险的,因为仅仅改变数组中的一个元素,都可能直接修改磁盘上的具体文件,因为修改数据和数据重新保存到磁盘是一样的。

3、文件锁FileLock

4、Scatter / Gather

Scattering read指的是从通道读取的操作能把数据写入多个buffer,也就是sctters代表了数据从一个channel到多个buffer的过程。

Gathering write则正好相反,表示的是从多个buffer把数据写入到一个channel中。

Scatter/Gather在有些场景下会非常有用,比如需要处理多份分开传输的数据。举例来说,假设一个消息包含了header和body,我们可能会把header和body保存在不同独立buffer中,这种分开处理header与body的做法会使开发更简明。

Scattering read:



ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);


read()方法内部会负责把数据按顺序写进传入的buffer数组内。一个buffer写满后,接着写到下一个buffer中。实际上,scattering read内部必须写满一个buffer后才会向后移动到下一个buffer,因此这并不适合消息大小会动态改变的部分,也就是说,如果你有一个header和body,并且header有一个固定的大小(比如128字节),这种情形下可以正常工作。

Gathering Writes:



ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body   = ByteBuffer.allocate(1024);

//write data into buffers

ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);


类似的传入一个buffer数组给write,内部机会按顺序将数组内的内容写进channel,这里需要注意,写入的时候针对的是buffer中position到limit之间的数据。也就是如果buffer的容量是128字节,但它只包含了58字节数据,那么写入的时候只有58字节会真正写入。因此gathering write是可以适用于可变大小的message的,这和scattering reads不同。

3、Selector

选择器是用来处理多个通道并监听其通道事件的组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

从操作系统的角度来看,切换线程开销是比较昂贵的,并且每个线程都需要占用系统资源,因此暂用线程越少越好。通过Selector我们可以实现单线程操作多个channel。



1、向Selector注册通道

Selector类常用方法:

public static Selector open() throws IOException;//打开(创建)一个选择器
public abstract int select() throws IOException;//选择一组键
public abstract Set<SelectionKey> selectedKeys();//返回此选择器已经选的Key


创建了选择器后,需要把通道注册进去,这时,需要使用SelectableChannel类,向Select类注册,此类提供了注册方法和阻塞模式。

SelectableChannel类常用方法:

//向指定选择器注册通道并设置Selector域,返回一个选择key
public final SelectionKey register(Selector sel, int ops)throws ClosedChannelException;
//调整通道阻塞模式,true为阻塞模式,false为非阻塞模式
public abstract SelectableChannel configureBlocking(boolean block)throws IOException;


SelectableChannel是抽象方法,需要子类实例化,ServerSocketChannel和SocketChannel都是他的子类。

ServerSocketChannel类常用方法:

public static ServerSocketChannel open() throws IOException;//打开服务器套接字通道
public abstract ServerSocket socket();//返回与此通道关联的服务器套接字


在使用register()方法时,需要指定一个选择器和select域,Selector对象可以通过Selector中的open方法取得,而Selector域则在SelectionKey类中定义:

SelectionKey.OP_CONNECT//连接操作
SelectionKey.OP_ACCEPT//相当于ServerSocket中的accept操作
SelectionKey.OP_READ//读操作
SelectionKey.OP_WRITE//写操作


以上四种代表我们关注的channel状态,一个channel触发了一个事件也可视作该事件处于就绪状态。因此当channel与server连接成功后,那么就是“连接就绪”状态。一个server socket channel准备好接收新进入的连接称为“接收就绪”。一个有数据可读的通道可以说是“读就绪”。等待写数据的通道可以说是“写就绪”。

如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;


注意, 如果一个 Channel 要注册到 Selector 中, 那么这个 Channel 必须是非阻塞的,

即channel.configureBlocking(false); 因为 Channel 必须要是非阻塞的, 因此 FileChannel

是不能够使用选择器的, 因为 FileChannel 都是阻塞的.

另外需要注意,一个 Channel 仅仅可以被注册到一个 Selector 一次, 如果将 Channel 注册到 Selector 多次, 那么其实就是相当于更新 SelectionKey 的 interest set. 例如:

channel.register(selector, SelectionKey.OP_READ);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);


上面的 channel 注册到同一个 Selector 两次了, 那么第二次的注册其实就是相当于更新这个 Channel 的 interest set 为 SelectionKey.OP_READ | SelectionKey.OP_WRITE.

注册通道的 register() 方法有一个重载方法,可以向选择器注册通道的时候,选择想要带上的附加对象:

public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;


例如,使用时附加上一个字符串:

String ch_name = "123";
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT,ch_name);


当然,注册时返回的SelectionKey对象也可以在使用时候附加你想要的附加对象:

selectionKey.attach(ch_name);


下面是个建立非阻塞服务端的例子:

public static void main(String[] args) throws Exception{
int ports = 8888;
Selector selector = Selector.open();
ServerSocketChannel initSer = ServerSocketChannel.open();//打开通道
initSer.configureBlocking(false);//设置为非阻塞
ServerSocket initSock = initSer.socket();//检索与此通道关联的服务器套接字
InetSocketAddress address = new InetSocketAddress(ports);//实例化绑定监听端口地址
initSock.bind(address);//绑定地址
initSer.register(selector, SelectionKey.OP_ACCEPT);//注册选择器,相当于accept()方法
System.out.println("服务器运行在"+ports+"端口");
}


2、SelectionKey

当向Selector注册Channel时,register()方法会返回一个SelectionKey对象,里面有如下常用方法:

public abstract SelectableChannel channel();//返回创建此key的通道
public abstract Selector selector();//返回注册的选择器
public final Object attachment();//返回附加值
public abstract int interestOps();//返回你所选择的感兴趣的事件集合
public final boolean isAcceptable()//判断此通道是否可以接受新的连接
public final boolean isConnectable();//判断此通道是否可以完成套接字的连接操作
public final boolean isReadable();//判断此通道是否可以进行读操作
public final boolean isWritable();//判断此通道是否可以进行写操作


3、 从Selector中选择channel

因为是一个线程通过选择器来操作通道,那么选择器在操作通道时,必定在处理一个通道的时候,另一个事件已就绪的通道处于等待状态。因为是一个线程通过选择器来操作通道,那么选择器在操作通道时,必定在处理一个通道的时候,另一个事件已就绪的通道处于等待状态。在确定一个通道事件就绪之后,才能去操作这个通道。上文中讲到使用注册方法register使用的代码示例,将ServerSocketChannel对象向选择器注册,同时关注了这个通道的OP_ACCEPT操作类型事件,那么我们什么时候能确定该通道的accept事件就绪,可以操作这个通道了。选择器为我们提供了三个重载的 select() 方法,这三个方法的主要功能就是选择是否阻塞直到该选择器中的通道所关注的事件就绪,来让程序继续往下运行。

int select()
int select(long timeout)
int selectNow()


首先看 select() 方法,该方法会一直阻塞下去,直到选择器中的通道关注的事件就绪,select(long timeout)和select做的事一样,不过他的阻塞有一个超时限制。selectNow()不会阻塞,根据当前状态立刻返回合适的channel。这三个方法的本质区别无非是选择器阻塞或者等待一个或多个通道的事件就绪有多长时间。select()方法的返回值是一个int整形,代表有多少channel处于就绪了。

由于调用select而被阻塞的线程,可以通过调用Selector.wakeup()来唤醒即便此时已然没有channel处于就绪状态。当操作Selector完毕后,需要调用close方法。close的调用会关闭Selector并使相关的SelectionKey都无效。channel本身不管被关闭。

4、SelectionKeys

调用了select()方法返回大于0,表示有通道就绪了,接下来就把这些就绪的通道找出来。可以通过调用selector的selectedKeys()方法,访问“已选择键集(selected key set)”中的就绪通道。

Set<SelectionKey> selectedKeys = selector.selectedKeys();


以上集合就是已就绪的键集,通过迭代可以取出每个SelectionKey,通过SelectionKey里的各种方法,判断通道是否是否可以接受新的连接(isAcceptable())等。

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {

SelectionKey key = keyIterator.next();

if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.

} else if (key.isConnectable()) {
// a connection was established with a remote server.

} else if (key.isReadable()) {
// a channel is ready for reading

} else if (key.isWritable()) {
// a channel is ready for writing
}

keyIterator.remove();
}


上述循环会迭代key集合,针对每个key我们单独判断他是处于何种就绪状态。

注意keyIterater.remove()方法的调用,Selector本身并不会移除SelectionKey对象,这个操作需要我们手动执行。当下次channel处于就绪是,Selector任然会吧这些key再次加入进来。

SelectionKey.channel返回的channel实例需要强转为我们实际使用的具体的channel类型,例如ServerSocketChannel或SocketChannel。

5、一个非阻塞服务器的完整例子:

此服务器向客户端返回当前系统时间

public static void main(String[] args) throws Exception{
int ports = 8888;
Selector selector = Selector.open();
ServerSocketChannel initSer = ServerSocketChannel.open();//打开通道
initSer.configureBlocking(false);//设置为非阻塞
ServerSocket initSock = initSer.socket();//检索与此通道关联的服务器套接字
InetSocketAddress address = new InetSocketAddress(ports);//实例化绑定监听端口地址
initSock.bind(address);//绑定地址
initSer.register(selector, SelectionKey.OP_ACCEPT);//注册选择器,相当于accept()方法
System.out.println("服务器运行在"+ports+"端口");

int keysAdd = 0;
while ((keysAdd = selector.select()) > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys();//已就绪通道
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
// 当获取一个 SelectionKey 后, 就要将它从集合删除, 表示我们已经对这个 IO 事件进行了处理.
iter.remove();
if (key.isAcceptable()) {//判断客户端已经连上
ServerSocketChannel server = (ServerSocketChannel)key.channel();//获取通道
SocketChannel client = server.accept();//接收新连接
client.configureBlocking(false);//非阻塞
ByteBuffer buf = ByteBuffer.allocateDirect(1024);//开辟缓冲区
buf.put(("当前时间:" + new Date()).getBytes());
buf.flip();
client.write(buf);
client.close();
}
}
}
}


客户端用telnet命令连接测试:

telnet localhost 8888

结果:

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  javanio