一、Channel概述
1. 介绍
- Channel是一种新的IO的访问方式,用于在字节缓冲区与通道另一侧的实体(可以是文件,也可以是Socket)之间进行传输数据
- Channel可以双向读写数据,也可以实现异步读写
- 程序不能直接访问Channel,Channel只能与Buffer缓冲区进行交互,即把通道中的数据读到Buffer缓冲区中,程序从缓冲区中读取数据;在写操作时,程序把数据写入Buffer缓冲区中,再把缓冲区的数据写入到Channel中
2. 常见的Channel
- FileChannel:读写文件的通道
- SocketChannel/ServerSocketChannel:读写Socket套接字中的数据
- DatagramChannel:通过UDP读写网络数据
- 总之
① 不管是哪个Channel,都不能通过构造器创建Channel
② 通道不能重复使用,打开一个通道,表示一个特定的IO服务来建立一个连接,通道关闭时,连接就没了
③ 通道可以阻塞或者非阻塞方式运行
二、Scatter/Gather
1. 介绍
-
Scatter(发散)、Gather(聚焦)是通道提供的一个重要功能(有时也称为矢量IO)
-
Scatter、Gather是指在多个缓冲区中实现一个简单的IO操作
-
Scatter是指从Channel通道中读取数据,把这些数据按照顺序分散写入到多个Buffer缓冲区中
-
Gather是指在写操作时,将多个Buffer缓冲区的数据写入到同一个Channel通道中
-
Scatter、Gather经常用于需要将传输的数据分开处理的场景
三、FileChannel
1. 介绍
- FileChannel通过RandomAccessFile、FileInputStream、FileOutputStream对象调用getChannel()方法获取
- FileChannel虽然是双向的,既可以读也可以写,但是从FileInputStream流中获得的通道只能读不能写,如果进行写操作或抛出异常。从FileOutputStream流中获得的通道只能写不能读
- 如果访问的文件是只读的,也不能执行写操作
- FileChannel是线程安全的,但是并不是所有的操作都是多线程的,如影响通道位置或者影响文件大小的操作都是单线程的
2. 内存映射文件
- FileChannel的map()方法可以把磁盘上的文件映射到计算机的虚拟内存中,把这块虚拟内存包装为一个MapedByteBuffer对象
- 当把一个文件映射到虚拟内存中,文件的内容通常不会从硬盘中读取到内存
package channel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
/**
* 内存映射文件:将文件中的数据映射到虚拟内存中,这种方式访问文件效率比较高
*
* @author swaggyhang
* @create 2023-06-11 11:24
*/
public class FileChannelMapDemo {
public static void main(String[] args) {
// 目标:把buffer/BufferCreateDemo.java源文件以内存映射的方式读取到out.txt文件中
// 1 创建当前文件的File对象
File file = new File("E:\\swaggyhang\\wlb4ssh\\NIO\\src\\buffer\\BufferCreateDemo.java");
// 从jdk 1.7开始,资源可以自动关闭
try (
// 2 获取输入和输出通道
FileChannel inChannel = new FileInputStream(file).getChannel();
FileChannel outChannel = new FileOutputStream("out.txt").getChannel()
) {
// 3 将inChannel中的数据映射到虚拟内存中
MappedByteBuffer buf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
// 把虚拟内存中的缓冲区中数据输出到outChannel中
outChannel.write(buf);
// 4 可以将buf中的内容打印出来
buf.flip(); // 翻转,切换为读模式
Charset charset = Charset.defaultCharset();
CharBuffer decode = charset.decode(buf);
System.out.println(decode);
} catch (Exception e) {
// todo
}
}
}
3. FileChannel的双向传输
- 通过RandomAccessFile获得的通道是双向传输的,既可以读也可以写
package channel;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* 使用RandomAccessFile获得的通道是双向传输的
*
* @author swaggyhang
* @create 2023-06-11 18:19
*/
public class RandomAccessFileDemo {
public static void main(String[] args) {
// 目标:将out.txt文件中的内容复制,追加到该文件的最后面
// 1 创建File对象
File file = new File("E:\\swaggyhang\\wlb4ssh\\out.txt");
System.out.println("file.length() = " + file.length()); // file.length() = 1724
try (
// 2 创建双向传输通道,可读可写
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel channel = raf.getChannel()
) {
// 3 将双向通道的数据映射到虚拟内存中
MappedByteBuffer mapbuf = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
System.out.println("mapbuf.capacity() = " + mapbuf.capacity()); // mapbuf.capacity() = 1724
// 4 设置通道的position为File对象的最大长度
System.out.println("channel.position() = " + channel.position()); // channel.position() = 0
channel.position(file.length());
// 5 将mapbuf缓冲区中的数据写入到通道中
channel.write(mapbuf);
} catch (Exception e) {
// todo
}
}
}
4. 设置缓冲区为固定大小
package channel;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* 读写文件时,设置缓冲区为固定大小
*
* @author swaggyhang
* @create 2023-06-11 19:12
*/
public class BufferFixSizeDemo {
public static void main(String[] args) {
// 目标:把out.txt文件的内容复制到out2.txt文件中
try (
// 1 创建输入和输出通道
FileChannel inChannel = new FileInputStream("E:\\swaggyhang\\wlb4ssh\\out.txt").getChannel();
FileChannel outChannel = new FileOutputStream("E:\\swaggyhang\\wlb4ssh\\out2.txt").getChannel()
) {
// 2 定义固定大小为48的字节缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
// 3 从inChannel中读取数据到buf中
int readBytes = inChannel.read(buf);
// 4 当readBytes不为-1时,表示没有读取到文件末尾
while (readBytes != -1) {
// 5 将缓冲区写入到输出通道中
buf.flip(); // 翻转,切换为读模式
outChannel.write(buf);
// 6 继续从输入通道读取数据到缓冲区中
buf.clear(); // 清空buf
readBytes = inChannel.read(buf);
}
} catch (Exception e) {
// todo
}
}
}
5. Channel到Channel的传输
- 经常需要把文件从一个位置批量传输到另一个位置,可以直接使用通道到通道之间的传输,不需要之间缓冲区传递数据
- 注意:只有FileChannel支持通道到通道之间的传输
- 通道到通道的传输非常快速,有的操作系统可以不使用用户空间,直接传递数据
package channel;
import java.io.File;
import java.io.FileOutputStream;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
/**
* 通道与通道之间的传输
*
* @author swaggyhang
* @create 2023-06-11 19:22
*/
public class Channel2ChannelDemo {
public static void main(String[] args) {
// 目标:将out.txt文件复制到out3.txt文件中
File file = new File("E:\\swaggyhang\\wlb4ssh\\out.txt");
try (
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel inChannel = raf.getChannel();
FileChannel outChannel = new FileOutputStream("E:\\swaggyhang\\wlb4ssh\\out3.txt").getChannel()
) {
// 将输入通道从0开始的所有字节传输到输出通道中
//inChannel.transferTo(0, file.length(), outChannel);
outChannel.transferFrom(inChannel, 0, file.length());
} catch (Exception e) {
// todo
}
}
}
6. Gather代码演示
- 把文件的属性和内容,通过Gather形式写到另外一个文件中
package channel;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 把文件的属性和文件的内容,通过Gather形式写到另外一个文件中
*
* @author swaggyhang
* @create 2023-06-11 19:43
*/
public class GatherChannelDemo {
public static void main(String[] args) throws Exception {
// 目标:把out.txt文件的属性及其内容写入到新文件中
// 1 获取文件的属性,将文件属性存储在header缓冲区中
File file = new File("E:\\swaggyhang\\wlb4ssh\\out.txt");
String path = file.getAbsolutePath();
long lastModified = file.lastModified();
Date lastModifiedDate = new Date(lastModified);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String headerText = "filename:" + path + "\n" + "last modified:" + sdf.format(lastModifiedDate) + "\n";
ByteBuffer header = ByteBuffer.wrap(headerText.getBytes());
// 2 将文件内容类型存储在contentBuf中
ByteBuffer contentBuf = ByteBuffer.allocate(128);
String contentType = "unknown";
long contentLength = -1;
// 3 创建一个缓冲区数组,用于存放文件属性和文件内容缓冲区
ByteBuffer[] gather = {header, contentBuf, null};
try {
FileChannel fc = new FileInputStream(file).getChannel();
// 3 把文件内容映射到虚拟内存中
MappedByteBuffer fileData = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
// 将文件内容缓冲区保存到gather中
gather[2] = fileData;
// 4 获取文件内容大小和文件内容类型
contentLength = fc.size();
contentType = URLConnection.guessContentTypeFromName(file.getAbsolutePath());
} catch (IOException e) {
ByteBuffer buffer = ByteBuffer.allocate(128);
String msg = "文件访问异常:" + e + "\n";
buffer.put(msg.getBytes());
buffer.flip();
gather[2] = buffer;
}
// 5 组装文件长度和文件内容类型的缓冲区
StringBuilder sb = new StringBuilder();
sb.append("Content-Length:").append(contentLength).append("\n");
sb.append("Content-Type:").append(contentType).append("\n");
contentBuf.put(sb.toString().getBytes());
contentBuf.flip();
// 6 将缓冲区数组gather写入到输出通道中
FileChannel outChannel = new FileOutputStream("E:\\swaggyhang\\wlb4ssh\\out4.txt").getChannel();
long writeLength = outChannel.write(gather);
// 防止gather缓冲区数组过大,一次性写不完
while (writeLength > 0) {
writeLength = outChannel.write(gather);
}
outChannel.close();
}
}
四、SocketChannel和ServerSocketChannel
1. 介绍
- ServerSocketChannel可以监听新进来的TCP通道
- SocketChannel是一个连接到TCP网络套接字的通道
2. ServerSocketChannel代码演示
package channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
/**
* @author swaggyhang
* @create 2023-06-11 21:11
*/
public class ServerSocketChannelDemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 1 建立一个未绑定ServerSocket服务器的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// ServerSocketChannel没有bind()绑定方法,需要先通过socket()方法获取ServerSocket对象,再进行绑定端口号
ssc.socket().bind(new InetSocketAddress(1234));
// 2 设置通道为非阻塞模式,当没有传入连接时,accept()方法返回null
ssc.configureBlocking(false);
// 3 服务器端一直监听
while (true) {
System.out.println("我是ServerSocket服务器,已经准备好了,就等你来了");
// 4 调用accept()方法,返回接收到的客户端socketChannel
SocketChannel sc = ssc.accept();
// 如果没有连接进来,那么sc为null
if (sc == null) {
Thread.sleep(1000);
} else {
// 5 如果客户端连接进来了,先给客户端发送一个问候
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("hello, I am from socketServer".getBytes());
buffer.flip();
sc.write(buffer);
// 6 再读取客户端中发送来的内容
System.out.println("from Socket Client:" + sc.socket().getRemoteSocketAddress());
buffer.clear();
sc.read(buffer);
buffer.flip();
Charset charset = Charset.defaultCharset();
CharBuffer decode = charset.decode(buffer);
System.out.println(decode);
sc.close();
}
}
}
}
3. SocketChannel代码演示
package channel;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
/**
* @author swaggyhang
* @create 2023-06-11 21:27
*/
public class SocketChannelDemo {
public static void main(String[] args) throws IOException {
// 1 创建一个未连接的客户端SocketChannel
SocketChannel sc = SocketChannel.open();
// 客户端与服务器建立连接
sc.connect(new InetSocketAddress("localhost", 1234));
// 2 TCP连接需要一定时间,两个连接的建立需要进行包对话
// 调用finishConnect()方法来完成连接过程
while (!sc.finishConnect()) {
System.out.println("等待连接过程中......");
}
System.out.println("连接成功");
// 3 向服务器发送消息
ByteBuffer buffer = ByteBuffer.wrap("hello, I am from client socket".getBytes());
while (buffer.hasRemaining()) {
sc.write(buffer);
}
buffer.clear();
int readLength = sc.read(buffer);
while (readLength != -1) {
readLength = sc.read(buffer);
}
buffer.flip();
Charset charset = Charset.defaultCharset();
CharBuffer decode = charset.decode(buffer);
System.out.println(decode);
}
}
五、DatagramChannel
1. 介绍
- DatagramChannel是对UDP无连接用户数据报协议的通道