一、Channel
概述
- Channel 是一个通道,可以通过它 读取 和 写入 数据。
就像水管一样,网络数据通过 Channel 读取 和 写入。
- 通道 与 流 的不同之处在于 通道是双向的。
流只是在一个方向上移动(一个流必须是InputStream
或OutputStream
的子类)。- 通道 可以用于 读、写 或者 同时用于 读写。
因为 Channel 是全双工的,所以它可以比 流 更好地映射底层操作系统的 API。
- NIO 中通过 Channel 封装了对数据源的操作。
- 通过 Channel 可以操作 数据源,但又不必关心 数据源 的具体物理结构。
- 这个 数据源 可能是多种的。
可以是 文件,也可以是 网络 Socket。- 在大多数应用中,Channel 与 文件描述符 或者 Socket 是一一对应的。
- Channel 用于在 字节缓冲区 和 通道 另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
二、Channel
接口源码
public interface Channel extends Closeable {
/**
* 该通道是否打开。
*/
public boolean isOpen();
/**
* 关闭此通道。
*/
public void close() throws IOException;
}
- 与缓冲区不同,通道 API 主要由接口指定。
- 不同的操作系统上,通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。
- 因此通道的实现,使用操作系统的本地代码。
- 通道接口,允许您以一种受控且可移植的方式,来访问底层的 IO服务。
- Channel 是一个对象,可以通过它 读取 和 写入 数据。
- 拿 NIO 与原来的 IO 做个比较,通道就像是流。
所有数据都通过 Buffer对象 来处理。- 您永远不会将 字节 直接写入通道中,相反,您是将数据写入包含 一个 或者 多个 字节的缓冲区。
- 同样,您不会直接从 通道 中读取字节,而是将数据从 通道 读入缓冲区,再从缓冲区获取这个字节。
- Java NIO 的通道类似流,但又有些不同。
- 可以从 通道 中读取数据,又可以写数据到 通道。
但流的 读写 通常是单向的。- 通道可以异步地读写。
- 通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。
- 从通道 读取 数据到 缓冲区,从 缓冲区 写入数据到通道,如下图所示。
三、Channel
接口实现
1. FileChannel
文件 IO
- 从文件中读写数据。
2. DatagramChannel
UDP
- 能通过 UDP 读写网络中的数据。
3. SocketChannel
TCP
- 能通过 TCP 读写网络中的数据。
4. ServerSocketChannel
网络 IO
- 可以监听新进来的 TCP 连接,像 Web 服务器那样。
- 对每一个新进来的连接都会创建一个 SocketChannel。
四、FileChannel
1. FileChannel
介绍
- FileChannel 可以实现常用的 Read、Write 以及 Scatter/Gather 操作。
- 同时也提供了很多专用于文件的新方法,这些方法都是我们所熟悉的文件操作。
2. 从 FileChannel
读取数据
/**
* @author: wy
* describe: 从 FileChannel 读取数据
* 1. 读取数据到缓冲区(Buffer)
* 2. 反转读写模式
* 3. 从缓冲区中读取数据
* 4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容
*/
public class FileChannel1 {
public static void main(String[] args) throws IOException {
/*
一、打开 FileChannel (通过 RandomAccessFile)
name: 文件路径
mode:`rw`读写模式
*/
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel1.txt", "rw");
FileChannel channel = accessFile.getChannel();
// 二、从 FileChannel 读取数据到缓冲区(Buffer)
ByteBuffer buffer = ByteBuffer.allocate(6);
int bytesRead = channel.read(buffer);
// 三、获取缓冲区中数据
while (bytesRead != -1) {
System.out.printf("当前读取到缓冲区的字节数: %s,分别是: ", bytesRead).println();
// 1. 反转读写模式
buffer.flip();
while (buffer.hasRemaining()) {
// 2. 从缓冲区中获取数据
byte b = buffer.get();
System.out.printf("%s=%s", b, (char) b).println();
}
// 3. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区中数据
buffer.clear();
// 4. 如果返回 -1,表示读到了文件末尾
bytesRead = channel.read(buffer);
}
// 四、 关闭 Channel
channel.close();
accessFile.close();
System.out.println("FileChannel1 结束");
}
}
2.1 打开 FileChannel
- 在使用 FileChannel 之前,必须先打开它。
- 无法直接打开一个 FileChannel。
- 需要使用一个
InputStream
、OutputStream
或RandomAccessFile
来获取一个 FileChannel 实例。
- 下面通过
RandomAccessFile
打开FileChannel
的示例。
/*
一、打开 FileChannel (通过 RandomAccessFile)
name: 文件路径
mode:rw读写模式
*/
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel1.txt", "rw");
FileChannel channel = accessFile.getChannel();
2.2 写入数据到缓冲区
- 先分配一个 Buffer。
- 从 FileChannel 中读取的数据将被写到 Buffer 中。
- 调用 FileChannel.read() 方法,将数据从 FileChannel 读取到 Buffer 中。
- read() 方法返回的 int 值,表示有多少字节被读到了 Buffer 中。
- 如果返回 -1,表示读到了文件末尾。
// 二、从 FileChannel 读取数据到缓冲区(Buffer)
ByteBuffer buffer = ByteBuffer.allocate(6);
int bytesRead = channel.read(buffer);
3. 向 FileChannel
写入数据
/**
* @author: wy
* describe: 向 FileChannel 写入数据
*/
public class FileChannel2 {
public static void main(String[] args) throws IOException {
/*
一、打开 FileChannel (通过 RandomAccessFile)
name: 文件路径
mode:`rw`读写模式
*/
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel1.txt", "rw");
FileChannel channel = accessFile.getChannel();
// 二、添加数据到缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
// 1. 加入缓冲区
buffer.put("qs".getBytes());
// 2. 反转读写模式
buffer.flip();
// 三、从缓冲区写入文件
while (buffer.hasRemaining()) {
// 1. 将 Buffer 中的数据写入到 Channel
channel.write(buffer);
}
// 2. 强制刷新到磁盘
channel.force(true);
// 四、关闭 Channel
channel.close();
accessFile.close();
System.out.println("FileChannel2 结束");
}
}
- 注意 FileChannel.write() 是在 while 循环中调用的。
- 因为无法保证 write() 方法,一次能向 FileChannel 写入多少字节。
- 因此需要重复调用 write() 方法,直到 Buffer 中已经没有尚未写入通道的字节。
3.1 关闭 FileChannel
// 四、关闭 Channel
channel.close();
4. position()
方法
- 有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。
- 可以通过调用 position() 方法获取 FileChannel 的当前位置。
- 也可以通过调用 position(long pos) 方法设置 FileChannel 的当前位置。
- 如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回
-1
(文件结束标志)。- 如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致 文件空洞,磁盘上物理文件中写入的数据间有空隙。
// 文件当前位置
long position = channel.position();
System.out.printf("position: %s", position).println();
// position: 0
channel.position(position + 100);
5. size()
方法
返回所关联文件的大小。
// 文件大小
long size = channel.size();
System.out.printf("size: %s", size).println();
// size: 12
6. truncate()
方法
- 截取一个文件。
- 截取文件时,文件将指定长度后面的部分删除。
// 截取文件的前 2 个字节。
FileChannel truncate = channel.truncate(2);
System.out.printf("truncate.size: %s", truncate.size()).println();
// truncate.size: 2
7. force()
方法
- 将通道里尚未写入磁盘的数据,强制写到磁盘上。
- 出于性能方面的考虑,操作系统会将数据缓存在内存中。
- 所以无法保证写入到 FileChannel 里的数据,一定会及时写到磁盘上。
- 要保证这一点,需要调用
force()
方法。
- force() 方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
8. transferTo()
和 transferFrom()
方法
- 通道之间的数据传输。
- 如果两个通道中有一个是 FileChannel,可以直接将数据从一个 Channel 传输到另外一个 Channel。
8.1 transferTo()
方法
将数据从 FileChannel 传输到其他的 Channel 中。
/**
* 1. transferFrom() 将数据从 FileChannel 传输到其他的 Channel 中。
*/
@Test
public void testTransferTo() throws IOException {
// 一、打开两个 FileChannel
RandomAccessFile fromAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel31.txt", "rw");
RandomAccessFile toAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel32.txt", "rw");
FileChannel fromChannel = fromAccessFile.getChannel();
FileChannel toChannel = toAccessFile.getChannel();
// 二、从 fromChannel 传输到 toChannel
// 开始位置 0
long position = 0;
// fromChannel 文件大小
long size = fromChannel.size();
System.out.printf("size: %s", size).println();
fromChannel.transferTo(position, size, toChannel);
// 三、关闭
fromAccessFile.close();
toAccessFile.close();
System.out.println("testTransferTo over");
}
8.2 transferFrom()
方法
- 将数据从源通道传输到 FileChannel 中。
- 在 JDK 文档中的解释为:将字节从给定的可读取字节通道,传输到此通道的文件中。
/**
* 2. transferFrom() 将数据从源通道传输到 FileChannel 中。
*/
@Test
public void testTransferFrom() throws IOException {
// 打开两个 FileChannel
RandomAccessFile fromAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel31.txt", "rw");
RandomAccessFile toAccessFile = new RandomAccessFile("E:\\TEMP\\nio\\FileChannel32.txt", "rw");
FileChannel fromChannel = fromAccessFile.getChannel();
FileChannel toChannel = toAccessFile.getChannel();
// 二、从 fromChannel 传输到 toChannel
// fromChannel 传输到 toChannel
// 开始位置 0
long position = 0;
// fromChannel 文件大小
long size = fromChannel.size();
System.out.printf("size: %s", size).println();
toChannel.transferFrom(fromChannel, position, size);
// 三、关闭
fromAccessFile.close();
toAccessFile.close();
System.out.println("testTransferFrom over");
}
position
参数:表示从 position 处开始向目标文件写入数据。count
参数:表示最多传输的字节数。
- 如果源通道的剩余空间小于
count
个字节,则所传输的字节数要小于请求的字节数。- 在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足
count
字节)。因此,SocketChannel 可能不会将请求的所有数据(count
个字节)全部传输到 FileChannel 中。
五、Socket
通道
- 新的 Socket 通道类。
- 可以运行非阻塞模式并且是可选择的。
- 可以激活大程序(如:网络服务器 和 中间件组件)巨大的可伸缩性和灵活性。
- 不需要为每个 Socket 连接使用一个线程了,也避免了管理大量线程所需的上下文交换开销。
- 新的 NIO 类,一个或几个线程就可以管理成百上千的活动 Socket 连接了,并且只有很少甚至可能没有性能损失。
- 所有的 Socket 通道类(
DatagramChannel
、SocketChannel
和ServerSocketChannel
)都继承了java.nio.channels.spi.AbstractSelectableChannel
。- 意味着我们可以用一个 Selector 对象来执行 Socket 通道的就绪选择(readiness selection)。
DatagramChannel
和SocketChannel
实现定义 读 和 写 功能的接口。ServerSocketChannel
负责监听传入的连接 和 创建新的SocketChannel
对象,它本身从不传输数据。
- Socket 和 Socket 通道之间的关系。
- 通道是一个连接 I/O 服务导管,并提供与该服务交互的方法。
- 对 Socket 而言,它不会再次实现与之对应的 Socket 通道类中的 Socket 协议 API。
- java.net 中已经存在的 Socket 通道,都可以被大多数协议操作重复使用。
- 所有的 Socket 通道类在被实例化时,都会创建一个对等 Socket 对象。
- 对等 Socket 可以通过调用 socket() 方法从一个通道上获取。
- 所有的 Socket 通道类,现在都有 getChannel() 方法。
- 把一个 Socket 通道置于非阻塞模式。
- 要依靠所有 Socket 通道类的公有超级类
SelectableChannel
。- 就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作(如:读或写)。
- 非阻塞 I/O 和 可选择性 是紧密相连的,那也正是管理阻塞模式的 API 代码,要在
SelectableChannel
超级类中定义的原因。
configureBlocking()
方法,设置或重新设置一个通道的阻塞模式。
- 参数 true:设为阻塞模式。
- 参数 false:设为非阻塞模式。
isBlocking()
方法,判断 Socket 通道当前处于哪种模式。
1. AbstractSelectableChannel.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 程序可以专注于用户请求,并且同时维护与一个或多个服务器的会话。
- 在很多程序上,非阻塞模式都是有用的。
blockingLock()
方法,防止 Socket 通道的阻塞模式被更改。
- 该方法会返回一个非透明的对象引用。
- 返回的对象是通道实现,修改阻塞模式时内部使用的。
- 只有拥有此对象的锁的线程,才能更改通道的阻塞模式。
2. 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 类型的对象,返回的对象能够在非阻塞模式下运行。
- Socket 的 accept() 方法会阻塞返回一个 Socket 对象。
- 如果 ServerSocketChannel 以非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept() 会立即返回
null
。- 这种检查连接而不阻塞的能力,实现了可伸缩性并降低了复杂性。可选择性也因此得到实现。
- 可以使用一个选择器实例,来注册 ServerSocketChannel 对象,以实现新连接到达时自动通知的功能。
3. ServerSocketChannel
示例
/**
* @author: wy
* describe: ServerSocketChannel 示例
*/
public class ServerSocketChannel1 {
/**
* http://127.0.0.1:8888/
*/
public static void main(String[] args) throws IOException, InterruptedException {
// 默认端口号
int port = 8888;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
// 数据加入缓冲区
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
// 一、打开 ServerSocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定端口
channel.socket().bind(new InetSocketAddress(port));
// 二、设置阻塞模式(false 非阻塞)
channel.configureBlocking(false);
// 三、监听有新连接
int count = 0;
while (true) {
System.out.println("accept()监听新的连接...");
// 1. 非阻塞时,accept()方法不会阻塞
SocketChannel socketChannel = channel.accept();
if (socketChannel == null) {
// 2. 没有新的连接,返回 null
System.out.println("没有新的连接");
TimeUnit.SECONDS.sleep(2);
} else {
// 3. 新的连接来自: /127.0.0.1:53917
System.out.printf("新的连接来自: %s", socketChannel.socket().getRemoteSocketAddress()).println();
// 指针0
buffer.rewind();
socketChannel.write(buffer);
socketChannel.close();
}
count++;
if (count == 20) {
break;
}
}
// 四、关闭 ServerSocketChannel
channel.close();
}
}
3.1 打开 ServerSocketChannel
// 一、打开 ServerSocketChannel
ServerSocketChannel channel = ServerSocketChannel.open();
// 1. 绑定端口
channel.socket().bind(new InetSocketAddress(port));
3.2 关闭 ServerSocketChannel
// 四、关闭 ServerSocketChannel
channel.close();
3.3 监听新的连接
- ServerSocketChannel.accept() 方法监听新进的连接。
- 当 accept() 方法返回的时候,它返回一个包含新进来的连接的 SocketChannel。
- 因此 accept() 方法会一直阻塞到有新连接到达。
- 通常不会只监听一个连接,在 while 循环中调用 accept() 方法。
while (true) {
System.out.println("accept()监听新的连接...");
// 1. 非阻塞时,accept()方法不会阻塞
SocketChannel socketChannel = channel.accept();
}
3.4 阻塞模式
会在
serverSocketChannel.accept();
阻塞进程。
// 二、设置阻塞模式(false 非阻塞)
channel.configureBlocking(true);
3.5 非阻塞模式
- 在非阻塞模式下,accept() 方法会立刻返回。
- 如果还没有新进来的连接,返回的将是 null。
- 因此,需要检查返回的 SocketChannel 是否是 null。
// 二、设置阻塞模式(false 非阻塞)
channel.configureBlocking(false);
4. SocketChannel
介绍
- Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
- SocketChannel 是用来连接 Socket 套接字。
- SocketChannel 主要用途用来处理网络 I/O 的通道。
- SocketChannel 是基于 TCP 连接传输。
- SocketChannel 实现了可选择通道,可以被多路复用的。
5. SocketChannel
特征
- 对于已经存在的 Socket 不能创建 SocketChannel。
- SocketChannel 中提供的 open() 方法,创建的 Channel 并没有进行网络连接,需要使用 connect() 方法连接到指定地址。
- 未进行连接的 SocketChannel 执行 I/O 操作时,会抛出 NotYetConnectedException。
- SocketChannel 支持两种 I/O 模式:阻塞 和 非阻塞。
- SocketChannel 支持异步关闭。
- 如果 SocketChannel 在一个线程上 read() 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput(),则读阻塞的线程将返回
-1
,表示没有读取任何数据。- 如果 SocketChannel 在一个线程上 write() 阻塞,另一个线程对该 SocketChannel 调用 shutdownWrite(),则写阻塞的线程将抛出 AsynchronousCloseException。
- SocketChannel 支持设定参数。
参数 | 描述 |
---|---|
SO_SNDBUF | 套接字发送缓冲区大小 |
SO_RCVBUF | 套接字接收缓冲区大小 |
SO_KEEPALIVE | 保活连接 |
O_REUSEADDR | 复用地址 |
SO_LINGER | 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用) |
TCP_NODELAY | 禁用 Nagle 算法 |
6. SocketChannel
示例
/**
* @author: wy
* describe: SocketChannel 示例
*/
public class SocketChannel1 {
public static void main(String[] args) throws IOException {
// 一、创建 SocketChannel
SocketChannel socketChannel = SocketChannel
.open(new InetSocketAddress("www.baidu.com", 80));
// 二、设置阻塞模式(false非阻塞)
socketChannel.configureBlocking(true);
// 三、读操作
ByteBuffer buffer = ByteBuffer.allocate(16);
socketChannel.read(buffer);
socketChannel.close();
/*
1. true: 为阻塞模式,当执行到 read() 线程将阻塞,控制台将无法打印 read over。
2. false: 为非阻塞模式,控制台将打印 read over。
3. 读写都是面向缓冲区,这个读写方式与 FileChannel 相同。
*/
System.out.println("read over");
}
}
6.1 创建 SocketChannel
// 一、创建 SocketChannel
SocketChannel socketChannel = SocketChannel
.open(new InetSocketAddress("www.baidu.com", 80));
// 1. 方式二、无参 open() 只是创建了一个 SocketChannel 对象,并没有进行实质的 tcp 连接
SocketChannel socketChannel2 = SocketChannel.open();
socketChannel2.connect(new InetSocketAddress("www.baidu.com", 80));
6.2 连接校验
// 2. 测试 SocketChannel 是否为 open 状态
boolean open = socketChannel.isOpen();
System.out.printf("open: %s", open).println();
// open: true
// 3. 测试 SocketChannel 是否已经被连接
boolean connected = socketChannel.isConnected();
System.out.printf("connected: %s", connected).println();
// connected: true
// 4. 测试 SocketChannel 是否正在进行连接
boolean connectionPending = socketChannel.isConnectionPending();
System.out.printf("connectionPending: %s", connectionPending).println();
// connectionPending: false
// 5. 校验正在进行套接字连接的 SocketChannel 是否已经完成连接
boolean finishConnect = socketChannel.finishConnect();
System.out.printf("finishConnect: %s", finishConnect).println();
// finishConnect: true
6.3 阻塞模式
支持 阻塞 和 非阻塞 两种模式。
// 二、设置阻塞模式(false非阻塞)
socketChannel.configureBlocking(true);
6.4 读操作
// 三、读操作
ByteBuffer buffer = ByteBuffer.allocate(16);
socketChannel.read(buffer);
socketChannel.close();
/*
1. true: 为阻塞模式,当执行到 read() 线程将阻塞,控制台将无法打印 read over。
2. false: 为非阻塞模式,控制台将打印 read over。
3. 读写都是面向缓冲区,这个读写方式与 FileChannel 相同。
*/
System.out.println("read over");
System.out.println(buffer);
6.5 设置和获取参数
- setOption() 方法,设置 Socket 套接字的相关参数。
- getOption() 方法,获取相关参数的值。
- 如默认的接收缓冲区大小是 8192 byte,SocketChannel 还支持多路复用。
// 1. setOptions 设置参数
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE)
.setOption(StandardSocketOptions.TCP_NODELAY, Boolean.TRUE);
// 2. getOption 获取参数
Boolean option = socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
System.out.println(option);// true
Integer option2 = socketChannel.getOption(StandardSocketOptions.SO_RCVBUF);
System.out.println(option2);// 65536
7. DatagramChannel
介绍
- SocketChannel 对应 Socket。
- SocketChannel 模拟连接导向的流协议(如:TCP/IP)。
- ServerSocketChannel 对应 ServerSocket。
- DatagramChannel 对应 DatagramSocket。
- DatagramChannel 模拟包导向的无连接协议(如:UDP/IP)。
- DatagramChannel 是无连接的。
- 每个数据包(Datagram)都是一个自包含的实体,拥有自己的目的地址,及不依赖其他数据包的数据负载。
- 与面向流的的 Socket 不同,DatagramChannel 可以发送单独的数据包给不同的目的地址。
- 同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据包都含有关于它来自何处的信息(源地址)。
8. DatagramChannel
示例
8.1 打开 DatagramChannel
- 打开 9999 端口接收 UDP 数据包。
// 一、打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
// 1. 绑定端口
channel.bind(new InetSocketAddress(9999));
// 2. 连接
channel.connect(new InetSocketAddress("127.0.0.1", 9999));
8.2 接收数据
- receive() 接收 UDP 包。
/**
* 接收包
*/
@Test
public void receiveDatagram() throws IOException {
// 一、打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
// 1. 绑定端口
channel.bind(new InetSocketAddress(9999));
// 2. 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
System.out.println("等待接收: ");
while (true) {
buffer.clear();
// 3. 接收数据
SocketAddress socketAddress = channel.receive(buffer);
buffer.flip();
// socketAddress 可以获得发包的Ip、端口等信息
System.out.printf("socketAddress: %s, ", socketAddress.toString());
System.out.printf("buffer: %s", StandardCharsets.UTF_8.decode(buffer)).println();
// socketAddress: /127.0.0.1:57443, buffer: qs: 1637043210121
}
}
8.3 发送数据
- send() 发送 UDP 包。
/**
* 发送包
*/
@Test
public void sendDatagram() throws IOException, InterruptedException {
// 一、打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 9999);
while (true) {
String msg = String.format("qs: %d", new Date().getTime());
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
// 1. 发送数据
channel.send(buffer, socketAddress);
System.out.printf("发送: %s", msg).println();
// 发送: qs: 1641566408995
TimeUnit.SECONDS.sleep(1);
}
}
8.4 连接
- UDP 不存在真正意义上的连接。
- 这里的连接是向特定服务地址,用 read() 接收 和 write() 发送数据包。
/**
* 连接 read() 接收包 和 write() 发送包
* 1. read() 和 write() 只有在 connect() 后才能使用,不然会抛 NotYetConnectedException 异常。
* 2. 用 read() 接收时,如果没有接收到包,会抛 PortUnreachableException 异常。
*/
@Test
public void testConnect() throws IOException {
// 一、打开 DatagramChannel
DatagramChannel channel = DatagramChannel.open();
// 1. 绑定端口
channel.bind(new InetSocketAddress(9999));
// 2. 连接
channel.connect(new InetSocketAddress("127.0.0.1", 9999));
// write
channel.write(ByteBuffer.wrap("qs".getBytes(StandardCharsets.UTF_8)));
System.out.printf("发送: %s", "qs").println();
// read
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = channel.read(buffer);
buffer.flip();
System.out.printf("接收: %s, %d", StandardCharsets.UTF_8.decode(buffer), read).println();
}
}
六、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 中,这样你可以方便的处理消息头和消息体。
1. Scattering Reads
- Scattering Reads 是指数据从一个 Channel 读取到多个 Buffer 中。如下图描述:
/**
* 1. 数据从一个 Channel 读取到多个 Buffer 中。
*/
@Test
public void testScatter() throws IOException {
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\Scatter.txt", "rw");
FileChannel channel = accessFile.getChannel();
ByteBuffer header = ByteBuffer.allocate(2);
ByteBuffer body = ByteBuffer.allocate(4);
ByteBuffer[] bufferArr = {header, body};
long bytesRead = channel.read(bufferArr);
System.out.printf("读取到缓冲区的大小: %s", bytesRead).println();// 读取到缓冲区的大小: 6
// 1. 反转读写模式
header.flip();
body.flip();
System.out.println((char) header.get());// a
System.out.println((char) body.get());// c
channel.close();
}
- 注意 Buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。
- read() 方法按照 Buffer 在数组中的顺序,将从 Channel 中读取的数据写入到 Buffer。
- 当一个 Buffer 被写满后,Channel 紧接着向另一个 Buffer 中写。
- Scattering Reads 在移动下一个 Buffer 前,必须填满当前的 Buffer。
- 这也意味着它不适用于动态消息(注:消息大小不固定)。
- 换句话说,如果存在消息头和消息体,消息头必须完成填充(如 128byte)Scattering Reads 才能正常工作。
2. Gathering Writes
- Gathering Writes 是指数据从多个 Buffer 写入到同一个 Channel。如下图描述:
/**
* 2. 数据从多个 Buffer 写入到同一个 Channel。
*/
@Test
public void testGather() throws IOException {
RandomAccessFile accessFile = new RandomAccessFile("E:\\TEMP\\nio\\Gather.txt", "rw");
FileChannel channel = accessFile.getChannel();
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
// 1. 将数据写入缓冲区
header.put("abcd".getBytes());
body.put("ABCD".getBytes());
// 2. 反转读写模式
header.flip();
body.flip();
ByteBuffer[] bufferArr = {header, body};
// 3. 将数据写入 Channel
channel.write(bufferArr);
channel.force(true);
channel.close();
}
- Buffers 数组是 write() 方法的入参。
- write() 方法会按照 Buffer 在数组中的顺序,将数据写入到 Channel。
- 注意只有 position 和 limit 之间的数据才会被写入。
- 因此,如果一个 Buffer 的容量为 128 byte,但是仅仅包含 58 byte 的数据,那么这 58 byte 的数据将被写入到 Channel 中。
- 因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息。