当前位置 博文首页 > 努力充实,远方可期:【网络通信】Java NIO
事件驱动+多路复用
写事件代表底层缓冲区是否有空间,有则响应true
NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。
对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。
那么NIO如何做到一个线程,一个while死循环就能监测1w个连接是否有数据可读的呢?这就是NIO模型中selector的作用,一条连接来了之后,现在不创建一个while死循环去监听是否有数据可读了,而是直接把这条连接注册到selector上,然后,通过检查这个selector,就可以批量监测出有数据可读的连接,进而读取数据
NIO和BIO之间第一个最大的区别是,
IO的各种流是阻塞的。这意味着,当一个线程调用read()
或 write()
时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
NIO是一种同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(多路复用器)。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(多路复用器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO 通过Channel(通道) 进行读写
java.nio 包中实现的以下几个 Channel:
Channel 经常翻译为通道,类似 IO 中的流,用于读取和写入。它与前面介绍的 Buffer 打交道,读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。
内存向CPU申请权限,得到DMA权限后就可由内存进行IO,无需占用CPU资源,CPU就可以进行别的计算了。
但DMA总线过多就造成总线冲突的问题,也会影响性能。所以就提出了通道的方式。通道是一个完全独立的处理器,专门进行IO操作。
通道用于源节点与目标节点的连接。在Java NIO中负责缓冲区中数据的传输。Channel本身并不存储数据,因此需要配合Buffer一起使用
方式1:通过流获取该流对应的通道。
// 流对象.getChannel() 方法
FileChannel fc = new FileOutputStream("data.txt").getChannel();
方式2:在JDK1.7中的NIO.2 针对各个通道提供了静态方法 FileChannel.open()
;
/*
java.nio.channels.Channel通道接口:
主要实现类
用于本地数据传输:
|-- FileChannel
|-- RandomAccessFile
用于网络数据传输:
|-- SocketChannel
|-- ServerSocketChannel
|-- DatagramChannel
*/
例:
方式3:在JDK1.7中的NIO2 的Files工具类的 newByteChannel()
;
我想文件操作对于大家来说应该是最熟悉的,不过我们在说 NIO 的时候,其实 FileChannel 并不是关注的重点。而且后面我们说非阻塞的时候会看到,FileChannel 是不支持非阻塞的。
这里算是简单介绍下常用的操作吧,感兴趣的读者瞄一眼就是了。
初始化:
FileInputStream inputStream = new FileInputStream(new File("/data.txt"));
FileChannel fileChannel = inputStream.getChannel();
当然了,我们也可以从 RandomAccessFile#getChannel 来得到 FileChannel。
读取文件内容:
ByteBuffer buffer = ByteBuffer.allocate(1024);
int num = fileChannel.read(buffer);
前面我们也说了,所有的 Channel 都是和 Buffer 打交道的。
写入文件内容:
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("随机写入一些内容到 Buffer 中".getBytes());
// Buffer 切换为读模式
buffer.flip();
while(buffer.hasRemaining()) {
// 将 Buffer 中的内容写入文件
fileChannel.write(buffer);
}
我们前面说了,我们可以将 SocketChannel 理解成一个 TCP 客户端。虽然这么理解有点狭隘,因为我们在介绍 ServerSocketChannel 的时候会看到另一种使用方式。
打开一个 TCP 连接:
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("https://www.javadoop.com", 80));
当然了,上面的这行代码等价于下面的两行:
// 打开一个通道
SocketChannel socketChannel = SocketChannel.open();
// 发起连接
socketChannel.connect(new InetSocketAddress("https://www.javadoop.com", 80));
SocketChannel 的读写和 FileChannel 没什么区别,就是操作缓冲区。
// 读取数据
socketChannel.read(buffer);
// 写入数据到网络连接中
while(buffer.hasRemaining()) {
socketChannel.write(buffer);
}
不要在这里停留太久,先继续往下走。
之前说 SocketChannel 是 TCP 客户端,这里说的 ServerSocketChannel 就是对应的服务端。
ServerSocketChannel 用于监听机器端口,管理从这个端口进来的 TCP 连接。
// 实例化
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听 8080 端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
while (true) {
// 一旦有一个 TCP 连接进来,就对应创建一个 SocketChannel 进行处理
SocketChannel socketChannel = serverSocketChannel.accept();
}
这里我们可以看到 SocketChannel 的第二个实例化方式
到这里,我们应该能理解 SocketChannel 了,它不仅仅是 TCP 客户端,它代表的是一个网络通道,可读可写。
ServerSocketChannel 不和 Buffer 打交道了,因为它并不实际处理数据,它一旦接收到请求后,实例化 SocketChannel,之后在这个连接通道上的数据传递它就不管了,因为它需要继续监听端口,等待下一个连接。
UDP 和 TCP 不一样,DatagramChannel 一个类处理了服务端和客户端。
科普一下,UDP 是面向无连接的,不需要和对方握手,不需要通知对方,就可以直接将数据包投出去,至于能不能送达,它是不知道的
监听端口:
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9090));
ByteBuffer buf = ByteBuffer.allocate(48);
channel.receive(buf);
发送数据:
String newData = "New String to write to file..."
+ System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("jenkov.com", 80));
ServerSocketChannel 在服务器端监听新的客户端 Socket 连接。
NIO 中的 ServerSocketChannel 功能类似 ServerSocket, SocketChannel 功能类似 Socket
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel{
protected ServerSocketChannel(SelectorProvider provider) {
super(provider);
}
// 得到一个ServerSocketChannel通道
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
// 给ServerSocketChannel绑定端口号
public final ServerSocketChannel bind(SocketAddress local)
throws IOException{
return bind(local, 0);
}
public abstract ServerSocket socket();
// 接收一个连接,返回代表这个连接的通道对象
public abstract SocketChannel accept() throws IOException;
// 设置阻塞或非阻塞,取值 false 表示采用非阻塞模式 // AbstractSelectableChannel.java
public final SelectableChannel configureBlocking(boolean block) {
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if (blocking == block)
return this;
if (block && haveValidKeys())
throw new IllegalBlockingModeException();
implConfigureBlocking(block);
blocking = block;
}
return this;
}
//注册一个选择器并设置监听事件
public final SelectionKey register(Selector sel, int ops),
SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{//多了 ByteChannel, ScatteringByteChannel, GatheringByteChannel 分散聚集功能
protected SocketChannel(SelectorProvider provider) {
super(provider);
}
//得到一个 SocketChannel 通道
public static SocketChannel open() throws IOException {
return SelectorProvider.provider().openSocketChannel();
}
//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public final SelectableChannel configureBlocking(boolean block);
public abstract Socket socket();
//从通道里读数据
public final long read(ByteBuffer[] dsts) throws IOException {
return read(dsts, 0,