Java NIO
Java NIO主要有三大核心部分:
- Buffer(缓冲区)
- Channel(通道)
- Selector(选择器)
Buffer
缓冲区其实是一个容器对象,也就是一个数组。在NIO中所有数据面向缓存区处理的(传统I/O处理数据都是面向流) ,在读取数据时,他是直接读取的缓冲区的数据。在写入数据的时候,他也是直接写入缓冲区。在缓冲区中有三个重要的属性:
- position:指定下一个将要被写入和读取的元素索引,他的值由get()/put()方法自动更新,在创建Buffer对象时,position初始化为0
- 指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)
- capacity:指定了缓冲区的最大数据容量
Channel
我们一般讲他翻译为 ”通道“ ,他和I/O中的Stream流差不多。但是Stream流是单向的,例如:InputStream和OutputStream。
Selector
传统的Client/Server模式基于TPR(Thread per Request),服务器会为每个客户端建立一个线程,由该线程负责客户端的请求。这种模式就会导致线程数量剧增,给服务器造成巨大的压力。
NIO中非阻塞I/O采用了Reactor工作模式,I/O调用不会被阻塞。NIO中实现非阻塞I/O的核心对象就是Selector,Selector是注册各种I/O事件的地方。而且当哪些事件发生时,就是Selector告诉我们所发生的事件,如下图:
当有读写等任何注册事件发生时,可以从Selector中获取相应的SelectionKey,同时从SelectionKey中可以找到发生的事件和该事件所发生的具体SelectableChannel
手写一个简单Tomcat
我们知道Tomcat是一个居于J2EE规范的web容器,主要入口是web.xml文件。web.xml文件中主要配置Servlet、Filter、Listener。我这实例主要重写一个Servlet,项目架构图如下:
Request与Response
因为是基于HTTP协议,主要构成如:
封装request代码如下:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
/**
* request
* @author feng
*/
public class NIORequest {
private String method;
private String url;
public NIORequest (SelectionKey selectionKey) throws IOException {
// 获取selectionKey实例中的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 从通道中读取数据到buffer
int length = 0;
length = channel.read(byteBuffer); // 从通道中读取数据到ByteBuffer容器中
if (length < 0){
selectionKey.cancel(); // 取消selectionKey实例
}else {
byteBuffer.flip();
//将byteBuffer转为String
String httpRequest = new String(byteBuffer.array()).trim();
byteBuffer.clear();
// 获取请求头
String requestHeaders = httpRequest.split("\n")[0];
// 获取请求路径url
url = requestHeaders.split("\\s")[1].split("\\?")[0];
// 获取请求方式
method = requestHeaders.split("\\s")[0];
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
}
response代码如下:
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
/**
* response
* @author feng
*/
public class NIOResponse {
private SelectionKey selectionKey;
public NIOResponse(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
public void write(String out) throws IOException {
// 输出也要遵循HTTP
// 状态码为200
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\n")
.append("Content-Type: text/html;\n")
.append("\r\n")
.append(out);
// 设置编码UTF-8
ByteBuffer byteBuffer = ByteBuffer.wrap(sb.toString().getBytes("UTF-8"));
// 从selectionKey实例中获取通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 向通道中写入数据
int length = channel.write(byteBuffer);
selectionKey.cancel();
channel.close();
}
}
Servlet代码:
**
* Servlet
* @author feng
*/
public abstract class NIOServlet {
// 简单支持POST
public void service(NIORequest request ,NIOResponse response) throws Exception{
if ("GET".equalsIgnoreCase(request.getMethod())){
doGet(request,response);
}else {
doPost(request,response);
}
}
public abstract void doGet(NIORequest request ,NIOResponse response) throws Exception;
public abstract void doPost(NIORequest request ,NIOResponse response) throws Exception;
}
TestServlet:自定义访问的servlet
public class TestServlet extends NIOServlet{
@Override
public void doGet(NIORequest request, NIOResponse response) throws Exception {
this.doPost(request,response);
}
@Override
public void doPost(NIORequest request, NIOResponse response) throws Exception {
response.write("This is Test Servlet!");
}
}
web.xml配置 这里我改用web.properties代替读取配置:
servlet.one.url=/testServlet
servlet.one.className=com.company.TestServlet
Tomcat服务端代码:
package com.company;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.*;
import java.nio.channels.spi.SelectorProvider;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author feng
*/
public class NIOTomcat {
// 端口
private int port;
private Selector selector;
//采用线程池处理数据
private ExecutorService executorService = Executors.newCachedThreadPool();
//储存servlet
private Map<String, NIOServlet> servletMap = new HashMap<>();
private Properties webXml = new Properties();
public NIOTomcat(int port) {
this.port = port;
}
/**
* 获取web-properties配置
*/
private void init() throws Exception {
try (FileInputStream fileInputStream = new
FileInputStream("web.properties")) {
webXml.load(fileInputStream);
for (Object web : webXml.keySet()) {
String key = web.toString();
if (key.endsWith(".url")) {
String servletName = key.replaceAll("\\.url$", "");
String url = webXml.getProperty(key);
String className = webXml.getProperty(servletName + ".className");
// 单实例 多线程
NIOServlet obj = (NIOServlet) Class.forName(className).newInstance();
servletMap.put(url, obj);
}
}
}
}
/**
* 启动服务器
*/
private void start() throws Exception {
// 1. 加载配置文件,初始化servletMap
init();
// 2.启动selector
selector = Selector.open();
// 3.开启Channel通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 4.配置通道非阻塞
serverSocketChannel.configureBlocking(false);
// 5.绑定地址端口
serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", port));
serverSocketChannel.accept();
// 6.将通道注册给监听器
SelectionKey register = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Tomcat 已启动,监听的端口是:" + this.port);
// 使用队列存储request和response
ConcurrentLinkedQueue<NIORequest> requestList = new ConcurrentLinkedQueue<>();
ConcurrentLinkedQueue<NIOResponse> responseList = new ConcurrentLinkedQueue<>();
// 7. 轮询查询就绪操作
while (true) {
// 选择一些I/O操作已经准备好的管道。每个管道对应着一个key。这个方法 是一个阻塞的选择操作。当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。
int n = selector.select();
if (n > 0) {
// 获取监听器的所有事件并进行业务处理
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 连接事件
if (selectionKey.isAcceptable()) {
//开启读取监听
doRead(selectionKey);
} else if (selectionKey.isValid() && selectionKey.isReadable()) { //可读事件
requestList.add(getRequest(selectionKey));
//切换可写的状态
selectionKey.interestOps(SelectionKey.OP_WRITE);
} else if (selectionKey.isValid() && selectionKey.isWritable()) { //可写事件
responseList.add(getResponse(selectionKey));
//切换可读的状态
selectionKey.interestOps(SelectionKey.OP_READ);
}
//请求和响应都准备好的时候进行处理
if (!requestList.isEmpty() && !responseList.isEmpty()) {
dopath(requestList.poll(), responseList.poll());
}
//删除事件,免得后面会重复处理该事件
iterator.remove();
}
}
}
}
/**
* 执行请求
*/
private void dopath(NIORequest request, NIOResponse response) throws Exception {
if (request == null) {
return;
}
if (response == null) {
return;
}
// 1.获取url
String url = request.getUrl();
executorService.execute(new Runnable() {
@Override
public void run() {
if (servletMap.containsKey(url)) {
// 6. servlet
try {
servletMap.get(url).service(request, response);
} catch (Exception e) {
e.printStackTrace();
}
} else {
try {
response.write("404 - Not Found");
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
/**
* 开启读取监听
*
* @param selectionKey
*/
private void doRead(SelectionKey selectionKey) {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
SocketChannel socketChannel = null;
try {
socketChannel = channel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从通道中获取请求,返回自定义NIORequest
*
* @param selectionKey selectionKey实例
* @return NIORequest
*/
private NIORequest getRequest(SelectionKey selectionKey) throws IOException {
return new NIORequest(selectionKey);
}
/**
* 从通道中获取请求,返回自定义NIOResponse
*
* @param selectionKey selectionKey实例
* @return NIOResponse
*/
private NIOResponse getResponse(SelectionKey selectionKey) throws IOException {
return new NIOResponse(selectionKey);
}
public static void main(String[] args) throws Exception {
new NIOTomcat(8888).start();
}
}
启动Tomcat
访问接口:
一个由基于NIO手写的Tomcat就完工了。