目录
简介
通道Channel是NIO里面的一个创新点,用于缓冲区和文件或者套接字之间的数据传输。通道Channel的继承体系相对比较复杂,主要在java.nio.channels,部分的channels类还会依赖java.nio.channels.spi子包。顶层的channe接口定义判断通道是否打开和关闭通道的方法。
package java.nio.channels;
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
我们知道传统的IO流都是单向的,比如FileInputStream只能读取数据和FileOutputStream只能写入数据。而通道Channel可以是单向的,也可以是双向的,如果一个channel类实现了定义read()方法的ReadableByteChannel接口或者实现了定义write()方法的WritableByteChannel接口都只是单向的,但同时实现了这两个接口就是双向通道(既可以读,又可以写),ByteChannel就是这样一个接口,继承了ReadableByteChannel接口和WritableByteChannel接口,所以ByteChannel子类通道都是双向的。
通道可以分为两种类型,一种是文件(file)通道和套接字(socket)通道。都属于ByteChannel子类,所以是双向的通道。
a.文件(file)通道
- FileChannel
b.套接字(socket)通道
- SocketChannel
- ServerSocketChannel
- DatagramChannel
文件通道FileChannel
1.FileChannel介绍
文件通道FileChannel里面定义常用的read,write,scatter/gatter操作,同时提供了操作文件的方法。文件通道FileChannel不能直接创建,只能通过打开的RandomAccessFile,FileInputStream,FileOutputStream对象上调用getChannel()方法获取。调用getChannel()所获取的FileChannel()对象与流对象连接的是同一个文件,并且具有相同访问权限。
所以尽管FileChannel定义上是双向通道,但一个FileInputStream对象上获取的FileChannel对象只能读取数据,不能写入数据。FileChannel总是在阻塞模式下,不能设置为非阻塞模式。
//将字节序列读取到给定的缓冲区
public abstract int read(ByteBuffer dst)
//将字节序列读取到给定的缓冲区
public final long read(ByteBuffer[] dsts)
//将字节序列读取到给定的缓冲区指定位置
public abstract long read(ByteBuffer[] dsts, int offset, int length)
//从给定的文件位置position开始,从通道中读取数据到缓冲区中
public abstract int read(ByteBuffer dst, long position)
//将给定的缓冲区中的字节序列写到通道中
public abstract int write(ByteBuffer src)
//将给定的多个缓冲区中字节序列写到通道中
public final long write(ByteBuffer[] srcs)
//将给定的缓冲区中字节序列写到通道中,文件位置从position开始
public abstract int write(ByteBuffer src, long position)
//将给定的多个缓冲区指定位置字节序列写到通道中
public abstract long write(ByteBuffer[] srcs, int offset, int length)
//返回此通道中文件位置
public abstract long position()
//设置此通道中文件位置
public abstract FileChannel position(long newPosition)
//将通道中的文件截取为指定的大小
public abstract FileChannel truncate(long size)
//强制将通道中的文件写到包含该文件的存储设备中
public abstract void force(boolean metaData)
访问文件的方法中可以看到不管read()还会write()方法都有position的方法,position在此处并不是缓冲区Buffer里面的索引位置,而是文件中读取或者写入的位置。可以将文件看做的是一个庞大的字节数组,利用position就可以实现将数据写到指定的文件位置或者读取文件中指定位置数据,操作类似于缓冲区。此外还提供了truncate()方法来丢弃文件中超过参数size以外的数据,force()方法会将通道中待修改的数据强制应用到磁盘上,做到对文件待定修改及时同步到磁盘,force(Boolean metaData)方法中布尔类型参数表示的是是否将元数据(文件所有者,访问权限,最后修改时间等信息)也进行同步。
2.文件锁
JDK1.4版本开始提供文件锁功能,文件锁有独占锁和共享锁两种。
//获取通道文件中指定位置的独占锁
public abstract FileLock lock(long position, long size, boolean shared)
//获取通道文件的独占锁
public final FileLock lock()
//尝试获取通道文件中指定位置的独占锁
public abstract FileLock tryLock(long position, long size, boolean shared)
//尝试获取通道文件的独占锁
public final FileLock tryLock()
可以看到文件锁有两种类型,lock()和tryLock()两种类型。
lock()除非文件指定位置锁定,通道关闭或者线程中断其中一种情况,否则方法将会一直阻塞。带有参数的lock()方法指定文件内部锁定的开始位置position以及锁定区域的size,shared参数表示想获取锁是否是共享的。
tryLock()方法,不管能否成功获取锁,将会立即返回,方法不会阻塞。获取一个其他程序已经占用的锁,将返回null,其他原因导致的获取锁失败,将会抛出异常。
有些操作系统不支持共享锁,所以请求共享锁将会自动转化成独占锁,一个请求过来获取的锁是共享还是独占的可通过调用FileLock中isShared()方法查看。锁的范围不一定是限制在size范围内,可以锁定未包含任何的内容的区域,这样下次写入数据时,对应区域就可以受到文件锁保护,如果只是锁定文件中某块内容,那么下次写到文件中的数据将会超出锁定区域,不会受到文件锁的保护。
需要注意的是文件锁对象FileLock与FileChannel实例关联,但锁的对象是文件,而不是通道和线程,所以使用完锁后要及时释放,不然会导致冲突或者死锁。
3.内存映射文件
调用FileChannel中的map()方法可以得到MappedByteBuffer对象(ByteBuffer的子类),一般读取文件简易过程是先将数据读取到内核内存缓冲区中临时存放,缓冲区满后,内核会将数据复制到用户空间的缓冲区中。内存映射大致就是将用户空间的缓冲区和内核空间缓冲区映射到同一个地方,但是实际存储数据的地方是磁盘上,这样省略了中间复制到内核缓冲区的过程。对于频繁访问文件或者更改比较大的文件时,使用内存映射文件将会极大提高效率。
public abstract MappedByteBuffer map (MapMode mode, long position,long size)
map()方法需要传三个参数,mode有三种模式:MapMode.READ_ONLY(只读模式),MapMode.READ_WRITE(读写模式), MapMode.PRIVATE(写时拷贝,调用put()修改的数据最终不会写到文件中,只能通过MappedByteBuffer实时调用get()查看)。
Socket通道
socket通道是以非阻塞模式运行,依靠他们共同的超父类是java.nio.channelss.spi中AbstractSelectableChannel,由此得到一种通道的选择机制,socket通道和选择器Selector联合使用,Selector可以看做用来管理通道的类,多个通道可以注册到选择器上,通过轮询来查看通道是否就绪状态(是否准备好读写操作)。设置通道的阻塞模式只要调用configureBlocking()方法即可。此外每个socket通道都关联一个java.net.socket对象,但通过传统方式创建Socket对象,不会关联到通道。
1.SocketChannel
SocketChannel通道针对的是点对点的连接,类似于TCP/IP,面向流的连接。只有连接成功情况下,才可以接收到数据或者发送数据给连接的地址。创建SocketChannel方式有两种:
- 调用SocketChannel中open()方法打开通道并连接到指定地址。
- ServerSocketChannel用来监听连接,连接过来时会创建SocketChannel。
打开SocketChannel通道
SocketChannel socketChannel = SocketChannel.open( );
socketChannel.connect (new InetSocketAddress ("address", port));
关闭SocketChannel通道
socketChannel.close()
读取数据
SocketChannel类中提供了多个read()方法:read(ByteBuffer dst),read(ByteBuffer[] dsts, int offset, int length),
read(ByteBuffer[] dsts)。可以看到参数都与缓冲区ByteBuffer关联,从socketChannel中读取的数据都会先放到缓冲区Buffer中。
ByteBuffer buffer = ByteBuffer.allocate(100);
socketChannel.read(buffer);
写入数据
SocketChnnel类提供了多个write()方法:write(ByteBuffer src), write(ByteBuffer[] srcs, int offset, int length), write(ByteBuffer[] srcs)。写入SocketChannel通道的数据,同样是先放到缓冲区中,然后再写入通道中。
ByteBuffer buffer = ByteBuffer.allocate(50);
buffer.put("hello".getBytes());
buffer.flip();
socketChannel.write(buffer);
2.ServerSocketChannel
ServerSocketChannel没有提供发送和接收数据的方法,只是用于监听TCP连接过来的通道。ServerSocketChannel类提供bind()方法或者调用socket()方法获取对应的ServerSocket对象来绑定指定地址/端口。
打开通道
调用静态方法open()创建一个未绑定的通道serverSocketChannel对象,调用bind()或者socket()对象获取ServerSocketChannel()对象绑定地址/端口。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
//或者获取对应ServerSocket对象绑定
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
关闭通道
serverSocketChannel.close()
监听连接
调用accept()会返回一个包含了新连接的SocketChannel对象,可以在非阻塞模式下运行。如果以非阻塞模式下运行,调用accept()方法时还没有连接进来,会立即返回null。
while(true){
SocketChannel socketChannel=serverSocketChannel.accept();
....
}
3.DatagramChannel
DatagramChannel模拟的是包导向的连接(UDP/IP),也就是不需要连接,就可以发送数据给不同目的地,也可以接受任意地址过来的数据(数据里面包含地址信息)。DatagramChannel可以通过调用静态方法open()打开通道,调用socket()方法可以得到对应的DatagramSocket对象。DatagramChannel既可以充当服务器(监听端)又可以充当客户端(接收端),用于监听的时候,需要将通道绑定到一个地址或者端口上面。调用send()方法并不能保证数据能够发送到另外一端,因为数据在传送过程会被拆分成多个数据碎片,到目的端再组合起来,如果其中一个碎片丢失了,将会导致整个数据报丢失。
DatagramChannel可以进行任意次数的断开和连接,应用场景就是客户端/服务端的方式,connect()方法中如果传入指定地址,就可以只接收指定地址过来的数据忽略其他来源的数据。
打开DatagramChannel通道
调用静态open()创建一个通道,监听时需要调用bind()方法绑定地址/端口。
DatagramChannel channel = DatagramChannel.open();
DatagramSocket socket = channel.socket();
//DatagramChannel用于监听时,需要绑定端口
socket.bind(new InetSocketAddress(port));
接收数据
阻塞模式下,receive()方法会一直等待,直到接收到数据。非阻塞模式下,如果没有接收到数据时,会返回null。如果接收到的数据超出了缓冲区的容量,会丢弃超出的部分。
ByteBuffer buffer = ByteBuffer.allocate(100);
channel.receive(buffer);
发送数据
调用send()方法会将缓冲区Buffer中的数据发送到DatagramChannel绑定的地址和端口上,如果通道处于阻塞模式下,调用线程可能会休眠知直到数据报加入到传送的队列中。如果处于非阻塞模式下,将返回缓冲区中字节数或者是“0”。
ByteBuffer buffer = ByteBuffer.allocate(48);
buffer.put("hello".getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("address", port));
特定连接
DatagramChannel中提供了传入地址的connect()方法,但是功能并不是和指定的地址建立连接,而是锁住了DatagramChannel,只接收指定地址过来的数据,而不接收其他地址过来的数据。
channel.connect(new InetSocketAddress("address", port));
其他内容
1.管道
Pipe类在java.nio.channels.spi包下面,调用静态方法open()创建通道Pipe对象。在Pipe对象上调用sink()方法可以得到Pipe.sinkChannel()(管道写入端),调用source()方法可以得到Pipe.sourceChannel(管道读取端)。功能类似于java.io.PipedOutputStream和java.io.PipedInputStream。
//创建管道对象
Pipe pipe = Pipe.open();
//写入数据管道
Pipe.SinkChannel sinkChannel = pipe.sink();
//读取数据管道
Pipe.SourceChannel sourceChannel = pipe.source();
2.通道工具类
在java.nio.channels包下面提供了通道的工具类Channels.如下方法都是静态的方法,直接通过Channels.xxxx()直接调用。
方法 | 返回 | 描述 |
---|---|---|
newChannel(final InputStream in) | ReadableByteChannel | 构造一个将从给定的输入流读取数据的通道。 |
newChannel(final OutputStream out) | WritableByteChannel | 构造一个将向给定的输出流写入数据的通道。 |
newReader(ReadableByteChannel ch, CharsetDecoder dec,int minBufferCap) | Reader | 从给定的通道读取字节并依据提供的CharsetDecoder 对读取到的字节进行解码 |
newReader(ReadableByteChannel ch, String csName) | Reader | 从给定的通道读取字节并依据提供的字符集名称将读取到的字节解码成字符。 |
newWriter(final WritableByteChannel ch, final CharsetEncoder enc, final int minBufferCap) | Writer | 使用给定的 CharsetEncoder 对象对字符编码后写到给定的通道中。 |
newWriter(WritableByteChannel ch, String csName) | Writer | 使用给定的字符集名称对字符编码后写到给定的通道中。 |
newInputStream(ReadableByteChannel ch) | InputStream | 构造一个从给定的通道读取字节的流。 |
newOutputStream(final WritableByteChannel ch) | OutputStream | 构造一个将向给定的通道写入字节的流。 |
3.Scatter/Gather
Scatter就是将从Channel读取的数据按顺序放到(分散)多个缓冲区Buffer中,会顺序填满每个缓冲区直到通道中数据读取完。
Gather就是将多个缓冲区中顺序抽取(聚集)到同一个Channel通道中。
使用场景一般是需要将数据分开进行发送,比如消息头和消息头分开发送,方便数据的处理。如下:从通道接收到字节为38个,那么会先header缓冲区填满,剩余28个字节会被放到body缓冲区中。聚集过程是将数据顺序抽取,先将header中的10个字节发送到通道中,然后是body中字节发送到通道中。
//scatter分散数据
ByteBuffer header = ByteBuffer.allocateDirect (10);
ByteBuffer body = ByteBuffer.allocateDirect (100);
ByteBuffer [] buffers = { header, body };
int bytesRead = channel.read (buffers);
//gather聚集数据
ByteBuffer header = ByteBuffer.allocate(10);
ByteBuffer body = ByteBuffer.allocate(100);
ByteBuffer[] bufferArray = { header, body };
channel.write(bufferArray);
案例
测试案例所有流关闭正常应该通过try{}catch{}finally在finally中关闭,而不是直接抛出异常的方式。
1.FileChannel相关
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
testFileChannel();
long start = System.currentTimeMillis();
copyFileByFileChannel();
long end = System.currentTimeMillis();
copyFileByFileChannel2();
long end1 = System.currentTimeMillis();
copyFileByMappedByteBuffer();
long end2 = System.currentTimeMillis();
System.out.println("Copy by FileChannel+Buffer----"+(end-start));
System.out.println("Copy by FileChannel+transferTo()----"+(end1-end));
System.out.println("Copy by MapByteBuffer---"+(end2-end1));
}
/**通过MappedByteBuffer实现文件复制
*/
private static void copyFileByMappedByteBuffer() throws FileNotFoundException, IOException {
FileInputStream fis = new FileInputStream(new File("D:\\java.txt"));
FileChannel fc = fis.getChannel();
MappedByteBuffer mapBuffer =fis.getChannel().map(MapMode.READ_ONLY,0,fc.size());
FileOutputStream fos = new FileOutputStream(new File("D:\\map.txt"));
FileChannel channel = fos.getChannel();
channel.write(mapBuffer);
fis.close();
fos.close();
}
/**
* 通过FileChannel读取文件
*/
private static void testFileChannel() throws IOException {
FileInputStream fis = new FileInputStream(new File("D:\\java.txt"));
FileChannel channel = fis.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int i ;
while((i=channel.read(buffer))!=-1) {
buffer.flip();
System.out.println(Charset.forName(System.getProperty("file.encoding")).decode(buffer));
buffer.clear();
}
channel.close();
fis.close();
}
/**使用fileChannel结合Buffer实现文件的复制
*/
private static void copyFileByFileChannel() throws IOException {
FileInputStream fis = new FileInputStream(new File("D:\\java.txt"));
FileOutputStream fos = new FileOutputStream(new File("D:\\copy.txt"));
FileChannel in = fis.getChannel();
FileChannel out = fos.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(in.read(buffer)!=-1) {
buffer.flip();
out.write(buffer);
buffer.clear();
}
fos.close();
fis.close();
}
/**
* 调用FileChannel中transferTo()可以实现两个通道的连接。
*/
private static void copyFileByFileChannel2() throws IOException {
FileInputStream fis = new FileInputStream(new File("D:\\java.txt"));
FileOutputStream fos = new FileOutputStream(new File("D:\\copy.txt"));
FileChannel in = fis.getChannel();
FileChannel out = fos.getChannel();
in.transferTo(0, in.size(), out);
fos.close();
fis.close();
}
}
运行结果:
Copy by FileChannel+Buffer----11
Copy by FileChannel+transferTo()----3
Copy by MapByteBuffer---0
2.Scatter/Gatter案例
public class MappedByteBufferDemo {
private static final String OUTPUT_FILE = "D:\\info.txt";
private static final String INPUT_FILE = "D:\\java.txt";
private static final String LINE_SEP = "\r\n";
private static final String SERVER_ID = "Server: Ronsoft Dummy Server";
private static final String HTTP_LINE = "HTTP/1.0 200 OK" + LINE_SEP + SERVER_ID + LINE_SEP;
public static void main(String[] argv) throws Exception {
ByteBuffer requestHeader = ByteBuffer.allocate(128);
ByteBuffer requestLine = ByteBuffer.wrap(bytes(HTTP_LINE));
//请求报文包含line,header,body。将三部分缓冲区内容写到文件中
ByteBuffer[] gather = {requestLine, requestHeader, null};
String contentType = "unknown/unknown";
long contentLength = -1;
try {
FileInputStream fis = new FileInputStream(INPUT_FILE);
FileChannel fc = fis.getChannel();
MappedByteBuffer requestBody = fc.map(MapMode.READ_ONLY, 0, fc.size());// MappedByteBuffer只读模式
gather[2] = requestBody;
contentLength = fc.size();
contentType = URLConnection.guessContentTypeFromName(INPUT_FILE);
} catch (IOException e) {
}
StringBuffer sb = new StringBuffer();
sb.append("Content-Length: " + contentLength);
sb.append(LINE_SEP);
sb.append("Content-Type: ").append(contentType);
sb.append(LINE_SEP).append(LINE_SEP);
requestHeader.put(bytes(sb.toString()));
requestHeader.flip();
FileOutputStream fos = new FileOutputStream(OUTPUT_FILE);
FileChannel out = fos.getChannel();
while (out.write(gather) > 0) {
}
out.close();
}
private static byte[] bytes(String string) throws Exception {
return string.getBytes("UTF-8");
}
}
3.Channels工具类的使用
public static void main(String[] args) throws IOException {
ReadableByteChannel source = Channels.newChannel(System.in);
WritableByteChannel dest = Channels.newChannel(System.out);
channelCopy(source,dest);
source.close();
dest.close();
}
private static void channelCopy(ReadableByteChannel source,WritableByteChannel dest) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(source.read(buffer)!=-1) {
buffer.flip();
dest.write(buffer);
buffer.clear();
}
}
}
总结
1.通道分为文件通道和套接字通道,都是双向通道,即可以写又可以读取。需要注意的是文件通道FileChannel没有直接创建的方法,只能在RandomAcessFile,FileInputStream,FileInputStream对象上调用getChannel()获取,所以会受到对应流对象的限制,比如FileInputStream对象上获取的FileChannel也只能读取数据,调用write()将会抛出异常。
2.文件通道里面提供了文件锁以及内存映射等功能,实现了文件安全以及高效率访问。
2.SocketChannel是面向连接的,类似于TCP/IP,只能发送和接收指定地址的数据。而DatagramChannel面向无连接,类似于UDP/IP,可以接收和发送到任意地址的数据。ServerSocketChannel本身没有提供读取方法,只是用于监听连接。