Channel 类(通道)
- Channel(通道):Channel 是一个对象,可以通过它读取和写入数据。可以把它看做是 IO 中的流,不同的是:
- 为所有的原始类型提供(Buffer)缓存支持;
- 字符集编码解决方案(Charset);
- Channel : 一个新的原始 I/O 抽象;
- 支持锁和内存映射文件的文件访问接口;
- 提供多路(non-bloking)非阻塞式的高伸缩性网路 I/O;
- 正如上面提到的,所有数据都通过 Buffer 对象处理,所以,你永远不会将字节直接写入到 Channel 中,相反,您是将数据写入到 Buffer 中;同样,您也不会从 Channel 中读取字节,而是将数据从 Channel 读入 Buffer,再从 Buffer 获取这个字节;
- 因为 Channel 是双向的,所以 Channel 可以比流更好地反映出底层操作系统的真实情况。特别是在 Unix 模型中,底层操作系统通常都是双向的;
- 在 Java NIO 中的 Channel 主要有如下几种类型:
- FileChannel:从文件读取数据;
- DatagramChannel:读写 UDP 网络协议数据;
- SocketChannel:读写 TCP 网络协议数据;
- ServerSocketChannel:可以监听 TCP 连接;
1. FileChannel 类的基本使用
- java.nio.channels.FileChannel:是用于读、写文件的通道;
- FileChannel 是抽象类,可以通过 FileInputStream 和 FileOutputStream 的 getChannel() 方法方便的获取一个它的子类对象;
FileInputStream fi = new FileInputStream(new File(src));
FileOutputStream fo = new FileOutputStream(new File(dst));
FileChannel inChannel = fi.getChannel();
FileChannel outChannel = fo.getChannel();
- 通过 CopyFile 的例子可以更好体会 NIO 的操作过程。CopyFile 执行三个基本的操作:创建一个 Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class Test {
public static void main(String[] args) throws Exception {
FileInputStream fi = new FileInputStream("C:\\Users\\80626\\Desktop\\1.png");
FileOutputStream fo = new FileOutputStream("C:\\Users\\80626\\Desktop\\1_copy.png");
FileChannel inChannel = fi.getChannel();
FileChannel outChannel = fo.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int eof = 0;
while ((eof = inChannel.read(buffer)) != -1) {
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
inChannel.close();
outChannel.close();
fi.close();
fo.close();
}
}
2. FileChannel 结合 MappedByteBuffer 实现高效读写
- 上例直接使用 FileChannel 结合 ByteBuffer 实现的管道读写,但并不能提高文件的读写效率,ByteBuffer 有个子类:MappedByteBuffer,它可以创建一个“直接缓冲区”,并可以将文件直接映射至内存,可以提高大文件的读写效率;
- ByteBuffer 是抽象类,MappedByteBuffer 也是抽象类;
- 可以调用 FileChannel 的 map() 方法获取一个 MappedByteBuffer,map() 方法的原型:
MappedByteBuffer map(MapMode mode, long position, long size);
:将节点中从 position 开始的 size 个字节映射到返回的 MappedByteBuffer 中;
a. 复制 2 GB 以下的文件
- 此例不能复制大于 2 G 的文件,因为 map 的第三个参数被限制在
Integer.MAX_VALUE(字节) = 2G
;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class Test {
public static void main(String[] args) throws Exception {
try {
RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
FileChannel in = source.getChannel();
FileChannel out = target.getChannel();
long size = in.size();
MappedByteBuffer mbbi = in.map(FileChannel.MapMode.READ_ONLY, 0, size);
MappedByteBuffer mbbo = out.map(FileChannel.MapMode.READ_WRITE, 0, size);
long start = System.currentTimeMillis();
System.out.println("开始...");
for (int i = 0; i < size; i++) {
byte b = mbbi.get(i);
mbbo.put(i, b);
}
long end = System.currentTimeMillis();
System.out.println("用时: " + (end - start) + " 毫秒");
source.close();
target.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- map() 方法的第一个参数 mode:映射的三种模式,在这三种模式下得到的将是三种不同的 MappedByteBuffer:三种模式都是 Channel 的内部类 MapMode 中定义的静态常量,这里以 FileChannel 举例:
(1)FileChannel.MapMode.READ_ONLY:得到的镜像只能读不能写(只能使用 get 之类的读取 Buffer 中的内容);
(2)FileChannel.MapMode.READ_WRITE:得到的镜像可读可写(既然可写了必然可读),对其写会直接更改到存储节点;
(3)FileChannel.MapMode.PRIVATE:得到一个私有的镜像,其实就是一个 (position, size) 区域的副本罢了,也是可读可写,只不过写不会影响到存储节点,就是一个普通的 ByteBuffer 了;
- 为什么使用 RandomAccessFile?
(1) 使用 InputStream 获得的 Channel 可以映射,使用 map 时只能指定为 READ_ONLY 模式,不能指定为 READ_WRITE 和 PRIVATE,否则会抛出运行时异常;
(2) 使用 OutputStream 得到的 Channel 不可以映射!并且 OutputStream 的 Channel 也只能 write不能 read;
(3) 只有 RandomAccessFile 获取的 Channel 才能开启任意的这三种模式;
b. 复制 2 GB 以上的文件
- 下例使用循环,将文件分块,可以高效的复制大于 2 G 的文件;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class Test {
public static void main(String[] args) throws Exception {
try {
RandomAccessFile source = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1.png", "r");
RandomAccessFile target = new RandomAccessFile("C:\\Users\\80626\\Desktop\\1_copy.png", "rw");
FileChannel in = source.getChannel();
FileChannel out = target.getChannel();
long size = in.size();
long count = 1;
long copySize = size;
long everySize = 1024 * 1024 * 512;
if (size > everySize) {
count = (int) (size % everySize != 0 ? size / everySize + 1 : size / everySize);
copySize = everySize;
}
MappedByteBuffer mbbi = null;
MappedByteBuffer mbbo = null;
long startIndex = 0;
long start = System.currentTimeMillis();
System.out.println("开始...");
for (int i = 0; i < count; i++) {
mbbi = in.map(FileChannel.MapMode.READ_ONLY, startIndex, copySize);
mbbo = out.map(FileChannel.MapMode.READ_WRITE, startIndex, copySize);
for (int j = 0; j < copySize; j++) {
byte b = mbbi.get(i);
mbbo.put(i, b);
}
startIndex += copySize;
copySize = in.size() - startIndex > everySize ? everySize : in.size() - startIndex;
}
long end = System.currentTimeMillis();
source.close();
target.close();
System.out.println("用时: " + (end - start) + " 毫秒");
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. ServerSocketChannel 和 SocketChannel 创建连接
a. 服务器端 ServerSocketChannel
- 该类用于连接的服务器端,它相当于:ServerSocket;(1) 调用 ServerSocketChannel 的静态方法 open():
ServerSocketChannel serverChannel = ServerSocketChannel.open();
,打开一个通道,新频道的套接字最初未绑定;必须通过其套接字的 bind 方法将其绑定到特定地址,才能接受连接;
(2) 调用 ServerSocketChannel 的实例方法 bind(SocketAddress add):serverChannel.bind(new InetSocketAddress(8888));
,绑定本机监听端口,准备接受连接。注意,java.net.SocketAddress 是一个抽象类,代表一个 Socket 地址,可以使用它的子类:java.net.InetSocketAddress 类,其构造方法 InetSocketAddress(int port)
可以指定本机监听端口;
(3) 调用 ServerSocketChannel 的实例方法 accept():SocketChannel accept = serverChannel.accept(); System.out.println("后续代码...");
,等待连接;
i. 示例:服务器端等待连接(默认-阻塞模式)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Test {
public static void main(String[] args) {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("【服务器】等待客户端连接...");
SocketChannel accept = serverChannel.accept();
} catch (IOException e) {
e.printStackTrace();
}
}
}
ii. 通过 ServerSocketChannel 的 configureBlocking(boolean b) 方法设置 accept() 是否阻塞
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Test {
public static void main(String[] args) throws Exception {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress(8888));
System.out.println("【服务器】等待客户端连接...");
serverChannel.configureBlocking(false);
SocketChannel accept = serverChannel.accept();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 可以看到,accept() 方法并没有阻塞,而是直接执行后续代码,返回值为 null;
- 这种非阻塞的方式,通常用于"客户端"先启动,"服务器端"后启动,来查看是否有客户端连接,有,则接受连接;没有,则继续工作;
b. 客户端 SocketChannel
- 该类用于连接的客户端,它相当于:Socket;
(1) 先调用 SocketChannel 的 open() 方法打开通道:ServerSocketChannel serverChannel = ServerSocketChannel.open();
;
(2) 调用 SocketChannel 的实例方法 connect(SocketAddress add) 连接服务器:socket.connect(new InetSocketAddress("localhost", 8888));
;
i. 示例:客户端连接服务器
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
public class Test {
public static void main(String[] args) {
try (SocketChannel socket = SocketChannel.open()) {
socket.connect(new InetSocketAddress("localhost", 8888));
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客户端完毕!");
}
}
4. ServerSocketChannel 和 SocketChannel 收发信息
a. 创建服务器端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class Server {
public static void main(String[] args) {
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.bind(new InetSocketAddress("localhost", 8888));
System.out.println("【服务器】等待客户端连接...");
SocketChannel accept = serverChannel.accept();
System.out.println("【服务器】有连接到达...");
ByteBuffer outBuffer = ByteBuffer.allocate(100);
outBuffer.put("你好客户端,我是服务器".getBytes());
outBuffer.flip();
accept.write(outBuffer);
ByteBuffer inBuffer = ByteBuffer.allocate(100);
accept.read(inBuffer);
inBuffer.flip();
String msg = new String(inBuffer.array(), 0, inBuffer.limit());
System.out.println("【服务器】收到信息:" + msg);
accept.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
b. 创建客户端
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) {
try (SocketChannel socket = SocketChannel.open()) {
socket.connect(new InetSocketAddress("localhost", 8888));
ByteBuffer buf = ByteBuffer.allocate(100);
buf.put("你好服务器,我是客户端".getBytes());
buf.flip();
socket.write(buf);
ByteBuffer inBuffer = ByteBuffer.allocate(100);
socket.read(inBuffer);
inBuffer.flip();
String msg = new String(inBuffer.array(), 0, inBuffer.limit());
System.out.println("【客户端】收到信息:" + msg);
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("客户端完毕!");
}
}
c. 效果展示
- 服务器端打印结果:
- 客户端打印结果: