NIO是非阻塞的IO,Java NIO由一下几个核心部分组成:Buffers
、Channels
、Selectors
。
Java NIO中有很多类和组件,但是Buffers
、Channels
、Selectors
构成了核心的API。其他组件如Pipe
和FileLock
,只不过是与其他三个核心组件共同使用的工具类。
- Channel:基本上所有的IO在NIO中都从一个
Channel
开始,Channel有点像流。数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中。Channel和Buffer有好多类型,Channel主要有:FileChannel、DataGramChannel、SocketChannel、ServerSocketChannel
。涵盖了UDP和TCP网络的IO以及文件IO。
-
Buffer:NIO主要的Buffer有:
ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer
,这些Buffer涵盖了你能通过IO发送的基本数据类型。 -
Selector:允许单线程处理多个
Channel
。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便。例如一个聊天服务器中。要使用Selector,得先向Selector注册Channel然后调用它的select()
方法。这个方法会一直堵塞知道某个注册的通道有事件就绪。一旦这个方法返回线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
缓冲区&Buffer
在整个Java新IO中,所有的操作都是以缓冲区进行的,使用缓冲区,则操作的性能将是最高的。
在Buffer中存在一系列的状态变量,随着写入或读取都有可能被改变,在缓冲区可以使用三个值表示缓冲区状态
- position 位置
- limit 界限
- capacity 容量
ByteBuffer的使用
- 创建ByteBuffer
(1)使用allocate()静态方法
ByteBuffer buffer=ByteBuffer.allocate(256);
以上方法将创建一个容量为256字节的ByteBuffer,如果发现创建的缓冲区容量太小,唯一的选择就是重新创建一个大小合适的缓冲区.
(2)通过包装一个已有的数组来创建
如下,通过包装的方法创建的缓冲区保留了被包装数组内保存的数据.
ByteBuffer buffer=ByteBuffer.wrap(byteArray);
如果要将一个字符串存入ByteBuffer,可以如下操作:
String sendString="你好,服务器. "; ByteBuffer sendBuffer=ByteBuffer.wrap(sendString.getBytes("UTF-16"));
- 缓冲区
- buffer.flip();
这个方法用来将缓冲区准备为数据传出状态,执行以上方法后,输出通道会从数据的开头而不是末尾开始.回绕保持缓冲区中的数据不变,只是准备写入而不是读取.
- buffer.clear();
这个方法实际上也不会改变缓冲区的数据,而只是简单的重置了缓冲区的主要索引值.不必为了每次读写都创建新的缓冲区,那样做会降低性能.相反,要重用现在的缓冲区,在再次读取之前要清除缓冲区.
ByteBuffer转为其他的Buffer,如:CharBuffer、DoubleBuffer、IntBuffer、LongBuffer、ShortBuffer,都有对应的asXXXBuffer()方法。
- 创建子缓冲区 buf.slice()
子缓冲区可以修改数据
import java.nio.IntBuffer ;
public class IntBufferDemo02{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ; // 准备出10个大小的缓冲区
IntBuffer sub = null ; // 定义子缓冲区
for(int i=0;i<10;i++){
buf.put(2 * i + 1) ; // 在主缓冲区中加入10个奇数
}
// 需要通过slice() 创建子缓冲区
buf.position(2) ;
buf.limit(6) ;
sub = buf.slice() ;
for(int i=0;i<sub.capacity();i++){
int temp = sub.get(i) ;
sub.put(temp-1) ;
}
buf.flip() ; // 重设缓冲区
buf.limit(buf.capacity()) ;
System.out.print("主缓冲区中的内容:") ;
while(buf.hasRemaining()){
int x = buf.get() ;
System.out.print(x + "、") ;
}
}
}
- 创建只读缓冲区 buf.asReadOnlyBuffer()
import java.nio.IntBuffer ;
public class IntBufferDemo03{
public static void main(String args[]){
IntBuffer buf = IntBuffer.allocate(10) ; // 准备出10个大小的缓冲区
IntBuffer read = null ; // 定义子缓冲区
for(int i=0;i<10;i++){
buf.put(2 * i + 1) ; // 在主缓冲区中加入10个奇数
}
read = buf.asReadOnlyBuffer() ;// 创建只读缓冲区
read.flip() ; // 重设缓冲区
System.out.print("主缓冲区中的内容:") ;
while(read.hasRemaining()){
int x = read.get() ;
System.out.print(x + "、") ;
}
read.put(30) ; // 修改,错误
}
}
- 创建直接缓冲区
public static ByteBuffer allocateDirect(int capacity)
只能提高一些尽可能的性能。
import java.nio.ByteBuffer ;
public class ByteBufferDemo01{
public static void main(String args[]){
ByteBuffer buf = ByteBuffer.allocateDirect(10) ; // 准备出10个大小的缓冲区
byte temp[] = {1,3,5,7,9} ; // 设置内容
buf.put(temp) ; // 设置一组内容
buf.flip() ;
System.out.print("主缓冲区中的内容:") ;
while(buf.hasRemaining()){
int x = buf.get() ;
System.out.print(x + "、") ;
}
}
}
通道
- 可读、可写。程序不会直接操作通道,所有的内容读到或者写入缓冲区,再通过缓冲区中取得或写入。
- 传统的流操作分为输入或输出流,而通道本身是双向操作的,可以完成输入也可以完成输出。
读入方式
- RandomAccessFile:较慢
- FileInputStream:较慢
- 缓存读取:速度较快
- 内存映射MapByteBuffer:最快!
FileChannel的三种内存映射模式
NO. | 常量 | 类型 | 描述 |
---|---|---|---|
1 | public static final FileChannel.MapMode.READ_ONLY | 常量 | 只读映射模式 |
2 | public static final FileChannel.MapMode.READ_WRITE | 常量 | 读取/写入映射模式 |
3 | public static final FileChannel.MapMode.PRIVATE | 常量 | 专用(写入时拷贝)映射模式 |
import java.nio.ByteBuffer ;
import java.nio.MappedByteBuffer ;
import java.nio.channels.FileChannel ;
import java.io.File ;
import java.io.FileOutputStream ;
import java.io.FileInputStream ;
public class FileChannelDemo03{
public static void main(String args[]) throws Exception{
File file = new File("d:" + File.separator + "mldn.txt") ;
FileInputStream input = null ;
input = new FileInputStream(file) ;
FileChannel fin = null ; // 定义输入的通道
fin = input.getChannel() ; // 得到输入的通道
MappedByteBuffer mbb = null ;
mbb = fin.map(FileChannel.MapMode.READ_ONLY,0,file.length()) ;
byte data[] = new byte[(int)file.length()] ; // 开辟空间接收内容
int foot = 0 ;
while(mbb.hasRemaining()){
data[foot++] = mbb.get() ; // 读取数据
}
System.out.println(new String(data)) ; // 输出内容
fin.close() ;
input.close() ;
}
}
内存映射在读取时速度快,但是如果在使用以上操作代码的时候,执行的是写入操作则有可能非常危险,因为仅仅是改变数组中单个元素这种简单的操作,就可能直接修改磁盘上的文件,因为修改数据与将数据保存在磁盘上是一样的。
文件锁FileLock
当一个线程将文件锁定之后,其它线程无法操作此文件,要想进行文件锁定操作,需要使用FileLock
类完成,此类的对象需要依靠FileChannel
进行实例化操作。
锁定方式
共享锁:允许多个线程进行文件的读/写操作
-
独占锁:只允许一个线程进行文件的读/写操作
字符集 Charset
整个NIO中,对于不同平台的编码操作,java都可以进行自动适应,因为可以使用字符集进行字符编码的转换。
在java中,所有信息都是以UNICODE进行编码,计算机中存在多种编码,NIO中提供了charset类来处理编码问题,包括了创建编码器(
CharsetEndoder
)和创建解码器(CharsetDecoder
)的操作。
Charset latin1 = Charset.forName("ISO-8859-1") ; // 只能表示的英文字符
CharsetEncoder encoder = latin1.newEncoder() ; // 得到编码器
CharsetDecoder decoder = latin1.newDecoder() ; // 得到解码器
// 通过CharBuffer类中的
CharBuffer cb = CharBuffer.wrap("啊哈哈哈哈") ;
ByteBuffer buf = encoder.encode(cb) ; // 进行编码操作
System.out.println(decoder.decode(buf)) ; // 进行解码操作
Selector
解决服务器端的通讯性能。
- 使用
Selector
可以构建一个非阻塞的网络服务 - 在NIO中实现网络程序需要依靠
ServerSocketChannel
类与SocketChannel
类
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Set;
import java.util.Iterator;
import java.util.Date;
import java.nio.channels.ServerSocketChannel;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
public class DateServer {
public static void main(String args[]) throws Exception {
int ports[] = { 8000, 8001, 8002, 8003, 8005, 8006 }; // 表示五个监听端口
Selector selector = Selector.open(); // 通过open()方法找到Selector
for (int i = 0; i < ports.length; i++) {
ServerSocketChannel initSer = null;
initSer = ServerSocketChannel.open(); // 打开服务器的通道
initSer.configureBlocking(false); // 服务器配置为非阻塞
ServerSocket initSock = initSer.socket();
InetSocketAddress address = null;
address = new InetSocketAddress(ports[i]); // 实例化绑定地址
initSock.bind(address); // 进行服务的绑定
initSer.register(selector, SelectionKey.OP_ACCEPT); // 等待连接
System.out.println("服务器运行,在" + ports[i] + "端口监听。");
}
// 要接收全部生成的key,并通过连接进行判断是否获取客户端的输出
int keysAdd = 0;
while ((keysAdd = selector.select()) > 0) { // 选择一组键,并且相应的通道已经准备就绪
Set<SelectionKey> selectedKeys = selector.selectedKeys();// 取出全部生成的key
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next(); // 取出每一个key
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept(); // 接收新连接
client.configureBlocking(false);// 配置为非阻塞
ByteBuffer outBuf = ByteBuffer.allocateDirect(1024); //
outBuf.put(("当前的时间为:" + new Date()).getBytes()); // 向缓冲区中设置内容
outBuf.flip();
client.write(outBuf); // 输出内容
client.close(); // 关闭
}
}
selectedKeys.clear(); // 清楚全部的key
}
}
}