【学习笔记】手写 Tomcat 六

目录

一、线程池

[1. 构建线程池的类](#1. 构建线程池的类)

[2. 创建任务](#2. 创建任务)

[3. 执行任务](#3. 执行任务)

测试

二、URL编码

解决方案

测试

三、如何接收客户端发送的全部信息

解决方案

测试

四、作业

[1. 了解工厂模式](#1. 了解工厂模式)

[2. 了解反射技术](#2. 了解反射技术)


一、线程池

昨天使用了数据库连接池,我们了解了连接池的优点,那么也可以使用线程池来管理线程,

java自带的线程池的参数有 核心线程数,最大线程数,线程活跃时间,时间单位,任务队列,线程工厂,拒绝策略

为了学习了解线程池,我们先手写一个简单的线程池,只需要做到核心线程可重复利用就行

1. 构建线程池的类

属性:核心线程数,任务队列

方法:获取线程(静态代码块),执行任务(需要的参数:线程任务 Runnable)

为了避免创建多个对象,还需要设置单例模式

java 复制代码
package com.shao.net;

import java.util.concurrent.LinkedBlockingQueue;

public class ThreadPool {
    // 定义一个成员静态变量,存储单例对象
    private static ThreadPool instance;

    // 线程池核心线程数
    private final static int MAX_THREAD_NUM = 10;
    // 存放任务的队列
    private static final LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();

    static {
        for (int i = 0; i < MAX_THREAD_NUM; i++) {
            final int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    /**
                     *  线程池的线程,从队列中取出任务,这时线程不在临界区了,自动释放锁,然后执行任务,当执行完任务后,
                     *  因为是while循环,所以会在 synchronized (taskQueue) 等待,
                     *  当锁释放后,并且当前线程被唤醒时,会尝试获取锁,
                     *  如果获取到锁,会进入临界区,如果队列中有任务,则取出,然后执行任务,如果没有,则等待
                     *  等待下次获取到锁,会继续从上次进入等待态的位置继续往下执行,也就是 taskQueue.wait() 开始往下执行
                     * */
                    while (true) {
                        Runnable task = null;
                        synchronized (taskQueue) {
                            System.out.println("线程" + finalI + "准备完成");
                            // 队列为空,等待
                            while (taskQueue.isEmpty()) {
                                try {
                                    taskQueue.wait();   // 使当前线程等待,释放锁
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }
                            System.out.println("线程" + finalI + "开始执行");
                            // 从队列中取出任务
                            task = taskQueue.poll();
                        }
                        if (task != null) {
                            // 执行任务
                            task.run();
                        }
                    }
                }

            }).start();
        }
    }

    // 私有化构造函数
    private ThreadPool() {
    }

    // 获取对象
    public static ThreadPool getInstance() {
        synchronized (ThreadPool.class) {
            if (instance == null) {
                instance = new ThreadPool();
            }
            return instance;
        }
    }

    public void execute(Runnable task) {
        // 当方法被调用时,会尝试获取锁,如果获取到锁,则将任务加入队列,并唤醒等待的线程
        synchronized (taskQueue) {
            taskQueue.add(task);
            taskQueue.notify();
        }
    }
}

2. 创建任务

这里的任务是之前线程执行的代码,我们把需要线程执行的任务放到一个类里,然后实现Runnable

java 复制代码
package com.shao.net;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class MyTask implements Runnable {
    private InputStream is;
    private OutputStream os;

    public MyTask(InputStream is, OutputStream os) {
        this.is = is;
        this.os = os;
    }

    @Override
    public void run() {
        // 定义一个字节数组,存放客户端发送的请求信息
        byte[] bytes = new byte[1024];

        // 读取客户端发送的数据,返回读取的字节数
        int len = 0;
        try {
            len = is.read(bytes);

            if (len == -1) {
                return;
            }
            // 将读取的字节数组转换为字符串
            String msg = new String(bytes, 0, len);

            // 调用HttpRequest类解析请求信息
            HttpRequest httpRequest = new HttpRequest(msg);

            // 拼接请求的静态资源的路径
            // 路径是相对路径,从模块的根路径开始
            String filePath = "webs/" + httpRequest.getRequestModule();
            HttpResponse httpResponse = new HttpResponse(os, httpRequest);
            // 响应数据
            httpResponse.response(filePath);

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

3. 执行任务

初始化线程池,来一个用户连接时,就创建一个任务,然后交给线程池,线程池取出一条线程执行任务的 run 方法

java 复制代码
package com.shao.net;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class Tomcat {

    // 初始化线程池
    ThreadPool threadPool = ThreadPool.getInstance();

    public Tomcat() {
        ServerSocket ss = null;
        try {
            ss = new ServerSocket(8080);
            while (true) {

                // 调用accept()方法阻塞等待,直到有客户端连接到服务器,返回一个Socket对象用于与该客户端通信
                Socket socket = ss.accept();

                System.out.println("客户端连接成功");

                // 获取Socket对象的输入流,用于读取客户端发送的数据
                InputStream is = socket.getInputStream();

                // 获取Socket对象的输出流,用于向客户端发送数据
                OutputStream os = socket.getOutputStream();

                // 创建一个任务对象,将输入输出流作为参数传过去
                MyTask myTask = new MyTask(is, os);

                // 把任务作为参数传递给ThreadPool的execute()方法,启动一个线程执行MyTask对象中的run()方法
                threadPool.execute(myTask);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //关闭连接通道
            try {
                ss.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

测试

二、URL编码

在HTTP请求中,如果参数包含中文字符,会进行URL编码,以避免乱码或传输错误。URL编码是一种将URL中的非ASCII字符(如中文字符)转换为可以在Web浏览器和服务器之间传输的格式的过程。

URL编码会将非ASCII字符转换为十六进制编码,以便于在HTTP请求中安全传输。

‌URL编码的基本原理‌: URL编码将非ASCII字符(如中文字符)转换为"%"后跟两位十六进制数字的形式。例如,空格在URL编码中通常被转换为"%20"。对于中文字符,它们会被转码为以"%E"开头,后面跟着若干位十六进制数字的字符串。

解决方案

在接收到请求信息后,先进行解码,然后再解析信息

java 复制代码
URLDecoder.decode(需要解码的字符串, 字符集或编码方式)

在 MyTask 类中添加

测试

三、如何接收客户端发送的全部信息

目前,我们的 Tomcat 最多只能一次接收 1KB,因为定义的字节数组只有1024个字节

但是,如果客户端发送的请求参数非常非常多呢?超过了 1024 个字节了怎么办?

把字节数组定义的大一点?不行的,因为网络传输一次最多传输 8KB,超过 8KB 就会分批传输,接收参数时也需要分批接收

那怎么判断参数已经接收完?

参数有很多一般是使用POST方法,而POST方法的请求头有 Content-Length 的字段,表示请求体的总长度

我们来试一下,打印一下请求的参数信息

这里可以看到 Content-Length 的值是 26,表示请求体的参数长度为26字节,图中显示参数的长度为 25,因为解析后没有显示参数连接符 &

我们来使用 Apipost 来压力测试一下,参数很多是什么样子

可以看到,只读取到了一部分 出师表 的内容,而且还有乱码,这是因为没有完整读取一个 汉字的字节,UTF-8 编码中一个汉字需要 3 个字节

解决方案

java 复制代码
package com.shao.net;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MyTask implements Runnable {
    private InputStream is;
    private OutputStream os;
    private int totalLength;
    private StringBuilder sb;

    public MyTask(InputStream is, OutputStream os) {
        this.is = is;
        this.os = os;
    }

    @Override
    public void run() {
        // 定义一个字节数组,存放客户端发送的请求信息
        byte[] bytes = new byte[1024];

        // 读取客户端发送的数据,返回读取的字节数
        int len = 0;
        try {

            // 第一次读取请求信息
            len = is.read(bytes);

            if (len == -1) {
                return;
            }
            // 将读取的字节数组转换为字符串
            String msg = new String(bytes, 0, len);

            // 调用HttpRequest类解析请求信息
            HttpRequest httpRequest = new HttpRequest(msg);

            /*
             *  如果已读取的数据长度等于请求体的总长度,并且请求方法是 POST,说明请求体可能还没有读取完,需要读取剩余的数据
             * */
            if (bytes.length == len && "POST".equals(httpRequest.getRequestMethod())) {
                // 创建一个StringBuilder对象,用于拼接请求信息
                sb = new StringBuilder();
                sb.append(msg);

                // 获取 POST 请求方法中的请求体的总长度
                String length = httpRequest.getRequestHeaderParams().get("Content-Length");
                if (length != null) {
                    totalLength = Integer.parseInt(length);
                }
                // 调用方法,读取剩余的请求体数据
                msg = getNotReadMsg(httpRequest, bytes, msg);
            }


            // 把请求信息进行URL解码,然后根据 UTF-8 进行编码
            String decodedMsg = URLDecoder.decode(msg, "utf-8");

            // 调用HttpRequest类解析请求信息
            httpRequest = new HttpRequest(decodedMsg);

            // 拼接请求的静态资源的路径
            // 路径是相对路径,从模块的根路径开始
            String filePath = "webs/" + httpRequest.getRequestModule();
            HttpResponse httpResponse = new HttpResponse(os, httpRequest);
            // 响应数据
            httpResponse.response(filePath);

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

    /**
     * 读取剩余的请求体数据
     */
    private String getNotReadMsg(HttpRequest httpRequest, byte[] bytes, String msg) throws IOException {
        int len;
        // 获取请求的参数
        HashMap<String, String> requestBodyParams = httpRequest.getRequestBodyParams();

        // 请求参数的数量
        int size = requestBodyParams.size();

        // 计算第一次读取到的请求体中参数的长度
        Set<Map.Entry<String, String>> entries = requestBodyParams.entrySet();
        int partLength = 0;
        for (Map.Entry<String, String> entry : entries) {
            partLength += (entry.getKey() + "=" + entry.getValue()).length();
        }

        // 减去第一次读取到的请求体中参数的长度,如果存在多个参数,需要考虑到 '&' 的个数
        if (size > 1) {
            totalLength -= (partLength + (size - 1));
        } else {
            totalLength -= partLength;
        }

        // 判断是否还有数据没有读完
        while (totalLength > 0) {
            // 第二次读取请求信息
            len = is.read(bytes);
            // 如果读取的字节数大于0,表示读取到数据了
            if (len > 0) {
                // 将读取的字节数组转换为字符串
                msg = new String(bytes, 0, len);
                // 拼接字符串
                sb.append(msg);
                // 减去读取的字节数
                totalLength -= len;
            } else {
                break;
            }
        }
        // 转成字符串格式返回
        return sb.toString();
    }
}

测试

可以看出已经全部读取到了,第二个参数也读取到了

四、作业

1. 了解工厂模式

优化 Dao,现在在 Service 层,调用Dao层都要 new 一下,这样就比较占内存,比如调用的都是 UserDao,那么只需要创建一次 UserDao 的对象就行了

2. 了解反射技术

优化 Servlet ,通过配置文件可以动态的创建 Servlet 对象

相关推荐
mghio6 小时前
Dubbo 中的集群容错
java·微服务·dubbo
咖啡教室11 小时前
java日常开发笔记和开发问题记录
java
咖啡教室11 小时前
java练习项目记录笔记
java
鱼樱前端11 小时前
maven的基础安装和使用--mac/window版本
java·后端
RainbowSea12 小时前
6. RabbitMQ 死信队列的详细操作编写
java·消息队列·rabbitmq
RainbowSea12 小时前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
李少兄14 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝14 小时前
【设计模式】原型模式
java·设计模式·原型模式
云上艺旅14 小时前
K8S学习之基础七十四:部署在线书店bookinfo
学习·云原生·容器·kubernetes