【Linux】39.一个基础的HTTP Web服务器

文章目录

  • [1. 实现一个基础的HTTP Web服务器](#1. 实现一个基础的HTTP Web服务器)
    • [1.1 功能实现:](#1.1 功能实现:)
    • [1.2 Log.hpp-日志记录器](#1.2 Log.hpp-日志记录器)
    • [1.3 HttpServer.hpp-网页服务器](#1.3 HttpServer.hpp-网页服务器)
    • [1.4 Socket.hpp-网络通信器](#1.4 Socket.hpp-网络通信器)
    • [1.5 HttpServer.cc-服务器启动器](#1.5 HttpServer.cc-服务器启动器)

1. 实现一个基础的HTTP Web服务器

1.1 功能实现:

  1. 总体功能

    • 提供Web服务,响应客户端(浏览器)的HTTP请求

    • 支持静态文件服务(如HTML、图片等)

    • 多线程处理并发请求

    • 带日志记录功能

  1. 具体工作流程
cpp 复制代码
浏览器 → 发送HTTP请求 → 服务器
                        ↓
                     解析请求
                        ↓
                     查找文件
                        ↓
                     返回响应
                        ↓
浏览器 ← 显示页面 ← 服务器
  1. 各模块职责:

日志记录器(Log.hpp)

  • 记录服务器运行状态
  • 错误追踪和调试

网页服务器(HttpServer.hpp)

  • 解析HTTP请求
  • 处理静态文件
  • 生成HTTP响应
  • 多线程处理请求

网络通信器(Socket.hpp)

  • 处理底层网络通信
  • 管理TCP连接

服务器启动器(HttpServer.cc)

  • 程序入口
  • 初始化和启动服务

1.2 Log.hpp-日志记录器

Log.hpp

cpp 复制代码
#pragma once  // 防止头文件重复包含

// 系统头文件包含
#include <iostream>     // 标准输入输出
#include <time.h>       // 时间相关函数
#include <stdarg.h>     // 可变参数处理
#include <sys/types.h>  // 基本系统数据类型
#include <sys/stat.h>   // 文件状态
#include <fcntl.h>      // 文件控制选项
#include <unistd.h>     // UNIX标准函数
#include <stdlib.h>     // 标准库函数

// 基础配置宏定义
#define SIZE 1024      // 缓冲区大小
#define LogFile "log.txt"  // 默认日志文件名

// 日志级别定义(按严重程度递增)
#define Info 0      // 普通信息:记录系统正常操作信息
#define Debug 1     // 调试信息:记录调试相关信息
#define Warning 2   // 警告信息:记录潜在问题
#define Error 3     // 错误信息:记录错误但不影响系统运行
#define Fatal 4     // 致命错误:记录导致系统崩溃的错误

// 日志输出方式定义
#define Screen 1     // 输出到屏幕:直接显示在终端
#define Onefile 2    // 输出到单个文件:所有日志记录到同一个文件
#define Classfile 3  // 分类输出:根据日志级别输出到不同文件

class Log {
private:
    int printMethod;      // 日志输出方式
    std::string path;     // 日志文件存储路径

public:
    // 构造函数:初始化日志系统
    Log() {
        printMethod = Screen;  // 默认输出到屏幕
        path = "./log/";       // 默认日志目录
    }

    // 设置日志输出方式
    void Enable(int method) {
        printMethod = method;
    }

    // 将日志级别转换为对应的字符串
    std::string levelToString(int level) {
        switch (level) {
            case Info:    return "Info";
            case Debug:   return "Debug";
            case Warning: return "Warning";
            case Error:   return "Error";
            case Fatal:   return "Fatal";
            default:      return "None";
        }
    }

    // 根据设置的输出方式打印日志
    void printLog(int level, const std::string &logtxt) {
        switch (printMethod) {
            case Screen:    // 输出到屏幕
                std::cout << logtxt << std::endl;
                break;
            case Onefile:   // 输出到单个文件
                printOneFile(LogFile, logtxt);
                break;
            case Classfile: // 根据日志级别输出到不同文件
                printClassFile(level, logtxt);
                break;
        }
    }

    // 将日志输出到指定文件
    void printOneFile(const std::string &logname, const std::string &logtxt) {
        std::string _logname = path + logname;
        // 以追加方式打开文件,如果不存在则创建
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd < 0) return;  // 打开失败则直接返回
        
        write(fd, logtxt.c_str(), logtxt.size());  // 写入日志内容
        close(fd);  // 关闭文件描述符
    }

    // 根据日志级别将日志输出到不同文件
    void printClassFile(int level, const std::string &logtxt) {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);  // 构造文件名,如"log.txt.Debug"
        printOneFile(filename, logtxt);
    }

    // 重载函数调用运算符,实现日志记录的核心功能
    void operator()(int level, const char *format, ...) {
        // 1. 获取当前时间
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        
        // 2. 格式化日志头部(时间和级别信息)
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), 
                "[%s][%d-%d-%d %d:%d:%d]", 
                levelToString(level).c_str(),
                ctime->tm_year + 1900, 
                ctime->tm_mon + 1, 
                ctime->tm_mday,
                ctime->tm_hour, 
                ctime->tm_min, 
                ctime->tm_sec);

        // 3. 处理可变参数,格式化日志内容
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 4. 组合完整的日志消息
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // 5. 输出日志
        printLog(level, logtxt);
    }
};

// 创建全局日志对象,方便在程序各处使用
Log lg;

/* 示例用法:
int main() {
    lg.Enable(Screen);  // 设置输出到屏幕
    lg(Info, "Server started on port %d", 8080);
    lg(Error, "Failed to connect to %s", "database");
    return 0;
}
*/

1.3 HttpServer.hpp-网页服务器

HttpServer.hpp

功能:

  • HTTP请求处理
  • 多线程服务
  • 静态文件响应
  • Cookie支持
  • 错误页面处理
cpp 复制代码
#pragma once  // 防止头文件重复包含

// 基础库和系统库引入
#include <iostream>     // 标准输入输出
#include <string>       // 字符串处理
#include <pthread.h>    // POSIX线程库
#include <fstream>      // 文件流操作
#include <vector>       // 动态数组
#include <sstream>      // 字符串流
#include <sys/types.h>  // 基本系统数据类型
#include <sys/socket.h> // Socket通信
#include <unordered_map> // 哈希表

// 自定义头文件
#include "Socket.hpp"   // Socket封装类
#include "Log.hpp"      // 日志系统

// 全局常量定义
const std::string wwwroot="./wwwroot"; // web服务器根目录
const std::string sep = "\r\n";        // HTTP消息分隔符
const std::string homepage = "index.html"; // 默认主页

static const int defaultport = 8082;    // 默认端口号

class HttpServer;  // 前向声明

// 线程数据结构:存储每个线程处理的连接信息
class ThreadData
{
public:
    ThreadData(int fd, HttpServer *s) : sockfd(fd), svr(s) {}

public:
    int sockfd;         // 客户端连接的socket描述符
    HttpServer *svr;    // HTTP服务器对象指针
};

// HTTP请求解析类
class HttpRequest
{
public:
    // 反序列化HTTP请求
    void Deserialize(std::string req) 
    {
        while(true)
        {
            std::size_t pos = req.find(sep);
            if(pos == std::string::npos) break;
            std::string temp = req.substr(0, pos);
            if(temp.empty()) break;
            req_header.push_back(temp);    // 保存请求头
            req.erase(0, pos+sep.size());  // 移除已处理部分
        }
        text = req;  // 保存请求体
    }

    // 解析HTTP请求,处理URL和文件路径
    void Parse()
    {
        // 解析请求行(方法、URL、HTTP版本)
        std::stringstream ss(req_header[0]);
        ss >> method >> url >> http_version;
        
        // 构建文件路径
        file_path = wwwroot;  
        if(url == "/" || url == "/index.html") {
            file_path += "/";
            file_path += homepage;  // 处理默认主页
        }
        else file_path += url;     // 其他页面

        // 获取文件后缀
        auto pos = file_path.rfind(".");
        if(pos == std::string::npos) suffix = ".html";
        else suffix = file_path.substr(pos);
    }

    // 调试打印函数
    void DebugPrint()
    {
        // 输出请求信息用于调试
        for(auto &line : req_header)
        {
            std::cout << "--------------------------------" << std::endl;
            std::cout << line << "\n\n";
        }

        std::cout << "method: " << method << std::endl;
        std::cout << "url: " << url << std::endl;
        std::cout << "http_version: " << http_version << std::endl;
        std::cout << "file_path: " << file_path << std::endl;
        std::cout << text << std::endl;
    }

public:
    std::vector<std::string> req_header;  // 请求头部
    std::string text;                     // 请求正文

    // 解析后的请求信息
    std::string method;       // 请求方法(GET、POST等)
    std::string url;         // 请求URL
    std::string http_version; // HTTP协议版本
    std::string file_path;   // 请求文件路径
    std::string suffix;      // 文件后缀
};

// HTTP服务器类
class HttpServer
{
public:
    // 构造函数:初始化端口和支持的内容类型
    HttpServer(uint16_t port = defaultport) : port_(port)
    {
        content_type.insert({".html", "text/html"});
        content_type.insert({".png", "image/png"});
    }

    // 启动服务器
    bool Start()
    {
        // 初始化Socket
        // 1. 创建Socket
        listensock_.Socket();
        /* 这一步完成以下操作:
           a) 调用系统函数 socket(AF_INET, SOCK_STREAM, 0) 创建TCP Socket
              - AF_INET: 使用IPv4协议族
              - SOCK_STREAM: 使用TCP协议
              - 0: 使用默认协议

           b) 设置Socket选项
              - SO_REUSEADDR: 允许地址重用,避免服务器重启时的"地址已被使用"错误
        */

        // 2. 绑定端口
        listensock_.Bind(port_);
        /* 这一步完成以下操作:
           a) 创建sockaddr_in结构体,设置:
              - sin_family = AF_INET (IPv4)
              - sin_port = htons(port_) (设置端口号,转换为网络字节序)
              - sin_addr.s_addr = INADDR_ANY (监听所有网卡接口)

           b) 调用bind()函数将Socket与地址绑定
              - 如果端口已被占用或权限不足,会失败
        */

        // 3. 开始监听
        listensock_.Listen();
        /* 这一步完成以下操作:
           a) 调用listen()函数,将Socket转换为监听状态
              - backlog参数设置为10,表示等待连接队列的最大长度
              - 超过此长度的新连接请求会被拒绝

           b) 此后Socket就能接受客户端连接请求
              - 服务器调用Accept()接受新的连接
        */

        // 主循环:接受并处理连接
        for (;;)
        {
            // 准备变量存储客户端信息
            std::string clientip;    // 将存储客户端的IP地址
            uint16_t clientport;     // 将存储客户端的端口号

            // 接受新的客户端连接
            int sockfd = listensock_.Accept(&clientip, &clientport);
            /* Accept函数做了这些事:
               1. 等待客户端连接
               2. 获取客户端的IP和端口
               3. 返回新的socket描述符用于与该客户端通信
            */

            // 连接失败则继续等待下一个连接
            if (sockfd < 0) continue;

            // 记录新连接日志
            lg(Info, "get a new connect, sockfd: %d", sockfd);

            // 创建新线程处理请求
            // 1. 声明线程ID变量
            pthread_t tid;    // 用于存储新创建线程的ID

            // 2. 创建线程数据结构,传入连接描述符和当前服务器对象
            ThreadData *td = new ThreadData(sockfd, this);
            /* ThreadData包含:
               - sockfd:与客户端通信的socket描述符
               - this:当前服务器对象的指针,用于访问服务器的方法
            */

            // 3. 创建新线程处理请求
            pthread_create(&tid, nullptr, ThreadRun, td);
            /* 参数含义:
               - &tid:存储新线程ID
               - nullptr:使用默认线程属性
               - ThreadRun:线程将执行的函数
               - td:传递给线程函数的参数
            */

            // 新线程会执行ThreadRun函数处理客户端请求
            // 主线程继续循环等待新的连接
        }
    }

    // 读取HTML文件内容
    static std::string ReadHtmlContent(const std::string &htmlpath)
    {
        // 1. 打开文件
        std::ifstream in(htmlpath, std::ios::binary);
        /* 说明:
           - binary模式打开确保文件按原样读取
           - 不会对换行符进行转换
        */

        // 文件打开失败则返回空字符串
        if(!in.is_open()) return "";

        // 2. 获取文件大小
        in.seekg(0, std::ios_base::end);   // 将读指针移到文件末尾
        auto len = in.tellg();             // 获取当前位置(即文件大小)
        in.seekg(0, std::ios_base::beg);   // 将读指针移回文件开头

        // 3. 读取文件内容
        std::string content;           // 用于存储文件内容
        content.resize(len);           // 预分配空间
        // 一次性读取整个文件内容到字符串中
        in.read((char*)content.c_str(), content.size());

        // 4. 关闭文件
        in.close();

        return content;  // 返回文件内容
    }

    // 根据文件后缀获取Content-Type
    std::string SuffixToDesc(const std::string &suffix)
    {
        // 在content_type映射表中查找文件后缀对应的MIME类型
        auto iter = content_type.find(suffix);

        // 如果找不到对应的MIME类型
        if(iter == content_type.end()) 
            return content_type[".html"];  // 默认返回html的MIME类型:"text/html"
        else 
            return content_type[suffix];   // 返回找到的MIME类型

        /* 例如:
           - .html -> "text/html"
           - .png  -> "image/png"
           这个MIME类型会被用在HTTP响应头的Content-Type字段中
        */
    }

    // 处理HTTP请求
    void HandlerHttp(int sockfd)
    {
        // 1. 接收HTTP请求
        char buffer[10240];
        ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
        /* 参数解释:
           1. sockfd: 套接字描述符,用于标识与客户端的连接
           2. buffer: 接收数据的缓冲区
           3. sizeof(buffer) - 1: 最大接收长度,预留1个字节给'\0'
           4. 0: 标志位,使用默认行为

           返回值n:
           - 大于0:实际接收的字节数
           - 等于0:连接已关闭
           - 小于0:接收错误
        */
        if (n > 0)
        {
            buffer[n] = 0;  // 字符串结束符

            // 2. 解析HTTP请求
            HttpRequest req;
            req.Deserialize(buffer);   // 反序列化请求内容
            req.Parse();               // 解析请求(获取方法、URL、版本等)

            // 3. 读取请求的文件内容
            std::string text;
            bool ok = true;
            text = ReadHtmlContent(req.file_path);  // 读取请求的文件
            if(text.empty())  // 文件不存在或读取失败
            {
                ok = false;
                // 返回错误页面
                std::string err_html = wwwroot + "/err.html";
                text = ReadHtmlContent(err_html);
            }

            // 4. 构建HTTP响应
            // 4.1 响应行
            std::string response_line;
            if(ok)
                response_line = "HTTP/1.0 200 OK\r\n";
            else
                response_line = "HTTP/1.0 404 Not Found\r\n";

            // 4.2 响应头
            std::string response_header = "Content-Length: ";
            response_header += std::to_string(text.size());
            response_header += "\r\n";
            response_header += "Content-Type: ";
            response_header += SuffixToDesc(req.suffix);  // 设置正确的MIME类型
            response_header += "\r\n";
            response_header += "Set-Cookie: name=haha&&passwd=12345";  // 设置Cookie
            response_header += "\r\n";

            // 4.3 空行
            std::string blank_line = "\r\n";

            // 4.4 组装完整响应(响应行+响应头+空行+响应体)
            std::string response = response_line + response_header + blank_line + text;

            // 5. 发送响应给客户端
            send(sockfd, response.c_str(), response.size(), 0);
        }

        // 6. 关闭连接
        close(sockfd);
    }

    // 线程运行函数
    static void *ThreadRun(void *args)
    {
        pthread_detach(pthread_self());  // 设置线程分离
        ThreadData *td = static_cast<ThreadData *>(args);
        td->svr->HandlerHttp(td->sockfd);
        delete td;
        return nullptr;
    }

    ~HttpServer() {}

private:
    Sock listensock_;    // 监听socket
    uint16_t port_;      // 服务器端口
    std::unordered_map<std::string, std::string> content_type;  // 支持的内容类型映射
};

1.4 Socket.hpp-网络通信器

Socket.hpp

功能:

  • TCP连接封装
  • 地址绑定
  • 端口监听
  • 客户端连接处理
  • 错误处理
cpp 复制代码
#pragma once  // 防止头文件重复包含

// 系统相关头文件
#include <iostream>     // 标准输入输出
#include <string>       // 字符串处理
#include <unistd.h>    // UNIX标准函数定义
#include <cstring>     // C字符串处理
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h>  // 文件状态
#include <sys/socket.h> // Socket接口
#include <arpa/inet.h> // IP地址转换函数
#include <netinet/in.h> // IP协议家族
#include "Log.hpp"     // 日志系统

// 错误枚举定义
enum
{
    SocketErr = 2,  // Socket创建错误
    BindErr,        // 绑定错误
    ListenErr,      // 监听错误
};

// 监听队列长度
const int backlog = 10;

// Socket封装类
class Sock
{
public:
    Sock() {}
    ~Sock() {}

public:
    // 创建Socket
    void Socket()
    {
        // 创建TCP Socket
        sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd_ < 0)
        {
            // 创建失败,记录错误日志并退出
            lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
            exit(SocketErr);
        }
        
        // 设置Socket选项:地址重用
        int opt = 1;
        setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }

    // 绑定端口
    void Bind(uint16_t port)
    {
        // 创建并初始化地址结构
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;           // IPv4
        local.sin_port = htons(port);         // 端口号
        local.sin_addr.s_addr = INADDR_ANY;   // 监听所有网卡

        // 绑定地址和端口
        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            // 绑定失败,记录错误日志并退出
            lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
            exit(BindErr);
        }
    }

    // 开始监听
    void Listen()
    {
        // 启动监听
        if (listen(sockfd_, backlog) < 0)
        {
            // 监听失败,记录错误日志并退出
            lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
            exit(ListenErr);
        }
    }

    // 接受连接
    int Accept(std::string *clientip, uint16_t *clientport)
    {
        // 准备接收客户端地址信息
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        // 接受新连接
        int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
        if(newfd < 0)
        {
            // 接受连接失败,记录警告日志
            lg(Warning, "accept error, %s: %d", strerror(errno), errno);
            return -1;
        }

        // 获取客户端IP地址
        char ipstr[64];
        inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
        *clientip = ipstr;
        // 获取客户端端口号
        *clientport = ntohs(peer.sin_port);

        return newfd;  // 返回新连接的文件描述符
    }

    // 连接服务器(客户端使用)
    bool Connect(const std::string &ip, const uint16_t &port)
    {
        // 准备服务器地址信息
        struct sockaddr_in peer;
        memset(&peer, 0, sizeof(peer));
        peer.sin_family = AF_INET;
        peer.sin_port = htons(port);
        inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));

        // 建立连接
        int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
        if(n == -1) 
        {
            // 连接失败
            std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
            return false;
        }
        return true;  // 连接成功
    }

    // 关闭Socket
    void Close()
    {
        close(sockfd_);
    }

    // 获取Socket文件描述符
    int Fd()
    {
        return sockfd_;
    }

private:
    int sockfd_;  // Socket文件描述符
};

1.5 HttpServer.cc-服务器启动器

HttpServer.cc

功能:

  • 程序入口
  • 参数解析
  • 服务器初始化
  • 智能指针管理
cpp 复制代码
// 包含必要的头文件
#include "HttpServer.hpp"  // HTTP服务器类定义
#include <iostream>        // 标准输入输出
#include <memory>         // 智能指针
#include <pthread.h>      // POSIX线程库
#include "Log.hpp"        // 日志系统

using namespace std;

int main(int argc, char *argv[])
{
    // 检查命令行参数
    if(argc != 2)  // 要求必须提供端口号参数
    {
        exit(1);   // 参数错误,退出程序
    }
    
    // 将命令行参数转换为端口号
    uint16_t port = std::stoi(argv[1]);  // 字符串转换为整数

    // 创建HTTP服务器实例
    // 以下是三种方式,注释掉的是不推荐的方式
    // 方式1(不推荐):普通指针,需要手动管理内存
    // HttpServer *svr = new HttpServer();
    
    // 方式2(语法错误):unique_ptr的错误声明方式
    // std::unique<HttpServer> svr(new HttpServer());
    
    // 方式3(推荐):使用智能指针unique_ptr,自动管理内存
    std::unique_ptr<HttpServer> svr(new HttpServer(port));

    // 启动服务器
    svr->Start();  // 开始监听和处理请求

    // 程序正常退出
    return 0;
}
相关推荐
洁✘1 小时前
shell编程正则表达式与文本处理器
linux·运维·正则表达式
深夜面包1 小时前
Ubuntu 安装与配置 Docker
linux·ubuntu·docker
猫猫与橙子1 小时前
ubuntu22.04安装dukto
linux·运维·服务器
2302_799525741 小时前
【Linux】su、su-、sudo、sudo -i、sudo su - 命令有什么区别?分别适用什么场景?
linux·运维·服务器
正点原子2 小时前
【正点原子STM32MP257连载】第四章 ATK-DLMP257B功能测试——EEPROM、SPI FLASH测试 #AT24C64 #W25Q128
linux·stm32·单片机·嵌入式硬件·stm32mp257
野生派蒙2 小时前
Linux:安装 CentOS 7(完整教程)
linux·运维·服务器·centos
2501_915106322 小时前
iOS 设备应用管理实践分享
websocket·网络协议·tcp/ip·http·网络安全·https·udp
noravinsc3 小时前
centos部署的openstack发布windows虚拟机
linux·windows·centos
肯德基疯狂星期四-V我503 小时前
【Ubuntu】【树莓派】Linux系统的远程终端登录、远程图形桌面访问、 X图形窗口访问和文件传输操作
linux·运维·ubuntu·树莓派
努力努力再努力wz3 小时前
【Linux实践系列】:匿名管道收尾+完善shell外壳程序
linux·运维·服务器·c++