纯手写Tomcat,看不懂你来揍我【附源码、图文详解】

源码放在了文章末尾

理论知识

何为Tomcat

        Tomcat是一个开源的Servlet容器,它实现了Java Servlet、JavaServer Pages (JSP)、WebSocket等Java EE规范,用于在Web服务器上运行Java Web应用程序。

        说的简单点,Tomcat能处理网络传输来的请求。

输入输出流

        也就是说,Tomcat要帮我们完成客户端和服务器之间的连接、传输。传输的时候是用输入输出流来传输的。

客户端和服务器的通信,说到底就是两个数据的传输,客户端发送inputStream给服务器,服务器回复outputStream给客户端。

HTTP请求

http请求也就是 web浏览器发送给web服务器(Tomcat)之间的传输数据协议。也就是商量好一个格式去传输,这样服务器收到了之后,就能对其进行解析了,就知道了浏览器想表达的意思,再对其进行反馈。

http请求协议部分数据

GET /user HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

第一部分:请求行:请求类型,资源路径以及http版本(上述第一行)

第二部分:请求头:紧接在请求行之后,用于说明服务器需要使用的附加信息(第二到第八行)

第三部分:空行(请求头和主体之间必须有换行)

第四部分:主体数据,可以添加任意数据

HTTP响应

        HTTP响应是Web服务器向客户端(通常是浏览器)返回的数据。当客户端发送HTTP请求后,服务器会根据请求的内容和要求生成一个HTTP响应,将其发送回客户端。在仿写Tomcat时,了解HTTP响应的结构和内容是很重要的。

        HTTP 响应是服务器向客户端发送的数据,用于回应客户端的请求。HTTP 响应需要满足一定的格式和要求,以下是一个标准的 HTTP 响应的格式

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 37

<html>
<head>
    <title>简单的HTTP响应示例</title>
</head>
<body>
    <h1>Hello, World!</h1>
</body>
</html>
  • HTTP/1.1 200 OK:状态行,表示HTTP协议版本为1.1,状态码为200,状态短语为OK。这表示请求成功。

  • Content-Type: text/html:响应头部,指定响应内容的类型为HTML。

  • Content-Length: 37:响应头部,指定响应内容的长度,以字节为单位。

  • 空行:用于分隔响应头部和响应体。

  • 响应体:实际的响应内容。在本例中,它是一个简单的HTML页面,显示了"Hello, World!"。

静态请求和动态请求

        在Web服务中,静态请求和动态请求是两种不同类型的HTTP请求,用于获取和呈现网页内容。它们有不同的特点和用途:

  1. 静态请求:静态请求是指浏览器请求服务器上的静态资源,如HTML、CSS、JavaScript、图像文件等,这些资源在服务器上存储为不可更改的文件。
  2. 动态请求:动态请求是指浏览器请求服务器上的动态生成内容,通常是通过服务器端的程序逻辑来生成的,如PHP、Python、Ruby等脚本语言。服务器会根据请求的参数和逻辑生成内容,然后将内容返回给浏览器。

        总之,静态请求和动态请求在Web服务中都起着重要作用,静态请求用于提供固定不变的资源,而动态请求用于生成个性化、实时更新的内容。

项目演示

启动Tomcat

 访问 首页

url输入错误,404页面测试

静态请求测试:访问html页面 

静态请求测试:css

 动态请求测试:登录

项目流程

项目目录

简单流程图

客户端(浏览器)发送一个请求,Tomcat一直在监听,当受到请求后,开始解析这个请求信息,解析完毕开始处理请求,处理完毕就封装响应信息,再返回给前端。 

详细流程图

 详细的流程请看下面讲解

1启动类:MyTomcat

  1. serverSocket: 是一个 ServerSocket 对象,它通过 ServerSocket 类创建,并在特定端口上监听连接请求。
  2. accept()方法是 ServerSocket 类的一个方法,它会阻塞程序执行,等待客户端连接。当有客户端连接到服务器时,accept() 方法将返回一个新的 Socket 对象,表示与客户端之间的连接。

2.线程任务处理类:ThreadTask

具体就是做下面四个步骤

  1. 对客户端收到的流数据进行解析与封装,得到request对象
  2. 根据流数据与request对象得到response对象
  3. 对静态请求与动态请求分开处理,完善响应对象
  4. 关闭连接

这四步骤也就是tomcat的全部了,但是具体的每个步骤的细分还有很多

 

 

3.请求类:HttpServletRequest

请求信息类,对客户端收到的输入流数据进行解析与封装,得到request对象

将输入流转换成String,开始解析

/**
 * 将输入流转换成String,开始解析
 */
public HttpServletRequest(InputStream iis) {
       //一次性读完所有请求信息
   StringBuilder sb = new StringBuilder();
   int length = -1;
   byte[] bs = new byte[100*1024];
   try {
      length = iis.read(bs);//读取socket输入流数据,将其放到byte数组里面
   } catch (IOException e) {
      e.printStackTrace();
      System.out.println("读取客户请求异常");
       }
   //将bs中的字节数据转为char
   for(int i = 0;i<length;i++){
      sb.append((char)bs[i]);
   }
   content = sb.toString();//将sb转换成String,存到content里面
   parseProtocol();      //开始解析
}

具体解析操作

解析协议

/**
 * 解析协议
 */
private void parseProtocol() {
   String[] ss = content.split(" ");
   //解析 请求方法类型,存到method
   this.method = ss[0];

   //解析 请求地址,存到requestURI
   this.requestURI = ss[1];

   //解析 请求参数,存到parameter的map中
   parseParameter();

   //解析 请求头,存到headers中
   parseHeader();

   //解析 请求cookie:从headers中取cookie
   parseCookie();

   //解析 sessionId:从cookie中取出jsessionid
   jsessionid = parseJSessionId();
}

各种解析方法

private String parseJSessionId() {
   if(cookies!=null&&cookies.size()>0) {
      for(Cookie c:cookies) {
         if("JSESSIONID".equals(c.getName())) {
            return c.getValue();
         }
      }
   } 
   return null;
}

   /**
    * headers中取出cookie,然后在解析出cookie对象存在cookies中
 * 取出协议中的 Cookie:xxxx  ,如果有则说明已经生成过Cookie  没有则表明是第一次请求,要生成Cookie编号
    */
private void parseCookie() {
   if(headers==null&&headers.size()<=0){
      return;
   }
   //从headers中取出键为cookie的 
   String cookieValue = headers.get("Cookie");
   if(cookieValue == null || cookieValue.length()<=0) {
      return;
   }
   String[] cvs = cookieValue.split(": ");
   if(cvs.length > 0) {
      for(String cv:cvs) {
         String[] str = cv.split("=");
         if(str.length > 0) {
            String key = str[0];
            String value = str[1];
            Cookie c = new Cookie(key,value);
            cookies.add(c);
         }
      }
   }
}
private void parseHeader() {
       //请求头
   String[] parts = this.content.split("\r\n\r\n");
       //GET /请求地址 HTTP/1.1
   String[] headerss = parts[0].split("\r\n");
      for(int i = 1;i<headerss.length;i++){
         String[] headPair = headerss[i].split(": ");
               //Host: localhost:8888     Connection: keep-alive ...
         headers.put(headPair[0], headPair[1]);
      }
}

   /**
    * 取参数
    */
private void parseParameter() {
   //requestURI: user.action?name=z&password=a
   int index = this.requestURI.indexOf("?");
   //有?的话
   if(index>=1){
      String[] pairs = this.requestURI.substring(index+1).split("&");
      for(String p:pairs){
         String[] po = p.split("=");
         parameter.put(po[0], po[1]);
      }
   }
   if(this.method.equals("POST")){
      String[] parts = this.content.split("\r\n\r\n");
      String entity = parts[1];
      String[] pairs = entity.split("&");
      for(String p:pairs){
         String[] po = p.split("=");
         parameter.put(po[0], po[1]);
      }
   }
}

4.响应类:HttpServletResponse

        响应类:根据传过来的请求对象拿到 URL,找到请求的资源文件,设置对应的响应类型,将文件写入到响应流中返回

        如果是静态请求,就会调用到相应类,因为静态请求就是要获取某个HTML、CSS、JavaScript、图像等文件,所以我们只需要从请求url中解析出文件的名称,再找到这个文件,再按照http响应的格式的写入到输出流中,返回给前端就行了。

按照不同类型调用send方法

//3、发送文件响应,不同的文件返回不同类型
      if(file.getName().endsWith(".jpg")){
          send(file,"application/x-jpg",code);
      }else if(file.getName().endsWith(".jpe")||file.getName().endsWith(".jpeg")){
          send(file,"image/jpeg",code);
      }else if(file.getName().endsWith(".gif")){
          send(file,"image/gif",code);
      }else if(file.getName().endsWith(".css")){
          send(file,"text/css",code);
      }else if(file.getName().endsWith(".js")){
          send(file,"application/x-javascript",code);
      }else if(file.getName().endsWith(".swf")){
          send(file,"application/x-shockwave-flash",code);
      }else{
          send(file,"text/html",code);
      }

send方法先调用genProtocol方法,先拼接好响应格式

   /**
    * 拼接响应协议
    */
private String genProtocol(long length, String contentType, int code) {
   String result = "HTTP/1.1 "+code+" OK\r\n";
   result+="Server: myTomcat\r\n";
   result+="Content-Type: "+contentType+";charset=utf-8\r\n"; 
   result+="Content-Length: "+length+"\r\n";
   result+="Date: "+new Date()+"\r\n"; 
   result+="\r\n";
   return result;
}

send方法再调用readFile方法把文件读出来,返回字节数组

   /**
    * 读取文件
    */
private byte[] readFile(File file) {
   ByteArrayOutputStream baos = new ByteArrayOutputStream();
   FileInputStream fis = null;
   
   try {
      fis = new FileInputStream(file);
      byte[] bs = new byte[1024];
      int length;
      while((length = fis.read(bs,0,bs.length))!=-1){
         baos.write(bs, 0, length);
         baos.flush();
      }
   } catch (Exception e) {
      e.printStackTrace();
   }finally{
      try {
               fis.close();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
   return baos.toByteArray();
}

最后send方法就返回给前端流数据了

   /**
    * 返回给前端响应流
    */
private void send(File file, String contentType, int code) {
   try {
      String responseHeader = genProtocol(file.length(),contentType,code);
      byte[] bs = readFile(file);
      
      this.oos.write(responseHeader.getBytes());
      this.oos.flush();//往前端传过去
      this.oos.write(bs);
      this.oos.flush();
   } catch (IOException e) {
      e.printStackTrace();
   }finally{
      try {
         this.oos.close();
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

5.处理接口、动态处理类、静态处理类

        静态处理就是前端需要一些静态的资源,例如HTML、CSS、图片等,后端直接找到文件,按照格式返回给前端就行了

        动态处理 就有对应得到业务了,要调用对应的servlet

        区分动态还是静态,我这里是这样区分的:URL中有.action的就是动态,其他就是静态,下面代码就是在线程任务处理类里面的代码。

处理接口主要就是一个处理类的接口,动态处理类和静态处理类实现了它

/**
 * 处理器
 * 处理请求的接口
 *
 * @author 康有为
 * @date 2023/08/17
 */
public interface Processor {
    /** 处理请求,给出响应
     * @param request 请求
     * @param response 响应
     */
   void process(HttpServletRequest request, HttpServletResponse response);
}

 静态处理类就很简单,直接调用相应类的sendRedirect方法,返回给前端就行了。

/**
 * 静态处理器
 *     实现处理接口
 *
 * @author 康有为
 * @date 2023/08/17
 */
public class StaticProcessor implements Processor {

   @Override
   public void process(HttpServletRequest request, HttpServletResponse response) {
      //调用响应类的 sendRedirect方法
      response.sendRedirect();
   }

}

动态处理类

因为动态处理的URL中肯定是有“.action”结尾的(我们自定义的),所以我们要解析URL,拿到.action 前面的那些东西。

例如:localhost:8888/User.action 这个请求,我们拿到User之后,再后面拼接一个“Servlet”,再利用反射,在对应的存放servlet实例文件目录中找到UserServlet,再调用其对应的servlet的方法就行了。

6.servlet实例类

这里就是对前端的动态请求进行处理和反馈了,具体的servlet实例就根据不同的业务来处理就行。

注意:servlet实例必须要放到指定的目录下“servlet”,因为我们处理动态请求的时候是用反射来扫描了“servlet”目录,放在其他目录下就找不到了

例如下图,是登录的servlet,那就返回给前端登录成功,并附上session即可。

参考文章:【Tomcat】——纯手写实现一个简单的Tomcat_手写tomcat实现部署功能_土豆是我的最爱的博客-CSDN博客

具体的详细代码,请看源码 康有为/手写Tomcat - 码云 - 开源中国 (gitee.com)

喜欢的话,点赞支持一波

猜你喜欢

转载自blog.csdn.net/KangYouWei6/article/details/132447561
今日推荐