【学习笔记】手写 Tomcat 六

目录

一、线程池

1. 构建线程池的类

2. 创建任务

3. 执行任务

测试

二、URL编码

解决方案

测试

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

解决方案

测试

四、作业

1. 了解工厂模式

2. 了解反射技术


一、线程池

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

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

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

1. 构建线程池的类

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

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

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

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 

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 方法

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"开头,后面跟着若干位十六进制数字的字符串。

解决方案

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

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

在 MyTask 类中添加

测试

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

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

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

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

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

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

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

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

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

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

解决方案

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 对象

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/885891.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

渗透测试--文件上传常用绕过方式

文件上传常用绕过方式 1.前端代码&#xff0c;限制只允许上传图片。修改png为php即可绕过前端校验。 2.后端校验Content-Type 校验文件格式 前端修改&#xff0c;抓取上传数据包&#xff0c;并且修改 Content-Type 3.服务端检测&#xff08;目录路径检测&#xff09; 对目…

医院体检管理系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;体检分类管理&#xff0c;体检套餐管理&#xff0c;体检预约管理&#xff0c;体检报告管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;体检套餐&a…

四、Drf认证组件

四、Drf认证组件 4.1 快速使用 from django.shortcuts import render,HttpResponse from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.authentication import BaseAuthentication from rest_framework.exception…

数据结构:将复杂的现实问题简化为计算机可以理解和处理的形式

整句话的总体意义是&#xff0c;**数据结构是用于将现实世界中的实体和关系抽象为数学模型&#xff0c;并在计算机中表示和实现的关键工具**。它不仅包括如何存储数据&#xff0c;还包括对这些数据的操作&#xff0c;能够有效支持计算机程序的运行。通过这一过程&#xff0c;数…

利用PDLP扩展线性规划求解能力

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Java项目实战II基于Java+Spring Boot+MySQL的甘肃非物质文化网站设计与实现(源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者 一、前言 甘肃省作为中国历史文化名省&#xff0c;拥有丰富的非物质文化遗产资源&#xff0c;涵盖表演艺术、手…

TypeScript 封装 Axios 1.7.7

随着Axios版本的不同&#xff0c;类型也在改变&#xff0c;以后怎么写类型&#xff1f; 1. 封装Axios 将Axios封装成一个类&#xff0c;同时重新封装request方法 重新封装request有几个好处&#xff1a; 所有的请求将从我们定义的requet请求中发送&#xff0c;这样以后更换…

Golang | Leetcode Golang题解之第441题排列硬币

题目&#xff1a; 题解&#xff1a; func arrangeCoins(n int) int {return sort.Search(n, func(k int) bool { k; return k*(k1) > 2*n }) }

【Unity服务】如何使用Unity Version Control

Unity上的线上服务有很多&#xff0c;我们接触到的第一个一般就是Version Control&#xff0c;用于对项目资源的版本管理。 本文介绍如何为项目添加Version Control&#xff0c;并如何使用&#xff0c;以及如何将项目与Version Control断开链接。 其实如果仅仅是对项目资源进…

09_OpenCV彩色图片直方图

import cv2 import numpy as np import matplotlib.pyplot as plt %matplotlib inlineimg cv2.imread(computer.jpeg, 1) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img) plt.show()plot绘制直方图 plt.hist(img.ravel(), 256) #ravel() 二维降一维 256灰度级…

学习记录:js算法(五十):二叉树的右视图

文章目录 二叉树的右视图我的思路网上思路 总结 二叉树的右视图 给定一个二叉树的 根节点 root&#xff0c;想象自己站在它的右侧&#xff0c;按照从顶部到底部的顺序&#xff0c;返回从右侧所能看到的节点值。 图一&#xff1a; 示例 1:如图一 输入: [1,2,3,null,5,null,4] …

C++面向对象基础

目录 一.函数 1.内联函数 2.函数重载 3.哑元函数 二.类和对象 2.1 类的定义 2.2 创建对象 三. 封装&#xff08;重点&#xff09; 四. 构造函数 constructor&#xff08;重点&#xff09; 4.1 基础使用 4.2 构造初始化列表 4.3 构造函数的调用方式&#xff08;掌握…

解决方法:PDF文件打开之后不能打印?

打开PDF文件之后&#xff0c;发现文件不能打印&#xff1f;这是什么原因&#xff1f;首先我们需要先查看一下自己的打印机是否能够正常运行&#xff0c;如果打印机是正常的&#xff0c;我们再查看一下&#xff0c;文件中的打印功能按钮是否是灰色的状态。 如果PDF中的大多数功…

秋招内推--招联金融2025

【投递方式】 直接扫下方二维码&#xff0c;或点击内推官网https://wecruit.hotjob.cn/SU61025e262f9d247b98e0a2c2/mc/position/campus&#xff0c;使用内推码 igcefb 投递&#xff09; 【招聘岗位】 后台开发 前端开发 数据开发 数据运营 算法开发 技术运维 软件测试 产品策…

数据结构-LRU缓存(C语言实现)

遇到困难&#xff0c;不必慌张&#xff0c;正是成长的时候&#xff0c;耐心一点&#xff01; 目录 前言一、题目介绍二、实现过程2.1 实现原理2.2 实现思路2.2.1 双向链表2.2.2 散列表 2.3 代码实现2.3.1 结构定义2.3.2 双向链表操作实现2.3.3 实现散列表的操作2.3.4 内存释放代…

SigmaStudio控件Cross Mixer\Signal Merger算法效果分析

衰减与叠加混音算法验证分析一 CH2:输入源为-20dB正弦波1khz CH1叠加混音&#xff1a;参考混音算法https://blog.csdn.net/weixin_48408892/article/details/129878036?spm1001.2014.3001.5502 Ch0衰减混音&#xff1a;外部多个输入源做混音时&#xff0c;建议参考该算法控件&…

在VMware虚拟机上部署polardb

免密登录到我们的虚拟机之后&#xff0c;要在虚拟机上部署polardb数据库&#xff0c;首先第一步要先克隆源码&#xff1a; 为了进SSH协议进行传输源码需要先进行下面的步骤&#xff1a; 将宿主机上的私钥文件复制到虚拟机上 scp "C:\Users\waitw\.ssh\id_rsa" ann…

Azkaban:大数据任务调度与编排工具的安装与使用

在当今大数据时代&#xff0c;数据处理和分析任务变得越来越复杂。一个完整的大数据分析系统通常由大量任务单元组成&#xff0c;如 shell 脚本程序、mapreduce 程序、hive 脚本、spark 程序等。这些任务单元之间存在时间先后及前后依赖关系&#xff0c;为了高效地组织和执行这…

Leetcode 每日一题:Crack The Safe

写在前面&#xff1a; 学期真的忙起来了&#xff0c;我的两个社团也在上一周终于完成了大部分招新活动。虽然后面有即将到来的期中考试和求职&#xff0c;但希望能有时间将帖子的频率提上去吧&#xff08;真的尽量因为从做题思考到写博客讲解思路需要大量的时间&#xff0c;在…

当人工智能拥抱餐饮业,传统与创新的交融

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 今天我们要聊一个充满烟火气的行业&#x…