(二)Java NIO(Channel)

一、Channel概述

Channel是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读、写或者同时用于读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。
NIO中通过channel封装了对数据源的操作,通过channel我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel与文件描述符或者socket是一一对应的。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
channel接口源码

package java.nio.channels;

import java.io.IOException;
import java.io.Closeable;

public interface Channel extends Closeable {

    public boolean isOpen();

    public void close() throws IOException;
}

与缓冲区不同,通道API主要由接口指定。不同的操作系统上通道实现(ChannelImplementation)会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
Channel是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做个比较,通道就像是流。所有数据都通过 Buffer对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
Java NIO的通道类似流,但又有些不同:
- 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
- 通道可以异步地读写。
- 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:

image

二、Channel实现

下面是Java NIO中最重要的Channel的实现:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
(1)FileChannel从文件中读写数据。
(2)DatagramChannel能通过UDP读写网络中的数据。
(3)SocketChannel能通过TCP读写网络中的数据。
(4)ServerSocketChannel可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
正如你所看到的,这些通道涵盖了UDP和TCP网络IO,以及文件IO。

三、FileChannel介绍和示例

FileChannel类可以实现常用的read,write以及scatter/gather操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。

方法 描述
int read(ByteBuffer dst) 从Channel中读取数据到ByteBuffer
long read(ByteBuffer[] dsts) 将Channel中的数据“分散”到ByteBuffer[]
int write(ByteBuffer src) 将ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs) 将ByteBuffer[]中的数据“聚集”到Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中
下面是一个使用FileChannel读取数据到Buffer中的示例:
public class FileChannelDemo {
    public static void main(String[] args) throws IOException {
        //创建FileChannel
        RandomAccessFile aFile = new RandomAccessFile("E:\\java\\io\\story.txt", "rw");
        FileChannel channel = aFile.getChannel();

        //创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        //读取数据到buffer中
        int bytesRead = channel.read(buffer);
        while (bytesRead != -1) {
            System.out.println("读取了:" + bytesRead);
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.println((char)buffer.get());
            }
            buffer.clear();
            bytesRead = channel.read(buffer);
        }
        aFile.close();
    }
}

四、FileChannel操作详解

4.1打开FileChannel

在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或
RandomAccessFile,来获取一个FileChannel实例。下面是通过RandomAccessFile打开FileChannel的示例:

RandomAccessFile aFile = new RandomAccessFile("E:\\java\\io\\story.txt", "rw");
FileChannel channel = aFile.getChannel();

4.2从FileChannel读取数据

调用多个read()方法之一从 FileChannel中读取数据。如:

ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);

首先,分配一个Buffer。从FileChannel中读取的数据将被读到Buffer中。然后,调用FileChannel.read()方法。该方法将数据从FileChannel读取到Buffer中。read()方法返回的int值表示了有多少字节被读到了Buffer中。如果返回-1,表示到了文件末尾。

4.3向FileChannel写数据

使用FileChannel.write()方法向FileChannel写数据,该方法的参数是一个Buffer。如:

public class FileChannelDemo2 {
    public static void main(String[] args) throws IOException {
        //创建FileChannel
        RandomAccessFile aFile = new RandomAccessFile("E:\\java\\io\\story.txt", "rw");
        FileChannel channel = aFile.getChannel();

        //创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        String data = "hello world";
        buffer.clear();

        //写入内容
        buffer.put(data.getBytes());
        buffer.flip();

        //FileChannel完成最终实现
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }

        channel.close();
    }
}

注意FileChannel.write()是在while循环中调用的。因为无法保证write()方法一次能向FileChannel写入多少字节,因此需要重复调用write()方法,直到Buffer中已经没有尚未写入通道的字节。

4.4关闭FileChannel

用完FileChannel后必须将其关闭。如:

channel.close();

4.5 FileChannel的position方法

有时可能需要在FileChannel的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel的当前位置。也可以通过调用position(long pos)方法设置FileChannel的当前位置。
这里有两个例子:

long pos = channel.position();
channel.position(pos +123);

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1(文件结束标志)。
如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

4.6 FileChannel的size方法

FileChannel实例的size()方法将返回该实例所关联文件的大小。如:

long fileSize = channel.size();

4.7 FileChannel的truncate方法

可以使用FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度后面的部分将被删除。如:

channel.truncate(1024);

这个例子截取文件的前1024个字节。

4.8 FileChannel的force方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel里的数据一定会即时写到磁盘上。要保证这一点,需要调用force()方法。<
force()方法有一个boolean类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

4.9 FileChannel的transferTo方法和transferFrom方法

通道之间的数据传输:
如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel传输到另外一个channel。
(1)transferFrom()方法
FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中(译者注:这个方法在JDK文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。下面是一个FileChannel完成文件间的复制的例子:

public class FileChannelDemo3 {
    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("E:\\java\\io\\story1.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();

        RandomAccessFile bFile = new RandomAccessFile("E:\\java\\io\\story2.txt", "rw");
        FileChannel toChannel = bFile.getChannel();

        //fromChannel 传输到 toChannel
        long position = 0;
        long size = fromChannel.size();
        toChannel.transferFrom(fromChannel, position, size);

        aFile.close();
        bFile.close();
    }
}

方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数。如果源通道的剩余空间小于count个字节,则所传输的字节数要小于请求的字节数。此外要注意,在SoketChannel的实现中,SocketChannel只会传输此刻准备好的数据(可能不足count字节)。因此,SocketChannel可能不会将请求的所有数据(count个字节)全部传输到FileChannel中。
(2)transferTo()方法
transferTo()方法将数据从FileChannel传输到其他的channel中。
下面是一个transferTo()方法的例子:

public class FileChannelDemo4 {
    public static void main(String[] args) throws IOException {
        RandomAccessFile aFile = new RandomAccessFile("E:\\java\\io\\story1.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();

        RandomAccessFile bFile = new RandomAccessFile("E:\\java\\io\\story2.txt", "rw");
        FileChannel toChannel = bFile.getChannel();

        //fromChannel 传输到 toChannel
        long position = 0;
        long size = fromChannel.size();
        fromChannel.transferTo(position, size, toChannel);

        aFile.close();
        bFile.close();
    }
}

五、Socket通道

(1)新的socket通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个socket连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。借助新的NIO类,一个或几个线程就可以管理成百上千的活动socket连接了并且只有很少甚至可能没有性能损失。所有的socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)都继承了位于java.nio.channels.spi包中的AbstractSelectableChannel。这意味着我们可以用一个Selector对象来执行socket通道的就绪选择(readiness selection)。
(2)请注意DatagramChannel和SocketChannel实现定义读和写功能的接口而ServerSocketChannel不实现。ServerSocketChannel负责监听传入的连接和创建新的SocketChannel对象,它本身从不传输数据。
(3)在我们具体讨论每一种socket 通道前,您应该了解socket和socket 通道之间的关系。通道是一个连接I/O服务导管并提供与该服务交互的方法。就某个socket而言,它不会再次实现与之对应的socket通道类中的socket协议API,而java.net中已经存在的socket通道都可以被大多数协议操作重复使用。
全部socket通道类(DatagramChannel、SocketChannel和ServerSocketChannel)在被实例化时都会创建一个对等socket对象。这些是我们所熟悉的来自java.net的类(Socket、ServerSocket和DatdgramSocket),它们已经被更新以识别通道。对等socket可以通过调用socket()方法从一个通道上获取。此外,这三个java.net类现在都有getChannel()方法。
(4)要把一个socket通道置于非阻塞模式,我们要依靠所有socket通道类的公有超级类:SelectableChannel。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞I/O和可选择性是紧密相连的,那也正是管理阻塞模式的API代码要在SelectableChannel超级类中定义的原因。
设置或重新设置一个通道的阻塞模式是很简单的,只要调用configureBlocking()方法即可,传递参数值为true则设为阻塞模式,参数值为false值设为非阻塞模式。可以通过调用isBlocking()方法来判断某个socket通道当前处于哪种模式。
AbstractSelectableChannel.java中实现的configureBlocking()方法如下:

public final SelectableChannel configureBlocking(boolean block)
        throws IOException
    {
        synchronized (regLock) {
            if (!isOpen())
                throw new ClosedChannelException();
            if (blocking == block)
                return this;
            if (block && haveValidKeys())
                throw new IllegalBlockingModeException();
            implConfigureBlocking(block);
            blocking = block;
        }
        return this;
    }

非阻塞socket通常被认为是服务端使用的,因为它们使同时管理很多socket通道变得更容易。但是,在客户端使用一个或几个非阻塞模式的socket通道也是有益处的,例如,借助非阻塞socket通道,GUI程序可以专注于用户请求并且同时维护与一个或多个服务器的会话。在很多程序上,非阻塞模式都是有用的。
偶尔地,我们也会需要防止socket通道的阻塞模式被更改。API中有一个
blockingLock()方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式。
下面分别介绍这3个通道。

5.1 ServerSocketChannel

ServerSocketChannel是一个基于通道的socket监听器。它同我们所熟悉的java.net.ServerSocket执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。
由于ServerSocketChannel没有bind(方法,因此有必要取出对等的socket并使用它来绑定到一个端口以开始监听连接。我们也是使用对等ServerSocket的API来根据需要设置其他的socket选项。
同java.net.ServerSocket一样,ServerSocketChannel也有accept()方法。一旦创建了一个ServerSocketChannel并用对等socket绑定了它,然后您就可以在其中一个上调用accept()。如果您选择在ServerSocket上调用accept()方法,那么它会同任何其他的ServerSocket表现一样的行为:总是阻塞并返回一个java.net.Socket对象。如果您选择在ServerSocketChannel上调用accept()方法则会返回SocketChannel类型的对象,返回的对象能够在非阻塞模式下运行。
换句话说:
ServerSocketChannel的accept()方法会返回SocketChannel类型对象,SocketChannel可以在非阻塞模式下运行。
其它Socket的accept()方法会阻塞返回一个Socket对象。如果ServerSocketChannel以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept()会立即返回null。正是这种检查连接而不阻塞的能力实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。我们可以使用一个选择器实例来注册ServerSocketChannel对象以实现新连接到达时自动通知的功能。
以下代码演示了如何使用一个非阻塞的accept()方法:

public class ServerSocketDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        int port = 8888;
        ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.socket().bind(new InetSocketAddress(port));

        //非阻塞模式
        ssc.configureBlocking(false);

        while (true) {
            SocketChannel sc = ssc.accept();
            if (sc == null) {
                System.out.println("");
                Thread.sleep(2000);
            } else {
                System.out.println(sc.socket().getRemoteSocketAddress());
                buffer.rewind();
                sc.write(buffer);
                sc.close();
            }
        }
    }
}

(1)打开ServerSocketChannel
通过调用ServerSocketChannel.open()方法来打开ServerSocketChannel。

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

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

serverSocketChannel.close();

(3)监听新的连接
通过ServerSocketChannel.accept()方法监听新进的连接。当accept()方法返回时候,它返回一个包含新进来的连接的SocketChannel。因此, accept()方法会一直阻塞到有新连接到达。
通常不会仅仅只监听一个连接,在while循环中调用accept()方法。如下面的例子:

while (true) {
      SocketChannel sc = ssc.accept();

(4)阻塞模式
会在SocketChannel sc = ssc.accept();这里阻塞住进程。
(5)非阻塞模式
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept()方法会立刻返回,如果还没有新进来的连接,返回的将是null。因此,需要检查返回的Channel是否null。如:

while (true) {
            SocketChannel sc = ssc.accept();
            if (sc == null) {
                System.out.println("");
                Thread.sleep(2000);
            } else {
                System.out.println(sc.socket().getRemoteSocketAddress());
                buffer.rewind();
                sc.write(buffer);
                sc.close();
            }
        }

5.2 SocketChannel

1、SocketChannel介绍

Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
A selectable channel for stream-oriented connecting sockets.
以上是Java docs中对于SocketChannel的描述:SocketChannel是一种面向流连接sockets套接字的可选择通道。从这里可以看出:
- SocketChannel是用来连接Socket套接字
- SocketChannel主要用途用来处理网络I/O的通道
- SocketChannel是基于TCP连接传输
- SocketChannel实现了可选择通道,可以被多路复用的

2、SocketChannel特征

(1)对于已经存在的socket不能创建SocketChanngl
(2)SocketChannel中提供的open接口创建的Channel并没有进行网络级联,需要使用connect接口连接到指定地址
(3)未进行连接的SosketChannle执行I/O操作时,会抛出NotYetConnectedException
(4)SocketChannel支持两种I/O模式:阻塞式和非阻塞式
(5)SocketChannel支持异步关闭。如果SocketChannel在一个线程上read阻塞,另一个线程对该SocketChannel调用shutdownInput,则读阻塞的线程将返回-1表示没有读取任何数据;如果SocketChannel在一个线程上write阻塞,另一个线程对该SocketChannel调用shutdownWrite,则写阻塞的线程将抛出AsvnchronousCloseException
(6)SocketChannel支持设定参数
SO_SNDBUF 套接字发送缓冲区大小SO_RCVBUF 套接字接收缓冲区大小SO_KEEPALIVE 保活连接
O_REUSEADDR 复用地址
SO_LINGER 有数据传输时延缓关闭Channel(只有在非阻塞模式下有用)
TCP_NODELAY 禁用Nagle算法

3、SocketChannel的使用

(1)创建SocketChannel
方式一:

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));

方式二:

SocketChannel socketChannel2 = SocketChannel.open();
socketChannel2.connect(new InetSocketAddress("www.baidu.com", 80));

直接使用有参open api或者使用无参open api,但是在无参open只是创建了一个SocketChannel对象,并没有进行实质的tcp连接。
(2)连接校验

socketChannel.isOpen(); //测试SocketChannel是否为open状态
socketChannel.isConnected(); //测试SocketChannel是否已被连接
socketChannel.isConnectionPending(); //测试SocketChannel是否正在进行连接
socketChannel.finishConnect(); //校验正在进行套接字连接的SocketChannel是否已经完成连接

(3)读写模式
前面提到SocketChannel支持阻塞和非阻塞两种模式:

socketChannel.configureBlocking(false);

通过以上方法设置SocketChannel的读写模式。false表示非阻塞,true表示阻塞。
(4)读写

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");

以上为阻塞式读,当执行到read出,线程将阻塞,控制台将无法打印read over。

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");

以上为非阻塞读,控制台将打印read over。
读写都是面向缓冲区,这个读写方式与前文中的FileChannel相同。
(5)设置和获取参数

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE)
.setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);

通过setOptions方法可以设置socket套接字的相关参数。

socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);

可以通过getOption,获取相关参数的值。如默认的接收缓冲区大小是8192byte。
SocketChannel还支持多路复用。

5.3 DatagramChannel

正如SocketChannel对应Socket,ServerSocketChannel对应ServerSocket,每一个 DatagramChannel对象也有一个关联的DatagramSocket对象。正如SocketChannel模拟连接导向的流协议(如TCP/IP),DatagramChannel则模拟包导向的无连接协议(如UDP/IP)。DatagramChannel是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载。与面向流的的socket不同,DatagramChannel,可以发送单独的数据报给不同的目的地址。同样,DatagramChannel对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)。

1、打开DatagramChannel
DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(10086));

此例子是打开10086端口接收UDP数据包。

2、接收数据

通过receive()接收UDP包:

ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);

SocketAddress可以获得发包的ip、端口等信息,用toString查看,格式如下:/127.0.0.1:57126

3、发送数据

通过send()发送UDP包

DatagramChannel server = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer, new InetSocketAddress("127.0.0.1", 10086));
4、连接

UDP不存在真正意义上的连接,这里的连接是向特定服务地址用read和write接收发送数据包。

client.connect(new InetSocketAddress("127.0.0.1", 10086));
int readSize = client.read(sendBuffer);
server.write(sendBuffer);

read()和write()只有在connect()后才能使用,不然会抛
NotYetConnectedException异常。用read()接收时,如果没有接收到包,会抛PortUnreachableException异常。

5、DatagramChannel示例

客户端发送,服务端接收的例子:

public class DatagramChannelDemo {

    //发送的实现
    @Test
    public void sendDatagram() throws IOException, InterruptedException {
        DatagramChannel sendChannel = DatagramChannel.open();
        InetSocketAddress sendAddress = new InetSocketAddress("127.0.0.1", 9999);

        while (true) {
            ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes("utf-8"));
            sendChannel.send(buffer, sendAddress);
            System.out.println("已经完成发送");
            Thread.sleep(1000);
        }
    }

    //接收的实现
    @Test
    public void receiveDatagram() throws IOException {
        DatagramChannel receiveChannel = DatagramChannel.open();
        InetSocketAddress receiveAddress = new InetSocketAddress(9999);

        receiveChannel.bind(receiveAddress);

        ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);

        while (true) {
            receiveBuffer.clear();
            SocketAddress socketAddress = receiveChannel.receive(receiveBuffer);
            receiveBuffer.flip();
            System.out.println(Charset.forName("UTF-8").decode(receiveBuffer));
        }
    }
}
@Test
    public void testConnect() throws IOException {
        DatagramChannel connChannel = DatagramChannel.open();
        connChannel.bind(new InetSocketAddress(9999));
        connChannel.connect(new InetSocketAddress("127.0.0.1", 9999));

        connChannel.write(ByteBuffer.wrap("hello".getBytes("UTF-8")));

        ByteBuffer readBuffer = ByteBuffer.allocate(1024);

        while (true) {
            readBuffer.clear();
            connChannel.read(readBuffer);
            readBuffer.flip();
            System.out.println(Charset.forName("UTF-8").decode(readBuffer));
        }
    }

六、Scatter/Gather

Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作。
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从 Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel将多个Buffer中的数据“聚集(gather)”后发送到Channel。
scatter/gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的 buffer中,这样你可以方便的处理消息头和消息体。

6.1 Scattering Reads

Scattering Reads是指数据从一个channel读取到多个buffer中。如下图描述:
image

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {header, body};
channel.read(buffers);

注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer 中写。
Scattering Reads在移动下一个 buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如128byte), Scattering Reads才能正常工作。

6.2 Gathering Writes

Gathering Writes是指数据从多个buffer写入到同一个channel。如下图描述:
image

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {header, body};
channel.write(buffers);

buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering writes能较好的处理动态消息。

posted @ 2021-09-26 17:08  SEVEN_CCODE  阅读(137)  评论(0)    收藏  举报