Web服务器实现|基于阻塞队列线程池的Http服务器|线程控制|Http协议

news2024/11/20 10:24:09

基于阻塞队列生产者消费者模型线程池的多线程Web服务器

代码地址:WebServer_GitHub_Addr

README

摘要

本实验通过C++语言,实现了一个基于阻塞队列线程池的多线程Web服务器。该服务器支持通过http协议发送报文,跨主机抓取服务器上特定资源。与此同时,该Web服务器后台通过C++语言,通过原生系统线程调用pthread.h,实现了一个基于阻塞队列的线程池,该线程池支持手动设置线程池中线程数量。与此同时,该线程池通过维护任务队列,每次Web服务器获取请求时,后台服务器就会将特定请求对应的特定任务加载到线程池中,等待线程池中线程的调用。由于在本项目中,每次的远端抓取都是短链接,因此在理论上,该Web服务器可以接收无数个请求。

按照实验要求,本Web服务器可以接受并解析 HTTP 请求,然后从服务器的文件系统中读取被 HTTP 请求的文件,并根据该文件是否存在而向客户端发送正确的响应消息。

在服务端终端中,服务器能够按照不同等级打印日志信息,并且在收到请求时打印报文信息。

执行效果

定义代码下./wwwroot目录为服务器资源的根目录,定义./wwwroot/index.html为服务器主页。

  • 当客户端请求资源存在时,服务器将返回对应资源。
  • 当客户端请求路径是/根目录时,服务器默认返回主页。
  • 当客户端申请路径不存在时,服务端返回./wwwroot/error/404.html作为返回资源。

服务器开机
在这里插入图片描述
浏览器远端连接

服务器是腾讯云的远端服务器,浏览器是本地浏览器。这是一个跨局域网的跨主机测试。
在这里插入图片描述
客户端申请不存在的资源
在这里插入图片描述
客户端发送请求时,后台服务器所获取到的报文
在这里插入图片描述
多个客户端发起请求

该服务器理论上支持无数个短链接进行请求
在这里插入图片描述

环境准备

系统: Linux version 3.10.0-1160.11.1.el7.x86_64

编译器版本: gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)

资源文件/源文件准备目录结构
在这里插入图片描述

  • MyThreadPool目录维护手写的线程池源代码
  • ./wwwroot/目录维护服务器资源,其中./wwwroot/是客户端访问的根目录
  • 其他文件均为HttpServer源代码

RUN

服务默认端口号:8080

tips: 如果运行过程中二进制程序HttpServer没有可执行权限 chmod 755 HttpServer

生成可执行:make clean;make

启动服务器:./HttpServer 8080

在浏览器中输入:ip:port即可访问服务器主页

或输入:ip:port/index.html也可以访问服务器主页

在这里插入图片描述
输入不存在的路径: 43:xxx:xxx:xxx:8080/a.txt
在这里插入图片描述

代码详解

Web服务器实现架构

第一层封装:对套接字的基本操作进行封装 Sock.hpp

/* 具体接口实现可以见源代码 */
/* Sock.hpp */
class Sock
{
private:
    const static int gbacklog = 20; 
public:
    Sock() {}
    ~Sock() {}
  	/* 以下接口内部调用的都是套接字的底层调用 */
    int Socket(); // 创建获取套接字 -- 返回值: 监听套接字
    void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"); // 绑定
    void Listen(int sock); // 设置监听状态
    int Accept(int listensock, std::string *ip, uint16_t *port); // 接收连接 -- 返回值: 服务套接字
    bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port);
};

第二层封装:对Http服务器的封装 HttpServer.hpp

  • 通过维护Task类,可以把线程所需要执行的动作进行封装。通过Task类的operator()()重载,线程可以直接执行内部绑定好的方法。
  • 通过维护HttpServer类,当初始化并执行HttpServer::Start()后,启动线程池,Accept成功后,把相对应的任务加载到线程池中,等待线程池中线程的调用。
#ifndef __Yufc_HttpServer
#define __Yufc_HttpServer

#include "Sock.hpp"
#include <functional>
#include "MyThreadPool/threadPool.hpp"
using func_t = std::function<void(int)>;
/* 任务类 */
struct Task
{
public:
    func_t func__;
    int sock__;
public:
    Task() {}
    Task(func_t func, int sock) : func__(func), sock__(sock) {}
    void operator()()
    {
        func__(sock__);
    }
};
/* http 服务器类 */
class HttpServer
{
private:
    int __listen_sock;
    uint16_t __port;
    Sock __sock;
    func_t __func;
    yufc_thread_pool::ThreadPool<Task> *__thread_pool = yufc_thread_pool::ThreadPool<Task>::getThreadPool();
public:
    HttpServer(const uint16_t &port, func_t func) : __port(port), __func(func);
    ~HttpServer();
    void Start();
};
#endif

Http服务器的启动 HttpServer.cc

执行HttpServer.cc后,main()创建一个服务器对象指针,并调用其中的Start()启动服务器。

void HandlerHttpRequest(int sockfd);函数维护每个线程所要执行的任务,其中包括以下内容。

  • 读取Http请求
  • 解析Http报文
  • 创建一个http响应
  • 发送响应至客户端
/* 一般http都要有自己的web根目录 */
#define ROOT "./wwwroot"
/* 如果客户端只请求了一个/ ,一般返回默认首页 */
#define HOME_PAGE "index.html"
void HandlerHttpRequest(int sockfd); // 具体实现可见源代码
int main(int argc, char **argv)
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::unique_ptr<HttpServer> httpserver(new HttpServer(atoi(argv[1]), HandlerHttpRequest));
    httpserver->Start();
    return 0;
}

ulity.hpp

提供Util类,里面提供static void cutString(const std::string &s, const std::string &sep, std::vector<std::string> *out)接口。在解析http报文时,可以把传入的字符串s按照间隔为sep的方式进行切割,并把结果放到out里面。

Usage.hpp

提供Usage函数,作为HttpServer的使用手册。

Log.hpp

提供void logMessage(int level, const char *format, ...)函数,负责打印服务器日志信息。

日志等级有:

DEBUG 调试

NORMAL 正常运行

WARNING 警告 – 进程继续运行

ERROR 非致命错误 – 进程继续运行

FATAL 致命错误 – 进程终止

线程池实现架构

thread.hpp

  • 对原生线程进行了简单封装
typedef void*(*func_t_)(void*); // 函数指针
class ThreadData
{
public:
    void* __args;
    std::string __name;
};
class Thread
{
private:
    std::string __name; // 线程名字
    pthread_t __tid;    // 线程tid
    func_t_ __func;      // 线程要调用的函数
    ThreadData __tdata; // 线程数据
public:
    Thread(int num, func_t_ callback, void* args); 
  	// num-自定义的线程编号 callback-线程要执行的任务 args-callback的参数
    ~Thread();
    void start();
    void join();
    std::string name(); // 返回线程名字
};

lockGuard.hpp

  • 用RAII的锁的封装风格对互斥锁进行封装
//封装一个锁
class Mutex
{
private:
    pthread_mutex_t *__pmtx;
public:
    Mutex(pthread_mutex_t *mtx)
        :__pmtx(mtx){}
    ~Mutex()
    {}
public:
    void lock() // 加锁
    {
        pthread_mutex_lock(__pmtx);
    }
    void unlock() // 解锁
    {
        pthread_mutex_unlock(__pmtx);
    }
};
class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx)
        :__mtx(mtx)
    {
        __mtx.lock();
    }
    ~lockGuard()
    {
        __mtx.unlock();
    }
private:
    Mutex __mtx;
};

threadPool.hpp

线程池整体架构
在这里插入图片描述

#define _DEBUG_MODE_ false
const int g_thread_num = 3; // 默认3个线程
/* 具体实现可见源代码 */
namespace yufc_thread_pool
{
    template <class T>
    class ThreadPool
    {
    private:
        std::vector<Thread *> __threads; // 线程们
        int __num; // 线程数量
        std::queue<T> __task_queue; // 任务队列
        pthread_mutex_t __lock; // 互斥锁
        pthread_cond_t __cond; // 条件变量
        static ThreadPool<T> *thread_ptr; // 单例模式
        static pthread_mutex_t __mutexForPool; // 保护getThreadPool对互斥锁
    private:
        //构造放成私有的 -- 让线程池成为单例模式
        ThreadPool(int thread_num = g_thread_num);
        ThreadPool(const ThreadPool<T>& other) = delete;
        const ThreadPool<T>& operator=(const ThreadPool<T>& other) = delete;
    public:
        ~ThreadPool();
    public:
        static ThreadPool<T>* getThreadPool(int num = g_thread_num); // 懒汉模式--获取线程池
        void run(); // 启动线程池
        void pushTask(const T &task); // 向线程池添加任务
        static void *routine(void *args); // 线程要做的事
    public:
        // 需要一批,外部成员访问内部属性的接口提供给static的routine,不然routine里面没法加锁
        // 下面这些接口,都是没有加锁的,因为我们认为,这些函数被调用的时候,都是在安全的上下文中被调用的
        // 因为这些函数调用之前,已经加锁了,调用完,lockGuard自动解锁
        pthread_mutex_t *getMutex();
        void waitCond(); // 等待条件变量就绪
        bool isEmpty(); // 判断任务队列是否为空
        T getTask(); // 获取一个任务
    };
    template <typename T>
    ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;
    template <typename T>
    pthread_mutex_t ThreadPool<T>::__mutexForPool = PTHREAD_MUTEX_INITIALIZER;
    //static/全局可以这样初始化,这把锁是用来保护getThreadPool的
}

实现的一些具体细节

Http报文解析

因为我们收到的都是http请求,因此,对http请求报文进行解析,可以获得客户端信息。

以下是一段完整的http报文

GET /index.html HTTP/1.1
Host: 43.xxx.xxx.xxx:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

因为我们需要提取客户需要访问的资源,因此我们需要提取客户输入的资源路径。

http报文的第一行的第二个字段,则表示客户端需要提取资源的路径。

与此同时,http报文每行的间隔是特殊字符串\r\n

因此在HttpServer.cc中的HandlerHttpRequest函数中,我们首先把http报文中的每一行分出来,并把第一行的第二个字符串提取出来,就能得到资源的路径。
在这里插入图片描述

单例模式的线程池

在本次实验中,我们希望一个进程只能产生一个线程池,因此我们把线程池设置成单例模式。这是一个懒汉方式的单例模式。

**懒汉:**第一次需要这个对象的时候构建该对象。

**饿汉:**在main()执行之前构建该对象。

  • 把构造函数私有化

  • 通过static ThreadPool<T>* getThreadPool(int num = g_thread_num)来获取线程池

    在这个接口中,如果执行流是第一次执行该函数,则该函数会构造一个线程池对象并返回它的指针。如果执行流不是第一次执行该函数,则该函数则会返回this,即自己。

另外,为了保证线程池运行时的线程安全,在线程池中多个操作中添加了互斥锁对操作进行保护。

实验结果分析和思考

改进部分:

  • 实现多执行流,支持多个客户端进行连接。
  • 封装线程池,使得服务器可以并行地对http请求进行响应

不足部分:

  • 给客户端返回对报文是静态网页,暂时没有实现可以支持多个客户端之间共同交互的功能
  • 回应报文比较粗糙,可以进一步美化。
  • 可以进一步优化服务器,把服务器进程设置成守护进程,让它长期执行并提供服务。
  • 没有模拟丢包的情况

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

火山引擎数智平台VeDI助力某办公软件企业营销线索转化提升14%

一条营销线索&#xff0c;从官网后台下载到完成成交&#xff0c;到底需要经历哪些环节&#xff1f; 在企业级市场的销售场景中&#xff0c;营销线索通常是指用户通过相关产品的官方网站或者营销活动界面&#xff0c;主动留下的联系方式&#xff1b;而根据线索价值的不同&#…

2023京东618全民拆快递互动活动玩法规则!

2023京东618全民拆快递&#xff0c;瓜分20亿活动规则&#xff01; 618无门槛红包29号开领&#xff01; ​手机京东搜索&#xff1a;好运红包210&#xff0c;领最高20618&#xff0c;每天可领三次&#xff01; ​手机京东搜索&#xff1a;能省就省50&#xff0c;领最高23888…

深度学习进阶篇-预训练模型[1]:预训练分词Subword、ELMo、Transformer模型原理;结构;技巧以及应用详解

【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化算法、卷积模型、序列模型、预训练模型、对抗神经网络等 专栏详细介绍&#xff1a;【深度学习入门到进阶】必看系列&#xff0c;含激活函数、优化策略、损失函数、模型调优、归一化…

算法设计与分析期末总结

前言&#xff1a;基本是为了我自己看的一些我容易忘记的东西&#xff0c;为考试作准备把&#xff0c;主要使后半部分的知识&#xff0c;前半部分请看算法设计与分析阶段考总结 第五章 回溯算法是一种系统地搜索问题的解的方法。某个问题的所有可能解的称为问题的解空间&#xf…

百度工程师移动开发避坑指南——Swift语言篇

作者 | 启明星小组 上一篇我们介绍了移动开发常见的内存泄漏问题&#xff0c;见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。 对于Swift开发者&#xff0c;Swift较于OC一个很大的不同就是引入了可选类型&#xff08;Optional&#…

Install Redis Cluster(1master-2slave) on Kubernetes

目录 Node & Software & Docker Images Lists Prerequisites Architecture Setting up your Redis cluster Creating Namespace Creating StorageClass Creating Persistent volumes Creating ConfigMap Creating StatefulSet Creating Headless Service …

中创|警惕AI骗局,10分钟被骗430万,AI诈骗正在全国爆发!

眼见为实&#xff1f;耳听为真&#xff1f;当心AI诈骗&#xff01; 只需要提供一张带脸的照片&#xff0c;就可以把自己置换成视频、电视剧中的男&#xff08;女&#xff09;主角&#xff0c;拟真度非常高&#xff0c;毫无违和感&#xff0c;这是最近爆火的AI换脸。 然而随着人…

浏览器数据存储方式

浏览器数据存储方式 常用的前端数据存储方法笼统来说有 3 种&#xff1a; local/session storagecookiesindexeddb 3 种方法各有各的优点和使用范围。 local/session storage local/session storage 保存的格式都为键值对&#xff0c;并且用法都是差不多&#xff0c;如下&…

如何选择高品质SPD浪涌保护器

了解了SPD的原理和技术参数和选型方法&#xff0c;但是面对市场上形形色色的SPD品牌&#xff0c;相差无几的参数&#xff0c;该如何去筛选高品质的SPD呢&#xff1f; 作为一个SPD开发人员&#xff0c;谈一下我的看法。前面提到&#xff0c;选择SPD时&#xff0c;有几个重要的参…

探索 Python Web 后端技术的发展之路

导语 Python 在 Web 后端开发领域中有着广泛的应用&#xff0c;它简洁的语法和强大的功能使得开发者们青睐有加。本文将更深入地探讨 Python Web 后端技术的发展趋势和路线&#xff0c;以及相关技术如何影响了 Web 开发的未来。 一、Python Web 框架的演变 Flask&#xff1a…

软件设计师 软件工程

** 判定覆盖 设置判定用例来保障真和假的结果都可以取到** 满足条件覆盖问题问需要多少个测试 ** 其实有技巧的&#xff08;就看最后面的 分支&#xff09;** **沟通路径&#xff1a;&#xff08;n-1&#xff09;n再/2 和主程序沟通那就是n-1条 ** ******************* 做题技…

HTTPS行为大赏(三分钟了解加密过程)

文章目录 前言1.没有加密的时候2.对称密钥加密传输3.非对称加密4.引入数字证书&#xff08;对称加密非对称加密&#xff09; 前言 既然要对HTTPS进行解读&#xff0c;我们首先了解&#xff0c;HTTPS是什么&#xff1f;HTTPS就相当于HTTPSSL/TLS这样的组合&#xff0c;HTTP&…

软考 软件设计师计算机网络笔记

网络设备 物理层的互联设备有中继器和集线器&#xff0c;集线器是一种特殊的多路多端口中继器 数据链路层的互连设备有网桥&#xff0c;交换机&#xff0c;交换机是一个多端口的网桥 网络层互连设备有路由器 协议簇 所有带T的除了TFTP其他都是TCP&#xff0c;所有不带T的除…

BFT 最前线 | ChatGPT登顶App Store;国产中文大语言模型「天河天元」发布;华为招募天才少年;阿里分拆上市

原创 | 文 BFT机器人 AI视界 TECHNOLOGY NEWS 01 ChatGPT上架App Store登顶榜首 OpenAI&#xff1a;很快也将出现在安卓上 近日&#xff0c;ChatGPT正式发布App版本&#xff0c;上架APP Store&#xff0c;支持iPhone和iPad设备。OpenAI表示&#xff0c;ChatGPT iOS APP可免费…

两阶段鲁棒优化及列与约束生成算法(CCG)的基本原理(超详细讲解,附matlab代码)

本文的主要参考文献&#xff1a; Zeng B , Zhao L . Solving Two-stage Robust Optimization Problems by A Constraint-and-Column Generation Method[J]. Operations Research Letters, 2013, 41(5):457-461. 1.两阶段鲁棒优化问题的引入 鲁棒优化是应对数据不确定性的一种优…

探索【Stable-Diffusion WEBUI】的图片超分辨插件:StableSR

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;图片放大&#xff08;二&#xff09;图片超分辨率放大脚本插件&#xff08;StableSR&#xff09;&#xff08;2.1&#xff09;下载组件&#xff08;2.2&#xff09;使用&#xff08;2.3&#xff09;实例对比…

bat脚本语法与实战

一、什么是bat脚本 bat脚本就是将一系列DOS命令按照一定顺序排列而形成的集合&#xff0c;运行在windows命令行环境上。通过本文的学习&#xff0c;基本可以实现一些简单的脚本。 二、为什么学习bat脚本&#xff1f; 使用bat可以提高办公效率&#xff0c;可以直接使用Notepad编…

JavaEE(系列12) -- 常见锁策略

目录 1. 乐观锁和悲观锁 2. 轻量级锁与重量级锁 3. 自旋锁和挂起等待锁 4. 互斥锁和读写锁 5. 可重入锁与不可重入锁 6. 死锁 6.1 死锁的必要条件 6.2 如何避免死锁 7. 公平锁和非公平锁 8. Synchronized原理及加锁过程 8.1 Synchronized 小结 8.2 加锁工作过程 8.2.1 偏向锁…

MySQL保证主备一致,如何解决循环复制?

备库只读&#xff0c;是如何和主库同步数据的&#xff1f; 你可能会问&#xff0c;我把备库设置成只读了&#xff0c;还怎么跟主库保持同步更新呢&#xff1f; 这个问题&#xff0c;你不用担心。因为 readonly 设置对超级 (super) 权限用户是无效的&#xff0c;而用于同步更新…

用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。

Vue3 的父子组件传值、绑定表单数据、UI库的二次封装、防抖等&#xff0c;想来大家都很熟悉了&#xff0c;本篇介绍一种使用 Typescript 的方式进行统一的封装的方法。 基础使用方法 Vue3对于表单的绑定提供了一种简单的方式&#xff1a;v-model。对于使用者来说非常方便&…