基于线程池手写一个简单web服务器并且顺带学会web服务基准测试工具ab?

简介: 基于线程池手写一个简单web服务器并且顺带学会web服务基准测试工具ab?

有经典,有干货,微信搜索【李子捌】关注这个每日更新的程序员。


简介:

目前浏览器作为web服务的客户端访问者,都支持并发多线程的访问。例如在浏览器访问一个web服务器上的HTML页面,此时HTML页面中的各种资源(图片、样式)会被浏览器并发的获取,这种并发访问使得用户不至于等待图片加载的同时也不能看到文字内容。

客户端既然是多线程并发访问,那么如果服务端仅仅是单线程处理客户端的请求,那么客户端的并发访问将会变得毫无意义。因此,大部分的web服务器也是支持并发访问的。常见的Java web服务器有Tomcat\Netty等等。

接下来我们通过结合线程池来写一个简单的web服务器,支持访问html(文本、图片)资源。


1、线程池实现:(有需要的看我上一章)手写线程池

线程池接口定义

package com.lizba.p3.threadpool;
/**
 * <p>
 *      线程池接口
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/17 22:28
 */
public interface ThreadPool<Job extends Runnable> {
    /**
     * 执行一个Job,这个Job需要实现Runnable
     * @param job
     */
    void execute(Job job);
    /**
     * 关闭线程池
     */
    void shutdown();
    /**
     * 增加工作者线程
     * @param num
     */
    void addWorkers(int num);
    /**
     * 减少工作者线程
     * @param num
     */
    void removeWorkers(int num);
    /**
     * 得到正在等待执行的任务数量
     * @return
     */
    int getJobSize();
}

线程池具体实现:

package com.lizba.p3.threadpool;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
/**
 * <p>
 *
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/17 22:34
 */
public class DefaultThreadPool<Job extends Runnable> implements ThreadPool<Job> {
    /** 线程池最大工作者线程数量 */
    private static final int MAX_WORKER_SIZE = 20;
    /** 线程池默认工作者线程数量 */
    private static final int DEFAULT_WORKER_SIZE = 5;
    /** 线程池最小工作者线程数量 */
    private static final int MIN_WORKER_SIZE = 5;
    /** 工作队列,也称任务队列,用来存放客户端提交的任务 */
    private final LinkedList<Job> jobs = new LinkedList<>();
    /** 工作者列表,需要具有同步性质,支持并发操作 */
    private final List<Worker> workers = Collections.synchronizedList(new ArrayList<Worker>());
    /** 工作线程的数量 */
    private int workerNum = DEFAULT_WORKER_SIZE;
    /** 线程编号生成器 */
    private AtomicLong threadNum = new AtomicLong();
    public DefaultThreadPool() {
        initWorker(DEFAULT_WORKER_SIZE);
    }
    public DefaultThreadPool(int size) {
        initWorker(size);
    }
    /**
     * 初始化线程工作者,并启动
     *
     * @param size  初始化工作着大小
     */
    private void initWorker(int size) {
        for (int i = 0; i < size; i++) {
            Worker worker = new Worker();
            workers.add(worker);
            Thread thread = new Thread(worker, "ThreadPool-Worker-" + threadNum.incrementAndGet());
            thread.start();
        }
    }
    @Override
    public void execute(Job job) {
        if (job != null) {
            // 添加一个任务,然后通知等待在jobs上的worker
            synchronized (jobs) {
                jobs.add(job);
                jobs.notifyAll();
            }
        }
    }
    @Override
    public void shutdown() {
        workers.forEach(worker -> worker.shutdown());
    }
    @Override
    public void addWorkers(int num) {
        // 此处要锁住jobs,因为worker会从jobs获取任务,需要jobs通知等待中的worker
        synchronized (jobs) {
            // 不允许工作者线程数操作最大值
            if (num + this.workerNum > MAX_WORKER_SIZE) {
                num = MAX_WORKER_SIZE - this.workerNum;
            }
            initWorker(num);
            this.workerNum += num;
        }
    }
    @Override
    public void removeWorkers(int num) {
        synchronized (jobs) {
            if (num > this.workerNum) {
                throw new IllegalArgumentException("超出工作者数目!");
            }
            int count = 0;
            while (count < num) {
                Worker worker = workers.get(count);
                // 如果移除成功则关闭工作者,工作者将不会继续获取任务执行
                if (workers.remove(worker)) {
                    worker.shutdown();
                    count++;
                }
                this.workerNum -= count;
            }
        }
    }
    @Override
    public int getJobSize() {
        return jobs.size();
    }
    /**
     * <p>
     *      工作者-负责消费客户端提交的任务
     * </p>
     *
     * @Author: Liziba
     * @Date: 2021/6/17 22:41
     */
    class Worker implements Runnable {
        /** 是否工作 */
        private volatile boolean running = Boolean.TRUE;
        @Override
        public void run() {
            while (running) {
                Job job = null;
                synchronized (jobs) {
                    while (jobs.isEmpty()) {
                        try {
                            jobs.wait();
                        } catch (InterruptedException e) {
                            // 如果感应到外部的中断通知,则自己主动中断返回
                            Thread.currentThread().interrupt();
                            return;
                        }
                    }
                    // 取出任务队列的第一个任务
                    job = jobs.removeFirst();
                }
                // 执行任务
                if (job != null) {
                    try {
                        job.run();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        /**
         * 关闭worker,全部关闭意味着线程池关闭
         */
        public void shutdown() {
            running = false;
        }
    }
}

2、web服务器实现

其主要功能和实现如下


服务端监听客户端的socket连接

接收到的socket连接封装到HttpRequestHandler线程中,当成任务提交给线程池去调度执行

HttpRequestHandler线程的run方法主要包含静态资源jpg图片的读取和输出(字节流),HTML文本读取和输出(字符流),关流等操作

package com.lizba.p3.http;
import com.lizba.p3.threadpool.DefaultThreadPool;
import com.lizba.p3.threadpool.ThreadPool;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * <p>
 * 简单HTTP服务器
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/6/18 11:47
 */
public class SimplerHttpServer {
    /** 设置线程池的默认大小 */
    private static ThreadPool<HttpRequestHandler> pool = new DefaultThreadPool<>(1);
    /** SimplerHttpServer根路径  */
    private static String basePath;
    /** 端口 */
    private static int port = 8888;
    /** serverSocket */
    private static ServerSocket serverSocket;
    public SimplerHttpServer(int port) {
        if (port < 0) {
            return;
        }
        SimplerHttpServer.port = port;
    }
    /**
     * 设置资源根路径
     *
     * @param basePath
     */
    public static void setBasePath(String basePath) {
        if (basePath == null || "".equals(basePath)) {
            return;
        }
        if (new File(basePath).exists() && new File(basePath).isDirectory()) {
            SimplerHttpServer.basePath = basePath;
        }
    }
    /**
     * 启动web服务
     *
     * @throws IOException
     */
    public static void start() throws IOException {
        serverSocket = new ServerSocket(port);
        Socket socket = null;
        while ((socket = serverSocket.accept()) != null) {
            pool.execute(new HttpRequestHandler(socket));
        }
        serverSocket.close();
    }
    /**
     * 将socket请求封装成一个HttpRequestHandler线程任务,将任务提交给线程池
     *
     */
    static class HttpRequestHandler implements Runnable {
        private Socket socket;
        public HttpRequestHandler(Socket socket) {
            this.socket = socket;
        }
        @Override
        public void run() {
            BufferedReader reader = null;
            BufferedReader br = null;
            PrintWriter out = null;
            InputStream in = null;
            String line;
            try {
                reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                String header = reader.readLine();
                // 计算绝对路径
                String absolutePath = basePath + header.split(" ")[1];
                out = new PrintWriter(socket.getOutputStream());
                // 图片资源处理,此处只支持jpg
                if (absolutePath.endsWith("jpg")) {
                    in = new FileInputStream(absolutePath);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int len = 0;
                    while ((len = in.read()) != -1) {
                        baos.write(len);
                    }
                    byte[] array = baos.toByteArray();
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server: Liziba");
                    out.println("Content-Type: image/jpeg");
                    out.println("Content-Length: " + array.length);
                    out.println("");
                    socket.getOutputStream().write(array, 0, array.length);
                } else {
                    // 其他资源例如HTML文本等资源(此处仅支持HTML文本资源)
                    br = new BufferedReader(new InputStreamReader(new FileInputStream(absolutePath)));
                    out = new PrintWriter(socket.getOutputStream());
                    out.println("HTTP/1.1 200 OK");
                    out.println("Server: Liziba");
                    out.println("Content-Type: text/html; charset=UTF-8");
                    out.println("");
                    while ((line = br.readLine()) != null) {
                        out.println(line);
                    }
                }
                out.flush();
            } catch (IOException e) {
                // 错误提示
                out.println("HTTP/1.1 500");
                out.println("");
                out.flush();
            } finally {
                close(br, in, reader, out, socket);
            }
        }
        /**
         * 关闭流
         * @param closeables
         */
        private static void close(Closeable... closeables) {
            if (closeables != null) {
                for (Closeable c : closeables) {
                    try {
                        if (c != null)
                            c.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

3、启动web服务

在启动服务前我们需要在本地提前准备资源,并将资源根目录指给SimplerHttpServer服务,我在D盘放了2张图和一个html文件。image.pngimage.pngimage.pngimage.pngimage.png5、测试

**测试工具 **


Apache HTTP server benchmarking tool(ab),简单说明一下这个测试工具。ab是一个Apache Http服务器基准测试工具。它可以测试HTTP服务器每秒最多可以处理多少请求。如果测试的是web应用服务,这个结果可以装换成整个应用每秒可以满足多少请求。它的缺点是用途比较有限,只能针对单个URL进行尽可能快的压力测试。


测试内容


使用ab分10个线程发起5000请求,每次测试结束后改变线程池的大小,初始大小为1,测试主要观察的是SimplerHttpServer的响应时间和每秒完成的查询数量,笔者的机器(CPU(AMD Ryzen 5 3600 6-Core Processor),内存16G)。


请求指令(具体参数说明请看我的ab工具使用章节)

Apache HTTP server benchmarking tool(ab)-服务器基准测试工具一文上手


ab -c 10 -n 10000 http://localhost:8888/test.html

这个表示同时处理10个线程的并发请求,一共请求10000次

image.pngimage.png在上述测试结果中,可以发现随着线程池的线程数目的增加,SimpleHttpServer的吞吐量不断增加,响应时间不断减小,因此线程池的实际作用是十分明显的,但是我们看到线程池中的线程由10改变为20的时候,SimpleHttpServer的响应时间没有减少反而有些变大了,因此线程池中的线程数目也不是越多也好的,线程池中的线程过多,反而会给系统增加无故开销,适得其反。在实际开发中,我们要根据业务具体需求,硬件资源等情况来设置线程池的大小,必要的时候也可以实现线程池的动态伸缩。


目录
相关文章
|
2月前
|
机器学习/深度学习 人工智能 测试技术
EdgeMark:嵌入式人工智能工具的自动化与基准测试系统——论文阅读
EdgeMark是一个面向嵌入式AI的自动化部署与基准测试系统,支持TensorFlow Lite Micro、Edge Impulse等主流工具,通过模块化架构实现模型生成、优化、转换与部署全流程自动化,并提供跨平台性能对比,助力开发者在资源受限设备上高效选择与部署AI模型。
291 9
EdgeMark:嵌入式人工智能工具的自动化与基准测试系统——论文阅读
|
2月前
|
Java 测试技术 API
自动化测试工具集成及实践
自动化测试用例的覆盖度及关键点最佳实践、自动化测试工具、集成方法、自动化脚本编写等(兼容多语言(Java、Python、Go、C++、C#等)、多框架(Spring、React、Vue等))
121 6
|
3月前
|
前端开发 Java jenkins
Jmeter压力测试工具全面教程和使用技巧。
JMeter是一个能够模拟高并发请求以检查应用程序各方面性能的工具,包括但不限于前端页面、后端服务及数据库系统。熟练使用JMeter不仅能够帮助发现性能瓶颈,还能在软件开发早期就预测系统在面对真实用户压力时的表现,确保软件质量和用户体验。在上述介绍的基础上,建议读者结合官方文档和社区最佳实践,持续深入学习和应用。
750 10
|
8月前
|
移动开发 数据挖掘 开发者
服务器发送事件(SSE)在现代Web开发中的关键作用
服务器发送事件(SSE)是HTML5标准协议,用于服务器主动向客户端推送实时数据,适合单向通信场景。相比WebSocket,SSE更简洁高效,基于HTTP协议,具备自动重连、事件驱动等特性。常见应用场景包括实时通知、新闻推送、数据分析等。通过Apipost等工具可轻松调试SSE,助力开发者构建高效实时Web应用。示例中,电商平台利用SSE实现秒杀活动通知,显著减少延迟并简化架构。掌握SSE技术,能大幅提升用户体验与开发效率。
|
3月前
|
监控 Java 数据挖掘
利用Jmeter工具进行HTTP接口的性能测试操作
基础上述步骤反复迭代调整直至满足预期目标达成满意水平结束本轮压力评估周期进入常态监控阶段持续关注系统运转状态及时发现处理新出现问题保障服务稳定高效运作
465 0
|
4月前
|
敏捷开发 运维 数据可视化
DevOps看板工具中的协作功能:如何打破开发、测试与运维之间的沟通壁垒
在DevOps实践中,看板工具通过可视化任务管理和自动化流程,提升开发与运维团队的协作效率。它支持敏捷开发、持续交付,助力团队高效应对需求变化,实现跨职能协作与流程优化。
|
5月前
|
缓存 安全 网络协议
如何使用Bluetown Cloud服务器及其CDN服务来掩护VPS的真实IP地址。
最终,通过Bluetown Cloud的CDN服务,你的VPS不仅仅是隐藏在一层又一层的保护之下,同时也因为CDN的全球节点而享受到加速访问的优势,无所不在又不被发现,像是一位能在互联网世界中自由穿梭的幽灵特工。
108 14
|
5月前
|
应用服务中间件 网络安全 数据安全/隐私保护
网关服务器配置指南:实现自动DHCP地址分配、HTTP服务和SSH无密码登录。
哇哈哈,道具都准备好了,咱们的魔术秀就要开始了。现在,你的网关服务器已经魔法满满,自动分配IP,提供网页服务,SSH登录如入无人之境。而整个世界,只会知道效果,不会知道是你在幕后操控一切。这就是真正的数字世界魔法师,随手拈来,手到擒来。
277 14
|
5月前
|
Java 测试技术 容器
Jmeter工具使用:HTTP接口性能测试实战
希望这篇文章能够帮助你初步理解如何使用JMeter进行HTTP接口性能测试,有兴趣的话,你可以研究更多关于JMeter的内容。记住,只有理解并掌握了这些工具,你才能充分利用它们发挥其应有的价值。+
898 23
|
5月前
|
数据可视化 测试技术 Go
Go 语言测试与调试:`go test` 工具用法
`go test` 是 Go 语言内置的测试工具,支持单元测试、基准测试、示例测试等功能。本文详解其常用参数、调试技巧及性能测试命令,并提供实际项目中的应用示例与最佳实践。

热门文章

最新文章