通道基础

Channel表示IO源与目标打开的连接。在NIO中所有的IO都是从通道开始的。从通道进行数据的读取或者从通道进行数据写入。
通道的顶层接口只Channel接口。其中定义了两个方法。

1
2
public boolean isOpen()
public void close() throws IOException;

由Channel引申的实现类都是面向ByteBuffer的。
将IO从广义上分类,可以分为File I/O和Stream I/O。相应的也有两种不同类型的通道,分别是文件通道,和套接字通道。分别是FileChannel和SocketChannel、ServerSocketChannel、DatagramChannel.

mX9uWT.png
通道可以是单向的也可以是双向的,这取决于它们对ReadableByteChannel接口和WritableByteChannel接口的实现情况。而ByteChannel做的工作仅仅是聚集接口。实现ByteChannel接口的通道可以是双向的。但它不一定就是双向的,比如从FileInputStream对象使用getChannel方法获取的FileChannel对象是只读的,这和权限有关。

每个通道都会连接一个特定的I/O服务,通道实例的性能取决于它所连接的I/O服务的特征限制。所以一个通道是否能够读或写,和其连接的I/O服务也有关。

简单的使用Channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) throws IOException {
ReadableByteChannel readableByteChannel= Channels.newChannel(System.in);
WritableByteChannel writableByteChannel=Channels.newChannel(System.out);
channelCopy(readableByteChannel,writableByteChannel);

}

private static void channelCopy(ReadableByteChannel src, WritableByteChannel desc) throws IOException {
//分配一个直接缓冲区
ByteBuffer buffer=ByteBuffer.allocateDirect(1024*16);

while (src.read(buffer)!=-1){
//将buffer切换到读模式
buffer.flip();
//确保buffer中的内容被全部写入
while (buffer.hasRemaining()){
desc.write(buffer);
}

//清除buffer,等待下次写入
buffer.clear();
}

}

关闭通道

通道被关闭时,通道于特定的I/O服务的连接就会丢失,然后通道将不会连接任何东西。
close()方法用来关闭通道,可以使用isOpen()来查看通道的状态。如果一个通道实现了InterruptibleChannel接口,如果一个线程在一个该通道上被阻塞并同时被中断,那么该通道将被关闭,该被阻塞线程也会产生一个ClosedByInterruptException异常。

Scatter/Gather

sctter读

1
2
3
4
ByteBuffer header=ByteBuffer.allocateDirect(10);
ByteBuffer body=ByteBuffer.allocateDirect(40);
ByteBuffer[] buffers={header,body};
long bytesRead=channel.read(buffers);

使用scatter读,能够依次填充缓冲区。它还有一个重载方法long read(ByteBuffer[] dsts,int offset,int length) throws IOException 其中offset参数是传输字节的第一个缓冲区的偏移量。length是要访问的缓冲区的数量。

Gather 写

1
2
3
header.flip();
body.flip();
channel.write(buffers);

gather写能够将多个缓冲区中的数据组合并发送出去。它也有一个重载方法long write(ByteBuffer[] srcs, int offset, int length).

文件通道

文件通道总是阻塞式的。对于文件I/O,其强大之处在于异步I/O。它允许一个进程从操作系统请求一个或多个I/O操作而不必等待这些操作的完成。异步I/O是一种高级性能,不是所有的操作系统都具备。FileChannel对象是线程安全的的,但影响通道位置和文件大小的操作都是单线程的。

文件通道的创建

FileChannel不能直接创建,一个FileChannel实例只能通道一个打开的File对象(RandomAccessFile,FileInputStreamFileOutputStream)上调用getChannel()方法获取。它会返回一个连接到相同文件的FileChannel它与file对象有着同样的访问权限。

使用文件通道

每个文件通道都与一个文件描述符一一对应。每个文件通道中都有一个File position的概念,这个值决定了文件中的哪一处的数据接下来将被读或写。可以使用position()方法来或取这个值,也可以使用potition(long newPosition)来修改这个值。值得注意的是如果将positon设置到超出文件尾,那么再对文件进行读取的时候会收到一个文件尾(文件大小不会变化),如果是写入的话,将会引起文件的增长以容纳写入的字节(文件空洞)。并且FileChannelposition是从底层的文件描述符获得的,也就是说position可能同时被其它对象修改,并且修改也是对其它对象可见的。

绝对读和绝对写

使用read(ByteBuffer dst, long position) write(ByteBuffer src, long position) 等带有position参数的写和读,称为绝对读和绝对写,它并不会改变文件本身的position属性,并且因为通道的状态无需更新而具有更高的效率。

截断文件

可以使用truncate(long size) 来截断文件,所有超出size的内容都将被丢弃。

force(boolean metaData)

该方法告诉通道强制将全部待定的修改都应用到磁盘的文件上。mageData参数表示是否更新元数据(所有者,访问权限,修改时间等信息)。如果一个文件位于本地文件系统,那么force方法返回,那么可以保证从通道创建到force方法调用时所有对文件的修改都被持久化到磁盘。对于事务等关键操作来说,这非常重要。因为操作系统可能会缓存数据和延迟磁盘文件的更新。

文件锁定

文件锁定的特定需要依赖本地的操作系统实现。另一个值得注意的是FileChannel实现的是文件锁定模型,锁的对象是文件而不是通道或线程,如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求独占锁,那么它也会成功。但如果这两个线程运行在不同的Java虚拟机上,那么第二个线程会阻塞。这个现象是因为锁最终是由操作系统或文件系统来判优的,并且几乎总是在进程级而非线程级上判优的

lock(long position, long size, boolean shared) 方法,锁定从position开始,size大小的空间,参数shared表示想要获取的锁是共享的还是独占的。想要获得一个共享锁,必须以只读的权限打开文件,而请求独占锁时则需要写权限。值得注意的时size可以超出文件的时间大小,我们可以利用这个性质提前锁定一部分文件区域。

lock() 该方法主要用来获取文件的独占锁。如果锁定的范围是有效的,那么lock()方法会被阻塞,它必须等待前面的锁都释放后,才能获得锁。

与之对应的还有tryLock(long position, long size, boolean shared) tryLock() 两个方法,它们和前面的两个方法非常的类似,只是它们是非阻塞的,如果请求的锁不能立即获取到则会返回一个null.调用锁定方法后,会产生一个FileLock对象。一个FileLock对象创建之后立即生效,直到它的release()被调用,或与其关联的通道被关闭时才会失效。可以通过调用isValid()方法来测试一个锁的有效性,使用isShared() 判断锁是否是共享的。还可以调用overlaps(long position, long size) 来判断锁定区域是否与指定区域重叠。

尽管一个FileLock对象是与某个特定的FileChannel关联的,但它代表的锁却是与一个底层文件关联的,而不是与通道关联的。所以需要注意对锁的管理.

1
2
3
4
5
6
7
8
9
FileLock lock = channel.lock();
try {
/*业务逻辑*/
}catch (IOException e){
/*异常处理*/

}finally {
lock.release();;
}

Channel-to-Channel传输

FileChannel提供了一些方法(只有FileChannel有这些方法),用于通道的交叉连接而不需要中间缓冲区来传递数据。

1
2
3
4
5
//将该通道文件的字节传输到给定的可写字节通道。
long transferTo(long position, long count, WritableByteChannel target)

//从给定的可读字节通道将字节传输到该通道的文件中。
long transferFrom(ReadableByteChannel src, long position, long count)

直接的通道传输,并不会更新position的值,数据的传输将从position开始,传输的字节不超过count的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws IOException {
WritableByteChannel channel = Channels.newChannel(System.out);
catFile(channel,"test.txt");
}

private static void catFile(WritableByteChannel target,String file) throws IOException{
FileInputStream fis=new FileInputStream(file);
FileChannel channel = fis.getChannel();
//通道与通道之间进行传输,而不需要buffer的介入
channel.transferTo(0,channel.size(),target);
channel.close();
fis.close();
}

内存映射文件

FileChannel类提供了一个MappedByteBuffer map(FileChannel.MapMode mode, long position, long size) 方法,它能够在一个打开的文件和一个特殊类型的ByteBuffer之间建立一个虚拟内存映射。由map()方法返回的MappedByteBuffer对象的行为与基于内存的缓冲区非常的类似,只不过该对象的数据存储在磁盘上,调用get()方法会从磁盘文件中获取数据。通过内存映射机制来访问一个文件会比使用常规方法高效。它不需要使用明确的系统调用,并且虚拟内存的自动缓存内存页.

1
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());

值得注意的是与文件锁不同,映射文件的范围不应超过文件的实际大小。

虚拟内存映射有三种模式,

1
2
3
4
5
6
7
8
9
10
11
12
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");

前两种的意思都显而易见,PRIVATE模式是指采用写时拷贝映射。这种模式的内存映射通过put()方法所作的任何修改都会导致产生一个私有类型的数据拷贝,在这个过程中并不会对底层文件进行修改。

在使用内存映射文件时还有一个注意点,映射缓存区没有绑定到创建它们的通道上。关闭与之关联的通道不会破坏映射。

Socket通道

Socket通道可以运行非阻塞模式并且是可选择的。

通道的非阻塞模式

想要把一个socket通道置于非阻塞模式下,我们需要依靠SelectableChannel类。

可以利用SelectableChannel configureBlocking(boolean block) 方法来设置一个通道的阻塞模式,参数值为true则为阻塞模式,参数值为false为非阻塞模式。通过调用boolean isBlocking() 方法来判断某个socket通道的属于哪种模式。为了防止socket通道的阻塞模式被更改。可以使用Object blockingLock() 方法,他会返回一个非透明的对象的引用,只有拥有这个对象的线程才能修改通道的阻塞模式。

1
2
3
4
5
6
7
Socket socket=null;
SocketChannel channel = socket.getChannel();
Object lock = channel.blockingLock();
synchronized (lock){
//修改通道的阻塞模式
channel.configureBlocking(true);
}

ServerSocketChannel

ServerSocketChannel是一个基于通道的socket监听器。创建一个ServerSocketChannel

1
2
3
4
5
6
//获取一个未绑定的通道
ServerSocketChannel ssc=ServerSocketChannel.open();
//取出对等的ServerSocket对象用于绑定端口
ServerSocket socket = ssc.socket();
//绑定端口
socket.bind(new InetSocketAddress(1234));

一个向socket通道写入数据的完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//待发送的字符串
String str="hello world 你好世界";
//获取一个未绑定的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//取出对等的ServerSocket对象用于绑定端口
ServerSocket socket = ssc.socket();
//绑定端口
socket.bind(new InetSocketAddress(1234));
//将通道设置为非阻塞式的
ssc.configureBlocking(false);
while (true) {
System.out.println("等待连接");
//接收与此套接字通道连接的连接
SocketChannel sc = ssc.accept();
if (sc == null) {
Thread.sleep(1000);
} else {
System.out.println("连接到" + sc.socket().getRemoteSocketAddress());
//将待发送的字符串转换为byte数组,并包装到buffer中
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes("GB2312"));
//向通道写入数据
sc.write(buffer);

}

}

SocketChannel

1
2
3
4
5
6
//使用open()方法可以创建一个新的SocketChannel对象(但未连接)
SocketChannel sc = SocketChannel.open();
//使用socket()方法可以获取一个与SocketChannel对等的socket对象
Socket socket = sc.socket();
//获取到的channel对象和之前的sc对象是同一个
SocketChannel channel = socket.getChannel();

获取SocketChannel对象并进行连接(两种等价写法)

1
2
3
4
5
//写法一
SocketChannel sc=SocketChannel.open(new InetSocketAddress(1234));
//写法二
SocketChannel sc2=SocketChannel.open();
sc2.connect(new InetSocketAddress(1234));

可以调用boolean finishConnect()判断是否已经完成连接.Socket通道是线程安全的,不过任何时候都只有一个读操作和一个写操作在进行中。并且sockets是面向流的而非包导向的。

DatagramChannel

DatagramChannel也是一个socket通道,每一个DatagramChannel也有一个关联的datagramSocket对象。DatagramChannel是模拟包导向的并且是无连接的。
创建DatagramChannel并绑定的方法:

1
2
3
4
5
DatagramChannel channel = DatagramChannel.open();
//获取关联的socket对象
DatagramSocket socket = channel.socket();
//绑定连接
socket.bind(new InetSocketAddress(123));

可以将DatagramChannel的状态设置为已连接的,以此来忽略除了它锁连接到的地址之外的任何其它源地址的数据报被忽略。因为数据报socket是无状态的,所以DatagramChannel没有单独的inishConnet()方法,但是我们可以使用isConnect()方法来测试一个数据报通道的连接状态。

管道

广义上讲管道就是在两个实体之间单向传输数据的导管。该管道是用于进程间使用的。
ni1Q5F.png

1
2
3
4
5
6
Pipe pipe = Pipe.open();
//获取此管道的接收通道
Pipe.SinkChannel sinkChannel = pipe.sink();
//获取此管道的源通道
Pipe.SourceChannel sourceChannel = pipe.source();