欢迎您的访问
专注于分享最有价值的互联网技术干货

白话NIO之Channel

几个T的资料等你来白嫖
双倍快乐

Channel简介

在标准的IO当中,都是基于字节流/字符流进行操作的,而在NIO中则是是基于Channel和Buffer进行操作,其中的Channel的虽然模拟了流的概念,实则大不相同。

区别 Stream Channel
支持异步 不支持 支持
是否可双向传输数据 不能,只能单向 可以,既可以从通道读取数据,也可以向通道写入数据
是否结合Buffer使用 必须结合Buffer使用
性能 较低 较高

Channel用于在字节缓冲区和位于通道另一侧的实体(通常是文件或者套接字)之间以便有效的进行数据传输。借助通道,可以用最小的总开销来访问操作系统本身的I/O服务。

通道必须结合Buffer使用,不能直接向通道中读/写数据,其结构如下图:
20210417223400081.png

通道的API主要由接口指定,在不同的操作系统上有不同的实现。该接口如下所示:

public interface Channel extends Closeable {

    public boolean isOpen();
    public void close() throws IOException;
}

从接口看来,所有的通道都有这两种操作:检查通道的开启状态和关闭通道。从Channel接口引申出的其他接口都是面向字节的子接口,也就是说通道本质上都是对自己缓冲区进行操作的。


Channel的分类

广义上来说通道可以被分为两类:File I/O和Stream I/O,也就是文件通道和套接字通道。如果分的更细致一点则是:

  • FileChannel 从文件读写数据
  • SocketChannel 通过TCP读写网络数据
  • ServerSocketChannel 可以监听新进来的TCP连接,并对每个链接创建对应的SocketChannel
  • DatagramChannel 通过UDP读写网络中的数据
  • Pipe

对于Socket通道来说存在直接创建新Socket通道的方法,而对于文件通道来说,升级之后的FileInputStream、FileOutputStream和RandomAccessFile提供了getChannel()方法来获取通道。需要注意的是java.net包中的socket类也存在getChannel()方法,但他返回的并非新通道。

通道既可以是单向的也可以是双向的。只实现ReadableByteChannel接口中的read()方法或者只实现WriteableByteChannel接口中的write()方法的通道皆为单向通道,同时实现ReadableByteChannel和WriteableByteChannel为双向通道,比如ByteChannel。对于socket通道来说,它们一直是双向的,而对于FileChannel来说,它同样实现了ByteChannel,但是我们知道通过FileInputStream的getChannel()获取的FileChannel只具有文件的只读权限,那此时的在该通道调用write()会出现什么情况?不出意外的抛出了NonWriteChannelException异常。
通过以上,我们得出结论:通道都与特定的I/O服务挂钩,并且通道的性能受限于所连接的I/O服务的性质。

通道的工作模式有两种:阻塞或非阻塞。在非阻塞模式下,调用的线程不会休眠,请求的操作会立刻返回结果;在阻塞模式下,调用的线程会产生休眠。另外除FileChannel不能运行在非阻塞模式下,其余的通道都可阻塞运行也可以以非阻塞的方式运行。

另外从SelectableChannel引申出的类可以和支持有条件选择的Selector结合使用,进而充分利用多路复用的I/O(multiplexed I/O)来提高性能.对于Socket通道类来说,通常与Selector共同使用以提高性能。需要注意的是通道不能被重复使用,一个打开的通道代表着与一个特定I/O服务进行连接并封装了该连接的状态,通道一旦关闭,该连接便会断开。通道的close()比较特殊,无论在通道时在阻塞模式下还是非阻塞模式下,由于close()方法的调用而导致底层I/O的关闭都可能会造成线程的暂时阻塞。在一个已关闭的通道上调用close()并没有任何意义,只会立即返回。


1. 文件通道(FileChannel)

Java NIO中的FileChannel是一个连接到文件的通道。可以通过文件通道读写文件。文件通道总是阻塞式的,因此FileChannel无法设置为非阻塞模式.

同大多数 I/O 相关的类一样,FileChannel是一个反映Java虚拟机外部一个具体对象的抽象。FileChannel类保证同一个Java虚拟机上的所有实例看到的某个文件的视图均是一致的,但是Java虚拟机却不能对超出它控制范围的因素提供担保。通过一个FileChannel实例看到的某个文件的视图与通过一个外部的非Java进程看到的该文件的视图可能一致,也可能不一致。一般而言,由运行在不同Java虚拟机上的FileChannel对象发起的对某个文件的并发访问和由非 Java 进程发起的对该文件的并发访问是一致的。

1. FileChannel的使用

  1. 创建FileChannel

升级之后之后的InputStream,OutputStream,RandomAccessFile可通过getChannel()方法获取FileChannel实例

  • 从FileChannel读取数据

    通过调用Channel的read()方法可将Channel中的数据读取到Buffer中

  • 想FileChannel写入数据

    通过Channel的write()方法可想FileChannel写入数据

  • 关闭FileChannel

    用完的FileChannel需要通过Channel的close()关闭。

  • 获取当前position或设置position的值

    通过position()方法获取当前FileChannel的当前位置,通过position(longpos)设置FileChannel的当前位置。和底层的文件描述符一样,每个FileChannel都有一个file position的概念。该position值决定了从文件的哪里开始读或者写(你可以把它当作我们编辑时出现的编辑光标),该position直接从底层文件描述符中获取,该position同时被作为通道引用获取来源的文件对象共享,在任何时候修改该position的值对其他对象也是可见的。可以通过position()来获取当前的”file position”,也可以通过position(newPosition)来设置。

  • 通过FileChannel实例的size()可获取FileChannel关联文件的大小
  • 通过truncate()方法截取所关联文件的指定长度。
  • 通过force(boolean metaData)强制将通道中未写入磁盘的数据立刻写入到磁盘。metaData为true时,表示将文件的元数据一同写入到磁盘。

    在现在操作系统中,其内部的文件系统多带有缓存数据和延迟磁盘文件更新的特点,如果在本地文件系统中调用force()方法则要求文件系统立刻将所有待修改的数据同步到磁盘上,如果在远程文件系统上(NFS等),调用force()则不能保证数据一定能同步到永久储存器重。


2. FileChannel的完整示例

//通过FileChannel写入数据
    public static void testFileChannelOnWrite() {
        try {
            RandomAccessFile accessFile = new RandomAccessFile("D://file1.txt","rw");
            FileChannel fc = accessFile.getChannel();
            byte[] bytes = new String("hello every one").getBytes();
            ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
            fc.write(byteBuffer);
            byteBuffer.clear();
            byteBuffer.put(new String(",a good boy").getBytes());
            byteBuffer.flip();
            fc.write(byteBuffer);
            fc.close();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //通过FileChannel读取数据
    private static void testFileChannelOnRead() {
        try {
            FileChannel fileChannel = new FileInputStream(new File("D://file.txt")).getChannel();
            ByteBuffer byteBuffer=ByteBuffer.allocate(size);
            int n=0;
            while (fileChannel.read(byteBuffer) != -1) {
                byteBuffer.flip();//缓冲区写——> 读
                while (byteBuffer.hasRemaining()) {
                    System.out.print((char) byteBuffer.get());
                }
                byteBuffer.clear();//缓冲区不会被自动覆盖,需要主动调用该方法
            }
            fileChannel.force(true);
            fileChannel.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

3. transferFrom()的使用

FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中(译者注:这个方法在JDK文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。

public static void testTransferFrom(){
        try {
            RandomAccessFile fromFile = new RandomAccessFile("D://file1.txt", "rw");
            FileChannel fromChannel = fromFile.getChannel();
            RandomAccessFile toFile = new RandomAccessFile("D://file2.txt", "rw");
            FileChannel toChannel = toFile.getChannel();

            long position =0;
            long count = fromChannel.size();
            toChannel.transferFrom(fromChannel, position, count);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

4. transferTo()的使用

transferTo()方法将数据从FileChannel传输到其他的channel中。下面是一个简单的例子:

public static void testTransferTo() {
    try {
        RandomAccessFile fromFile = new RandomAccessFile("D://file1.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();
        RandomAccessFile toFile = new RandomAccessFile("D://file3.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        long position=0;
        long count = fromChannel.size();
        fromChannel.transferTo(position,count,toChannel);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2. ServerSocketChannel

Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 类似ServerSocket一样。要注意的是和DatagramChannel和SocketChannel不同,ServerSocketChannel本身不具备传输数据的能力,而只是负责监听传入的连接和创建新的SocketChannel。

1. ServerSocketChannel的使用

  1. 创建ServerSocketChannel

通过 ServerSocketChannel.open()方法来创建一个新的ServerSocketChannel对象,该对象关联了一个未绑定ServerSocket的通道.通过调用该对象上的socket()方法可以获取与之关联的ServerSocket。

  1. 为ServerSocketChannel绑定监听端口号

    在jdk 1.7之前,ServerSocketChannel没有bind()方法,因此需要通过他关联的的socket对象的socket()来绑定。从jdk1.7及以后,可以直接通过ServerSocketChannel的socket()方法来绑定端口号。

  2. 监听新进来的连接

    通过 ServerSocketChannel.accept() 方法监听新进来的连接。在阻塞模式下当 accept()方法返回的时候,它返回一个包含新进来的连接的SocketChannel,否则accept()方法会一直阻塞到有新连接到达。在非阻塞模式下,在没有新连接的情况下,accept()会立即返回null,该模式下通常不会仅仅只监听一个连接,因此需在while循环中调用 accept()方法.

  3. 关闭ServerSocketChannel

    通过调用ServerSocketChannel.close() 方法来关闭ServerSocketChannel.


2. ServerSocketChannel完整示例

public class ServerSocketChannelTest {

    private int size=1024;

    public void initChannel() throws IOException {
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        socketChannel.configureBlocking(false);
        //socketChannel.socket().bind(new InetSocketAddress(9999));jdk 1.7之前
        socketChannel.bind(new InetSocketAddress(9999));
        ByteBuffer byteBuffer = ByteBuffer.allocate(size);
        while (true) {
            SocketChannel channel = socketChannel.accept();
            if (channel != null) {
                InetSocketAddress remoteAddress = (InetSocketAddress) channel.getRemoteAddress();
                System.out.println(remoteAddress.getAddress());
                System.out.println(remoteAddress.getPort());
                channel.read(byteBuffer);
                byteBuffer.flip();
                while (byteBuffer.hasRemaining()) {
                    System.out.print((char) byteBuffer.get());
                }
            }
        }
    }

    public static void main(String[] args) {
        try {
            new ServerSocketChannelTest().initChannel();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3. SocketChannel

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道,它是Socket类的对等类,通常SocketChannel用在客户端以向服务器发起
连接请求。每个SocketChannel对象创建时都关联一个对等的Socket对象。同样SocketChannel可以运行在非阻塞模式下。

1. SocketChannel的使用

可以通过以下2种方式创建SocketChannel:
– 打开一个SocketChannel并连接到互联网上的某台服务器
– 一个新连接到达ServerSocketChannel时,会创建一个SocketChannel

  1. 创建SocketChannel

    通过SocketChannel的静态方法open()创建SocketChannel对象.此时通道虽打开,但并未连接,此时如果进行I/O操作会抛出NotYetConnectedException异常。

  2. 连接指定服务器

    通过SocketChannel对象的connect()连接指定地址。该通道一旦连接他将保持连接状态直到被关闭。可通过isConnected()来确定某个SocketChannel当前是否已连接。

  3. 从SocketChannel读数据

    利用SocketChannel对象的read()将数据读取到Buffer

  4. 向SocketChannel写数据

    利用SocketChannel对象的write()将Buffer的数据写入。

  5. 关闭SocketChannel

    利用SocketChannel对象的close()方法

当SocketChannel在非阻塞模式下异步调用connect(), read() 和write()时,需要注意connect(),write(),read()方法的行为:

  1. 如果SocketChannel在非阻塞模式下,此时调用connect(),该方法可能在连接建立之前就返回了。为了确定连接是否建立,可以调用finishConnect()的方法。

    socketChannel.configureBlocking(false);
    socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));
    
    while(! socketChannel.finishConnect() ){
        //wait, or do something else...
    }
  2. 非阻塞模式下,write()方法在尚未写出任何内容时可能就返回了。所以需要在循环中调用write()。
  3. 非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值.

2. SocketChannel完整示例

public class SocketChannelTest {

    private int size=1024;

    public void connectServer() throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
        ByteBuffer byteBuffer=ByteBuffer.allocate(size);
        byteBuffer.put(new String("hello server").getBytes());
        byteBuffer.flip();
            while (byteBuffer.hasRemaining()) {
                socketChannel.write(byteBuffer);
            }
    }

    public static void main(String[] args) throws IOException {
        new SocketChannelTest().connectServer();
    }
}

4. DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道,其底层实现为DatagramSocket+Selector。DatagramChannel可以调用socket()方法获取对等DatagramSocket对象。
DatagramChannel对象既可以充当服务端(监听者),也可以充当客户端(发送者)。如果需要新创建的通道负责监听,那么该通道必须绑定一个端口(或端口组):

DatagramChannel channel = DatagramChannel.open();
  channel.socket().bind(new InetSocketAddress(9999));//委托到对等的Socket上。

一个未绑定具体的端口DatagramChannel仍可以接受数据包,当一个底层的socket被创建时,一个随机的端口号被分配给他。不论通道是否绑定,所发送的数据包都会含有发送地址和接受地址(包括端口号),未绑定的DatagramChannel可以接受发送给它的端口的包,通常是用来回应该通道之前发生的一个数据包,经常用在在通过udp来维护客户端心跳。

1. DatagramChannel的使用

  1. 创建DatagramChannel通道.

    通过DatagramChannel的open()方法来创建。需要注意DatagramChannel的open()方法只是打开获得通道,但此时尚未连接。尽管DatagramChannel无需建立连接(远端连接),但仍然可以通过isConnect()检测当前的channel是否声明了远端连接地址。

  2. 从DatagramChannel中接受数据。

    通过receive()方法接受DatagramChannel中数据。从该方法将传入的数据报的数据将被复制到ByteBuffer中,同时返回一个SocketAddress对象以指出数据来源。在阻塞模式下,receive()将会阻塞至有数据包到来,非阻塞模式下,如果没有可接受的包则返回null。如果包内的数据大小超过缓冲区容量时,多出的数据会被悄悄抛弃(具体见TCP、UDP详解)。

  3. 通过DatagramChannel发送数据

    通过send()方法将ByteBuffer中的内容发送到指定的SocketAddress对象所描述的地址。在阻塞模式下,调用线程会被阻塞至有数据包被加入传输队列。非阻塞模式下,如果发送内容为空则返回0,否则返回发送的字节数。发送数据报是一个全有或全无(all-or-nothing)的行为。如果传输队列没有足够空间来承载整个数据报,那么什么内容都不会被发送。
    请注意send()方法返回的非零值并不表示数据报到达了目的地,仅代表数据报被成功加到本地网络层的传输队列。此外,传输过程中的协议可能将数据报分解成碎片,被分解的数据报在目的地会被重新组合起来,接收者将看不到碎片。但是,如果有一个碎片不能按时到达,那么整个数据报将被丢弃。分解有助于发送大数据报,但也会会造成较高的丢包率。

  4. 通过connect()连接到特定地址

    可以将DatagramChannel“连接”到网络中的特定地址,但DatagramChannel对数据报socket的连接语义不同于对流socket的连接语义。由于UDP是无连接的,连接到特定地址并不会像TCP通道那样创建一个真正的连接。而是锁住DatagramChannel ,让其只能从特定地址收发数据。这样的好处在于:除了它所“连接”到的地址之外的任何来自其他地址的数据报被忽略。通常使用该种方式的场景多在以下:

    一个基于UDP通讯协议的“客户端/服务器”的实时游戏。每个客户端都只和同一台服务器进行会话而希望忽视任何其他来源地数据包。通过该种方式将将客户端的DatagramChannel实例置于已连接状态能够防止接受非法服务器的数据报。

    不同于SocketChannel(必须连接了才有用并且只能连接一次),DatagramChannel对象可以任意次数地进行连接或断开连接。每次连接都可以到一个不同的远程地址。当一个DatagramChannel处于已连接状态时,发送数据将不用提供目的地址而且接收时的源地址也是已知的(这点类似SocketChannel),那么此时可以使用常规的read()和write()方法,包括scatter/gather形式的读写来组合或分拆包的数据,只是在数据传送方面没有任何保证。如下:

    int bytesRead = channel.read(buf);
    int bytesWritten = channel.write(but);

    read()方法返回读取字节的数量,如果通道处于非阻塞模式的话这个返回值可能是0。write()方法的返回值同send()方法一致:要么返回您的缓冲区中的字节数量,要么返回0(如果由于通道处于非阻塞模式而导致数据报不能被发送)。当通道不是已连接状态时调用read()或write()方法,都将产生NotYetConnectedException异常。


2. DatagramChannel完整示例

//发送方
public class DatagramChannelSendTest {

    public void sendData() throws IOException {
        DatagramChannel channel = DatagramChannel.open();
        ByteBuffer byteBuffer = ByteBuffer.wrap(new String("i 'm client").getBytes());
        int bytesSent = channel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 9999));
    }

    public static void main(String[] args) {
        try {
            new DatagramChannelSendTest().sendData();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//接收方
public class DatagramChannelRecieveTest {

    private int size=1024;

    private void receiveData() throws IOException {
        DatagramChannel channel = DatagramChannel.open();
        channel.socket().bind(new InetSocketAddress(9999));
        ByteBuffer byteBuffer = ByteBuffer.allocate(size);
        byteBuffer.clear();
        SocketAddress address = channel.receive(byteBuffer);//receive data
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()) {
            System.out.print((char) byteBuffer.get());
        }
    }

    public static void main(String[] args) {
        try {
            new DatagramChannelRecieveTest().receiveData();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5. 通道工具类Channels

NIO通道提供了一个便捷的通道类Channels,其中定义了几种静态的工厂方法以便更容易的和流打交道。其中常用的方法如下:

方法 返回 描述
newChannel(InputStream in) ReadableByteChannel 返回一个将从给定的输入流读取数据的通道。
newChannel(OutputStream out) WritableByteChannel 返回一个将向给定的输出流写入数据的通道。
newInputStream(ReadableByteChannel ch) InputStream 返回一个将从给定的通道读取字节的流。
newOutputStream(WritableByteChannel ch) OutputStream 返回一个将向给定的通道写入字节的流。
newReader(ReadableByteChannel ch, CharsetDecoder dec, int minBufferCap) Reader 返回一个reader,它将从给定的通道读取字节并依据提供的字符集名称对读取到的字节进行解码。
newReader(ReadableByteChannel ch, String csName) Reader 返回一个reader,它将从给定的通道读取字节并依据提供的字符集名称将读取到的字节解码成字符。
newWriter(WritableByteChannel ch, CharsetEncoder dec, int minBufferCap) Writer 返回一个writer,它将使用提供的字符集名称对字符编码并写到给定的通道中。
newWriter(WritableByteChannel ch, String csName) Writer 返回一个writer,它将依据提供的字符集名称对字符编码并写到给定的通道中。
赞(0) 打赏
版权归原创作者所有,任何形式转载请联系我们:大白菜博客 » 白话NIO之Channel

评论 抢沙发

0 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏