1. 阻塞与非阻塞
- 传统的IO流都是阻塞式的。也就是说,当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
- Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
2. 选择器(Selector)
2.1 基本概念
选择器(Selector)是SelectableChannle 对象的多路复用器,Selector可以同时监控多个SelectableChannel的IO状况,也就是说,利用Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
SelectableChannel是一个抽象类,是一个可通过Selector复用的通道。其实现类如下图所示
为了与一个选择器被使用,这个类的一个实例必须首先经由注册 register方法。此方法返回一个新的SelectionKey对象,表示通道与选择器的注册。
一旦注册了选择器,通道保持注册,直到它被注销。这包括通过选择器取消分配给通道的任何资源。
2.2 选择器的应用
- 创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
- 向选择器注册通道: SelectableChannel.register(Selector sel, int ops)
当调用 register(Selector sel, int ops)将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。
可以监听的事件类型(可使用 SelectionKey 的四个常量表示):
- 读 : SelectionKey.OP_READ (1)
- 写 : SelectionKey.OP_WRITE (4)
- 连接 : SelectionKey.OP_CONNECT (8)
- 接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接
Selector 的常用方法
方 法 | 描 述 |
---|---|
Set keys() | 所有的 SelectionKey 集合。代表注册在该Selector上的Channel |
selectedKeys() | 被选择的 SelectionKey 集合。返回此Selector的已选择键集 |
int select() | 监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的SelectionKey集合中,该方法返回这些 Channel 的数量。 |
int select(long timeout) | 可以设置超时时长的 select() 操作 |
int selectNow() | 执行一个立即返回的 select() 操作,该方法不会阻塞线程 |
Selector wakeup() | 使一个还未返回的 select() 方法立即返回 |
void close() | 关闭该选择器 |
2.3 SelectionKey
SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操
作。
方 法 | 描 述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
3 SocketChannel和ServerSocketChannel
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道。
操作步骤:
- 打开 SocketChannel
- 读写数据
- 关闭 SocketChannel
Java NIO中的 ServerSocketChannel是一个可以监听新进来的TCP连接的通道,就像标准IO中的ServerSocket一样。
3.1 实现文件从客户端传递到服务端(阻塞式)示例
public class TestBlockingNIO {
public static void main(String[] args) throws Exception{
client();
}
public static void client() throws Exception{
System.out.println("client start");
//1. 获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
FileChannel inChannel = FileChannel.open(Paths.get("e:\\demo\\study.txt"), StandardOpenOption.READ);
//2.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//3.读取本地文件,发送到服务器
while (inChannel.read(buf) != -1){
buf.flip();
socketChannel.write(buf);
buf.clear();
}
//接收服务端的反馈(可以不必要这段代码,这段代码主要是测试阻塞)
//阻断
socketChannel.shutdownOutput();
int len = 0;
while ((len = socketChannel.read(buffer)) != -1) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
buffer.clear();
}
//4. 关闭通道
inChannel.close();
socketChannel.close();
System.out.println("client stop");
}
}
class Server{
public static void main(String[] args) throws Exception{
server();
}
public static void server() throws Exception{
System.out.println("server start");
//1.获取通道
ServerSocketChannel serverSocket = ServerSocketChannel.open();
FileChannel outChannel = FileChannel.open(Paths.get("e:\\demo\\copy\\2.txt")
,StandardOpenOption.WRITE,StandardOpenOption.CREATE);
//2. 绑定连接
serverSocket.bind(new InetSocketAddress(9898));
//3. 获取客户端的通道
SocketChannel socketChannel = serverSocket.accept();
//4. 分配指定大小的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//5.接收客户端的数据,并保存到本地
while(socketChannel.read(buffer) != -1){
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
//发送反馈给客户端(可以不必要这段代码,这段代码主要是测试阻塞)
buffer.put("服务端接收客户端数据成功".getBytes());
buffer.flip();
socketChannel.write(buffer);
//6.关闭通道
socketChannel.close();
outChannel.close();
serverSocket.close();
System.out.println("server stop");
}
}
启动时先启动服务端,然后启动客户端
3.2 使用NIO实现非阻塞式服务端和客户端
- 客户端
public class TestNonBlockingNIO {
public static void main(String[] args) throws Exception{
client();
}
/**
* 客户端
* @throws Exception
*/
public static void client() throws Exception{
// 1.获取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2. 切换非阻塞模式
socketChannel.configureBlocking(false);
//3. 分配指定大小的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//4. 发送数据给服务端
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buffer.put((new Date().toString() + "\n" + str).getBytes());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
//5. 关闭通道
socketChannel.close();
}
}
- 服务端
public class NonBlockingNIOServer{
public static void main(String[] args) throws Exception{
server();
}
/**
* 服务端
* @throws Exception
*/
public static void server() throws Exception{
//1. 获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
serverSocketChannel.configureBlocking(false);
//3. 绑定连接
serverSocketChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5.将通道注册到选择器,指定监听事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//6.轮询式地获取选择器上准备就绪的事件
while(selector.select() > 0){
//7. 获取当前选择器中所有注册的“选择键”(已就绪的监听事件)
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//8.迭代获取
Iterator<SelectionKey> it = selectionKeys.iterator();
while (it.hasNext()){
SelectionKey selectionKey = it.next();
//9. 判断具体是什么事件就绪
if(selectionKey.isAcceptable()){
//10. 若“接收就绪”,获取客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
//11. 切换非阻塞模式
socketChannel.configureBlocking(false);
//12. 将通道注册到选择器上
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(selectionKey.isReadable()){
// 13.获取当前选择器上“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
//14. 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while((len = socketChannel.read(buffer)) > 0){
buffer.flip();
System.out.println(new String(buffer.array(),0,len));
buffer.clear();
}
}
}
//15. 取消选择键 SelectionKey
it.remove();
}
}
}
先启动服务端,再启动客户端。可以同时启动多个客户端,其实就是一个简易的聊天室
4 DatagramChannel
Java NIO中的DatagramChannel是一个能收发UDP包的通道。
操作步骤:
- 打开 DatagramChannel
- 接收/发送数据
示例代码
package com.zjx.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Scanner;
/**
* @author zhaojiaxing
* @date 2020-02-04 22:15
*/
public class TestNonBlockingNIO2 {
public static void main(String[] args) throws Exception{
send();
}
public static void send() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String str = scanner.next();
buffer.put(str.getBytes());
buffer.flip();
dc.send(buffer,new InetSocketAddress("127.0.0.1",9898));
buffer.clear();
}
dc.close();
}
}
class TestServer{
public static void main(String[] args) throws Exception{
receive();
}
public static void receive() throws IOException{
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()){
SelectionKey selectionKey = it.next();
if(selectionKey.isReadable()){
ByteBuffer buffer = ByteBuffer.allocate(1024);
dc.receive(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0,buffer.limit()));
buffer.clear();
}
}
it.remove();
}
}
}
5. 管道 (Pipe)
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
使用示例
public static void main(String[] args) throws Exception{
//1. 获取管道
Pipe pipe = Pipe.open();
//2. 将缓冲区中的数据写入管道
ByteBuffer buf = ByteBuffer.allocate(1024);
Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通过单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
//3. 读取缓冲区中的数据
Pipe.SourceChannel sourceChannel = pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(), 0, len));
sourceChannel.close();
sinkChannel.close();
}