传统请求应答模型
传统Server/Client模式会基于TPR(Thread Pre Request),服务器会为每一个客户端请求创建一个线程,由该线程单独负责处理一个客户请求;这种模式存在一个问题,那就是在高并发时,会创建大量线程,最终会导致服务扛不住而宕机。大多数的实现为了避免宕机的风险而采用线程池,并设置线程池的最大数量,但这又会带来新的问题;假如最大线程数是200,这时刚好有200个用户都在进行大文件的下载,那么第201个用户请求到达时会得不到正常响应,即便只是请求几KB大小的数据。
NIO模型
NIO中采用非阻塞I/O基于Reactor模式的工作方式,IO调用不会被阻塞,而是注册感兴趣的IO事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们,NIO中实现非阻塞IO的核心对象就是Selector,Selector就是各种注册IO事件的地方,而且当那些事件发生时,就是这个对象告诉我们所发生的事件。
当有读或写等任何注册事件发生时,可以从Selector中获取得到相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体SelectableChannel,以获得客户端发送过来的数据。
使用NIO大体可以分以下步骤:
-
向Selector对象注册感兴趣事件
-
从Selector中获取感兴趣事件
-
根据不同的事件进行相应的处理
package com.tiger.io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* 打开 cmd【telnet localhost 8080】 客户端
* 使用NIO大体可以分以下步骤:
* 1. 向Selector对象注册感兴趣事件
* 2. 从Selector中获取感兴趣事件
* 3. 根据不同的事件进行相应的处理
*
* @description:
* @author: tiger
* @create: 2021-05-02 21:56
*/
public class NIODemo {
static final int PORT = 8080;
public static void main(String[] args) throws IOException {
listen(getSelector());
}
/**
* 1、注册感兴趣事件步骤:
* 创建ServerSocketChannel对象,并配置为非阻塞模式,
* 接着绑定到具体端口,最后向Selector中注册事件,此处指定的参数是OP_ACCEPT,
* 即指定我们要监听accept事件,也就是新的连接发生时所产生的事件,
* 对于ServerSocketChannel通道来说,我们唯一可以指定的参数就是OP_ACCEPT
*
* @return
* @throws IOException
*/
private static Selector getSelector() throws IOException {
// 创建Selector对象
Selector selector = Selector.open();
// 创建可选择通道,并配置为非阻塞模式
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
// 绑定通道到指定端口
ServerSocket socket = server.socket();
InetSocketAddress addr = new InetSocketAddress(PORT);
socket.bind(addr);
// 向selector中注册感兴趣事件,即监听accept事件
server.register(selector, SelectionKey.OP_ACCEPT);
return selector;
}
/**
* 2、从Selector中获取感兴趣事件,开始监听,内部不断循环
* 非阻塞IO内部循环模式基本遵循这种方式,首先调用select()阻塞,
* 直到至少有一个事件发生,然后再使用selectedKeys()得到SelectionKey,
* 再进行迭代循环,操作业务逻辑
*/
private static void listen(Selector selector) {
System.out.println("listen on:" + PORT);
try {
while (true) {
// 该调用会阻塞,直到至少有一个事件发生
int select = selector.select();
if (select == 0) {
continue;
}
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
process(key);
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 3、根据不同的事件做对应的处理
* 分别判断是接收请求、读数据还是写事件,分别作不同的处理。
* 在java1.4之前的IO系统中,提供的都是面向流的IO系统,
* 系统一次一个字节地处理,一个输入流产生一个字节数据,
* 一个输出流消费一个字节数据,面向流的IO速度非常慢。
* 而在java1.4中退出的NIO,这是面向块的IO系统,系统以块的方式处理数据,
* 每一个操作在一步中产生或消费一个块数据,因此速度提升了不少
*
* @param key
*/
private static void process(SelectionKey key) throws IOException {
Selector selector = key.selector();
// 接收请求,判断客户端是否已经连上来了
if (key.isAcceptable()) {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept();
System.out.println(String.format("接收请求(isAcceptable) - Connection from %s", clientChannel.getRemoteAddress()));
clientChannel.configureBlocking(false);
//设置为 OP_READ ,说明要读数据
clientChannel.register(selector, SelectionKey.OP_READ);
}
// 读取信息
else if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = clientChannel.read(buffer);
if (len > 0) {
buffer.flip();
String content = new String(buffer.array(), 0, len);
System.out.println(String.format("读取信息(isReadable) - From %s : %s", clientChannel.getRemoteAddress(), content));
SelectionKey skey = clientChannel.register(selector, SelectionKey.OP_WRITE);
skey.attach(content);
} else {
clientChannel.close();
}
buffer.clear();
}
// 写事件
else if (key.isWritable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
String content = (String) key.attachment();
System.out.println(String.format("写事件(isWritable) - From %s : %s", clientChannel.getRemoteAddress(), content));
ByteBuffer buffer = ByteBuffer.wrap(("输出内容:" + content).getBytes());
if (buffer != null) {
clientChannel.write(buffer);
} else {
clientChannel.close();
}
}
// isConnectable
else if (key.isConnectable()) {
System.out.println("=== isConnectable ===");
}
// isValid
else if (key.isValid()) {
}
}
}