Java-NIO篇章(3)——Channel通道类详解

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的时候,老是搞不清楚ServerSocketChannelSocketChannel的关系,这次我不允许我的读者也搞不清。用大白话讲就是通道你可以理解为数据传输的管道,这个管道是双向传输的,即既可以通过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
            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

SocketChannelServerSocketChannel套接字通道

很多人都搞不拎清这两个通道的区别,它们都是涉及网络连接的通道,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和端口)。 示例代码如下:

//把缓冲区翻转到读取模式
buffer.flip();
//调用 send 方法,把数据发送到目标 IP+端口
dChannel.send(buffer, new InetSocketAddress("127.0.0.1",18899));
//清空缓冲区,切换到写入模式
buffer.clear();
//简单关闭即可
dChannel.close();

至此,几种通道基本用法就介绍完毕了,如果不过瘾是因为没有结合Selector来讲,结合Selector才是最知识盛宴。在Selector中将结合Channel和Buffer全面进行介绍。

你可能感兴趣的:(技术提升篇,nio,NIO,Channel,Channel,NIO,通道,Java,NIO通道)