作者简介:☕️大家好,我是Aomsir,一个爱折腾的开发者!
个人主页:Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客
当前专栏:Netty应用专栏_Aomsir的博客-CSDN博客
文章目录
- 参考文献
- 前言
- Channel
- 简介
- 常见Channel
- Channel获取方式
- Buffer简介
- 演示案例
- IO流
- Channel
- 错误案例
- 改进案例
- 注意
- 总结
参考文献
- 孙哥suns说Netty
- Netty官方文档
前言
从本篇文章开始,我们将深入学习 NIO(非阻塞I/O)编程的相关内容,从头到尾详尽分析,包括Selector和Reactor模型,这将为我们后续学习Netty等更高层次的网络编程库打下坚实的基础。NIO编程的核心元素主要包括Channel和Buffer。
NIO编程使用Channel来进行通信。在服务端,我们还引入了Selector选择器,它帮助我们主动监控客户端的Channel,确保这些Channel能够正常通信(即正常连接且没有阻塞)。通过监控客户端的请求链路,Selector的作用是,一旦发现某个客户端阻塞,它可以将分配给该客户端的线程重新分配给其他可用客户端,这样一个线程就能为多个客户端提供服务。需要注意的是,每个客户端都拥有自己独立的Channel,不共享一个Channel。这篇文章将深入学习和理解Channel的概念,为后续的内容打下坚实的基础。
Channel
简介
Channel是一种用于IO通信的管道,类似于传统的InputStream
和OutputStream
。然而,与传统IO流不同,其中有输入流和输出流的方向性,NIO中的Channel是 无方向性。在传统IO开发中,为了读取文件并在JVM中进行操作后将结果写回文件,我们需要一个InputStream输入流将文件读入JVM,然后使用OutputStream输出流将结果写回文件。这些流是单向的,每个用于特定的目的。而在NIO中,Channel既可以用于读取数据,也可以用于写入数据,这为通用的双向数据传输提供了更大的灵活性
常见Channel
- 文件IO操作
FileChannel
:读和写文件中的数据
- 网络IO操作
ServerSocketChannel
:监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接创建一个SocketChannelSocketChannel
:通过TCP读写网络中的数据DatagramChannel
:通过UDP读写网络中的数据
Channel获取方式
- 文件IO操作
FileInputStream/FileOutputStream
获取RandonAccessFile
获取
- 网络通信
Socket获取
ServerSocket获取
DatagramSocket获取
FileChannel channel = new FileInputStream(TestNIO1.class.getClassLoader().getResource("data.txt").getFile()).getChannel();
FileChannel channel = new RandomAccessFile("data.txt", "rw").getChannel();
Buffer简介
因在下一篇文章中,我们将详细讲解Buffer,因为它是NIO中的一个非常核心的概念。掌握好Buffer对于理解后续的内容,包括Netty,至关重要。
Buffer可以被看作是JVM内存中的一块区域,它类似于一个缓冲区。在传统的流通信中,我们通常使用字节数组来装载接收到的数据。Buffer也类似于这个字节数组,但不同之处在于它具有读写指针,用于标识内存中的数据是用于读取还是写入。这一特性使得Buffer非常适用于NIO中的数据处理
演示案例
IO流
如下是使用Stream流的方式的进行读操作
public class TestNIO1 {
private static final Logger log = LoggerFactory.getLogger(TestNIO1.class);
public static void main(String[] args) throws IOException {
// 创建输入流
InputStream inputStream = TestNIO1.class.getClassLoader().getResourceAsStream("data.txt");
// 创建缓冲区
byte[] bytes = new byte[1024];
// 读取数据到缓冲区
while (inputStream.read(bytes) != -1){
String s = new String(bytes);
log.info("s = {}", s);
}
}
}
Channel
错误案例
在下面的案例中,我们设置Buffer的大小为10个字节。但是,如果我们尝试读取一个13字节的数据流,那么后面的3个字节将一直保留在缓冲区中,无法读取。动态调整Buffer的大小可能不是一个明智的选择,因为这会引入复杂性。
为了解决这个问题,我们可以采用改进案例中的方法。不再固定Buffer的大小,而是使用循环不断读取,直到所有数据被消耗。这种方式可以有效地处理不定长度的数据,而不需要频繁地调整缓冲区大小
public class TestNIO1 {
private static final Logger log = LoggerFactory.getLogger(TestNIO1.class);
public static void main(String[] args) throws IOException {
// 1.创建Channel通道 - FileChannel
FileChannel channel = new FileInputStream("/Users/aomsir/MyStudyProject/Java/Netty/netty-basic-01/data.txt").getChannel();
// 2.创建Buffer缓冲区,容量为10字节,具体根据文件编码来定
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
// 3.把channel读取的数据放入buffer,读完以后返回的是-1
int read = channel.read(buffer);
if (-1 == read) {
break;
}
// 4.设置buffer为读模式,代表程序从buffer中读取数据
buffer.flip();
// 5.循环读取缓冲区数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
System.out.println((char) b);
}
// 6.操作之后将buffer设置为写模式
buffer.clear();
}
}
}
改进案例
改进案例非常简单,我们可以通过循环复用Buffer来处理未读取的数据。这意味着我们不需要不断调整Buffer的大小,而是在一个循环中不断读取数据,直到所有数据都被处理。这种方法能够有效地解决数据流长度不确定的情况,确保不会漏掉任何数据。同时上一个错误案例没有做异常处理,此改进版本做了异常处理。
public class TestNIO2 {
private static final Logger log = LoggerFactory.getLogger(TestNIO2.class);
public static void main(String[] args) {
FileChannel channel = null;
try {
// 1.通过FileInputStream获取对应的FileChannel管道
channel = new FileInputStream(TestNIO1.class.getClassLoader().getResource("data.txt").getFile()).getChannel();
// 2.创建Buffer缓冲区,容量为10字节,具体根据文件编码来定
ByteBuffer buffer = ByteBuffer.allocate(10);
while (true) {
// 3.让buffer从channel中读取数据,如果没有读到数据则返回-1
int read = channel.read(buffer);
if (-1 == read) {
break;
}
// 4.设置buffer为读模式,代表程序从buffer中读取数据
buffer.flip();
// 5.循环读取缓冲区数据
while (buffer.hasRemaining()) {
byte b = buffer.get();
log.info("(char)b = {}", (char)b);
}
// 6.操作之后将buffer设置为写模式,方便下一次写入数据
buffer.clear();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
throw new RuntimeException();
}
}
}
}
}
注意
在Buffer中,通过使用flip()方法
,我们可以切换为读操作的模式。这表示其他程序可以从Buffer中读取数据。另一方面,要切换为写操作模式,我们可以使用clear()方法。这表示后续的程序可以向Buffer中写入数据,或者从Channel中读取数据并放入Buffer中进行进一步处理。
总结
在本篇文章中,我们将深入研究NIO中的Channel,探讨其双向可读可写的特性,介绍一些常见的Channel类型以及它们的创建方式。我们还将详细演示FileChannel的使用,而在后续的内容中,我们将逐渐学习SocketChannel、ServerSocketChannel等更多内容。这些知识将有助于我们更好地理解和充分利用NIO中的通道,为后续的学习和应用奠定坚实的基础。