• 使用 NIO 模式简单模拟一个 Tomcat 运行原理


    问题:一直对 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("

    接受到你的请求参数是:" + allParameterValue + "

    "
    ); // 处理完成,开始会写给客户端数据 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 中解析到的内容,基本就是 一个访问路径对应一个处理器 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,注意此处会存在空转问题 * 采用多线程处理客户端请求 *

    * 分成三部分处理: * 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("
    "
    ); parameterValue.forEach((k, v) -> { stringBuffer.append(k + "=" + v + "
    "
    ); }); return stringBuffer.toString(); } public String getParameterValue(String key) { return parameterValue.get(key); } public void setParameterValue(String key, String value) { parameterValue.put(key, value); } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    • 279
    • 280
    • 281
    • 282
    • 283
    • 284
    • 285
    • 286
    • 287
    • 288
    • 289
    • 290
    • 291
    • 292
    • 293
    • 294
    • 295
    • 296
    • 297
    • 298
    • 299
    • 300
    • 301
    • 302
    • 303
    • 304
    • 305
    • 306
    • 307
    • 308
    • 309
    • 310
    • 311
    • 312
    • 313
    • 314
    • 315

    然后开启多个浏览器窗口,地址栏输入

    http://localhost:8080/login?userName=10&password=1
    http://localhost:8080/login
    http://localhost:8080/abcxxx?userName=10&password=1
    http://localhost:8080/abcxxx
    
    • 1
    • 2
    • 3
    • 4

    控制台输出日志,如下:

    Tomcat is started,port is 8080...
    2022-08-09T17:29:39.679ForkJoinPool.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.681ForkJoinPool.commonPool-worker-9】线程===>正在处理客户端请求,需耗时3s...
    开始倒计时:
    3
    2
    1
    2022-08-09T17:29:43.701ForkJoinPool.commonPool-worker-9】线程===>处理成功
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    页面展示,如下:

    在这里插入图片描述

    总结

    Tomcat 服务就是一个 Server,具备两点核心功能: Start 启动服务器,Stop 停止服务器;底层通过监听 Socket、接受请求、处理请求、响应请求等主要功能。

  • 相关阅读:
    identity4 系列————持久化配置篇[五]
    Revit如何使用插件实现【参数同步】?
    内存卡数据恢复,5 个免费好用的数据恢复方法工具全解
    YOLOv6-4.0部分代码阅读笔记-voc2yolo.py
    活体检测综述 Deep Learning for Face Anti-Spoofing: A Survey 阅读记录
    带绝对值的数可导性问题的讨论
    Linux UWB Stack实现——MCPS帧处理
    1308. 不同性别每日分数总计
    基于区块链技术的域名系统设计与实现
    FPGA优质开源项目 – UDP万兆光纤以太网通信
  • 原文地址:https://blog.csdn.net/qq_35971258/article/details/126249820