【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;
}
相关推荐
phoenix098136 分钟前
Linux入门DAY29
linux·运维
叔叔别拉了我害怕1 小时前
封装FTPSClient连接ftps服务器
服务器·git·github
入秋1 小时前
Linux服务器安装部署 Nginx、Redis、PostgreSQL、Docker
linux·前端
不甘懦弱1 小时前
阿里云搭建flask服务器
服务器·python·flask
Mr. Cao code2 小时前
使用Tomcat Clustering和Redis Session Manager实现Session共享
java·linux·运维·redis·缓存·tomcat
zcz16071278212 小时前
Linux 网络命令大全
linux·运维·网络
the sun342 小时前
Reactor设计模式及其在epoll中的应用
linux·运维·服务器·c++
VVVVWeiYee2 小时前
BGP高级特性
运维·服务器·网络
喜欢你,还有大家2 小时前
Linux笔记7——shell编程基础-1
linux·运维·笔记
运维成长记2 小时前
Top 100 Linux Interview Questions and Answers
linux·运维·服务器