Java NIO 中的 Channel 详解

4,950 阅读19分钟

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」。

Channel 概述

Channel是一个通道,可以通过它读取和写入数据,它就像是水管一样,网络数据通过 Channel 进行读取和写入。通道和流的不同之处在与通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStram 的子类),而且通道上可以用于读,写或者同事用于读写。因为 Channel 是全双工的,所以它可以比流更好的映射底层操作系统的 API。

NIO 中通过 Channel 封装了对数据源的操作,通过channel 我们可以操作数据源, 但是又不关心数据源的具体数据结构。这个数据源可能是很多种,比如,可以是文件,也可是网络 socket 。 在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。 channel 在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效的传输数据。

Channel 接口源码

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

与缓冲区不同,通道 api 主要是由接口指定。不同的操作系统通道实现(ChannelImplementation) 会有根本性差异,所以通道 API 仅仅描述了可以做什么。因此很自然地,通道实现机场使用操作系统本地代码。通道接口允许您以一种受控且可移植方式来访问底层的 I/O 服务。

Channel 是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做一个比较,通道就是流,所有的数据通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入了一个包含多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道中读入缓冲区,再从缓冲区中获取这些字节。

Java NIO 的通道类似流,但是又有些不同:

  • 既可以通道汇总读取数据,又可以写数据到通道,但流的读写通常是单向的。

  • 通道可以异步地读写。

  • 通道中的数据总是先读取到 buffer , 或者总是需要从一个 buffer 写入。

正如上面所说, 从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:

Channel 实现

下面是 Java NIO 中最重要的 Channel 实现:

  • FileChannel

  • DatagramChannel

  • SocketChannel

  • ServerSocketChannel

(1) FileChannel 从文件中读写数据

(2) DatagramChannel 能够通过 UDP 读写网络中的数据

(3) SocketChannel 能够通过 TCP 写网络中的数据

(4) ServerSocketChannel 可以监听新进来的 TCP 连接, 就像 WBE 服务那样,对每个新进来的连接都会创建一个 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 newPosition )设置此通道的文件位置
long size ()返回此通道的文件的当前大小
FileChannel truncate *( *long size )将此通道的文件截取为给定大小
void force *( *boolean metaData )强制将所有对此通道文件更新到写入到存储设备中

下面是一个使用 FileChannel 读取数据到 buffer 中的一个实例:

public class FileChannelDemo {

	// FileChannel 读取数据到 buffer 中
	public static void main(String[] args) throws IOException {
		// 创建 FileChannel
		RandomAccessFile accessFile = new RandomAccessFile("C:\a.txt", "rw");
		FileChannel fileChannel = accessFile.getChannel();

		// 创建 buffer
		ByteBuffer byteBuffer = ByteBuffer.allocate(48);

		while (fileChannel.read(byteBuffer) != -1) {
			System.out.println("读取到了: " + byteBuffer);
			byteBuffer.flip();
			while (byteBuffer.hasRemaining()) {
				System.out.println((char) byteBuffer.get());
			}
			byteBuffer.clear();
		}
		fileChannel.close();
		System.out.println("end");
	}
}

Buffer 通常的操作


将数据写入缓冲区

调用 buffer.filp() 反转读写模式

从缓冲区读取数据

调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容

FileChannel 操作和详解

打开 FileChannel

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

RandomAccessFile accessFile = new RandomAccessFile("C:\a.txt", "rw");
FileChannel fileChannel = accessFile.getChannel();

从 FileChannel 中读取数据

调用读取数据 read() 方法之一从 File Channel 中读取数据。如:

ByteBuffer byteBuffer = ByteBuffer.allocate(48);
byteBuffer = accessFile.read(byteBuffer);

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

向 FileChannel 写数据

通过 FileChannel.write() 方法向 FileChannel 写数据, 该方法的一个参数是 buffer。

如:

public class FileChannelDemo2 {

	// FileChannel 读取数据到 buffer 中
	public static void main(String[] args) throws IOException {
		// 创建 FileChannel
		RandomAccessFile accessFile = new RandomAccessFile("C:\a.txt", "rw");
		FileChannel fileChannel = accessFile.getChannel();

		String newData = "new string to write to file ..." + System.currentTimeMillis();
		// 创建 buffer
		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
		byteBuffer.clear();
		byteBuffer.put(newData.getBytes());

		byteBuffer.flip();

		while (byteBuffer.hasRemaining()) {
			fileChannel.write(byteBuffer);
		}
		fileChannel.close();
		System.out.println("end");
	}
}

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

关闭 FileChannel

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

fileChannel.close();

FileChannel 的 position 方法

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

这里有两个例子:

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

如果设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回 -1 (文件结束标识)。

如果将位置这只在文件结束符之后,然后向通道写数据,文件将撑大到当前位置并且写入数据。这可能导致 “文件空洞”, 磁盘上无理文件中写入的数据间有空隙。

FileChannel 的 size 方法

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

long fileSize = channel.size()

FileChannel 的 truncate 方法

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

channel.truncate(1024);

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

FileChannel 的 force 方法

FileChannel.force() 方法将通道里尚未写入磁盘的数据强制写到磁盘上。处于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写入到磁盘上。要保证这一点需要调用 force 方法。

force() 方法有一个 boolean 类型的参数,指明是否将文件元数据(权限信息等)写到磁盘上。

FileChannel 的 transferTo 和 transferFrom

通道之前的两数据传输:

如果两个通道中有一个是 FileChannel , 那你可以直接将数据从一个 channel 传输到另外一个 channel 。

(1)transferForm() 方法

fileChannel 的 transforFrom() 方法可以将数据重源通道传输到 FileChannel 中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。下面是一个 FileChannel 完成文件间的复制的例子:

public class FileChannelWrite {

	public static void main(String[] args) throws IOException {
		// 创建 FileChannel
		RandomAccessFile fromFile = new RandomAccessFile("C:\a.txt", "rw");
		FileChannel formChannel = fromFile.getChannel();

		// 创建 FileChannel
		RandomAccessFile toFile = new RandomAccessFile("C:\c.txt", "rw");
		FileChannel toChannel = toFile.getChannel();

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

		formChannel.close();
		toChannel.close();
		System.out.println("end");
	}
}

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

(1)transferTo() 方法

transferTo() 方法是将数据从 FileChannel 传输到其他的 channel 中

下面是一个 transferTo() 方法的例子:

public class FileChannelWrite2 {

	public static void main(String[] args) throws IOException {
		// 创建 FileChannel
		RandomAccessFile fromFile = new RandomAccessFile("C:\a.txt", "rw");
		FileChannel formChannel = fromFile.getChannel();

		// 创建 FileChannel
		RandomAccessFile toFile = new RandomAccessFile("C:\c.txt", "rw");
		FileChannel toChannel = toFile.getChannel();

		long position = 0;
		long count = formChannel.size();
		formChannel.transferTo(position, count, toChannel);

		formChannel.close();
		toChannel.close();
		System.out.println("end");
	}
}

Socket 通道

(1)新的 Socket 通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。借助新的 NIO 类。一个或几个线程就可以管理成百上千的活动 socket 连接了并且只有很少甚至可能没有性能损失。所有的 socket 通道类(DatagramChannel 、SocketChannel和 ServeSocketChannel ). 都继承了 java.nio.channel.spi 包中的 AbstractSelectableChannel . 这意味着我们可以用一个 Selector 对象来执行 socket 通道的就绪选择 (readiness selection)。

(2) 请注意 DatagramChannel 和 SocketChannel 实现定义读和写功能的接口而 ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身不传输数据。

(3) 我们在具体讨论每一种 socket 通道前,您还应该了解 sokcet 和 socket 通道之间的关系。通道是一个连接 I/O 服务导管并提供与该服务交互的方法。就某个 Socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket API 协议,而 java.net 中已经存在的 socket 通道都可以被大多数协议操作重复使用

全部 socket 通道类(DatagramChannel 、SocketChannel、ServerSocketChannel)在被实例化都会创建一个对等的 Socket 对象。这些使我们所熟悉的来自 java.net 的类(Socket、ServerSocket 和 DatagramSocket), 它们已经被更新以识别通道。对等 socket 可以通过 socket() 方法从一个通道上获取。此外,这三个 java.net 类都有

getChannel() 方法。

(4)把一个 socket 通道置于非阻塞模式,我们要依靠所有 socket 通道类的公有超类:SelectableChannel 。就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写。非阻塞 I/O 和可选择性是紧密连接的,那也是正是管理阻塞模式的 API 代码要在 SelectableChannel 超级类中定义的原因。

设置或重新设置一个通道的阻塞模式是简单的,只要调用configureBlocking() 方法即可,传递参数值为 true 则设为阻塞模式,参数值为 flase 值设为非阻塞模式。可以通过调用 isBlocking() 方法来判断某个 socket 同党当前处于那种模式。

AbstractSelectableChannel.java 中实现的 configureBlocking ( boolean block) 方法如下:

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个通道的介绍:

ServerSocketChannel

ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行。

由于 ServerSocketChannel 没有 bind() 方法,因此有必要取出对等的 socket 并使用它来绑定到一个端口以开启监听连接。我们也就是使用 ServerSocket 的 API 来根据需要设置其他的 socket 选项。

同 java.net.ServeerSocket 一样,ServerScocketChannel 也有 accept() 方法。一旦创建了一个 ServerSocketChannel 并用对等的 socket 绑定了它,然后您就可以在其中一个上调用 accept() 。 如果您选择 ServerSocket 上调用 accept() 方法,那么它会同任何其他的 ServerSocket 变现一样的行为:总是阻塞并返回一个 java.net.Socket 对象。如果您选择在 ServerSocketChannel 上调用 accept() 方法则会返回

ServerSocketChannel 类型的对象,返回的对象能在非阻塞模式下运行。

换句话说:

ServerSocketChannel 的 accept() 方法会返回 SocketChannel 类型对象

SocketChannel 可以在非阻塞模式下运行。

其他 Socket 的 accept() 方法会阻塞返回 Socket 对象。

如果 ServerSocketChannel 以非阻塞模式被调用,当没有传入连接等待时

ServerSocketChannel.accept() 会立即返回 null. 正是这种检查连接而不阻塞的能力实现了伸缩性并且降低了复杂性。可选择性因此得到实现。我们可以使用一个选择器实例来注册 ServerSocketChannel 对象以实现新连接达到自动通知的功能。

public class ServerSocketChannelDemo {

	public static void main(String[] args) throws IOException, InterruptedException {
		int port = 8888;
		ByteBuffer buffer = ByteBuffer.wrap("hell0 world!".getBytes(StandardCharsets.UTF_8));

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

		socketChannel.configureBlocking(false);

		while (true) {
			System.out.println("Waiting for connections");
			SocketChannel sc = socketChannel.accept();
			if (sc == null) {
				System.out.println("null");
				TimeUnit.SECONDS.sleep(2);
			} else {
				System.out.println("Incoming conection form: " +
						sc.socket().getRemoteSocketAddress());
				buffer.rewind();
				sc.write(buffer);
				sc.close();
			}
		}

	}
}

访问的结果

(1)打开 ServerSocketChannel

调用 ServerSocketChannel.open() 方法来打开 ServerSocketChannel

ServerSocketChannel socketChannel = ServerSocketChannel.open();

(2)关闭 ServerSocketChannel

调用 ServerSocketChannel.close() 方法来打开 ServerSocketChannel

socketChannel.close()

(3)监听新的连接

通过调用 ServerSocketChannel.close() 方法来监听新进的连接。当 accept() 方法返回时,它返回一个包含进来新的 SocketChannel . 因此, accept() 方法会一直阻塞知道有新的连接到达。

通常不仅仅只监听一个连接。在 while 循环中调用 accept 方法。如下面的例子:

while (true) {
			System.out.println("Waiting for connections");
			SocketChannel sc = socketChannel.accept();
			if (sc == null) {
				System.out.println("null");
				TimeUnit.SECONDS.sleep(2);
			} else {
				System.out.println("Incoming conection form: " +
						sc.socket().getRemoteSocketAddress());
				buffer.rewind();
				sc.write(buffer);
				sc.close();
			}
		}

(4)阻塞模式

会在 SocketChannel sc = socketChannel.accept();这里阻塞住进程。

(5)非阻塞模式

ServerSocketChannel 可以设置为非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回, 如果没有新的连接进来将会是返回 null , 因此,需要检查返回的 SocketChannel 是否是 null , 如:

SocketChannel sc = socketChannel.accept();
if (sc == null) {
    System.out.println("null");
    TimeUnit.SECONDS.sleep(2);
} else {
    System.out.println("Incoming conection form: " +
                       sc.socket().getRemoteSocketAddress());
    buffer.rewind();
    sc.write(buffer);
    sc.close();
}

SocketChannel

SocketChannel 介绍

JAVA NIO 中 SocketChannel 是一个连接到 TCP 网络套接字的通道

A selectable channel for stream-oriented connection sockets

以上是 Java docs 中对于 SocketChannel 的描述: SocketChannel 是一种面向连接 socket 涛姐字的可选择通道从这里可以看出:

  • SocketChannel 是用来连接 Socket 套接字
  • SocketChannel 主要用途是用来处理网络 I/O 通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用

SokectChannel 特征

(1)对于已经存在的 socket 不能创建 SocketChannel

(2)SocketChannel 中提供的 open 接口创建的 Channel 并没有尽享网络级联,需要使用 conect 接口连接到指定地址

(3)未进行连接的 SocketChannel 执行 I/O 操作时,会抛出 NotYesConnectedException

(4)SocketChannel 支持两种 I/O 模式:阻塞式和非阻塞式

(5)SocketChannel 支持异步关闭。如果 SocketChannel 一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput. 则读阻塞的线程将返回 -1, 表示没有任何数据;如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该 SocketChannel 调用 shutdownwrite , 则写阻塞的线程将抛出 AsynchronousCloseException.

(6)SocketChannel 支持设定参数:

SO_SNDBUF 套接字发送缓冲区大小

SO_RCVBUF 套接字接受缓冲区大小

SO_KEEPALIVE 保活连接

O_REUSEADDR 复用地址

SO_LINGER 有数据传输时延迟关闭 Channel (只有在非阻塞模式下有用)

TCP_NODELAY 禁用 Nagle 算法

SocketChannel 使用

(1)创建 SokcetChannel

方式一

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

方式二

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

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

(2)链接校验

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

(3)读写模式

前面提到 SocketChannel 支持阻塞和非阻塞两种模式

socketChannel.configureBlocking(false);

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

(4)读写

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

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

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

以上为非阻塞读,控制台将打印 read over

读写都是面向缓冲区,这个读写方式与前文中的 FileChannel 相同。\

(5)设置和获取参数

socketChannel5.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE)
    .setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);

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

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

可以通过 getOption 获取相关的参数值,如果默认接受缓冲区大小式 8192 byte

SocketChannel 还支持多路复用,多路复用后面内容会详细描述。

DatagramChannel

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

打开 DatagramChannel

DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(25000));

这个例子是打开 25000 端口接受 UDP 数据包。

接受数据

通过 receive() 接受 UDP 包

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

SocketAddress 可以获取发包的 ip 、端口等信息,通过 toString 查看,格式如下:

/127.0.0.1:57126

发送数据

通过 send() 发送 UDP 包

DatagramChannel server1 = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client hello!".getBytes());
server1.send(sendBuffer, new InetSocketAddress("127.0.0.1", 25000));

连接

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

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

read() 和 write() 自由 connect() 后才能使用,不能会抛出 NotYetConnectedException 异常。用 read() 接受时, 如果没有接受到包, 会抛出 PortNnreachableException 异常。

DatagramChannel 实例

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

// 发送客户端
public class DatagramChannelClient {

	public static void main(String[] args) throws IOException, InterruptedException {
		DatagramChannel sendChannel = DatagramChannel.open();
		InetSocketAddress socketAddress =
				new InetSocketAddress("127.0.0.1", 25000);
		while (true) {
			ByteBuffer buffer = ByteBuffer.wrap("发送 DataGram 测试数据".getBytes(StandardCharsets.UTF_8));
			sendChannel.send(buffer, socketAddress);
			System.out.println("send success!");
			TimeUnit.SECONDS.sleep(2);
		}
	}
}

// 接受服务端
public class DatagramChannelServer {

	public static void main(String[] args) throws IOException {
		DatagramChannel server = DatagramChannel.open();
		server.socket().bind(new InetSocketAddress(25000));

		ByteBuffer receiveBuffer = ByteBuffer.allocate(1024);
		while (true) {
			receiveBuffer.clear();
			SocketAddress receiveAddress = server.receive(receiveBuffer);
			receiveBuffer.flip();
			System.out.println(receiveAddress.toString());
			System.out.println(StandardCharsets.UTF_8.decode(receiveBuffer));
		}
	}
}

服务端打印:

\

客户端打印:

连接测试代码

public class DatagramChannelClient2 {

	public static void main(String[] args) throws IOException, InterruptedException {
		DatagramChannel channel = DatagramChannel.open();
		InetSocketAddress socketAddress =
				new InetSocketAddress("127.0.0.1", 25000);
		channel.bind(socketAddress);
		channel.connect(socketAddress);
		ByteBuffer buffer = ByteBuffer.wrap("发送 DataGram 测试数据".getBytes(StandardCharsets.UTF_8));
		channel.write(buffer);
		System.out.println("write success!");

		// buffer
		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
		while (true) {
			readBuffer.clear();
			channel.read(readBuffer);
			readBuffer.flip();
			System.out.println(StandardCharsets.UTF_8.decode(readBuffer));
			TimeUnit.SECONDS.sleep(2);
		}
	}
}

数据打印

Scatter/Gather

Java NIO 开始支持 scatter/gather , scatter/gather 用于描述从 channel 中读取或者写入到 channel 操作。

分散(scatter) 从channel 中读取是指在读操作时会将读取的数据写入多个 buffer 中,因此,channel 将从 channel z中读取的数据 “分散(scatter)” 到多个 buffer 中

聚集(gather) 写入 channel 是指在写操作时候将多个 buffer 的数据写同一个channel , 因此,channel 将多个 buffer 的数据“聚集(gather)”后发送到 channel

scatter/gather 经常用于需要将传输的数据分开处理得场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的 buffer 中,这样你可以方便的处理消息头和消息体。

Scattering Reads

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

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = [heander, body];

channel.read(bufferArray);

注意 buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。

read() 方法按照 buffer 在数组中的顺序从 channel 中读取的数据写入到 buffer , 当一个 buffer 被写满后,channel 紧接着向另外一个 buffer 中写。

Scattering Reads 在移动下一个 buffer 之前,必须填满当前的 buffer , 这就意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息同和消息体,消息头必须完成填充(例如 128 byte),Scattering Reads 才能正常工作。

Gathing Writes

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

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = [heander, body];

channel.write(bufferArray);

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