0%

(二)NIO - Channel

1. 通道的概念

通道(Channel):由 java.nio.channels 包定义的。 Channel 表示 IO 源与目标打开的连接。Channel 类似于传统的“流”。只不过 Channel本身不能直接访问数据, Channel 只能与Buffer 进行交互。

2. 主要实现类

Java 为 Channel 接口提供的最主要实现类如下:

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

3. 获取通道

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:

FileInputStream、FileOutputStream、RandomAccessFile、DatagramSocket、Socket 和 ServerSocket。

获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。

4. FileChannel 通道的数据传输

下图所示的是FileChannel类的一些常用方法:
在这里插入图片描述

4.1 文件复制

示例一:下面演示一下使用通道和非直接缓冲区实现文件的复制:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Test
public void fileCopy() {
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
try {
fis = new FileInputStream("1.txt");
fos = new FileOutputStream("2.txt");

// 1. 获取通道
inChannel = fis.getChannel();
outChannel = fos.getChannel();

// 2. 分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);

// 3. 读取通道中的数据并写入缓冲区中,position后移
while (inChannel.read(buf) != -1) {
// 将缓冲区切换读取数据的模式,position为0,limit改变
buf.flip();
// 4. 读取缓冲区中的数据并写入通道中
outChannel.write(buf);
buf.clear();// 清空缓冲区
}
} finally {
if (outChannel != null)
outChannel.close();
if (inChannel != null)
inChannel.close();
if (fos != null)
fos.close();
if (fis != null)
fis.close();
}

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

示例二:下面演示一下使用通道和直接缓冲区实现文件的复制:

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
27
28
29
30
31
32
33
@Test
public void fileCopyDirect() {
try {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
//建立文件读取通道。StandardOpenOption.READ指明了以读取的方式打开文件
inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
//建立文件输出通道。StandardOpenOption.CREATE表明如果文件不存在则创建
outChannel = FileChannel.open(Paths.get("3.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ,
StandardOpenOption.CREATE);
//使用内存映射文件
//第一个参数执行映射时的模式,分别有只读和读写等模式
//第二个和第三个参数用于控制将Channel指定范围的数据映射成ByteBuffer
MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuf.limit()];
//将缓冲区的数据存储进数组中
inMappedBuf.get(dst);
//将数组中的数据存储进缓冲区中
outMappedBuf.put(dst);
} finally {
if (inChannel != null)
inChannel.close();
if (outChannel != null)
outChannel.close();
}

} catch (IOException e) {
e.printStackTrace();
}
}

示例三:下面演示一下使用通道之间的数据传输来实现文件的复制:

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
@Test
public void fileCopyDirect2() {
try {
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
// 建立文件读取通道。StandardOpenOption.READ指明了以读取的方式打开文件
inChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ);
// 建立文件输出通道。StandardOpenOption.CREATE表明如果文件不存在则创建
outChannel = FileChannel.open(Paths.get("5.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ,
StandardOpenOption.CREATE);

//将inChannel通道指定范围大小的数据传输到outChannel中
// inChannel.transferTo(0, inChannel.size(), outChannel);
//从通道inChannel中以指定范围大小的数据传输到outChannel
outChannel.transferFrom(inChannel, 0, inChannel.size());
} finally {
inChannel.close();
outChannel.close();
}

} catch (IOException e) {
e.printStackTrace();
}
}

4.2 分散(Scatter)和聚集(Gather)

分散读取(Scattering Reads)是指从 Channel 中读取的数据“分散” 到多个 Buffer 中。需要按照缓冲区的顺序,从 Channel 中读取的数据依次将 Buffer 填满。而聚集写入(Gathering Writes)是指将多个 Buffer 中的数据“聚集”到 Channel。按照缓冲区的顺序,写入 position 和 limit 之间的数据到 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Test
public void ScatteringRead() {
try {
RandomAccessFile raf = null;
FileChannel channel = null;
RandomAccessFile raf2 = null;
FileChannel channel2 = null;
try {
raf = new RandomAccessFile("1.txt", "rw");
//获取通道
channel = raf.getChannel();
//分配指定大小的缓冲区
ByteBuffer buf1 = ByteBuffer.allocate(10);
ByteBuffer buf2 = ByteBuffer.allocate(100);
//分散读取
ByteBuffer[] bufs = {buf1, buf2};
channel.read(bufs);

for (ByteBuffer byteBuffer : bufs) {
//因为channel.read(bufs);执行时会将数据写入缓冲区中,即position已经改变,需flip
byteBuffer.flip();
}
System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
System.out.println("----------------");
System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));

//聚集写入
raf2 = new RandomAccessFile("2.txt", "rw");
channel2 = raf2.getChannel();
channel2.write(bufs);

} finally {
if (channel != null)
channel.close();
if (raf != null)
raf.close();
if (channel2 != null)
channel2.close();
if (raf2 != null)
raf2.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}

控制台输出如下结果:

1
2
3
4
5
asfasfasfa
----------------
sf‘sa
阿是大师大师的
啊实打实的看见爱好

文件 2.txt保存有文件1.txt中的如下数据,同时也是从多个缓冲区中读到的数据。

1
2
3
4
asfasfasfa
sf‘sa
阿是大师大师的
啊实打实的看见爱好

5. 字符集 Charset

Charset类提供了一个availableCharsets静态方法来获取当前JDK所支持的所有字符集。下面代码用于获取输出该JDK所支持的所有字符集:

1
2
3
4
5
6
7
8
9
10
@Test
public void getCharsets() {
SortedMap<String,Charset> map = Charset.availableCharsets();

Set<Entry<String,Charset>> set = map.entrySet();

for (Entry<String, Charset> entry : set) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}

运行结果:

1
2
3
4
5
6
7
8
Big5=Big5
Big5-HKSCS=Big5-HKSCS
CESU-8=CESU-8
EUC-JP=EUC-JP
EUC-KR=EUC-KR
GB18030=GB18030
GB2312=GB2312
...

下面演示一下对缓冲区的数据进行指定编码和解码的操作:

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
27
28
29
30
31
32
33
34
35
@Test
public void testEncoding() throws CharacterCodingException {
Charset cs = Charset.forName("GBK");
//获取编码器
CharsetEncoder encoder = cs.newEncoder();
//获取解码器
CharsetDecoder decoder = cs.newDecoder();

//向字符缓冲区添加数据
CharBuffer cBuf = CharBuffer.allocate(1024);
cBuf.put("你好");
cBuf.flip();

//对缓冲区的数据进行编码,encode会改变cBuf的position位置
ByteBuffer bBuf = encoder.encode(cBuf);

//输出-60-29-70-61
for (int i = 0; i < bBuf.limit(); i++) {
System.out.print(bBuf.get());
}

//对缓冲区的数据进行解码
bBuf.flip();
//decode会改变缓冲区中position的位置
CharBuffer cBuf2 = decoder.decode(bBuf);
System.out.println(cBuf2.toString());//你好

//使用其他解码器进行解码,会导致乱码
bBuf.flip();
Charset csUTF8 = Charset.forName("ISO-8859-1");
CharsetDecoder decoderUTF8 = csUTF8.newDecoder();
CharBuffer cBufUTF8 = decoderUTF8.decode(bBuf);
System.out.println(cBufUTF8.toString());//ÄãºÃ

}

注意:要时刻注意缓冲区中position的位置!无论是get方法、put方法还是encode方法和decode方法都会改变缓冲区中的position位置。

对于 DatagramChannel、SocketChannel 和 ServerSocketChannel 这三个通道类将放于下一篇博客讲。

------ 本文结束------