高效的数据容器,除布尔类型,所有原始数据类型都有Buffer实现。
使用步骤:
flip()
方法调整指针位置准备读取clear()
方法或者compact()
方法清理缓存主要属性值:
capacity
:Buffer缓冲区大小,只能写入capacity个原始类型。不会改变。position
:下一个要被读写的数据的索引。limit
:第一个不被读写的数据的索引位置,通常就是缓冲区中实际数据的字节数。主要方法:
flip()
:反转缓冲区,将limit的值设为position的值, 然后position的值设为0。为从缓冲区读取字节做准备。rewind()
:从头再读或再写,limit不变,position设置为0。mark()
:标记当前的position值,和reset()配合使用。reset()
:将当前position设为mark标记的值。hasRemaining()
:position和limit之前是否还有元素。clear()
:清空整个缓冲区(没有擦除)。 position的值设为0, limit的值设为capacity,mark的值被丢弃。为把字节写到缓冲区做准备。compact()
:只清空已经读过的数据(没有擦除)。未读数据复制到缓冲区的起始处,position设到最后一个未读数据后。Buffer的分配:要想获得一个Buffer对象首先要进行分配。 每一个Buffer类都有一个静态 allocate(int)
方法。
写数据到Buffer 有两种方式:
从Buffer中读取数据 有两种方式:
equals()
,当满足下列条件时,表示两个Buffer相等:
compareTo()
,比较两个同类型Buffer的剩余元素, 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
ByteBuffer 是唯一直接与通道交互的缓冲器。
静态方法:
ByteBuffer wrap(byte[] array)
:使用“支持”数组初始化一个 ByteBuffer,数组的和缓存器的数据相互关联。实例方法:
asXXBuffer 系列方法
可以获得特定基本数据类型的视图。ByteBuffer 依然是实际存储数据的地方。slice()
创建新的 ByteBuffer 对象,和原来对象的字节数据相互关联,它们的区别是指针对象相互独立。把字符集包装为对象,提供字符集的编码器和解码器。
Charset.forName("UTF-8").encode("Hello World!");
Java NIO 的通道类似流,但又有些不同:
主要的通道实现:
创建:
无法直接打开一个FileChannel,需要通过 InputStream、OutputStream 或 RandomAccessFile 的 getChannel 方法来获取一个 FileChannel 实例。
FileChannel 常用方法:
read(ByteBuffer)
,从文件通道读取数据到ByteBuffer,-1 表示已经到达输入的末尾。write(ByteBuffer)
,从字节缓存区写到文件通道。close()
,关闭。transferFrom()
,将数据从源通道传输到FileChannel中 toChannel.transferFrom(position, count, fromChannel);
transferTo()
,将数据从FileChannel传输到其他的channel中 fromChannel.transferTo(position, count, toChannel);
FileChannel其它方法:
position()
获取当前位置position(int)
设置当前位置size()
返回该实例所关联文件的大小Scattering Reads,数据从一个channel读取到多个buffer中。不适用于动态消息(消息大小不固定)。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.read(bufferArray);
Gathering Writes 是指数据从多个buffer写入到同一个channel。能较好的处理动态消息。
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
复制文件的应用
说了这么多,让我们在代码中感受一下 FileChannel 的使用:
我们要实现一个复制文件的 API,现在有两种方式。第一种采用传统的 IO 流加装饰缓存数组来实现,第二种采用 NIO 流中 FileChannel 的 transferTo 函数来实现通道对通道的传输。用函数运行时间进行测试发现,小文件(不超过十几M时)拷贝时,用流更快,大文件用通道更快。
因为通道的 transferTo 不经过用户态,直接在内核态传输数据,减少上下文切换和额外IO操作,不仅仅在文件拷贝时可以这么用,在读取磁盘文件然后进行 Socket 发送时也可以如此。使用流读写时,进行了多次上下文切换,比如应用读取数据时,首先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。
public static void copyFileByStream(File source, File dest) throws FileNotFoundException, IOException {
try (InputStream is = new FileInputStream(source);
OutputStream os = new FileOutputStream(dest);) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
}
}
@SuppressWarnings("resource")
public static void copyFileByChannel(File source, File dest) throws FileNotFoundException, IOException {
try (FileChannel srcChannel = new FileInputStream(source).getChannel();
FileChannel destChannel = new FileOutputStream(dest).getChannel();) {
for (long len = srcChannel.size(); len > 0;) {
long l = srcChannel.transferTo(srcChannel.position(), len, destChannel);
len -= l;
}
}
}
内存映射文件
通过 FileChannel 的 map 方法,允许我们创建和修改那些因为太大而不能放入内存的文件,我们可以假定整个文件都放在内存中(实际上只是一部分),可以把它当做非常大的数组来访问,可以很容易的修改。但是,它的缺点是,创建映射文件的花费大于以常规方式读写几十 MB 的数据,所以只在操作较大文件是才推荐这么做。
MappedByteBuffer 本质上是一种 Direct Buffer。Direct Buffer 生命周期内内存地址都不会发生改变,进而内核可以安全地对其进行访问,很多 IO 操作会很高效,同时它保存在堆外,减少了堆内对象存储的可能额外维护工作。但是,它的创建和销毁过程,都会比一般的堆类 BUffer 增加部分开销。通常都建议用于长期使用、数据量较大的场景。
FileChannel fc = new RandomAccessFile(new File("bigfile.txt"), "rw").getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());
...
fc.close();
文件锁
通过文件加锁来同步访问作为共享资源的文件,文件锁对其他的操作系统进程时可见的,因为 Java 的文件加锁直接映射到了本地操作系统的加锁工具。
通过 FileChannel 对象的 lock 或 tryLock 方法可以对文件加锁并返回 FileLock 对象,通过 FileLock 对象的 release 方法可以释放锁。
连接到TCP网络套接字的通道。
创建方式
1.打开一个SocketChannel并连接到互联网上的某台服务器。
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
2.一个新连接到达ServerSocketChannel时,会创建一个SocketChannel。
close()
关闭。read(ByteBuffer)
读取数据write(ByteBuffer)
写入数据 while(buf.hasRemaining()) {channel.write(buf);}
非阻塞模式
configureBlocking(false)
设置套接字通道为非阻塞模式。此时:
可以监听新进来的TCP连接。
打开:ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
关闭:serverSocketChannel.close();
监听新进来的连接,会一直阻塞到有新连接到达:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
非阻塞模式
非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel =serverSocketChannel.accept();
if(socketChannel != null){//do something with socketChannel...}
}
能收发UDP包的通道。因为UDP是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。
打开:
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
接收数据:
- receive(ByteBuffer) 会将接收到的数据包内容复制到指定的Buffer. 如果Buffer容不下收到的数据,多出的数据将被丢弃。
发送数据:
// 发送一串字符到”jenkov.com”服务器的UDP端口80
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
连接到特定的地址:
由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。
channel.connect(new InetSocketAddress("jenkov.com", 80));
连接后,也可以使用read()和write()方法,就像在用传统的通道一样。只是在数据传送方面没有任何保证
int bytesRead = channel.read(buf);
int bytesWritten = channel.write(but);
管道是2个线程之间的单向数据连接。Pipe 有一个 source 通道和一个 sink 通道。数据会被写到 sink 通道,从source通道读取。
创建 Pipe pipe = Pipe.open();
写数据到 sink 通道
Pipe.SinkChannel sinkChannel = pipe.sink();
while(buf.hasRemaining()) {sinkChannel.write(buf);}
从source通道读数据
Pipe.SourceChannel sourceChannel = pipe.source();
int bytesRead = sourceChannel.read(buf);
选择器,能够检测一到多个 NIO 通道,并能够知晓通道是否为诸如读写事件做好准备。一个单独的线程可以管理多个 Channel,从而管理多个网络连接。
非阻塞 IO 的核心在于使用一个 Selector 来管理多个通道,可以是 SocketChannel,也可以是 ServerSocketChannel,将各个通道注册到 Selector 上,指定监听的事件。
之后可以只用一个线程来轮询这个 Selector,看看上面是否有通道是准备好的,当通道准备好可读或可写,然后才去开始真正的读写,这样速度就很快了。我们就完全没有必要给每个通道都起一个线程。
/*
作者:见参考资料 1
*/
public class SelectorServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
// 将 server socket 通道注册到 Selector 中,监听 OP_ACCEPT 事件
server.configureBlocking(false); // 非阻塞
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 需要不断地去调用 select() 方法获取最新的准备好的通道
// select 方法时阻塞的,直到有通道就绪才返回
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
Set readyKeys = selector.selectedKeys();
// 遍历
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove(); // 手动从 selected-key set 中移除
// 当“连接就绪”时,服务器通道建立新的客户端连接
if (key.isAcceptable()) {
// 有已经接受的新的到服务端的连接
SocketChannel socketChannel = server.accept();
// 有新的连接并不代表这个通道就有数据,
// 这里将这个新的代表客户端连接的 SocketChannel 注册到 Selector,监听 OP_READ 事件,等待数据
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 剩下的条件,过滤的是客户端通道的就绪状态
else if (key.isReadable()) {
// 有数据可读
// 上面一个 if 分支中注册了监听 OP_READ 事件的 SocketChannel
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int num = socketChannel.read(readBuffer);
if (num > 0) {
// 处理进来的数据...
System.out.println("收到数据:" + new String(readBuffer.array()).trim());
socketChannel.register(selector, SelectionKey.OP_WRITE);
} else if (num == -1) {
// -1 代表连接已经关闭
socketChannel.close();
}
}
else if (key.isWritable()) {
// 通道可写
// 给用户返回数据的通道可以进行写操作了
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap("返回给客户端的数据...".getBytes());
socketChannel.write(buffer);
// 重新注册这个通道,监听 OP_READ 事件,客户端还可以继续发送内容过来
socketChannel.register(selector, SelectionKey.OP_READ);
}
}
}
}
}
客户端代码:
class NIOClient{
public static void main(String[] args) {
try (SocketChannel socketChannel = SocketChannel.open();) {
socketChannel.connect(new InetSocketAddress(InetAddress.getLocalHost(), 8080));
socketChannel.write(ByteBuffer.wrap("发送给服务器的数据...".getBytes()));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
socketChannel.read(byteBuffer);
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print(Charset.defaultCharset().decode(byteBuffer));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
优点:
通过调用Selector.open()方法创建一个Selector
通过SelectableChannel.register()方法来实现。Channel 必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为 FileChannel 不能切换到非阻塞模式。而套接字通道都可以。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
Channel 注册时,第二个 int 参数可以表明它关注的操作状态(如连接、接受、读或写)。可以用|
来监听多个事件:
int 型的操作位 | 描述 |
---|---|
OP_CONNECT | 某个 channel 成功连接到另一个服务器称为“连接就绪” |
OP_ACCEPT | 一个 server socket channel 准备好接收新进入的连接称为“接收就绪” |
OP_READ | 一个有数据可读的通道可以说是“读就绪” |
OP_WRITE | 等待写数据的通道可以说是“写就绪” |
操作键(SelectionKey)代表着 Selector 上注册过的一个 SelectableChannel 对象。
一个 Selector 对象有三个 SelectionKey 的集合:
key set
,代表所有注册过的通道,用 keys 方法返回。
当通道通过 register() 方法向 Selector 注册时,会向 Selector 对象的 keys set 中添加一个 SelectionKey 对象。在 selection 操作时被取消的键会从 key set 中删除。
selected-key set
,代表符合 Selector 关注点(多个关注点时至少符合一个)已就绪的通道,用 selectedKeys 方法返回。
selection 操作时会往 selected-key set 中添加键,这些键只能通过该集合或集合迭代器的 remove 方法来移除。
cancelled-key set
,代表已经取消但还没有解注册的通道,无法直接访问。
当一个键被取消时(关闭通道,或调用 cancel 方法),它会被加入 cancelled-key set 中。下一次 selection 操作时,被取消的键会被解注册并从 cancelled-key set 中移除。
SelectionKey 代表着一个 SelecteableChannel 对象,它两个标记状态的操作集:
interestOps()
方法可以返回代表关注点的 int 值。readyOps()
方法返回代表该集合的 int 值。可以用 isAcceptable()
、isConnectable()
、isReadable()
、isWritable()
这4个方法来过滤就绪状态。常用方法还有:
channel()
SelectionKey 对应的通道对象selector()
通道注册的选择器对象返回监听事件已经就绪的那些通道的键集。
select()
阻塞到至少有一个通道在注册的事件上就绪了。select(long)
和select()一样,除了最长会阻塞long毫秒。selectNow()
不会阻塞,不管什么通道就绪都立刻返回,没有通道变成可选择的,则此方法直接返回 null。选择操作有三个步骤:
选择之后,返回就绪的键集合,遍历这个已选择的键集合来访问就绪的通道
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}