基于线程池手写一个简单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的响应时间没有减少反而有些变大了,因此线程池中的线程数目也不是越多也好的,线程池中的线程过多,反而会给系统增加无故开销,适得其反。在实际开发中,我们要根据业务具体需求,硬件资源等情况来设置线程池的大小,必要的时候也可以实现线程池的动态伸缩。


目录
相关文章
|
7月前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
6月前
|
SQL 搜索推荐 Android开发
AB测试实战(一)
AB测试是一种数据驱动的产品优化方法,用于比较不同版本的网页、应用界面或营销策略的效果。
|
3月前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
47 2
|
19天前
|
JSON Java 测试技术
SpringCloud2023实战之接口服务测试工具SpringBootTest
SpringBootTest同时集成了JUnit Jupiter、AssertJ、Hamcrest测试辅助库,使得更容易编写但愿测试代码。
52 3
|
6月前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
104 0
|
4月前
|
测试技术
基于LangChain手工测试用例转Web自动化测试生成工具
该方案探索了利用大模型自动生成Web自动化测试用例的方法,替代传统的手动编写或录制方式。通过清晰定义功能测试步骤,结合LangChain的Agent和工具包,实现了从功能测试到自动化测试的转换,极大提升了效率。不仅减少了人工干预,还提高了测试用例的可维护性和实用性。
|
4月前
|
Java
Java使用FileInputStream&&FileOutputStream模拟客户端向服务器端上传文件(单线程)
Java使用FileInputStream&&FileOutputStream模拟客户端向服务器端上传文件(单线程)
88 1
|
4月前
|
API Windows
揭秘网络通信的魔法:Win32多线程技术如何让服务器化身超级英雄,同时与成千上万客户端对话!
【8月更文挑战第16天】在网络编程中,客户/服务器模型让客户端向服务器发送请求并接收响应。Win32 API支持在Windows上构建此类应用。首先要初始化网络环境并通过`socket`函数创建套接字。服务器需绑定地址和端口,使用`bind`和`listen`函数准备接收连接。对每个客户端调用`accept`函数并在新线程中处理。客户端则通过`connect`建立连接,双方可通过`send`和`recv`交换数据。多线程提升服务器处理能力,确保高效响应。
56 6
|
5月前
|
Java
如何在Java中实现多线程的Socket服务器?
在Java中,多线程Socket服务器能同时处理多个客户端连接以提升并发性能。示例代码展示了如何创建此类服务器:监听指定端口,并为每个新连接启动一个`ClientHandler`线程进行通信处理。使用线程池管理这些线程,提高了效率。`ClientHandler`读取客户端消息并响应,支持简单的文本交互,如发送欢迎信息及处理退出命令。
|
4月前
|
C语言
【C语言】多线程服务器
【C语言】多线程服务器
35 0