从零手写实现 tomcat-03-根本的 socket 实现

打印 上一主题 下一主题

主题 893|帖子 893|积分 2681

创作缘由

平时使用 tomcat 等 web 服务器不可谓不多,但是一直一知半解。
于是想着自己实现一个简单版本,学习一下 tomcat 的精髓。
系列教程

从零手写实现 apache Tomcat-01-入门先容
从零手写实现 apache Tomcat-02-web.xml 入门详细先容
从零手写实现 tomcat-03-根本的 socket 实现
从零手写实现 tomcat-04-哀求和响应的抽象
从零手写实现 tomcat-05-servlet 处理支持
从零手写实现 tomcat-06-servlet bio/thread/nio/netty 池化处理
从零手写实现 tomcat-07-war 怎样剖析处理三方的 war 包?
从零手写实现 tomcat-08-tomcat 怎样与 springboot 集成?
从零手写实现 tomcat-09-servlet 处理类
从零手写实现 tomcat-10-static resource 静态资源文件
从零手写实现 tomcat-11-filter 过滤器
从零手写实现 tomcat-12-listener 监听器
团体思路

我们通过 socket 套接字,实现最简单的服务监听。
然后直接输出一个固定的响应到页面。
套接字是个啥?

Java套接字(Socket)可以想象成一个网络通信的“管道”。就像你用水管道把水从一个地方输送到另一个地方,Java套接字则是用来在网络中传输数据的。
它允许你的Java步伐和网络中的其他步伐进行通信,无论是在同一台机器上还是活着界的另一端。
在Java中,套接字主要分为两大类:

  • 服务器套接字(ServerSocket):它的作用是监听网络上的连接哀求。你可以把它想象成一个电话总机,它不主动打给别人,而是等着别人打进来。当有哀求进来时,服务器套接字就会创建一个新的通信“管道”(也就是另一个套接字),专门用来和哀求者进行数据交换。
  • 客户端套接字(Socket):它的作用是主动去连接服务器套接字。就像你用电话拨打别人一样,客户端套接字会指定一个服务器的地址和端口,然后尝试创建连接。一旦连接成功,它也可以创建一个通信“管道”来发送和接收数据。
和 tomcat 有啥关系?

Tomcat作为一个Web服务器,需要和大量的客户端进行通信,比如浏览器。当浏览器哀求一个网页时,Tomcat需要接收这个哀求,并返回相应的网页数据。这个过程就需要用到Java套接字:

  • 监听连接:Tomcat使用ServerSocket来监听指定端口上的HTTP哀求。当浏览器发送哀求时,Tomcat的ServerSocket就会担当这个哀求,并创建一个新的套接字来处理它。
  • 数据交换:一旦连接创建,Tomcat就会通过这个套接字和浏览器进行数据交换。浏览器通过这个“管道”发送哀求,Tomcat接收哀求后,处理它,并把响应数据通过同一个“管道”发送回浏览器。
  • 多线程处理:由于可能有成千上万的客户端同时哀求,Tomcat会为每个连接创建一个新的线程,这样每个哀求就可以并行处理,而不会互相干扰。
简而言之,Java套接字是Tomcat实现网络通信的核心,它允许Tomcat接收客户端的哀求,并发送响应,从而实现Web服务的功能。
v1-根本代码

核心实现
  1. package com.github.houbb.minicat.bs;
  2. import com.github.houbb.log.integration.core.Log;
  3. import com.github.houbb.log.integration.core.LogFactory;
  4. import com.github.houbb.minicat.exception.MiniCatException;
  5. import java.io.IOException;
  6. import java.io.OutputStream;
  7. import java.net.ServerSocket;
  8. import java.net.Socket;
  9. /**
  10. * @author 老马啸西风
  11. * @since 0.1.0
  12. */
  13. public class MiniCatBootstrap {
  14.     private static final Log logger = LogFactory.getLog(MiniCatBootstrap.class);
  15.     /**
  16.      * 启动端口号
  17.      */
  18.     private final int port;
  19.     public MiniCatBootstrap(int port) {
  20.         this.port = port;
  21.     }
  22.     public MiniCatBootstrap() {
  23.         this(8080);
  24.     }
  25.     public void start() {
  26.         logger.info("[MiniCat] start listen on port {}", port);
  27.         logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
  28.         try {
  29.             ServerSocket serverSocket = new ServerSocket(port);
  30.             while(true){
  31.                 Socket socket = serverSocket.accept();
  32.                 OutputStream outputStream = socket.getOutputStream();
  33.                 outputStream.write("Hello miniCat!".getBytes());
  34.                 socket.close();
  35.             }
  36.         } catch (IOException e) {
  37.             logger.error("[MiniCat] meet ex", e);
  38.             throw new MiniCatException(e);
  39.         }
  40.     }
  41. }
复制代码
启动测试
  1. MiniCatBootstrap bootstrap = new MiniCatBootstrap();
  2. bootstrap.start();
复制代码
日志:
  1. [INFO] [2024-04-01 16:55:56.705] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] start listen on port 8080
  2. [INFO] [2024-04-01 16:55:56.705] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] visit url http://127.0.0.1:8080
复制代码
我们浏览器访问 http://127.0.0.1:8080,却报错了
  1. 该网页无法正常运作127.0.0.1 发送的响应无效。
  2. ERR_INVALID_HTTP_RESPONSE
复制代码
为什么会报错呢?

在这个 MiniCatBootstrap 类中,服务器接收到哀求后,直接向客户端发送了 "Hello miniCat!" 字符串。
然而,HTTP 协议规定了一定的格式要求,而 "Hello miniCat!" 并不符合这些格式要求,因此客户端无法精确剖析这个响应,导致出现 "ERR_INVALID_HTTP_RESPONSE" 错误。
要修复这个题目,你需要修改 MiniCatBootstrap 类,以便生成符合 HTTP 格式的响应。
比方,你可以将 "Hello miniCat!" 包装在一个正当的 HTTP 响应中,如下所示:
  1. String response = "HTTP/1.1 200 OK\r\n" +
  2.                   "Content-Type: text/plain\r\n" +
  3.                   "\r\n" +
  4.                   "Hello miniCat!";
  5. outputStream.write(response.getBytes());
复制代码
这个响应包括了 HTTP 状态行("HTTP/1.1 200 OK")、Content-Type 头部("Content-Type: text/plain")和一个空行("\r\n"),然后是 "Hello miniCat!" 字符串。
这样生成的响应就符合了 HTTP 协议的要求,客户端应该能够精确剖析它。
代码调解

我们把原来的原始字符串调解下:
  1. outputStream.write(InnerHttpUtil.httpResp("Hello miniCat!").getBytes());
复制代码
工具类如下:
  1.     /**
  2.      * 符合 http 标准的字符串
  3.      * @param rawText 原始文本
  4.      * @return 结果
  5.      */
  6.     public static String httpResp(String rawText) {
  7.         String format = "HTTP/1.1 200 OK\r\n" +
  8.                 "Content-Type: text/plain\r\n" +
  9.                 "\r\n" +
  10.                 "%s";
  11.         return String.format(format, rawText);
  12.     }
复制代码
再次访问,就一切都正常了。
v2-代码优化+支持stop

上面的方法不支持 stop,这有点不敷优雅。
代码调解

调解后的代码实现如下:
  1. package com.github.houbb.minicat.bs;
  2. import com.github.houbb.log.integration.core.Log;
  3. import com.github.houbb.log.integration.core.LogFactory;
  4. import com.github.houbb.minicat.exception.MiniCatException;
  5. import com.github.houbb.minicat.util.InnerHttpUtil;
  6. import java.io.IOException;
  7. import java.io.OutputStream;
  8. import java.net.ServerSocket;
  9. import java.net.Socket;
  10. /**
  11. * @author 老马啸西风
  12. * @since 0.1.0
  13. */
  14. public class MiniCatBootstrap {
  15.     private static final Log logger = LogFactory.getLog(MiniCatBootstrap.class);
  16.     /**
  17.      * 启动端口号
  18.      */
  19.     private final int port;
  20.     /**
  21.      * 是否运行的标识
  22.      */
  23.     private volatile boolean runningFlag = false;
  24.     /**
  25.      * 服务端 socket
  26.      */
  27.     private ServerSocket serverSocket;
  28.     public MiniCatBootstrap(int port) {
  29.         this.port = port;
  30.     }
  31.     public MiniCatBootstrap() {
  32.         this(8080);
  33.     }
  34.     /**
  35.      * 服务的启动
  36.      */
  37.     public synchronized void start() {
  38.         if(runningFlag) {
  39.             logger.warn("[MiniCat] server is already start!");
  40.             return;
  41.         }
  42.         logger.info("[MiniCat] start listen on port {}", port);
  43.         logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
  44.         try {
  45.             this.serverSocket = new ServerSocket(port);
  46.             runningFlag = true;
  47.             while(runningFlag){
  48.                 Socket socket = serverSocket.accept();
  49.                 OutputStream outputStream = socket.getOutputStream();
  50.                 outputStream.write(InnerHttpUtil.httpResp("Hello miniCat!").getBytes());
  51.                 socket.close();
  52.             }
  53.             logger.info("[MiniCat] end listen on port {}", port);
  54.         } catch (IOException e) {
  55.             logger.error("[MiniCat] start meet ex", e);
  56.             throw new MiniCatException(e);
  57.         }
  58.     }
  59.     /**
  60.      * 服务的启动
  61.      */
  62.     public synchronized void stop() {
  63.         if(!runningFlag) {
  64.             logger.warn("[MiniCat] server is not start!");
  65.             return;
  66.         }
  67.         try {
  68.             if(this.serverSocket != null) {
  69.                 serverSocket.close();
  70.             }
  71.             this.runningFlag = false;
  72.             logger.info("[MiniCat] stop listen on port {}", port);
  73.         } catch (IOException e) {
  74.             logger.error("[MiniCat] stop meet ex", e);
  75.             throw new MiniCatException(e);
  76.         }
  77.     }
  78. }
复制代码
我们定义一个 runingFlag 变量标识,stop 之后就可以根据这个属性判断是否继续执行。
测试代码

我们预期服务启动 30S 之后,然后关闭。
代码如下:
  1. MiniCatBootstrap bootstrap = new MiniCatBootstrap();
  2. bootstrap.start();TimeUnit.SECONDS.sleep(30);bootstrap.stop();
复制代码
这里会按照我们预期执行吗?为什么?
测试结果

测试日志:
  1. [DEBUG] [2024-04-01 17:23:55.012] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
  2. [INFO] [2024-04-01 17:23:55.014] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] start listen on port 8080
  3. [INFO] [2024-04-01 17:23:55.015] [main] [c.g.h.m.b.MiniCatBootstrap.start] - [MiniCat] visit url http://127.0.0.1:8080
复制代码
我们等待好久,也并没有比及服务关闭。
为什么?

纵然在修改后的代码中添加了 stop() 方法来制止服务器,但是 start() 方法仍旧会在一个无限循环中监听连接哀求,导致主线程被阻塞。
这是因为 start() 方法中的 while 循环会一直执行,直到 stop() 方法被调用将 runningFlag 设置为 false。
要办理这个题目,可以将服务器的监听逻辑放在一个单独的线程中执行,这样 start() 方法就可以立即返回,不会阻塞主线程。
v3-办理主线程阻塞题目

思路

我们把主线程运行放到一个异步线程,不去阻塞主线存。
实现
  1. package com.github.houbb.minicat.bs;
  2. import com.github.houbb.log.integration.core.Log;
  3. import com.github.houbb.log.integration.core.LogFactory;
  4. import com.github.houbb.minicat.exception.MiniCatException;
  5. import com.github.houbb.minicat.util.InnerHttpUtil;
  6. import java.io.IOException;
  7. import java.io.OutputStream;
  8. import java.net.ServerSocket;
  9. import java.net.Socket;
  10. /**
  11. * @since 0.1.0
  12. * @author 老马啸西风
  13. */
  14. public class MiniCatBootstrap {
  15.     private static final Log logger = LogFactory.getLog(MiniCatBootstrap.class);
  16.     /**
  17.      * 启动端口号
  18.      */
  19.     private final int port;
  20.     /**
  21.      * 是否运行的标识
  22.      */
  23.     private volatile boolean runningFlag = false;
  24.     /**
  25.      * 服务端 socket
  26.      */
  27.     private ServerSocket serverSocket;
  28.     public MiniCatBootstrap(int port) {
  29.         this.port = port;
  30.     }
  31.     public MiniCatBootstrap() {
  32.         this(8080);
  33.     }
  34.     public synchronized void start() {
  35.         // 引入线程池
  36.         Thread serverThread = new Thread(new Runnable() {
  37.             @Override
  38.             public void run() {
  39.                 startSync();
  40.             }
  41.         });
  42.         // 启动
  43.         serverThread.start();
  44.     }
  45.     /**
  46.      * 服务的启动
  47.      */
  48.     public void startSync() {
  49.         if(runningFlag) {
  50.             logger.warn("[MiniCat] server is already start!");
  51.             return;
  52.         }
  53.         logger.info("[MiniCat] start listen on port {}", port);
  54.         logger.info("[MiniCat] visit url http://{}:{}", "127.0.0.1", port);
  55.         try {
  56.             this.serverSocket = new ServerSocket(port);
  57.             runningFlag = true;
  58.             while(runningFlag && !serverSocket.isClosed()){
  59.                 Socket socket = serverSocket.accept();
  60.                 OutputStream outputStream = socket.getOutputStream();
  61.                 outputStream.write(InnerHttpUtil.httpResp("Hello miniCat!").getBytes());
  62.                 socket.close();
  63.             }
  64.             logger.info("[MiniCat] end listen on port {}", port);
  65.         } catch (IOException e) {
  66.             logger.error("[MiniCat] start meet ex", e);
  67.             throw new MiniCatException(e);
  68.         }
  69.     }
  70.     /**
  71.      * 服务的暂停
  72.      */
  73.     public void stop() {
  74.         logger.info("[MiniCat] stop called!");
  75.         if(!runningFlag) {
  76.             logger.warn("[MiniCat] server is not start!");
  77.             return;
  78.         }
  79.         try {
  80.             if(this.serverSocket != null) {
  81.                 serverSocket.close();
  82.             }
  83.             this.runningFlag = false;
  84.             logger.info("[MiniCat] stop listen on port {}", port);
  85.         } catch (IOException e) {
  86.             logger.error("[MiniCat] stop meet ex", e);
  87.             throw new MiniCatException(e);
  88.         }
  89.     }
  90. }
复制代码
启动测试
  1. MiniCatBootstrap bootstrap = new MiniCatBootstrap();
  2. bootstrap.start();System.out.println("main START sleep");TimeUnit.SECONDS.sleep(10);System.out.println("main END sleep");bootstrap.stop();
复制代码
日志如下:
  1. main START sleep
  2. [INFO] [2024-04-02 09:03:41.604] [Thread-0] [c.g.h.m.b.MiniCatBootstrap.startSync] - [MiniCat] start listen on port 8080
  3. [INFO] [2024-04-02 09:03:41.604] [Thread-0] [c.g.h.m.b.MiniCatBootstrap.startSync] - [MiniCat] visit url http://127.0.0.1:8080
  4. main END sleep
  5. [INFO] [2024-04-02 09:03:51.592] [main] [c.g.h.m.b.MiniCatBootstrap.stop] - [MiniCat] stop called!
  6. [ERROR] [2024-04-02 09:03:51.592] [Thread-0] [c.g.h.m.b.MiniCatBootstrap.startSync] - [MiniCat] start meet ex
  7. java.net.SocketException: socket closed
  8.         at java.net.DualStackPlainSocketImpl.accept0(Native Method)
  9.         at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:127)
  10.         at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:535)
  11.         at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:189)
  12.         at java.net.ServerSocket.implAccept(ServerSocket.java:545)
  13.         at java.net.ServerSocket.accept(ServerSocket.java:513)
  14.         at com.github.houbb.minicat.bs.MiniCatBootstrap.startSync(MiniCatBootstrap.java:74)
  15.         at com.github.houbb.minicat.bs.MiniCatBootstrap$1.run(MiniCatBootstrap.java:49)
  16.         at java.lang.Thread.run(Thread.java:750)
  17. Exception in thread "Thread-0" com.github.houbb.minicat.exception.MiniCatException: java.net.SocketException: socket closed
  18.         at com.github.houbb.minicat.bs.MiniCatBootstrap.startSync(MiniCatBootstrap.java:83)
  19.         at com.github.houbb.minicat.bs.MiniCatBootstrap$1.run(MiniCatBootstrap.java:49)
  20.         at java.lang.Thread.run(Thread.java:750)
  21. Caused by: java.net.SocketException: socket closed
  22.         at java.net.DualStackPlainSocketImpl.accept0(Native Method)
  23.         at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:127)
  24.         at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:535)
  25.         at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:189)
  26.         at java.net.ServerSocket.implAccept(ServerSocket.java:545)
  27.         at java.net.ServerSocket.accept(ServerSocket.java:513)
  28.         at com.github.houbb.minicat.bs.MiniCatBootstrap.startSync(MiniCatBootstrap.java:74)
  29.         ... 2 more
  30. [INFO] [2024-04-02 09:03:51.613] [main] [c.g.h.m.b.MiniCatBootstrap.stop] - [MiniCat] stop listen on port 8080
  31. Process finished with exit code 0
复制代码
已经可以正常的关闭。
开源地址
  1. /\_/\  
  2. ( o.o )
  3. > ^ <
复制代码
mini-cat 是简易版本的 tomcat 实现。别称【嗅虎】(心有猛虎,轻嗅蔷薇。)
开源地址:https://github.com/houbb/minicat

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

南七星之家

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表