Java语言进阶:Channel(通道)
Channel概述
Channel(通道):Channel是一个接口,可以通过它读取和写入数据, 可以把它看做是IO中的流,不同的是:Channel是双向的, Channel对象既可以调用读取的方法, 也可以调用写出的方法 。
-
输入流: 读
-
输出流: 写
-
Channel: 读,写
Channel 的分类
在JavaNIO中的Channel主要有如下几种类型:
- FileChannel:从文件读取数据的 输入流和输出流
- DatagramChannel:读写UDP网络协议数据 Datagram
- SocketChannel:读写TCP网络协议数据 Socket
- ServerSocketChannel:可以监听TCP连接 ServerSocket
FileChannel类的基本使用
- 使用FileChannel类完成文件的复制
路径
- 获取FileChannel类的对象
- 使用FileChannel类完成文件的复制
讲解
获取FileChannel类的对象
-
java.nio.channels.FileChannel (抽象类):用于读、写文件的通道。
-
FileChannel是抽象类,我们可以通过FileInputStream和FileOutputStream的getChannel()方法方便的获取一个它的子类对象。
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
//获得传输通道channel
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
使用FileChannel类完成文件的复制
- 我们将通过CopyFile这个示例让大家体会NIO的操作过程。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。
public static void main(String[] args) throws Exception{
FileInputStream fis = new FileInputStream("day19\\aaa\\a.txt");
FileOutputStream fos = new FileOutputStream("day19\\aaa\\aCopy1.txt");
// 获得FileChannel管道对象
FileChannel c1 = fis.getChannel();
FileChannel c2 = fos.getChannel();
// 创建ByteBuffer数组
ByteBuffer b = ByteBuffer.allocate(1000);
// 循环读取数据
while ((c1.read(b)) != -1){// 读取的字节会填充postion到limit位置之间
// 重置 postion为0,limit为postion的位置
b.flip();
// 写出数据
c2.write(b);// 会把postion到limit之间的数据写出
// 还原
b.clear();// positon为:0 limit为: capacity 用于下次读取
}
// 释放资源
c2.close();
c1.close();
fos.close();
fis.close();
/*byte[] bys = new byte[8192];
int len;
while ((len = fis.read(bys)) != -1){
fos.write(bys,0,len);
}
fos.close();
fis.close();*/
}
FileChannel结合MappedByteBuffer实现高效读写
路径
- MappedByteBuffer类的概述
- FileChannel结合MapppedByteBuffer复制2G以下文件
- FileChannel结合MapppedByteBuffer复制2G以上文件
讲解
MappedByteBuffer类的概述
-
上例直接使用FileChannel结合ByteBuffer实现的管道读写,但并不能提高文件的读写效率。
-
ByteBuffer有个抽象子类:MappedByteBuffer,它可以将文件直接映射至内存,把硬盘中的读写变成内存中的读写, 所以可以提高大文件的读写效率。
-
可以调用FileChannel的map()方法获取一个MappedByteBuffer,map()方法的原型:
MappedByteBuffer map(MapMode mode, long position, long size);
说明:将节点中从position开始的size个字节映射到返回的MappedByteBuffer中。
复制2GB以下的文件
- 复制d:\b.rar文件,此文件大概600多兆,复制完毕用时不到2秒。此例不能复制大于2G的文件,因为map的第三个参数被限制在Integer.MAX_VALUE(字节) = 2G。
public static void main(String[] args) throws Exception{
//java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
//"r"表示:只读--输入流,只读就可以。
RandomAccessFile r1 = new RandomAccessFile("day19\\aaa\\a.txt","r");
//"rw"表示:读、写--输出流,需要读、写。
RandomAccessFile r2 = new RandomAccessFile("day19\\aaa\\aCopy2.txt","rw");
// 获得FileChannel管道对象
FileChannel c1 = r1.getChannel();
FileChannel c2 = r2.getChannel();
// 获取文件的大小
long size = c1.size();
// 直接把硬盘中的文件映射到内存中
MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, 0, size);
MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, 0, size);
// 循环读取数据
for (long i = 0; i < size; i++) {
// 读取字节
byte b = b1.get();
// 保存到第二个数组中
b2.put(b);
}
// 释放资源
c2.close();
c1.close();
r2.close();
r1.close();
}
-
代码说明:
-
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才能开启任意的这三种模式!
复制2GB以上的文件
- 下例使用循环,将文件分块,可以高效的复制大于2G的文件
public static void main(String[] args) throws Exception{
//java.io.RandomAccessFile类,可以设置读、写模式的IO流类。
//"r"表示:只读--输入流,只读就可以。
RandomAccessFile r1 = new RandomAccessFile("H:\\课堂资料.zip","r");
//"rw"表示:读、写--输出流,需要读、写。
RandomAccessFile r2 = new RandomAccessFile("H:\\课堂资料2.zip","rw");
// 获得FileChannel管道对象
FileChannel c1 = r1.getChannel();
FileChannel c2 = r2.getChannel();
// 获取文件的大小
long size = c1.size();
// 每次期望复制500M
int everySize = 1024*1024*500;
// 总共需要复制多少次
long count = size % everySize == 0 ? size/everySize : size/everySize+1;
// 开始复制
for (long i = 0; i < count; i++) {
// 每次开始复制的位置
long start = everySize*i;
// 每次复制的实际大小
long trueSize = size - start > everySize ? everySize : size - start;
// 直接把硬盘中的文件映射到内存中
MappedByteBuffer b1 = c1.map(FileChannel.MapMode.READ_ONLY, start, trueSize);
MappedByteBuffer b2 = c2.map(FileChannel.MapMode.READ_WRITE, start, trueSize);
// 循环读取数据
for (long j = 0; j < trueSize; j++) {
// 读取字节
byte b = b1.get();
// 保存到第二个数组中
b2.put(b);
}
}
// 释放资源
c2.close();
c1.close();
r2.close();
r1.close();
}
ServerSocketChannel和SocketChannel创建连接
路径
- SocketChannel创建连接
- ServerSocketChanne创建连接
讲解
SocketChannel创建连接
-
客户端:SocketChannel类用于连接的客户端,它相当于:Socket。
1). 先调用SocketChannel的open()方法打开通道:
SocketChannel socket = SocketChannel.open()
2). 调用SocketChannel的实例方法connect(SocketAddress add)连接服务器:
socket.connect(new InetSocketAddress("localhost", 8888));
示例:客户端连接服务器:
public class Client { public static void main(String[] args) throws Exception { SocketChannel socket = SocketChannel.open(); socket.connect(new InetSocketAddress("localhost", 8888)); System.out.println("后续代码......"); } }
ServerSocketChanne创建连接
-
服务器端:ServerSocketChannel类用于连接的服务器端,它相当于:ServerSocket。
-
调用ServerSocketChannel的静态方法open()就可以获得ServerSocketChannel对象, 但并没有指定端口号, 必须通过其套接字的bind方法将其绑定到特定地址,才能接受连接。
ServerSocketChannel serverChannel = ServerSocketChannel.open()
-
调用ServerSocketChannel的实例方法bind(SocketAddress add):绑定本机监听端口,准备接受连接。
注:java.net.SocketAddress(抽象类):代表一个Socket地址。
我们可以使用它的子类:java.net.InetSocketAddress(类)
构造方法:InetSocketAddress(int port):指定本机监听端口。
serverChannel.bind(new InetSocketAddress(8888));
-
调用ServerSocketChannel的实例方法accept():等待连接。
SocketChannel accept = serverChannel.accept(); System.out.println("后续代码...");
示例:服务器端等待连接(默认-阻塞模式)
public class Server { public static void main(String[] args) throws Exception{ ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(8888)); System.out.println("【服务器】等待客户端连接..."); SocketChannel accept = serverChannel.accept(); System.out.println("后续代码......"); } }
运行后结果:
【服务器】等待客户端连接...
-
我们可以通过ServerSocketChannel的configureBlocking(boolean b)方法设置accept()是否阻塞
public class Server { public static void main(String[] args) throws Exception { ServerSocketChannel ssc = ServerSocketChannel.open(); ssc.bind(new InetSocketAddress(8888)); System.out.println("【服务器】等待客户端连接..."); //设置非阻塞连接 ssc.configureBlocking(false);// 写成false叫非阻塞, 写成true叫阻塞 while(true) { //获取客户端连接 SocketChannel sc = ssc.accept(); if(sc != null){ //不等于null说明连接上了客户端 System.out.println("连接上了。。"); //读取数据操作 break; }else{ //没连接上客户端 System.out.println("打会儿游戏~"); Thread.sleep(2000); } } } }
运行后结果:
【服务器】等待客户端连接... 打会儿游戏~ 有客户端来了就输出: 连接上了。。
NIO网络编程收发信息
- 使用ServerSocketChannel代替之前的ServerSocket,来完成网络编程的收发数据。
书写服务器代码
public class Server {
public static void main(String[] args) throws IOException{
//创建对象
//ServerSocket ss = new ServerSocket(8888);
//创建
ServerSocketChannel ssc = ServerSocketChannel.open();
//服务器绑定端口
ssc.bind(new InetSocketAddress(8888));
//连接上客户端
SocketChannel sc = ssc.accept();
//服务器端接受数据
//创建数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//接收数据
int len = sc.read(buffer);
//打印结构
System.out.println(new String(buffer.array(),0,len));
//关闭资源
sc.close();
}
}
书写客户端代码
public class Client {
public static void main(String[] args) {
//创建对象
//Socket s = new Socket("127.0.0.1",8888);
//创建对象
SocketChannel sc = SocketChannel.open();
//连接服务器
sc.connect(new InetSocketAddress("127.0.0.1",8888));
//客户端发数据
//创建数组
ByteBuffer buffer = ByteBuffer.allocate(1024);
//数组中添加数据
buffer.put("你好啊~".getBytes());
//切换
buffer.flip();
//发出数据
sc.write(buffer);
//关流
sc.close();
}
}