问题:一直对 Tomcat 的运行原理充满着好奇,今天终于自己通过 NIO 模拟了一个简约版 Tomcat ,直接看下面代码:
package main.tomcat;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 定义同一规范的接口用来处理客户端请求
*/
interface Servlet {
void service(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
/**
* 处理客户单请求登录的 Servlet
* request 接受客户端发送的请求参数
* response 把处理结果发送给客户端
*/
class LoginServlet implements Servlet {
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws Exception {
String userName = request.getParameterValue("userName");
String password = request.getParameterValue("password");
String uri = request.getParameterValue("uri");
String allParameterValue = request.getAllParameterValue();
System.out.println(LocalDateTime.now() + " 【" + Thread.currentThread().getName() + "】线程===>正在处理客户端请求,需耗时3s...");
System.out.println("开始倒计时:");
TimeUnit.SECONDS.sleep(1);
System.out.println("3");
TimeUnit.SECONDS.sleep(1);
System.out.println("2");
TimeUnit.SECONDS.sleep(1);
System.out.println("1");
TimeUnit.SECONDS.sleep(1);
SocketChannel printWriter = response.getPrintWriter();
StringBuffer result = ResponsePage.build(request.getParameterValue("uri"));
result.append("<h1><font color='green'>接受到你的请求参数是:" + allParameterValue + "</font></h1>");
// 处理完成,开始会写给客户端数据
System.out.println(LocalDateTime.now() + " 【" + Thread.currentThread().getName() + "】线程===>处理成功");
printWriter.write(ByteBuffer.wrap(result.toString().getBytes(StandardCharsets.UTF_8)));
System.out.println("\n\n\n\n");
}
}
/**
* 定义一些返回结果页面,用于展示、方便浏览器查看
*/
class ResponsePage {
public static StringBuffer build(String pageIndex) {
StringBuffer buffer = new StringBuffer();
buffer.append("HTTP/1.1 200 ok\n");
buffer.append("Content-Type: text/html; Charset=utf-8\n\n");
try {
FileInputStream fileInputStream = null;
if (pageIndex.contains("login")) {
fileInputStream = new FileInputStream("/Users/gongweiming/IdeaProjects/NettyProDemo/src/main/tomcat/login.html");
} else {
fileInputStream = new FileInputStream("/Users/gongweiming/IdeaProjects/NettyProDemo/src/main/tomcat/404.html");
}
FileChannel channel = fileInputStream.getChannel();
ByteBuffer byteBuf = ByteBuffer.allocate(1024);
int len = 0;
while ((len = channel.read(byteBuf)) != -1) {
}
String content = new String(byteBuf.array(), 0, 1024 - byteBuf.remaining());
// System.out.println(">>>>>>>>>"+content);
buffer.append(content);
/* BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
String line = "";
while ((line = reader.readLine()) != null) {
buffer.append(line);
}*/
} catch (Exception e) {
e.printStackTrace();
}
return buffer;
}
}
/**
* =============================启动 Tomcat 服务器=============================
* 1、首先加载并解析 web.xml 内容,每一个客户端访问都会有一个对应的 Servlet 进行相应处理
* 2、真正开始处理客户端请求,也就是调用 Servlet 接口中定义的 service(request,response) 方法
* 3、service(request,response) 可以从 request 中获取客户端发送的数据,可以使用 response 进行响应客户端请求
*/
public class TomcatBootStrap {
/**
* 用于存放从 web.xml 中解析到的内容,基本就是 </login,LoginServlet> 一个访问路径对应一个处理器 Servlet
*/
private static final ConcurrentHashMap<String, Servlet> servletMap = new ConcurrentHashMap<>();
/**
* Tomcat 启动入口
*/
public static void main(String[] args) throws Exception {
/** 1、解析 web.xml 中的内容,将请求 uri 和 servlet 解析到一个 Map 中存放好 */
initWebXML();
/** 2、使用 NIO 中的 Channel 通道进行通信,调用对应的 Servlet 处理业务 */
startTomcat(8080);
}
/**
* 只是一个做了一个简单的解析,局限性严重,推荐使用 Dom4j 树相关工具类解析
*/
private static void initWebXML() throws Exception {
FileInputStream fileInputStream = new FileInputStream("/Users/gongweiming/IdeaProjects/NettyProDemo/src/main/tomcat/web.xml");
BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
String line = "";
String key = null;
String value = null;
while ((line = reader.readLine()) != null) {
if (line.contains("pattern")) {
String[] split = line.split("/");
key = "/" + split[1].substring(0, split[1].length() - 1);
}
if (line.contains("class")) {
value = line.split(">")[1].split("<")[0];
}
}
if (Objects.nonNull(key) && Objects.nonNull(value)) {
servletMap.put(key, new LoginServlet());
}
}
/**
* 启动 Tomcat 服务,这里使用的是 NIO 异步非阻塞模式,也可以使用 Selector、Netty(Reactor 模式)
*/
private static void startTomcat(int port) throws Exception {
// 开启服务通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 监听端口
serverSocketChannel.bind(new InetSocketAddress(port));
// 设置成异步非阻塞模式,如果没有事件发生就返回 null
serverSocketChannel.configureBlocking(false);
// 开启线程处理业务
doExecute(serverSocketChannel);
// 关闭资源
// 如果将 serverSocketChannel.accept() 方法放到另一个线程中执行的话这里不要关闭,否则拿不到 SocketChannel,
// 如果使用 Completable 接口调用 serverSocketChannel.accept() 方法,注意它使用的是 ForkJoinPool 是个守护线程
// 所以启动主线程,如果使用这个 ForkJoinPool 框架开启新线程,可以在主线程上面阻塞住,不要关闭了,因为 ForkJoinPool 是守护线程
serverSocketChannel.close();
}
private static final int BUFFER_SIZE = 1024;
/**
* accept() 方法是非阻塞的,没有监听到事件返回 null,注意此处会存在空转问题
* 采用多线程处理客户端请求
* <p>
* 分成三部分处理:
* 1、通过 Socket 获取到客户端发送的数据包,然后解析封装成 HttpServletRequest 对象
* 2、根据 web.xml 中的配置,找到对应的处理器 handler 或者说是 Servlet
* 3、开始真正处理业务,处理完之后通过 HttpServletResponse 进行响应客户端
*/
private static void doExecute(ServerSocketChannel serverSocketChannel) throws Exception {
System.out.println("Tomcat is started,port is 8080...");
while (true) {
// 如果有 accept 事件发生,就不会返回 null
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
CompletableFuture.runAsync(() -> {
try {
HttpServletResponse response = new HttpServletResponse();
response.setPrintWriter(socketChannel);
/** 1、读取数据,解析客户端发送过来的请求 */
HttpServletRequest request = requestResolver(socketChannel);
if (Objects.isNull(request)) {
return;
}
/** 2、根据 web.xml 中配置的路由映射中找到对应的处理 handler */
Servlet handler = dispatcherServlet(request);
if (Objects.isNull(handler)) {
// 跳转到 404 页面,终结此次请求
response.getPrintWriter().write(ByteBuffer.wrap(ResponsePage.build("404").toString().getBytes(StandardCharsets.UTF_8)));
socketChannel.close();
return;
}
/** 3、对应的 handler 开始处理客户端请求 */
handler.service(request, response);
response.getPrintWriter().close();
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
}
/**
* 将请求路径分发到对的 Servlet 处理器上
*/
private static Servlet dispatcherServlet(HttpServletRequest request) throws InterruptedException {
return servletMap.get(request.getParameterValue("uri"));
}
/**
* 解析客户端发送的数据包,将其封装成 HttpServletRequest
*
* @param socketChannel
* @return
* @throws Exception
*/
private static HttpServletRequest requestResolver(SocketChannel socketChannel) throws Exception {
HttpServletRequest request = new HttpServletRequest();
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
socketChannel.read(buffer);
byte[] bytes = buffer.array();
String content = new String(bytes, 0, BUFFER_SIZE - buffer.remaining());
System.out.println(LocalDateTime.now() + "【" + Thread.currentThread().getName() + "】线程===>接收到客户端请求,解析后,数据如下所示:\n");
if (content.contains("favicon")) {
return null;
}
System.out.println("=============================================================================================================");
System.out.println(content);
System.out.println("=================================================++++++【开始处理客户单请求】++++================================");
if (!content.contains("favicon")) {
String firstLine = content.split("\n")[0];
if (firstLine.contains("HTTP")) {
request.setParameterValue("uri", "/" + (firstLine.contains("?") ? firstLine.split("/")[1].split("\\?")[0] : firstLine.split("/")[1].split(" ")[0]));
;
if (firstLine.contains("?") && firstLine.contains("=")) {
String s1 = firstLine.split("\\?")[1].split(" ")[0];
String[] split = s1.split("&");
Arrays.stream(split).forEach(e -> {
String[] split1 = e.split("=");
request.setParameterValue(split1[0], split1[1]);
});
}
}
}
return request;
}
}
/**
* 特别注意 Response 类中封装了可以给客户端会写内容的对象 Socket/SocketChannel
* 这里也可以封装 PrintWriter 类,怎么方便怎么来
*/
class HttpServletResponse {
private SocketChannel printWriter;
public static HttpServletResponse SUCCESS() {
HttpServletResponse response = new HttpServletResponse();
return response;
}
public SocketChannel getPrintWriter() {
return printWriter;
}
public void setPrintWriter(SocketChannel printWriter) {
this.printWriter = printWriter;
}
}
/**
* 定义用于封装客户端请求参数实体 HttpServletRequest
*/
class HttpServletRequest {
private Map<String, String> parameterValue = new ConcurrentHashMap<>();
public String getAllParameterValue() {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("<br/>");
parameterValue.forEach((k, v) -> {
stringBuffer.append(k + "=" + v + "<br/>");
});
return stringBuffer.toString();
}
public String getParameterValue(String key) {
return parameterValue.get(key);
}
public void setParameterValue(String key, String value) {
parameterValue.put(key, value);
}
}
然后开启多个浏览器窗口,地址栏输入
http://localhost:8080/login?userName=10&password=1
http://localhost:8080/login
http://localhost:8080/abcxxx?userName=10&password=1
http://localhost:8080/abcxxx
控制台输出日志,如下:
Tomcat is started,port is 8080...
2022-08-09T17:29:39.679【ForkJoinPool.commonPool-worker-9】线程===>接收到客户端请求,解析后,数据如下所示:
=============================================================================================================
GET /login HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: XXL_JOB_LOGIN_IDENTITY=7b226964223a312c22757365726e616d65223a2261646d696e222c2270617373776f7264223a226531306164633339343962613539616262653536653035376632306638383365222c22726f6c65223a312c227065726d697373696f6e223a6e756c6c7d; Idea-159af402=a56b3ddf-5c52-4ba7-a513-b6bc69bef0f1
=================================================++++++【开始处理客户单请求】++++================================
2022-08-09T17:29:39.681 【ForkJoinPool.commonPool-worker-9】线程===>正在处理客户端请求,需耗时3s...
开始倒计时:
3
2
1
2022-08-09T17:29:43.701 【ForkJoinPool.commonPool-worker-9】线程===>处理成功
页面展示,如下:
总结
Tomcat
服务就是一个 Server
,具备两点核心功能: Start
启动服务器,Stop
停止服务器;底层通过监听 Socket
、接受请求、处理请求、响应请求等主要功能。