Java NIO中,一个socket连接使用一个Channel(通道)来表示。对应到不同的网络传输协议类型,在Java中都有不同的NIO Channel(通道) 相对应。其中最为重要的四种Channel(通道)实现: FileChannel、 SocketChannel、 ServerSocketChannel、 DatagramChannel :
-
FileChannel
文件通道,用于文件的数据读写; (管文件的传输通道) -
SocketChannel
套接字通道,用于Socket套接字TCP连接的数据读写; (管TCP数据传输的通道) -
ServerSocketChannel
服务器套接字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道; (只管与服务器 TCP连接 的通道) -
DatagramChannel
数据报通道,用于UDP协议的数据读写。 (管UDP数据传输的通道)
我在学习Channel的时候,老是搞不清楚ServerSocketChannel
和SocketChannel
的关系,这次我不允许我的读者也搞不清。用大白话讲就是通道你可以理解为数据传输的管道,这个管道是双向传输的,即既可以通过Channel向文件或者网络客户端写数据也可以从文件或者网络客户端读数据。如果你要读取文件的数据,使用FileChannel
;如果需要建立网络连接,在服务器使用ServerSocketChannel
来作为客户端连接请求的通道,也就是说它只负责服务器端的连接请求的数据传输。通过ServerSocketChannel
就可以和服务器建立连接,然后通过ServerSocketChannel
创建SocketChannel
通道进行TCP数据传输。下面分别介绍每一个通道的用法。
FileChannel文件通道
FileChannel是专门操作文件的通道。通过FileChannel,既可以从一个文件中读取数据,也可以将数据写入到文件中。特别申明一下, FileChannel为阻塞模式,不能设置为非阻塞模式。不说你也知道,学习IO操作可以首先要获取FileChannel通道 、然后读取FileChannel通道中的数据或者将数据写入FileChannel通道,然后关闭通道。最后补充一个就是强制将通道的数据刷盘到磁盘的方法即可,那么就按照上面的步骤开始吧!
获取到FileChannel对象
获取FileChannel对象有三种方式,第一种方式可以通过文件的输入流、输出流获取FileChannel文件通道,代码如下:
//创建一个文件输入流
FileInputStream fis = new FileInputStream("word.txt");
//获取文件流的通道,只能从通道中读取数据,不能写入数据
FileChannel inChannel = fis.getChannel();
//创建一个文件输出流
FileOutputStream fos = new FileOutputStream("word.txt");
//获取文件流的通道,只能向通道中写入数据,不能读取数据
FileChannel outchannel = fos.getChannel();
也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道实例,代码如下:
// 创建 既可以写也可以读的随机访问类 RandomAccessFile 随机访问对象
// 参数"rw"表示可读可写,如果只读可以给"r",只写给"w"即可
RandomAccessFile rFile = new RandomAccessFile("word.txt", "rw");
//获取文件流的通道(可读可写)
FileChannel channel = rFile.getChannel();
从FileChannel中读取数据
下面给出标准的读取数据的代码,具体解释在注释中,代码中channel.read(buffer)将通道的数据读到缓冲区上,虽然是读取通道的数据,对于通道来说是读取模式,但是对于ByteBuffer缓冲区来说则是写入数据,这时, ByteBuffer缓冲区处于写入模式 ,而buffer.get()才是从通道读取数据,需要flip()切换读模式:
try(FileChannel channel = new RandomAccessFile("word.txt", "rw").getChannel()){
// 准备缓冲区,分配10字节的空间
ByteBuffer buffer = ByteBuffer.allocate(10);
int len = -1;
while ((len=channel.read(buffer))!=-1){ // 将channel中的数据读取到缓存区中,返回读到的数据长度,没读到数据返回-1
buffer.flip(); // 切换读取模式,左右指针指向已存数据首位
while (buffer.hasRemaining()){// 如果position<limit,即还可以读
byte b = buffer.get();//读取字节流,读指针向后移动一个位置,补充buffer.get(i)可以读取指定坐标的字节
log.debug("读取到的字节:"+(char)b); // log.debug 可以换成 System.out.println
}
buffer.clear(); // 读完了buffer,将buffer的指针重新回归buffer首尾
//buffer.compact(); // 如果未读完, 压缩,将未读的放在左边
}
}catch (IOException e){
log.error("文件未找到");
}
上面将byte转为char类型需要解释一下:字节是8位,而char
是16位,因此在将字节转换为char
时,只有低8位的数据被使用,高8位的数据被丢弃。这意味着字节的范围[-128, 127]将被映射到char
的范围[0, 255],只看整数部分相当于int类型转为long类型,上转型。如果字节表示的是ASCII字符,那么这种转换通常是安全的,因为ASCII字符的范围是0到127。因为wold.txt中的只有英文字符所以没问题,有汉字不行。一般不会这样使用,只是举下例子!
输出结果:输出wold.txt中的字符:
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:h
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:e
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:l
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:l
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:o
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:w
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:o
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:r
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:l
20:41:37 [DEBUG] [main] c.c.FileChannelTest - 读取到的字节:d
向FileChannel通道中写数据
写入数据到通道,在大部分应用场景,都会调用通道的write(ByteBuffer)方法,此方法的参数是一个ByteBuffer缓冲区实例,是待写数据的来源。write(ByteBuffer)方法的作用,是从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。如果 buffer 处于写入模式(如刚写完数据),需要 flip 翻转 buffer,使其变成读取模式,代码如下:
@Test
public void test2(){
// wrap 方法执行完自动切换wrapBuffer为读模式
ByteBuffer wrapBuffer = ByteBuffer.wrap("你好世界!".getBytes());
try(FileChannel channel = new RandomAccessFile("word.txt","rw").getChannel()){
int len = 0;
while ((len = channel.write(wrapBuffer))!=0){
System.out.println("已经写入字节数为:"+len); //已经写入字节数为:15
}
}catch (IOException e){
log.error("文件未找到");
}
}
// 关闭通道
channel.close();
//强制刷新到磁盘
channel.force(true);
注意,写入数据会将word.txt原有的数据擦除!当通道使用完成后,必须将其关闭。关闭非常简单,调用close( )方法即可 。如果在将缓冲数据写入通道时,需要保证数据能立即写入到磁盘,可以在写入后调用一下FileChannel的force()方法。关于FileChannel通道需要掌握的大概就是上面这些,那么下面的内容是作为开发中的补充内容。
文件操作补充内容
字符串与ByteBuffer
缓存的相互转换
// 字符串转为ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("hello world".getBytes());
debugAll(buffer);
buffer.flip(); //这种方式需要切换读模式才可以
CharBuffer hw = StandardCharsets.UTF_8.decode(buffer);
System.out.println(hw.toString());
// 使用Charset类
ByteBuffer encodeBuffer = StandardCharsets.UTF_8.encode("hello");
debugAll(encodeBuffer);
CharBuffer decode = StandardCharsets.UTF_8.decode(encodeBuffer);
System.out.println(decode.toString());
// 使用wrap
ByteBuffer wrapBuffer = ByteBuffer.wrap("hello".getBytes());
debugAll(wrapBuffer);
通道与通道直接发送数据(零拷贝)
public static void main(String[] args) {
try(
FileChannel from = new FileInputStream("world.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel()
) {
long size = from.size();
for (long left = size; left >0 ; ) {
// 该方法每次最多传输2g的数据量
long n = from.transferTo((size-left), from.size(), to);
left -= n;
}
}catch (IOException ie){
ie.printStackTrace();
}
}
NIO
提供的关于File
的操作
遍历目录文件:
public class FileTest {
public static void main(String[] args) throws IOException {
// 访问文件夹
visitorFile();
// 拷贝文件夹
copyFile();
// 删除文件夹
deleteFile();
}
private static void copyFile() throws IOException {
String source = "C:\\Users\\cheney\\Documents\\CFSystem";
String target = "C:\\Users\\cheney\\Documents\\CFSystem_bak";
Files.walk(Paths.get(source)).forEach(path -> {
// 替换成新的路径
String targetName = path.toString().replace(source, target);
try {
if(Files.isDirectory(path)){
Files.createDirectory(Paths.get(targetName));
}else if(Files.isRegularFile(path)){
Files.copy(path,Paths.get(targetName));
}
}catch (IOException e){
e.printStackTrace();
}
});
}
private static void deleteFile() throws IOException {
Files.walkFileTree(Paths.get("C:\\Users\\cheney\\Documents\\CFSystem_bak"),new SimpleFileVisitor<Path>(){
@Override
// 在访问目录之前被调用。你可以在这里执行预处理操作。
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// 进入文件夹时不能删除文件夹,因为里面还有文件
System.out.println("进入------>"+dir);
return super.preVisitDirectory(dir, attrs);
}
@Override
// 在访问某个目录的文件时被调用。你可以在这里执行对文件的操作。
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 删除文件夹
System.out.println("删除xxxxxx:"+file);
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
// 在访问文件失败时被调用。例如,由于权限问题或其他原因,无法访问文件。
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return super.visitFileFailed(file, exc);
}
@Override
// 在访问目录之后被调用。你可以在这里执行后处理操作。
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
// 如果退出之前遍历删除过文件,那么可以删除文件夹
System.out.println("退出<------"+dir);
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
private static void visitorFile() throws IOException {
AtomicInteger dirCount = new AtomicInteger();
// 遍历文件夹,SimpleFileVisitor访问者模式
Files.walkFileTree(Paths.get("C:\\Users\\cheney\\Documents\\CFSystem"),new SimpleFileVisitor<Path>(){
// 文件访问之前的操作,即访问到文件夹
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
System.out.println("==========>"+dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
// 文件访问时操作,即访问到文件
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
System.out.println("==========>"+file);
return super.visitFile(file, attrs);
}
});
System.out.println("文件夹个数:"+dirCount.get());
}
}
检查文件是否存在
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
创建一级目录
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
- 如果目录已存在,会抛异常 FileAlreadyExistsException
- 不能一次创建多级目录,否则会抛异常 NoSuchFileException
创建多级目录用
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
拷贝文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);
- 如果文件已存在,会抛异常 FileAlreadyExistsException
如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
移动文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
- StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
删除文件
Path target = Paths.get("helloword/target.txt");
Files.delete(target);
- 如果文件不存在,会抛异常 NoSuchFileException
删除目录
Path target = Paths.get("helloword/d1");
Files.delete(target);
- 如果目录还有内容,会抛异常 DirectoryNotEmptyException
SocketChannel和ServerSocketChannel套接字通道
很多人都搞不拎清这两个通道的区别,它们都是涉及网络连接的通道,SocketChannel负责连接的数据传输,另一个是ServerSocketChannel负责连接的监听。要想和服务器建立TCP通信,必须先连接服务器,而ServerSocketChannel就是符合客户端连接请求的通道,只有三次握手连接完成了才可以使用SocketChannel进行通信。ServerSocketChannel仅仅应用于服务器端,而SocketChannel则同时处于服务器端和客户端,所以,对应于一个连接,两端都有一个负责传输的SocketChannel传输通道。同样下面讲解将按照获取通道、读取通道数据、数据写入到通道中、关闭通道等步骤介绍。
获取SocketChannel传输通道
在客户端,先通过SocketChannel静态方法open()获得一个套接字传输通道;然后,将socket套接字设置为非阻塞模式;最后,通过connect()实例方法,对服务器的IP和端口发起连接。
//获得一个套接字传输通道
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//对服务器的 IP 和端口发起连接
socketChannel.connect(new InetSocketAddress("127.0.0.1", 80));
在服务器端,需要在连接建立的事件到来时,服务器端的ServerSocketChannel能成功地查询出这个新连接事件,并且通过调用服务器端ServerSocketChannel监听套接字的accept()方法,来获取新连接的套接字通道:
//新连接事件到来,首先通过事件,获取服务器监听通道,这个key如何来的后面Selector会介绍
ServerSocketChannel server = (ServerSocketChannel) key.channel();
//获取新连接的套接字通道
SocketChannel socketChannel = server.accept();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
看见了没,服务器端需要先使用ServerSocketChannel建立连接才能使用套接字传输通道!
读取SocketChannel传输通道
当SocketChannel传输通道可读时,可以从SocketChannel读取数据,具体方法与前面的文件通道读取方法是相同的。调用read方法,将数据读入缓冲区ByteBuffer。 这部分和前面文件传输通道FileChannel是一样的,都是通过缓冲区从通道读取和写入数据,如下:
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
在读取时,因为是异步的,因此我们必须检查read的返回值,以便判断当前是否读取到了数据。 read()方法的返回值是读取的字节数,如果返回-1,那么表示读取到对方的输出结束标志,对方已经输出结束,准备关闭连接。实际上,通过read方法读数据,本身是很简单的,比较困难的是,在非阻塞模式下,如何知道通道何时是可读的呢?这就需要用到NIO的新组件——Selector通道选择器,稍后介绍。
向SocketChannel传输通道写入数据
//写入前需要读取缓冲区,要求 ByteBuffer 是读取模式
buffer.flip();
socketChannel.write(buffer);
关闭通道
//调用终止输出方法,向对方发送一个输出的结束标志
socketChannel.shutdownOutput();
//关闭套接字连接
IOUtil.closeQuietly(socketChannel);
DatagramChannel数据报通道
在Java中使用UDP协议传输数据,比TCP协议更加简单。和Socket套接字的TCP传输协议不同, UDP协议不是面向连接的协议。使用UDP协议时,只要知道服务器的IP和端口,就可以直接向对方发送数据。在Java NIO中,使用DatagramChannel数据报通道来处理UDP协议的数据传输。
获取DatagramChannel数据报通道
//获取 DatagramChannel 数据报通道
DatagramChannel channel = DatagramChannel.open();
//设置为非阻塞模式
datagramChannel.configureBlocking(false);
如果需要接收数据,还需要调用bind方法绑定一个数据报的监听端口,具体如下://调用 bind 方法绑定一个数据报的监听端口```
channel.socket().bind(new InetSocketAddress(18080));
读取DatagramChannel数据报通道数据
当DatagramChannel通道可读时,可以从DatagramChannel读取数据。和前面的SocketChannel读取方式不同,这里不调用read方法,而是调用receive(ByteBufferbuf)方法将数据从DatagramChannel读入,再写入到ByteBuffer缓冲区中。通道读取receive(ByteBufferbuf)方法虽然读取了数据到buf缓冲区,但是其返回值是SocketAddress类型,表示返回发送端的连接地址(包括IP和端口)。通过receive方法读取数据非常简单,但是,在非阻塞模式下,如何知道DatagramChannel通道何时是可读的呢?和SocketChannel一样,同样需要用到NIO的新组件—Selector通道选择器,稍后介绍。
//创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//从 DatagramChannel 读入,再写入到 ByteBuffer 缓冲区
SocketAddress clientAddr= datagramChannel.receive(buf);
写入DatagramChannel数据报通道
向DatagramChannel发送数据,和向SocketChannel通道发送数据的方法也是不同的。这里不是调用write方法,而是调用send方法。由于UDP是面向非连接的协议,因此,在调用send方法发送数据的时候,需要指定接收方的地址(IP和端口)。 示例代码如下:文章来源:https://www.toymoban.com/news/detail-809213.html
//把缓冲区翻转到读取模式
buffer.flip();
//调用 send 方法,把数据发送到目标 IP+端口
dChannel.send(buffer, new InetSocketAddress("127.0.0.1",18899));
//清空缓冲区,切换到写入模式
buffer.clear();
//简单关闭即可
dChannel.close();
至此,几种通道基本用法就介绍完毕了,如果不过瘾是因为没有结合Selector来讲,结合Selector才是最知识盛宴。在Selector中将结合Channel和Buffer全面进行介绍。文章来源地址https://www.toymoban.com/news/detail-809213.html
到了这里,关于Java-NIO篇章(3)——Channel通道类详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!