手写一个简易版的tomcat

比眉伴天荒 2022-12-28 01:47 243阅读 0赞

文章目录

  • 手写一个简易版的tomcat
    • 前言
    • 思考
    • 具体实现
    • 说明
    • 代码地址

手写一个简易版的tomcat

前言

使用tomcat的时候当浏览器输入url之后,开始发送http请求,这个请求发送到哪儿呢,Url解析的过程中

  • 1 先通过域名解析请求得到ip
  • 2 然后通过ip找到对应的主机
  • 3 再通过响应的端口找到进程
  • 4 然后再去根据程序去处理这个请求,再到原路返回

思考

对于1,2步骤我们本地测试可以不用去扣这个,明白这么回事儿就可以,本地localhost实际上对应我们自己本机127.0.0.1

对于第三部,我们本地可以去通过一个socket去监听响应的端口,去获取到请求,然后再响应给客户端让客户端浏览器去解析我们返回的http报文,从而展示数据;

具体如下步骤:

1)提供服务,接收请求(可以使用Socket通信)
2)请求信息封装成Request(Response)
3)客户端请求资源,资源分为静态资源(html)和动态资源(Servlet)
4)资源返回给客户端浏览器

具体实现

首先新建maven工程
在这里插入图片描述

然后定义一个启动类Bootstrap然后实现一个启动方法start,在这个方法中启动一个socket监听8080端口

  1. package com.udeam.v1;
  2. import com.udeam.util.HttpUtil;
  3. import java.io.IOException;
  4. import java.io.InputStream;
  5. import java.io.OutputStream;
  6. import java.net.ServerSocket;
  7. import java.net.Socket;
  8. /** * 启动类入库 * 用于启动tomcat */
  9. public class Bootstrap {
  10. /** * 监听端口号 * 用于启动socket监听的端口号 */
  11. private int port = 8080;
  12. /** * 启动方法 */
  13. public void start() throws IOException {
  14. //返回固定字符串到客户端
  15. ServerSocket socket = new ServerSocket(port);
  16. System.out.println("--------- start port : " + port);
  17. while (true) {
  18. Socket accept = socket.accept();
  19. //获取输入流
  20. //InputStream inputStream = accept.getInputStream();
  21. //输出流
  22. OutputStream outputStream = accept.getOutputStream();
  23. System.out.println(" ------ 响应返回内容 : " + result);
  24. outputStream.write("hello world ...".getBytes());
  25. outputStream.flush();
  26. outputStream.close();
  27. socket.close();
  28. }
  29. }
  30. public static void main(String[] args) {
  31. try {
  32. new Bootstrap().start();
  33. } catch (IOException e) {
  34. e.printStackTrace();
  35. }
  36. }
  37. }

通过这个Socket返回hello world...给客户端
我们浏览器输入127
可以看到后台代码输出信息
在这里插入图片描述

前台浏览器显示信息

在这里插入图片描述
返回信息浏览器不认,响应无效,出现这种情况是浏览器只认识http报文,故此需要包装一个返回浏览器,然后浏览器才能解析

新建一个http包装类HttpUtil包装响应信息给浏览器
这里我们只返回200和404状态的

  1. package com.udeam.util;
  2. /** * 封装http响应 */
  3. public class HttpUtil {
  4. /** * 404 page */
  5. private static final String content = "<H2>404 page... </H2>";
  6. /** * 添加响应头信息 * <p> * http响应体格式 * <p> * 响应头(多参数空格换行) * 换行 * 响应体 */
  7. public static String addHeadParam(int len) {
  8. String head = "HTTP/1.1 200 OK \n";
  9. head += "Content-Type: text/html; charset=UTF-8 \n";
  10. head += "Content-Length: " + len + " \n" + "\r\n";
  11. return head;
  12. }
  13. /** * 4040响应 * * @return */
  14. public static String resp_404() {
  15. String head = "HTTP/1.1 404 not found \n";
  16. head += "Content-Type: text/html; charset=UTF-8 \n";
  17. head += "Content-Length: " + content.length() + " \n" + "\r\n";
  18. return head + content;
  19. }
  20. /** * 200响应 * * @param content 响应内容 * @return */
  21. public static String resp_200(String content) {
  22. return addHeadParam(content.length()) + content;
  23. }
  24. }

然后再请求,可以看到成功返回信息

在这里插入图片描述

然后我们再去请求一个静态页面index.html

新建一个html页面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. hello tomcat....
  9. </body>
  10. </html>

这次前台请求Url是http://localhost:8080/index.html
还是从Socket进行监听8080端口

请求静态的html,那如何去在后台找到这个资源呢?

通过url也就是/index.html去找到这个文件,后台文件我们去放到resource

那如何获取url呢?

浏览器在请求后台的时候发送的也是http请求,我们可以获取http请求报文
可以看一下,请求报文
在这里插入图片描述
从请求头中获取到url以及method等
获取输入流

  1. Socket accept = socket.accept();
  2. //获取输入流
  3. InputStream inputStream = accept.getInputStream();

然后对输入流进行解析,通过解析http请求头第一行得到url和method封装到Request对象中

  1. /** * 封装的请求实体类 */
  2. public class Request {
  3. /** * 请求方式 */
  4. private String method;
  5. /** * 请求url */
  6. private String url;
  7. /** * 输入流 */
  8. public InputStream inputStream;
  9. public Request() {
  10. }
  11. //构造器输入流
  12. public Request(InputStream inputStream) throws IOException {
  13. this.inputStream = inputStream;
  14. //读取请求信息,封装属性
  15. int count = 0;
  16. //读取请求信息
  17. while (count == 0) {
  18. count = inputStream.available();
  19. }
  20. byte[] b = new byte[count];
  21. inputStream.read(b);
  22. String reqStr = new String(b);
  23. System.out.println("请求信息 : " + reqStr);
  24. //根据http请求报文 换行符截取
  25. String[] split = reqStr.split("\\n");
  26. //获取第一行请求头信息
  27. String s = split[0];
  28. //根据空格进行截取请求方式和url
  29. String[] s1 = s.split(" ");
  30. System.out.println("method : " + s1[0]);
  31. System.out.println("url : " + s1[1]);
  32. this.method = s1[0];
  33. this.url = s1[1];
  34. }
  35. //.... get set省略
  36. }

然后根据请求的url从磁盘找到静态资源读取到然后以流的形式返回给浏览器

这里封装返回对象Response

  1. public class Response {
  2. /** * 响应流 */
  3. private OutputStream outputStream;
  4. public Response(OutputStream outputStream) {
  5. this.outputStream = outputStream;
  6. }
  7. //输出指定字符串
  8. public void outPutStr(String content) throws IOException {
  9. outputStream.write(content.getBytes());
  10. outputStream.flush();
  11. outputStream.close();
  12. }
  13. }

根据url获取静态资源

  1. public void outPutHtml(String url) throws IOException {
  2. //排除浏览器的/favicon.ico请求
  3. if (("/favicon.ico").equals(url)){
  4. return;
  5. }
  6. //获取静态资源的绝对路径
  7. String abPath = ResourUtil.getStaticPath(url);
  8. //查询静态资源是否存在
  9. File file = new File(abPath);
  10. if (file.exists()) {
  11. //输出静态资源
  12. ResourUtil.readFile(new FileInputStream(abPath), outputStream);
  13. } else {
  14. //404
  15. try {
  16. outPutStr(HttpUtil.resp_404());
  17. } catch (IOException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. }

ResourUtil 工具类
封装解析读取静态资源

  1. /** * 静态资源工具类 */
  2. public class ResourUtil {
  3. /** * 获取classes文件目录 */
  4. private static URL url = ResourUtil.class.getClassLoader().getResource("\\\\");
  5. /** * 获取静态资源文件路径 * * @param path * @return */
  6. public static String getStaticPath(String path) throws UnsupportedEncodingException {
  7. //获取目录的绝对路径
  8. try {
  9. String decode = URLDecoder.decode(url.getPath(), "UTF-8");
  10. String replace1 = decode.replace("\\", "/");
  11. String replace2 = replace1.replace("//", "");
  12. replace2 = replace2.substring(0,replace2.lastIndexOf("/")) + path;
  13. return replace2;
  14. } catch (UnsupportedEncodingException e) {
  15. e.printStackTrace();
  16. }
  17. return null;
  18. }
  19. /** * 读取静态资源文件输入流 * * @param inputStream */
  20. public static void readFile(InputStream inputStream, OutputStream outputStream) throws IOException {
  21. int count = 0;
  22. //读取请求信息
  23. while (count == 0) {
  24. count = inputStream.available();
  25. }
  26. int content = 0;
  27. //读取文件
  28. content = count;
  29. //输出头
  30. outputStream.write(HttpUtil.addHeadParam(content).getBytes());
  31. //输出内容
  32. long written = 0;
  33. int byteSize = 1024;
  34. byte[] b = new byte[byteSize];
  35. //读取
  36. while (written < content) {
  37. if (written + 1024 > content) {
  38. byteSize = (int) (content - written);
  39. b = new byte[byteSize];
  40. }
  41. inputStream.read(b);
  42. outputStream.write(b);
  43. outputStream.flush();
  44. written += byteSize;
  45. }
  46. }
  47. }

socket中完整请求代码

  1. public void start() throws IOException {
  2. //返回固定字符串到客户端
  3. ServerSocket socket = new ServerSocket(port);
  4. System.out.println("--------- start port : " + port);
  5. while (true) {
  6. Socket accept = socket.accept();
  7. //获取输入流
  8. InputStream inputStream = accept.getInputStream();
  9. //封装请求和响应对象
  10. Request request = new Request(inputStream);
  11. Response response = new Response(accept.getOutputStream());
  12. response.outPutHtml(request.getUrl());
  13. }
  14. }

浏览器测试,可以看到正确返回
在这里插入图片描述
接下来实现定义请求动态资源,具体实现在java web中处理一个请求是使用servlet请求

tomcat处理servlet请求需要实现servlet规范

什么是servlet规范呢?

简单来说就是http请求在接收到请求之后将请求交给Servlet容器来处理,Servlet容器通过Servlet接口来调用不同的业务类,这一整套称作Servlet规范;

接口规范

  1. /** * 自定义servlet规范 */
  2. public interface Servlet {
  3. void init() throws Exception;
  4. void destory() throws Exception;
  5. void service(Request request, Response response) throws Exception;
  6. }

实现

  1. /** * 实现servlet规范 */
  2. public abstract class HttpServlet implements Servlet {
  3. public abstract void doGet(Request request, Response response);
  4. public abstract void doPost(Request request, Response response);
  5. @Override
  6. public void service(Request request, Response response) throws Exception {
  7. if ("GET".equalsIgnoreCase( request.getMethod()
  8. )) {
  9. doGet(request, response);
  10. } else {
  11. doPost(request, response);
  12. }
  13. }
  14. }

业务请求servlet

  1. /** * 业务类servelt */
  2. public class MyServlet extends HttpServlet {
  3. @Override
  4. public void init() throws Exception {
  5. }
  6. @Override
  7. public void doGet(Request request, Response response) {
  8. //动态业务请求
  9. String content = "<h2> GET 业务请求</h2>";
  10. try {
  11. response.outPutStr(HttpUtil.resp_200(content));
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }
  15. }
  16. @Override
  17. public void doPost(Request request, Response response) {
  18. //动态业务请求
  19. String content = "<h2> Post 业务请求</h2>";
  20. try {
  21. response.outPutStr(HttpUtil.resp_200(content));
  22. } catch (IOException e) {
  23. e.printStackTrace();
  24. }
  25. }
  26. @Override
  27. public void destory() throws Exception {
  28. }
  29. }

定义完之后,如何请求呢,如何根据请求Ur去得到相应的servlet
在Java web中我们是在web.xml中进行配置,同样新建web.xml,配置servlet

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <web-app>
  3. <!-- v3版本 单线程 多个请求会阻塞 -->
  4. <!-- <servlet>
  5. <servlet-name>test</servlet-name>
  6. <servlet-class>com.udeam.v3.service.MyServlet</servlet-class>
  7. </servlet>-->
  8. <!-- v4版本 多线程 不阻塞-->
  9. <servlet>
  10. <servlet-name>test</servlet-name>
  11. <servlet-class>com.udeam.v4.MyServlet</servlet-class>
  12. </servlet>
  13. <servlet-mapping>
  14. <servlet-name>test</servlet-name>
  15. <url-pattern>/test</url-pattern>
  16. </servlet-mapping>
  17. </web-app>

解析web.xml文件

讲url和每一个servlet对应起来存储到map中

  1. /** * 加载解析web.xml,初始化Servlet */
  2. private void loadServlet() {
  3. InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream("web.xml");
  4. SAXReader saxReader = new SAXReader();
  5. try {
  6. Document document = saxReader.read(resourceAsStream);
  7. Element rootElement = document.getRootElement();
  8. List<Element> selectNodes = rootElement.selectNodes("//servlet");
  9. for (int i = 0; i < selectNodes.size(); i++) {
  10. Element element = selectNodes.get(i);
  11. // <servlet-name>test</servlet-name>
  12. Element servletnameElement = (Element) element.selectSingleNode("servlet-name");
  13. String servletName = servletnameElement.getStringValue();
  14. Element servletclassElement = (Element) element.selectSingleNode("servlet-class");
  15. String servletClass = servletclassElement.getStringValue();
  16. // 根据servlet-name的值找到url-pattern
  17. Element servletMapping = (Element) rootElement.selectSingleNode("/web-app/servlet-mapping[servlet-name='" + servletName + "']");
  18. // /test
  19. String urlPattern = servletMapping.selectSingleNode("url-pattern").getStringValue();
  20. servletMap.put(urlPattern, (HttpServlet) Class.forName(servletClass).newInstance());
  21. }
  22. } catch (DocumentException e) {
  23. e.printStackTrace();
  24. } catch (IllegalAccessException e) {
  25. e.printStackTrace();
  26. } catch (InstantiationException e) {
  27. e.printStackTrace();
  28. } catch (ClassNotFoundException e) {
  29. e.printStackTrace();
  30. }
  31. }

启动方法
根据url找到servlet去执行service方法

  1. private static final Map<String, HttpServlet> servletMap = new HashMap<>();
  2. public void start() throws IOException {
  3. ServerSocket socket = new ServerSocket(port);
  4. System.out.println("--------- start port : " + port);
  5. while (true) {
  6. Socket accept = socket.accept();
  7. //获取输入流
  8. InputStream inputStream = accept.getInputStream();
  9. //封装请求和响应对象
  10. Request request = new Request(inputStream);
  11. Response response = new Response(accept.getOutputStream());
  12. //静态资源
  13. if (request.getUrl().contains(".html")) {
  14. response.outPutHtml(request.getUrl());
  15. } else {
  16. if (!servletMap.containsKey(request.getUrl())) {
  17. response.outPutStr(HttpUtil.resp_200(request.getUrl() + " is not found ... "));
  18. } else {
  19. HttpServlet httpServlet = servletMap.get(request.getUrl());
  20. try {
  21. //处理请求
  22. httpServlet.service(request, response);
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. }
  29. }

然后请求http://localhost:8080/test可以看到正确返回
在这里插入图片描述

这里在doget方法中增加睡眠停顿模拟业务请求时间

  1. try {
  2. Thread.sleep(10_000);
  3. } catch (InterruptedException e) {
  4. e.printStackTrace();
  5. }

请求可以可以看到请求阻塞,这是因为同一个socket 当前test这个没有请求结束,第二个请求进来然后阻塞;
必须等到第一个请求结束后才能处理请求
在这里插入图片描述
然后再请求index.html
在这里插入图片描述
发现,并不是静态资源并不是立即返回,需要等到test请求结束后才能返回

故此需要对这个进行改造,让彼此请求互不干扰

我们可以使用多线程来进行解决,线程互不干扰,每个请求去执行

在Socket中添加方法

  1. //1 单线程处理
  2. MyThread myThread = new MyThread(httpServlet, response, request);
  3. new Thread(myThread).start();

线程是宝贵的资源,频繁创建和销毁线程对开销很大,故此使用线程池来解决

  1. /** * 参数可以配置在xml里面 */
  2. private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 1, TimeUnit.HOURS, new ArrayBlockingQueue<>(500));
  3. //2 线程池执行 多线程这儿有个问题,多次读取一个流的问题。
  4. threadPoolExecutor.submit(myThread);
  5. threadPoolExecutor.shutdown();

这样子就可以立即返回响应,互不干扰;

简易版的tomcat实现就可以实现了,代码的话没有像tomcat那样子可以将war包解析之类的…
而且代码耦合性也大,tomcat和业务代码在一个Maven中…

说明

分别在指定包下如v1,v2,v3,v4每个代表一个版本

  • v1 简单的返回指定字符串
  • v2 返回静态页面
  • v3 单线程处理servelt请求(多个请求会阻塞)
  • v4 多线程处理

其中需要用到解析xml依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>dom4j</groupId>
  4. <artifactId>dom4j</artifactId>
  5. <version>1.6.1</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>jaxen</groupId>
  9. <artifactId>jaxen</artifactId>
  10. <version>1.1.6</version>
  11. </dependency>
  12. </dependencies>

代码地址

仓库

发表评论

表情:
评论列表 (有 0 条评论,243人围观)

还没有评论,来说两句吧...

相关阅读

    相关 一个Tomcat

    作为一个java学习的起步者,对tomcat的认识还是有很多的欠缺,在无意中发现了这篇文章,便在自己的环境下尝试搭建,收获良多: 分以下几个步骤: (1)提供Socke