【学习笔记】手写 Tomcat 六

手写一个Tomcat (参考了公众号:java架构沉思录中的文章:教你写一个迷你版的toncat 原文:https://www.jianshu.com/p/dcelee01fb90) 作为一个java学习的起步者,对tomcat的认识还是有很多的欠缺,在无意中发现了这篇文章,便在自己的环境下尝试搭建,收获良多: 分以下几个步骤: (1)提供Socket服务 (2)进行请求的转发 (3)把请求和响应封装成request/... 阅读详情

目录

一、线程池

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

Hadoop与Kafka集群部署与优化实践指南 分布式系统是现代大数据处理的核心架构,其中Hadoop作为分布式存储与计算框架,Kafka作为分布式消息队列系统,共同构成了实时数据处理的基础设施。通过HDFS实现海量数据存储,结合YARN进行资源调度,而Kafka则保障了数据流的实时传输与高吞吐。这种技术组合在电商用户行为分析、物联网数据采集等场景中表现卓越。部署过程中需关注硬件资源配置、关键参数调优以及集群监控方案,例如通过调整HDFS块大小和Kafka partition数量来优化性能。合理运用Flume、Spark Streaming等工具可实现系 阅读详情

相关推荐

MATLAB去雾算法实战:5种经典方法对比+1种改进方案(附完整源码)

本文详细对比了MATLAB中5种经典去雾算法(包括暗通道先验、多尺度Retinex等)的实现效果,并提出一种创新的自适应融合改进方案。通过完整的源码展示和性能对比,帮助开发者掌握图像去雾的核心技术,提升计算机视觉应用中的图像质量。特别针对去雾算法中的光晕效应问题,给出了工程实践中的优化技巧。

weixin_26801149的博客 412

tomcat线程池

tomcat线程池

weixin_43803688的博客 8949

昇腾NPU实战:PaddleSpeech语音模型高效部署与性能优化指南

本文详细介绍了如何在华为昇腾NPU上高效部署PaddleSpeech语音识别模型。通过使用官方Docker镜像、解决依赖冲突、修改源码适配NPU算子,并应用混合精度与批处理等优化技巧,实现模型在昇腾平台上的稳定运行与性能提升,为AI语音应用提供高性价比的国产硬件部署方案。

weixin_29214691的博客 181

Tomcat 线程池

Tomcat 是一个流行的 Java Web 服务器,它使用线程池来处理客户端请求。线程池是一组预先创建的线程,用于执行并发任务,以提高性能和资源利用率。在 Tomcat 中,线程池被用来处理传入的 HTTP 请求。当客户端发送请求时,Tomcat 会从线程池中获取一个可用的线程来处理该请求。处理完请求后,线程将返回线程池,并在下一个请求到来时再次被重用。Tomcat 的线程池配置可以通过编辑server.xml文件来进行。maxThreads:指定线程池中的最大线程数。

一个菜鸡的博客 4245

【最全选型攻略-盘点篇】市面上有哪些靠谱的工业3D相机品牌?(双目、结构光、TOF、激光三角)

选工业3D相机,选最靠谱的品牌,选最Match你的产品,看这一篇就够了!

jiqishijueyingdi的博客 1万+

ADS新手必看:用Filter DesignGuide快速搞定一个4GHz LC低通滤波器(附保姆级避坑指南)

本文为ADS新手提供了4GHz LC低通滤波器的完整设计流程,重点介绍了如何使用Filter DesignGuide避免参数设置陷阱、正确解读仿真结果以及解决实际电路与理论值的偏差问题。通过真实工程案例,详细讲解了工程创建、Filter DesignGuide配置、电路优化、仿真设置及实际工程中的进阶技巧,帮助工程师快速掌握LC滤波器设计。

weixin_30878501的博客 300

【线程池】Tomcat线程池

最近看到篇关于 Tomcat 线程池的博客,因为之前只看过 JDK 线程池,记录一下。在微服务横行的今天,确实还是有必要研究研究 Tomcat 的线程池Tomcat 线程池和 JDK 线程池最大的不同就是它先把最大线程数用完,然后再提交任务到队列里面。深入理解线程池(ThreadPoolExecutor)——细致入微的讲源码。workerstarted = true-优快云博客本文主要介绍了 Tomcat 线程池的运行流程,和 JDK 线程池的流程比起来,它确实不一样。

m0_64944491的博客 1724

第44节:cesium 大雾效果(cesium自带)(含源码+视频)

cesium 大雾效果(cesium自带)

qq_29384639的博客 1702

详解tomcat的连接数与线程池

前言 在使用tomcat时,经常会遇到连接数、线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector)。 在前面的文章详解Tomcat配置文件server.xml中写到过:Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据;然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine。当Engine处理完请求后,也会通过Connecto..

haogenmin的博客 2435

最小二乘(MLS)求点云法向量

MLS算法的基本思想是通过在点云的局部邻域内拟合一个光滑曲面,如平面或多项式曲面,以此来重建和处理点云数据。对于法向量估计,MLS通常在每个点的邻域内拟合局部平面,然后从该平面的法向量作为点的法向量。

twnkie的博客 744

Python爬虫实战:逆向解析天猫商品详情API,精准捕获规格、SKU与促销活动

及浏览器开发者工具),绕过前端渲染的复杂性,直接逆向分析并调用其内部数据接口,从而高效、稳定地抓取商品的关键信息,包括商品规格、多维度SKU信息以及复杂的促销活动。print(f"SKU ID: {sku.sku_id}, 价格: ¥{sku.price}, 库存: {sku.stock}, 规格: {sku.props}")当你访问一个商品链接时,浏览器首先会加载一个几乎为空的HTML骨架,然后通过执行大量的JavaScript代码,向后台服务器发起多个异步请求,最终将获取到的数据填充到页面中。

2201_76125261的博客 1129

并发编程学习(十四):tomcat线程池

Tomcat 的核心功能有两个,分别是负责接收和反馈外部请求的连接器 Connector,和负责处理请求的容器 Container。其中连接器和容器相辅相成,一起构成了基本的 web 服务 Service。每个 Tomcat 服务器可以管理多个 Service。ConnectorContainer由 Engine、Host、Context和Wrapper 四个容器组成ServiceConnector 和 Container 两个核心组件。

weixin_40482816的博客 2867

使用verilog设计实现QR分解

QR(正交三角)分解法是求一般矩阵全部特征值的最有效并广泛应用的方法,一般矩阵先经过正交相似变化成为Hessenberg矩阵,然后再应用QR方法求特征值和特征向量。 使用verilog对QR实现方法进行设计并进行功能仿真: 如下: 顶层模块代码: module TOP( H1R, H1I, H2R, H2I, H3R, H3I, H4R, H4I, clk, rst_n, Y1R, Y1I, Y2R, Y2I, X1R, X1I, X2R, X2I ); parameter width = 16; // I

QQ_778132974的博客 2149

手写Tomcat

tomcat提供的服务: 第一:提供Socket服务 第二:进行请求的分发 第三:需要把请求和响应封装成request/response

书生的博客 4098

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

自己手写tomcat项目

在Servlet(接口中)有:1.init():初始化servlet2.getServletConfig():获取当前servlet的配置信息3.service():服务器(在HttpServlet中实现,目的是为了更好的匹配http的请求方式)4.getServletInfo():获取servlet当前运行过程中的信息5.destroy():销毁,回收内存在I/O中应包含:1.请求头(f12+网络) 2.请求方式(get/post) 3.请求内容。

2302_79585857的博客 1799

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

【学习笔记】手写 Tomcat -- 预备知识

I 是 Input,O 是Output,也就是输入输出流。当我们操作数据时,以内存为基准,把数据读取到内存中是输入,把数据从内存写入到硬盘是输出。

LearnTech_123的博客 1644

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

18_1手写Tomcat学习笔记

tomcat学习笔记 文章目录1 tomcat简介1.1 web服务器1.2 tomcat服务器1.2.1 下载1.2.2 安装1.2.3 配置1.2.4 tomcat目录结构1.2.5 如何去访问一个web应用的某个文件1.2.6 在web服务器中安装web应用1.2.7 配置虚拟目录1.2.8 虚拟主机1.3 浏览器访问web站点原理2 手写tomcat_静态资源3 手写tomcat_动态资源3.1 tomcat工作原理3.2 http get请求格式3.2 http post请...

tangguanlin2008的博客 3741

六. 手写Tomcat

六. 手写Tomcat 1. 什么是 Tomcat Tomcat 本质上是一款开源轻量级 Web 应用服务器,是一款优秀的 Servlet 容器实现。 核心 2 点,Web 服务器、Servlet 容器。 阿里中间件团队:http://jm.taobao.org/about/ 这个域名404了, 可以通过ip访问 http://140.205.61.252/about/ 很多公司都手写或者重写 Tomcat.比如 aliTomcat : https://help.aliyun.com/document_de

努力学习 632

手写简易Tomcat

通过手写tomcat对java中的网络通信有了更进一步的了解

Java新生代 1411

【学习笔记】手写 Tomcat 二

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

LearnTech_123的博客 1485

从零开始手写Tomcat,一文彻底搞懂Tomcat运行流程(附源码)

一:理论铺垫 tomcat的功能是什么? tomcat是java的一个中间件,浏览器发出HTTP请求后经过tomcat中间件,转发到目的服务器,目的服务器返回响应消息,通过tomcat返回给浏览器。tomcat的使用很简单,但是作为合格的程序员,光会用可不行,接下来就通过手写一个tomcat彻底搞懂tomcat。 在手写Tomcat之前,首先重温一下http和servlet,http协议...

Java鱼仔的博客 7764

【学习笔记】手写 Tomcat 五

我们把功能都写在了 Servlet,这样有几个缺点,一是不够低耦合 高内聚,二是不方便协同开发等。我们可以使用三层架构来解决这个问题。三层架构区分层次的目的是为了 “高内聚,低耦合”。

LearnTech_123的博客 1184

手写Tomcat框架

模型核心逻辑并发能力复杂度BIO一请求一线程,全程阻塞低简单NIO线程 + Selector 管多连接中高中等AIO异步通知,线程不等待高复杂2.扫描包的类用的是一个工具类进行实现的2.1首先 进行遍历获取类信息2.2通过反射机制,从目标类(clazz)中提取它上面标注的@WebServlet注解对象2.3这里获取类的无参构造方法 ---创建新的对象并强转向上转型为。

2302_80192044的博客 978
上一篇: 【学习笔记】手写 Tomcat 五
下一篇: 【学习笔记】手写 Tomcat 七
LearnTech_123
博客等级 码龄3年 445粉丝 31原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值