【学习笔记】手写Tomcat 二

目录

响应静态资源

HTTP协议请求格式

[1. 解析请求信息](#1. 解析请求信息)

[创建解析请求类 HttpRequest](#创建解析请求类 HttpRequest)

[2. 创建静态资源目录 webs](#2. 创建静态资源目录 webs)

[3. 封装响应信息](#3. 封装响应信息)

[创建静态资源处理器 StaticResourceHandler](#创建静态资源处理器 StaticResourceHandler)

[创建响应类 HttpResponse](#创建响应类 HttpResponse)

然后就可以调用响应类了

测试

静态资源的路径说明

作业

[1. 绘制 请求解析类 HttpRequest 和响应类 HttpResponse 的封装流程图](#1. 绘制 请求解析类 HttpRequest 和响应类 HttpResponse 的封装流程图)

[2. 优化客户端的连接](#2. 优化客户端的连接)


响应静态资源

在昨天的基础上,再进一步优化,能够响应HTML文件,图片等静态资源

在响应静态资源之前,我们要先确定客户端请求的是哪个静态资源,是HTML页面还是图片呢?

我们先打印一下客户端的请求信息

HTTP协议请求格式

可以看到,第一行也就是 请求行 ,它有请求方法,请求URL和请求协议

请求URL就是需要请求的资源

1. 解析请求信息

那如何拿到 请求的URL呢?然后根据请求URL响应对应的资源

我们看一下 HTTP协议的请求格式,不难发现,都是以回车符和换行符结尾

以 回车符和换行符进行分割,然后再根据 空格 分割就可以拿到请求URL了

创建解析请求类 HttpRequest
java 复制代码
package com.shao.net;

import java.util.HashMap;

public class HttpRequest {
    /**
     * 请求信息
     */
    private String msg;

    /**
     * 请求行
     */
    private String requestLine;
    /**
     * 请求行的请求方法
     */
    private String requestMethod;

    /**
     * 请求行的请求URI,请求路径
     */
    private String requestURI;
    /**
     * 请求行的请求模块,例如:/index.html?a=1 请求模块为:/index.html
     */
    private String requestModule;
    /**
     * 请求行的请求协议
     */
    private String requestProtocol;

    /**
     * 存储请求头参数
     */
    private HashMap<String, String> requestHeaderParams = new HashMap<>();

    /**
     * 存储请求体参数
     */
    private HashMap<String, String> requestBodyParams = new HashMap<>();

    /**
     * 构造函数
     */
    public HttpRequest(String msg) {
        this.msg = msg;

        // 根据HTTP协议格式 分割请求信息
        String[] requestArr = msg.split("\r\n");
        // 把数组中的第一个元素赋值给请求行
        requestLine = requestArr[0];


        // 1. 解析请求行
        parseRequestLine();

        // 2. 解析请求头
        parseRequestHeader(requestArr);

        // 3. 解析请求体
        parseRequestBody();
    }

    // 1. 解析请求行
    private void parseRequestLine() {

        // 把请求行按空格分割
        String[] requestParts = requestLine.split(" ");
        // 请求方法
        requestMethod = requestParts[0];
        // 请求资源路径
        requestURI = requestParts[1];
        // 请求协议
        requestProtocol = requestParts[2];

        // 如果请求方法是GET,则根据 ? 号分割,获取请求模块,例如:/index.html?a=1 请求模块为:/index.html
        String[] split = requestURI.split("\\?");
        requestModule = split[0];

        System.out.println("请求方法:" + requestMethod);
        System.out.println("请求uri:" + requestURI);
        System.out.println("请求协议:" + requestProtocol);
        System.out.println("请求模块:" + requestModule);

    }

    // 2. 解析请求头
    private void parseRequestHeader(String[] requestArr) {
        // 检查请求数组,判断是否为空,判断有没有请求头
        if (requestArr == null || requestArr.length <= 1) {
            return;
        }
        // 遍历请求数组,从第二个元素开始,因为第一个元素是请求行
        for (int i = 1; i < requestArr.length; i++) {
            // 判断是否为空,如果为空,表示请求头结束,因为 HTTP 协议中,请求头和请求体之间有2个回车符 换行符
            String headerLine = requestArr[i];
            if (headerLine.length() == 0) {
                break;
            }
            // 把请求头按 :  分割,获取请求头参数
            // 注意:这里需要使用 ": ",而不是 ":"
            String[] headerParts = headerLine.split(": ");
            // 判断请求头的格式是否正确
            if (headerParts.length >= 2) {
                requestHeaderParams.put(headerParts[0], headerParts[1]);
            }
        }
        System.out.println("请求头参数:" + requestHeaderParams);

    }

    // 3. 解析请求体
    private void parseRequestBody() {
        // POST 方法的请求参数是在请求体,GET 方法的请求参数在请求行
        if (this.requestMethod.equalsIgnoreCase("POST")) {
            // 分割请求信息
            String[] split = msg.split("\r\n\r\n");
            // 如果分割后的长度 >= 2,表示有请求体
            if (split.length >= 2) {
                // 分割请求体
                splitRequestBody(split[1]);
            }
        } else if (this.requestMethod.equalsIgnoreCase("GET")) {
            // 把请求行按空格分割,获取请求参数
            String[] requestLineParts = this.requestLine.split(" ");
            String[] split = requestLineParts[1].split("\\?");
            if (split.length >= 2) {
                // 分割请求体
                splitRequestBody(split[1]);
            }
        }
        System.out.println("请求体参数:" + requestBodyParams);
    }

    private void splitRequestBody(String requestBody) {
        // 分割请求参数,例如:a=1&b=2&c=3
        String[] requestBodyParts = requestBody.split("&");
        // 遍历请求体
        for (int i = 0; i < requestBodyParts.length; i++) {
            String part = requestBodyParts[i];
            // 把请求参数按 = 分割,获取键值对,最多分割2部分,如果有多个 = ,只保留第一个 = 之前和之后的部分
            String[] keyValue = part.split("=", 2);
            if (keyValue.length == 2) {
                requestBodyParams.put(keyValue[0], keyValue[1]);
            } else {
                System.out.println("警告:非法格式的请求体:" + part);
            }
        }
    }

    /**
     * 获取
     * @return msg
     */
    public String getMsg() {
        return msg;
    }

    /**
     * 设置
     * @param msg
     */
    public void setMsg(String msg) {
        this.msg = msg;
    }

    /**
     * 获取
     * @return requestLine
     */
    public String getRequestLine() {
        return requestLine;
    }

    /**
     * 设置
     * @param requestLine
     */
    public void setRequestLine(String requestLine) {
        this.requestLine = requestLine;
    }

    /**
     * 获取
     * @return requestMethod
     */
    public String getRequestMethod() {
        return requestMethod;
    }

    /**
     * 设置
     * @param requestMethod
     */
    public void setRequestMethod(String requestMethod) {
        this.requestMethod = requestMethod;
    }

    /**
     * 获取
     * @return requestURI
     */
    public String getRequestURI() {
        return requestURI;
    }

    /**
     * 设置
     * @param requestURI
     */
    public void setRequestURI(String requestURI) {
        this.requestURI = requestURI;
    }

    /**
     * 获取
     * @return requestModule
     */
    public String getRequestModule() {
        return requestModule;
    }

    /**
     * 设置
     * @param requestModule
     */
    public void setRequestModule(String requestModule) {
        this.requestModule = requestModule;
    }

    /**
     * 获取
     * @return requestProtocol
     */
    public String getRequestProtocol() {
        return requestProtocol;
    }

    /**
     * 设置
     * @param requestProtocol
     */
    public void setRequestProtocol(String requestProtocol) {
        this.requestProtocol = requestProtocol;
    }

    /**
     * 获取
     * @return requestHeaderParams
     */
    public HashMap<String, String> getRequestHeaderParams() {
        return requestHeaderParams;
    }

    /**
     * 设置
     * @param requestHeaderParams
     */
    public void setRequestHeaderParams(HashMap<String, String> requestHeaderParams) {
        this.requestHeaderParams = requestHeaderParams;
    }

    /**
     * 获取
     * @return requestBodyParams
     */
    public HashMap<String, String> getRequestBodyParams() {
        return requestBodyParams;
    }

    /**
     * 设置
     * @param requestBodyParams
     */
    public void setRequestBodyParams(HashMap<String, String> requestBodyParams) {
        this.requestBodyParams = requestBodyParams;
    }

    public String toString() {
        return "HttpRequest{msg = " + msg + ", requestLine = " + requestLine + ", requestMethod = " + requestMethod + ", requestURI = " + requestURI + ", requestModule = " + requestModule + ", requestProtocol = " + requestProtocol + ", requestHeaderParams = " + requestHeaderParams + ", requestBodyParams = " + requestBodyParams + "}";
    }
}

调用 HttpRequest 解析请求信息

游览器发送请求

打印解析后的请求信息,可以看到请求的静态资源是 /index.html

拿到了请求uri,我们就可以根据 请求uri 响应静态资源了

那这个静态资源放在哪儿呢?

2. 创建静态资源目录 webs

创建一个 HTML 文件,内容先写英文,写中文需要在响应头加上 UTF-8 字符编码

那现在 index.html 有了,怎么响应给客户端呢?

读取 index.html 文件,将读取到的内容发送到游览器,游览器会自动渲染页面

3. 封装响应信息

把响应信息的代码拿出来,单独放到一个类里,这个类就只需要响应数据

这样可以降低代码的耦合度

在创建响应类之前有两个问题

**1. 怎么确定请求的是静态资源?**动态资源一般需要操作数据库

2. 响应的媒体类型怎么写?

如果响应的是图片,那么就需要修改为 Content-Type:image/jpeg 或 image/png 等

所以,就需要动态的判断响应的静态资源是什么媒体类型。

创建静态资源处理器 StaticResourceHandler
java 复制代码
package com.shao.net;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class StaticResourceHandler {

    // 常见静态资源扩展名
    private static final String[] staticExtensions = new String[]{".html", ".css", ".js", ".jpg", ".png", ".gif", ".ico", ".svg", ".pdf", ".txt"};

    /**
     * 判断是否静态资源
     * 如果是静态资源则返回 true ,否则返回 false
     */
    public static boolean isLikelyStaticResource(String fileName) {

        // 检查文件的扩展名是否存在于静态资源扩展名数组中
        for (String ext : staticExtensions) {
            if (fileName.endsWith(ext)) {
                return true;
            }
        }
        return false;
    }


    /**
     * 根据文件路径获取文件内容
     */
    public static byte[] getFileContents(String filePath) {
        // 1. 获取文件对象
        File file = new File(filePath);
        // 2. 判断文件是否存在
        if (!file.exists()) {
            return null;
        }

        // 3. 获取文件内容

        // 定义一个字节数组,数组大小根据文件大小确定
        byte[] fileContents = new byte[0];
        try {
            FileInputStream fis = new FileInputStream(file);

            // fis.available() 是读取的文件的字节数
            fileContents = new byte[fis.available()];

            // 读取文件存放到 fileContents 数组中
            fis.read(fileContents);

            // 关闭文件输入流
            fis.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fileContents;
    }

    /**
     * 获取文件的媒体类型
     */
    public static String getFileMimeType(String filePath) {
        // 1 获取文件对象
        File file = new File(filePath);

        // 2 判断文件是否存在
        if (!file.exists()) {
            return "text/html";
        }
        // 3 获取文件的媒体类型
        String fileType = null;
        try {
            fileType = Files.probeContentType(Paths.get(filePath));

        } catch (IOException e) {
            e.printStackTrace();
        }
        return fileType;
    }

}
创建响应类 HttpResponse
java 复制代码
package com.shao.net;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;

public class HttpResponse {

    /**
     * 输出流
     */
    private OutputStream os;

    /**
     * 解析请求信息的对象
     */
    private HttpRequest httpRequest;

    public HttpResponse(OutputStream os, HttpRequest httpRequest) {
        this.os = os;
        this.httpRequest = httpRequest;
    }

    public void response(String filePath) {
        //判断请求的是否为静态文件
        if (StaticResourceHandler.isLikelyStaticResource(httpRequest.getRequestModule())) {
            // 获取静态资源一般是 GET 请求方法
            if (httpRequest.getRequestMethod().equals("GET")) {
                // 响应静态资源
                responseStaticResource(filePath);
            }
        } else {
            // 处理动态请求
            System.out.println("请求动态资源");
        }
    }

    /**
     * 响应静态资源
     */
    private void responseStaticResource(String filePath) {
        // 读取文件
        byte[] fileContents = StaticResourceHandler.getFileContents(filePath);

        // 判断文件是否存在,不存在则返回 404 的页面
        if (fileContents == null) {
            try {
                FileInputStream fis = new FileInputStream("webs/pages/not_Found404.html");

                fileContents = new byte[fis.available()];

                fis.read(fileContents);

                fis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 响应协议
        String protocol = httpRequest.getRequestProtocol();
        // 文件媒体类型
        String fileMimeType = StaticResourceHandler.getFileMimeType(filePath);
        try {
            os.write((protocol + " 200 OK\r\n").getBytes());
            os.write(("Content-Type: " + fileMimeType + "\r\n").getBytes());
            os.write(("Content-Length: " + fileContents.length + "\r\n").getBytes());
            os.write("\r\n".getBytes());
            os.write(fileContents);
            os.flush();
            System.out.println("响应成功");
            os.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
然后就可以调用响应类了
测试
静态资源的路径说明

可以直接复制文件的相对路径

作业

1. 绘制 请求解析类 HttpRequest 和响应类 HttpResponse 的封装流程图

2. 优化客户端的连接

现在只有主线程处理客户端连接,如果同一时刻有多个客户端发起连接,同一时间只能处理一个客户端的连接,这样效率不高,怎么处理?

解决方案是来一个客户端连接就创建一条线程去处理

相关推荐
狂飙的张兴发23 分钟前
认知小文2《成功之路:习惯、学习与实践》
学习·考研·职场和发展·跳槽·学习方法·改行学it·高考
小张同学(恩师白云)26 分钟前
SpringDataJPA基础增删改查
java·数据库
爱编程的小新☆27 分钟前
C语言内存函数
c语言·开发语言·学习
尘浮生1 小时前
Java项目实战II基于Spring Boot的宠物商城网站设计与实现
java·开发语言·spring boot·后端·spring·maven·intellij-idea
Grey Zeng1 小时前
Java SE 23 新增特性
java·jdk·jdk新特性·jdk23
勤奋的小王同学~1 小时前
怎么修改mvn的java版本
java·开发语言
夜清寒风1 小时前
opencv学习:图像掩码处理和直方图分析及完整代码
人工智能·opencv·学习·算法·机器学习·计算机视觉
越过难题1 小时前
若依的使用
java
doc_wei1 小时前
Java小区物业管理系统
java·开发语言·spring boot·spring·毕业设计·课程设计·毕设
生产队队长2 小时前
SpringBoot2:web开发常用功能实现及原理解析-@ControllerAdvice实现全局异常统一处理
java·spring boot