1.阻塞与非阻塞
(1)传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不 能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会 阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。
(2)Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数 据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时 间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入 和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同 时处理连接到服务器端的所有客户端。
2.阻塞式NIO代码演示
public class TestBlockingNIO {
//客户端
@Test
public void client() {
SocketChannel outChannel = null;
try {
//1.获取通道
outChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
//2.分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//3.读取本地文件,并发送到服务端
while (true) {
byteBuffer.put(("LOL怎么可能有Dota2好玩"+new Random().nextInt(100)).getBytes());
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务端
@Test
public void server() {
ServerSocketChannel channel = null;
SocketChannel inChannel = null;
try {
//1.获取通道
channel = ServerSocketChannel.open();
//2.绑定端口
channel.bind(new InetSocketAddress(9898));
//3.获取客户端连接的通道
inChannel = channel.accept();
//4.接收客户端的数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len;
while ((len = inChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
System.out.println("客户端说:" + new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
当我们运行服务端和客户端的时候,结果如下图:
然后我们把客户端发送的信息改一下,改成如下图所示,在启动一个客户端
运行结果如下图:
可见服务端和两个客户端都启动了,但是服务端并没有接收第二个客户端发送的信息,这种IO就是阻塞的。只有当服务端与客户端的通信完成之后,服务端才能去处理下一个客户端。因此,在完成网络通信进行 IO 操作时,由于线程会 阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理, 当服务器端需要处理大量客户端时,性能急剧下降。
3.选择器(Selector)
(1)选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可 以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
(2) SelectableChannle 的结构如下图:
4.选择器的应用
(1)创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。
//4.获取选择器
Selector selector = Selector.open();
(2)向选择器注册通道:SelectableChannel.register(Selector sel, int ops)
//1.获取通道
channel = ServerSocketChannel.open();
//2.绑定端口
channel.bind(new InetSocketAddress(9898));
//3.切换成非阻塞模式
channel.configureBlocking(false);
//4.获取选择器
Selector selector = Selector.open();
//5.将通道注册到选择器上,并监听接收事件
channel.register(selector, SelectionKey.OP_ACCEPT);
(3)当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器 对通道的监听事件,需要通过第二个参数 ops 指定。
(4)可以监听的事件类型(可使用 SelectionKey 的四个常量表示):
读 : SelectionKey.OP_READ (1<<0)
写 : SelectionKey.OP_WRITE (1<<2)
连接 : SelectionKey.OP_CONNECT (1<<3)
接收 : SelectionKey.OP_ACCEPT (1<<4)
(5)若注册时不止监听一个事件,则可以使用“位或”操作符连接。
channel.register(selector, SelectionKey.OP_ACCEPT|SelectionKey.OP_READ);
5.SelectionKey
(1) SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向 选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整 数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操
作。
(2)SelectionKey常用方法:
方法 | 描述 |
---|---|
int interestOps() | 获取感兴趣事件集合 |
int readyOps() | 获取通道已经准备就绪的操作的集合 |
SelectableChannel channel() | 获取注册通道 |
Selector selector() | 返回选择器 |
boolean isReadable() | 检测 Channal 中读事件是否就绪 |
boolean isWritable() | 检测 Channal 中写事件是否就绪 |
boolean isConnectable() | 检测 Channel 中连接是否就绪 |
boolean isAcceptable() | 检测 Channel 中接收是否就绪 |
6.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() | 关闭该选择器 |
7.非阻塞式IO代码演示
public class TestNoBlockingNIO {
//客户端
@Test
public void client() {
SocketChannel outChannel = null;
try {
//1.获取通道
outChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
//2.把通道切换成非阻塞
outChannel.configureBlocking(false);
//3.分配缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//4.读取本地文件,并发送到服务端
while (true) {
byteBuffer.put(("LOL怎么可能有Dota2好玩"+new Random().nextInt(100)).getBytes());
byteBuffer.flip();
outChannel.write(byteBuffer);
byteBuffer.clear();
Thread.sleep(1000);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
} finally {
if (outChannel != null) {
try {
outChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
//服务端
@Test
public void server() {
ServerSocketChannel channel = null;
SocketChannel inChannel = null;
SocketChannel readChannel = null;
try {
//1.获取通道
channel = ServerSocketChannel.open();
//2.绑定端口
channel.bind(new InetSocketAddress(9898));
//3.切换成非阻塞模式
channel.configureBlocking(false);
//4.获取选择器
Selector selector = Selector.open();
//5.将通道注册到选择器上,并监听接收事件
channel.register(selector, SelectionKey.OP_ACCEPT);
//6.循环获取选择器上已经“准备就绪”的事件
while (selector.select() > 0) {
//7.获取当前选择器中所有注册以就绪的事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
//8.获取准备就绪的事件
SelectionKey key = iterator.next();
//9.判断具体是什么事件
if (key.isAcceptable()) {
//10.获取客户端连接的通道
inChannel = channel.accept();
//11.切换成非阻塞模式
inChannel.configureBlocking(false);
//12.将通道注册到选择器上,并监听接收事件
inChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
//13.获取当前选择器上读就绪的通道
readChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len;
while ((len = readChannel.read(byteBuffer)) > 0) {
byteBuffer.flip();
System.out.println("客户端说:" + new String(byteBuffer.array(), 0, len));
byteBuffer.clear();
}
}
//14.取消选择键,不然下次还会成功
iterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (readChannel != null) {
try {
readChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (inChannel != null) {
try {
inChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
当我们运行服务端和客户端的时候,结果如下图:
然后我们把客户端发送的信息改一下,改成如下图所示,在启动一个客户端
运行结果如下图:
可见服务端和两个客户端都启动了,服务端也接收第二个客户端发送的信息,这种IO就是非阻塞的。因此,NIO 可以让服务器端使用一个或有限几个线程来同 时处理连接到服务器端的所有客户端。
转载:https://blog.csdn.net/killerofjava/article/details/106842271