【Linux】应用层协议 HTTP

应用层协议 HTTP

  • [一. HTTP 协议](#一. HTTP 协议)
    • [1. URL 地址](#1. URL 地址)
    • [2. urlencode 和 urldecode](#2. urlencode 和 urldecode)
    • [3. 请求与响应格式](#3. 请求与响应格式)
  • [二. HTTP 请求方法](#二. HTTP 请求方法)
    • [1. GET 和 POST (重点)](#1. GET 和 POST (重点))
  • [三. HTTP 状态码](#三. HTTP 状态码)
  • [四. HTTP 常见报头](#四. HTTP 常见报头)
  • [五. 手写 HTTP 服务器](#五. 手写 HTTP 服务器)

HTTP(超文本传输协议)是一种应用层协议,用于在万维网上进行超文本传输。它是现代互联网的基础协议之一,主要用于浏览器和服务器之间的通信,用于请求和响应网页内容。HTTP协议是无连接的、无状态的,基于请求-响应模型。

  • 无连接:客户端和服务器之间不需要建立长期的连接,每个请求/响应对完成后,连接即被关闭。
  • 无状态:请求/响应对都是独立的,服务器不会保存客户端请求之间的任何状态信息。

一. HTTP 协议

1. URL 地址

平时我们俗称的 "网址" 其实就是说的 URL(Uniform Resource Locator),"统一资源定位符"

例如:https://news.qq.com/rain/a/20250326A01C0V00

  • news.qq.com:域名,公网 IP 地址。
  • rain/a/20250326A01C0V00:服务器路径下的文件(html、css、js)

前置知识:

  1. 我的数据给别人,别人的数据给我,就是 IO 操作,也就是说:上网的行为就是 IO
  2. 请求的资源:图片,视频,音频,文本,本质就是文件。
  3. 先要确认我要的资源在那一台服务器上(IP 地址),在什么路径下(文件路径)
  4. URL 中的 "/" 不一定是根目录,它是 Web 根目录,二者不一样。
  5. 为什么没有端口号?在成熟的应用层协议中,默认存在固定的端口号,HTTP 的默认端口号是80

2. urlencode 和 urldecode

像 / ? : 等这样的字符,已经被 url 当做特殊意义理解了,因此这些字符不能随意出现,比如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义,转义的规则如下:

将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足 4 位直接处理),每 2 位做一位,前面加上%,编码成%XY 格式,例如:

3. 请求与响应格式

HTTP 请求:

  • 首行:[请求方法] + [url] + [版本]
  • Header:请求的属性,冒号分割的键值对。每组属性之间使用\r\n 分隔,遇到空行表示 Header 部分结束。
  • Body:空行后面的内容都是 Body,Body 允许为空字符串,如果 Body 存在,则在Header 中会有一个 Content-Length 属性来标识 Body 的长度。

HTTP 响应:

  • 首行:[版本号] + [状态码] + [状态码解释]
  • Header:请求的属性,冒号分割的键值对,每组属性之间使用\r\n 分隔,遇到空行表示 Header 部分结束。
  • Body:空行后面的内容都是 Body,Body 允许为空字符串,如果 Body 存在,则在 Header 中会有一个 Content-Length 属性来标识 Body 的长度,如果服务器返回了一个 html 页面, 那么 html 页面内容就是在 body 中。

基本的应答格式:

二. HTTP 请求方法

方法 说明 支持的 HTTP 协议版本
GET 获取资源 1.0、1.1
POST 传输实体主体 1.0、1.1
PUT 传输文件 1.0、1.1
HEAD 获取报文首部 1.0、1.1
DELETE 删除文件 1.0、1.1
OPTIONS 询问支持的方法 1.1
TRACE 追踪路径 1.1
CONNECT 要求用隧道协议连接代理 1.1
LINK 建立和资源之间的联系 1.0
UNLINK 断开链接关系 1.0

GET 和 POST 是 HTTP 协议中最常用的两种请求方法,用于客户端与服务器之间的数据交互。

1. GET 和 POST (重点)

特性 GET POST
用途 用于请求 URL 指定的资源 提交数据到服务器
数据位置 参数附加在 URL 中 参数放在请求体(Body)中
数据可见性 URL 中明文显示,不安全 数据不可见,相对安全
数据长度限制 受限于 URL 长度(通常 ≤ 2048 字节) 无限制(理论上)
常见场景 搜索、浏览页面、获取 API 数据 表单提交、上传文件、用户登录
  • GET 的参数:通过 ? 附加在 URL 后,多个参数用 & 分隔!
  • 浏览器默认使用 GET 发起请求(例如:直接输入 URL 或点击链接)
  • HTTP 协议本身是明文传输的,无论是 GET 还是 POST 方法,数据在网络中传输时都可能被抓包,需要 HTTPS 协议对数据进行加密!

三. HTTP 状态码

状态码 类别 说明
1XX Informational(信息性状态码) 接收的请求正在处理
2XX Success(成功状态码) 请求正常处理方式
3XX Redirection(重定向状态码) 需要进行附加操作以完成请求
4XX Client Error(客户端错误状态码) 服务器无法处理请求
5XX Server Error(服务器错误状态码) 服务器处理错误请求

最常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)

状态码 状态码描述 应用样例
100 Continue 上传大文件时,服务器告诉客户端可以继续上传
200 OK 访问网站首页,服务器返回网页内容
201 Created 发布新文章,服务器返回文章创建成功的信息
204 No Content 删除文章后,服务器返回"无内容"表示操作成功
301 Moved Permanently 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用
302 Found 或 See Other 用户登录成功后,重定向到用户首页
304 Not Modified 浏览器缓存机制,对未修改的资源返回 304 状态码
400 Bad Request 填写表单时,格式不正确导致提交失败
401 Unauthorized 访问需要登录的页面时,未登录或认证失败
403 Forbidden 尝试访问你没有权限查看的页面
404 Not Found 访问不存在的网页链接
500 Internal Server Error 服务器崩溃或数据库错误导致页面无法加载
502 Bad Gateway 使用代理服务器时,代理服务器无法从上游服务器获取有效响应
503 Service Unavailable 服务器维护或过载,暂时无法处理请求

以下是仅包含重定向相关状态码的表格:

状态码 状态码描述 重定向类型 应用样例
301 Moved Permanently 永久重定向 网站换域名后,自动跳转到新域名;搜索引擎更新网站链接时使用
302 Found 或 See Other 临时重定向 用户登录成功后,重定向到用户首页
307 Temporary Redirect 临时重定向 临时重定向资源到新的位置(较少使用)
308 Permanent Redirect 永久重定向 永久重定向资源到新的位置(较少使用)
  • HTTP 状态码 301(永久重定向)和 302(临时重定向)都依赖 Location 选项。以下是关于两者依赖 Location 选项的详细说明:

HTTP 状态码 301(永久重定向):

  • 当服务器返回 HTTP 301 状态码时,表示请求的资源已经被永久移动到新的位置。
  • 在这种情况下,服务器会在响应中添加一个 Location 头部,用于指定资源的新位置。这个 Location 头部包含了新的 URL 地址,浏览器会自动重定向到该地址。
  • 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:
bash 复制代码
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n

HTTP 状态码 302(临时重定向):

  • 当服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
  • 同样地,服务器也会在响应中添加一个 Location 头部来指定资源的新位置。浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。
  • 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:
bash 复制代码
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

总结:无论是 HTTP 301 还是 HTTP 302 重定向,都需要依赖 Location 选项来指定资源的新位置。这个 Location 选项是一个标准的 HTTP 响应头部,用于告诉浏览器应该将请求重定向到哪个新的 URL 地址。

  • 爬虫原理:模拟浏览器向目标网站发送 HTTP/HTTPS 请求,获取服务器返回的 HTML/XML 页面内容,从当前页面提取所有 URL,加入待爬队列(避免重复抓取,通过 URL 去重),将提取的数据存入数据库/文件/内存中。
  • 搜索引擎:核心功能是从互联网上获取信息并为用户提供精准的搜索结果,而这一过程的基础正是爬虫能力

四. HTTP 常见报头

  • Content-Type:数据类型(例如:text/html)
  • Content-Length:正文的长度。
  • Host:客户端告知服务器,所请求的资源是在哪个主机的哪个端口上。
  • User-Agent:声明用户的操作系统和浏览器版本信息。
  • Referer:当前页面是从哪个页面跳转过来的。
  • Location:搭配 3XX 状态码使用,告诉客户端接下来要去哪里访问。
  • Set-Cookie:用于在客户端存储少量信息。通常用于实现会话(session)的功能。

五. 手写 HTTP 服务器

  1. Makefile
bash 复制代码
httpserver:HttpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -rf httpserver
  1. Mutex.hpp
cpp 复制代码
#pragma once

#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
        Mutex(const Mutex &m) = delete;
        const Mutex &operator=(const Mutex &m) = delete;

    public:
        Mutex()
        {
            ::pthread_mutex_init(&_mutex, nullptr);
        }

        ~Mutex()
        {
            ::pthread_mutex_destroy(&_mutex);
        }

        void Lock()
        {
            ::pthread_mutex_lock(&_mutex);
        }

        void Unlock()
        {
            ::pthread_mutex_unlock(&_mutex);
        }

        pthread_mutex_t *LockAddr() { return &_mutex; }

    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
            : _mutex(mutex)
        {
            _mutex.Lock();
        }

        ~LockGuard()
        {
            _mutex.Unlock();
        }

    private:
        Mutex &_mutex; // 使用引用: 互斥锁不支持拷贝
    };
}
  1. Socket.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdlib>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

const int gdefaultsockfd = -1;
const int gbacklog = 8;

namespace SocketModule
{
    class Socket;
    using SockPtr = std::shared_ptr<Socket>;

    // 模版方法模式
    // 基类: 规定创建Socket方法
    class Socket
    {
    public:
        virtual ~Socket() = default;
        virtual void SocketOrDie() = 0;
        virtual void SetSocketOpt() = 0;
        virtual bool BindOrDie(int port) = 0;
        virtual bool ListenOrDie() = 0;
        virtual SockPtr AcceptOrDie(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string *out) = 0;
        virtual int Send(const std::string &in) = 0;
        virtual int Fd() = 0;

        // 提供创建TCP套接字的固定格式
        void BuildTcpSocketMethod(int port)
        {
            SocketOrDie();
            SetSocketOpt();
            BindOrDie(port);
            ListenOrDie();
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int sockfd = gdefaultsockfd)
            : _sockfd(sockfd)
        {}

        virtual ~TcpSocket() {}

        virtual void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::DEBUG) << "socket error";
                exit(SOCKET_ERR);
            }
            LOG(LogLevel::DEBUG) << "socket success, sockfd: " << _sockfd;
        }

        virtual void SetSocketOpt() override
        {
            // 保证服务器在异常断开之后可以立即重启, 不会存在bind error问题!
            int opt = 1;
            ::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        }

        virtual bool BindOrDie(int port) override
        {
            if (_sockfd == gdefaultsockfd)
                return false;
            InetAddr addr(port);
            int n = ::bind(_sockfd, addr.NetAddr(), addr.NetAddrLen());
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::DEBUG) << "bind success, sockfd: " << _sockfd;
            return true;
        }

        virtual bool ListenOrDie() override
        {
            if (_sockfd == gdefaultsockfd)
                return false;
            int n = ::listen(_sockfd, gbacklog);
            if (n < 0)
            {
                LOG(LogLevel::DEBUG) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::DEBUG) << "listen success, sockfd: " << _sockfd;
            return true;
        }

        // 返回: 文件描述符 && 客户端信息
        virtual SockPtr AcceptOrDie(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int newsockfd = ::accept(_sockfd, CONV(&peer), &len);
            if (newsockfd < 0)
            {
                LOG(LogLevel::DEBUG) << "accept error";
                return nullptr;
            }
            client->SetAddr(peer);
            return std::make_shared<TcpSocket>(newsockfd);
        }

        virtual void Close() override
        {
            if (_sockfd == gdefaultsockfd)
                return;
            ::close(_sockfd);
        }

        virtual int Recv(std::string *out) override
        {
            char buffer[1024 * 8];
            int n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if(n > 0)
            {
                buffer[n] = 0;
                *out = buffer;
            }
            return n;
        }

        virtual int Send(const std::string &in) override
        {
            int n = ::send(_sockfd, in.c_str(), in.size(), 0);
            return n;
        }

        virtual int Fd() override
        {
            return _sockfd;
        }

    private:
        int _sockfd;
    };
}
  1. Log.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    // 获取系统时间
    std::string CurrentTime()
    {
        time_t time_stamp = ::time(nullptr); // 获取时间戳
        struct tm curr;
        localtime_r(&time_stamp, &curr); // 将时间戳转化为可读性强的信息

        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 curr.tm_year + 1900,
                 curr.tm_mon + 1,
                 curr.tm_mday,
                 curr.tm_hour,
                 curr.tm_min,
                 curr.tm_sec);

        return buffer;
    }

    // 日志文件: 默认路径和默认文件名
    const std::string defaultlogpath = "./log/";
    const std::string defaultlogname = "log.txt";

    // 日志等级
    enum class LogLevel
    {
        DEBUG = 1,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    std::string Level2String(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "NONE";
        }
    }

    // 3. 策略模式: 刷新策略
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        // 纯虚函数: 无法实例化对象, 派生类可以重载该函数, 实现不同的刷新方式
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 3.1 控制台策略
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy() {}
        ~ConsoleLogStrategy() {}

        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cout << message << std::endl;
        }

    private:
        Mutex _mutex;
    };

    // 3.2 文件级(磁盘)策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname)
            : _logpath(logpath), _logname(logname)
        {
            // 判断_logpath目录是否存在
            if (std::filesystem::exists(_logpath))
            {
                return;
            }
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << std::endl;
            }
        }
        ~FileLogStrategy() {}

        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::string log = _logpath + _logname;
            std::ofstream out(log, std::ios::app); // 以追加的方式打开文件
            if (!out.is_open())
            {
                return;
            }
            out << message << "\n"; // 将信息刷新到out流中
            out.close();
        }

    private:
        std::string _logpath;
        std::string _logname;
        Mutex _mutex;
    };

    // 4. 日志类: 构建日志字符串, 根据策略进行刷新
    class Logger
    {
    public:
        Logger()
        {
            // 默认往控制台上刷新
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }
        ~Logger() {}

        void EnableConsoleLog()
        {
            _strategy = std::make_shared<ConsoleLogStrategy>();
        }

        void EnableFileLog()
        {
            _strategy = std::make_shared<FileLogStrategy>();
        }

        // 内部类: 记录完整的日志信息
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)
                : _currtime(CurrentTime()), _level(level), _pid(::getpid())
                , _filename(filename), _line(line), _logger(logger)
            {
                std::stringstream ssbuffer;
                ssbuffer << "[" << _currtime << "] "
                         << "[" << Level2String(_level) << "] "
                         << "[" << _pid << "] "
                         << "[" << _filename << "] "
                         << "[" << _line << "] - ";

                _loginfo = ssbuffer.str();
            }
            ~LogMessage()
            {
                if(_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

            template <class T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ssbuffer;
                ssbuffer << info;
                _loginfo += ssbuffer.str();
                return *this;
            }

        private:
            std::string _currtime;  // 当前日志时间
            LogLevel _level;       // 日志水平
            pid_t _pid;            // 进程pid
            std::string _filename; // 文件名
            uint32_t _line;        // 日志行号
            Logger &_logger;       // 负责根据不同的策略进行刷新
            std::string _loginfo;  // 日志信息
        };

        // 故意拷贝, 形成LogMessage临时对象, 后续在被<<时,会被持续引用,
        // 直到完成输入,才会自动析构临时LogMessage, 至此完成了日志的刷新,
        // 同时形成的临时对象内包含独立日志数据, 未来采用宏替换, 获取文件名和代码行数
        LogMessage operator()(LogLevel level, const std::string &filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

    private:
        // 纯虚类不能实例化对象, 但是可以定义指针
        std::shared_ptr<LogStrategy> _strategy; // 日志刷新策略方案
    };

    // 定义全局logger对象
    Logger logger;

// 编译时进行宏替换: 方便随时获取行号和文件名
#define LOG(level) logger(level, __FILE__, __LINE__)

// 提供选择使用何种日志策略的方法
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()
}
  1. Common.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>

#define Die(code)   \
    do              \
    {               \
        exit(code); \
    } while (0)

#define CONV(v) (struct sockaddr *)(v)

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR
};

bool ParseOneLine(std::string &str, std::string *out, const std::string &sep)
{
    auto pos = str.find(sep);
    if (pos == std::string::npos)
        return false;
    *out = str.substr(0, pos);
    str.erase(0, pos + sep.size());
    return true;
}

// Connection: keep-alive
// 解析后: key = Connection; value = keep-alive
bool SplitString(const std::string &header, const std::string sep, std::string *key, std::string *value)
{
    auto pos = header.find(sep);
    if (pos == std::string::npos)
        return false;
    *key = header.substr(0, pos);
    *value = header.substr(pos + sep.size());
    return true;
}
  1. Deamon.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define ROOT "/"
#define devnull "/dev/null"

void Deamon(bool ischdir, bool isclose)
{
    // 1. 守护进程一般要屏蔽一些特定的信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

    // 2. 成为非组长进程: 创建子进程
    if (fork())
        exit(0);

    // 3. 建立新会话
    setsid();

    // 4. 每一个进程都有自己的CWD, 是否将其修改为根目录
    if (ischdir)
        chdir(ROOT);

    // 5. 脱离终端: 将标准输入、输出重定向到字符文件"/dev/null"中
    if (isclose)
    {
        ::close(0);
        ::close(1);
        ::close(2);
    }
    else
    {
        // 建议这样!
        int fd = ::open(devnull, O_WRONLY);
        if (fd > 0)
        {
            ::dup2(fd, 0);
            ::dup2(fd, 1);
            ::dup2(fd, 2);
            ::close(fd);
        }
    }
}
  1. InetAddr.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Common.hpp"

class InetAddr
{
private:
    // 端口号: 网络序列->主机序列
    void PortNetToHost()
    {
        _port = ::ntohs(_net_addr.sin_port);
    }

    // IP: 网络序列->主机序列
    void IpNetToHost()
    {
        char ipbuffer[64];
        ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }

public:
    InetAddr() {}

    InetAddr(const struct sockaddr_in &addr)
        : _net_addr(addr)
    {
        PortNetToHost();
        IpNetToHost();
    }

    InetAddr(uint16_t port)
        : _port(port), _ip("")
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = ::htons(_port);
        _net_addr.sin_addr.s_addr = INADDR_ANY;
    }

    ~InetAddr() {}

    bool operator==(const InetAddr &addr) { return _ip == addr._ip && _port == addr._port; }

    struct sockaddr *NetAddr() { return CONV(&_net_addr); }
    socklen_t NetAddrLen() { return sizeof(_net_addr); }

    std::string Ip() { return _ip; }
    uint16_t Port() { return _port; }
    std::string Addr() { return Ip() + ":" + std::to_string(Port()); }

    void SetAddr(sockaddr_in &client)
    {
        _net_addr = client;
        PortNetToHost();
        IpNetToHost();
    }

private:
    struct sockaddr_in _net_addr;
    std::string _ip; // 主机序列: IP
    uint16_t _port;  // 主机序列: 端口号
};
  1. TcpServer.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <sys/wait.h>

#include "Socket.hpp"
#include "InetAddr.hpp"

using namespace SocketModule;
using namespace LogModule;

using tcphandler_t = std::function<bool(SockPtr, InetAddr)>;

namespace TcpServerModule
{
    class TcpServer
    {
    public:
        TcpServer(int port)
            : _listensockp(std::make_unique<TcpSocket>())
            , _isrunning(false)
            , _port(port)
        {}

        ~TcpServer()
        {
            _listensockp->Close();
        }

        void InitServer(tcphandler_t handler)
        {
            _listensockp->BuildTcpSocketMethod(_port);
            _handler = handler;
        }

        void Loop()
        {
            _isrunning = true;
            while (_isrunning)
            {
                // 1. 获取连接: 获取网络通信sockfd && 客户端的
                InetAddr clientaddr;
                auto sockfd = _listensockp->AcceptOrDie(&clientaddr);
                if (sockfd == nullptr)
                    continue;
                LOG(LogLevel::DEBUG) << "get a new client info is: " << clientaddr.Addr();

                // 2. IO处理
                pid_t id = fork();
                if (id == 0)
                {
                    // 子进程关闭listensockfd
                    _listensockp->Close();
                    if (fork() > 0)
                        exit(0); // 子进程直接退出

                    // 孙子进程进行IO处理
                    _handler(sockfd, clientaddr);
                    exit(0);
                }
                // 父进程关闭sockfd
                sockfd->Close();
                waitpid(id, nullptr, 0); // 子进程直接退出, 父进程无需阻塞等待
            }
            _isrunning = false;
        }

    private:
        std::unique_ptr<Socket> _listensockp;
        bool _isrunning;
        tcphandler_t _handler;
        int _port;
    };
}
  1. HttpProtocol.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <sstream>
#include <fstream>

#include "Common.hpp"

const std::string Sep = "\r\n";
const std::string LineSep = " ";
const std::string HeaderLineSep = ": ";
const std::string BlankLine = "\r\n";

const std::string default_home_path = "wwwroot"; // 浏览器的请求的默认服务器路径
const std::string http_version = "HTTP/1.0";     // http的版本
const std::string page_404 = "wwwroot/404.html"; // 404页面
const std::string first_page = "index.html";     // 首页

// 浏览器/服务器模式(B/S): 浏览器充当客户端, 发送请求; 输入: 123.60.170.90:8080
class HttpRequset
{
public:
    HttpRequset() {}
    ~HttpRequset() {}

    // 浏览器具有自动识别http请求的能力, 可以充当客户端
    // 浏览器发送的http请求(序列化数据)如下:
    // GET /favicon.ico HTTP/1.1
    // Host: 123.60.170.90:8080
    // Connection: keep-alive
    // User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0
    // Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
    // Referer: http://123.60.170.90:8080/
    // Accept-Encoding: gzip, deflate
    // Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6

    void ParseReqHeaderKV()
    {
        std::string key, value;
        for (auto &header : _req_header)
        {
            if (SplitString(header, HeaderLineSep, &key, &value))
            {
                _header_kv.insert(std::make_pair(key, value));
            }
        }
    }

    void ParseReqHeader(std::string &requset)
    {
        std::string line;
        while (true)
        {
            bool ret = ParseOneLine(requset, &line, Sep);
            if (ret && !line.empty())
            {
                _req_header.push_back(line);
            }
            else
            {
                break;
            }
        }
        // 提取请求报头每一行
        ParseReqHeaderKV();
    }

    // 解析请求行中详细的字段
    // GET /index.html HTTP/1.1
    void ParseReqLine(std::string &_req_line, const std::string &sep)
    {
        std::stringstream ss(_req_line);
        ss >> _req_method >> _uri >> _http_version;
    }

    // 对http请求进行反序列化
    void Deserialize(std::string &requset)
    {
        // 提取请求行
        if (ParseOneLine(requset, &_req_line, Sep))
        {
            // 提取请求行中的详细字段
            ParseReqLine(_req_line, LineSep);

            // 提取请求报文
            ParseReqHeader(requset);

            _blank_line = Sep;
            _req_body = requset;

            // 分析请求中是否含有参数
            if (_req_method == "POST") // 默认POST带参数
            {
                // 参数在正文_req_body部分: name=zhangsan&password=123456
                _isexec = true;
                _args = _req_body;
                _path = _uri;
            }
            else if (_req_method == "GET")
            {
                // 参数在URI中: login?name=zhangsan&password=123456
                auto pos = _uri.find("?"); 
                if (pos != std::string::npos) // 存在?带参数
                {
                    _isexec = true;
                    _path = _uri.substr(0, pos);
                    _args = _uri.substr(pos + 1);
                }
                else // 不存在?不带参数
                {
                    _isexec = false;
                }
            }
        }
    }

    // 返回请求的资源: uri
    std::string GetContent(const std::string &path)
    {
        // 既支持文本文件, 又支持二进制图片
        std::string content;
        std::ifstream in(path, std::ios::binary);
        if (!in.is_open())
            return std::string();
        in.seekg(0, in.end);
        int filesize = in.tellg();
        in.seekg(0, in.beg);
        content.resize(filesize);
        in.read((char *)content.c_str(), filesize);
        in.close();
        return content;

        // 只支持读取文本文件, 不支持二进制图片
        // std::string content;
        // std::ifstream in(path);
        // if (!in.is_open())
        //     return std::string();
        // std::string line;
        // while (std::getline(in, line))
        // {
        //     content += line;
        // }
        // return content;
    }

    // 获取资源的文件后缀
    std::string Suffix()
    {
        // _uri -> wwwroot/index.html wwwroot/image/1.jpg
        auto pos = _uri.rfind(".");
        if (pos == std::string::npos)
            return std::string(".html");
        else
            return _uri.substr(pos);
    }

    std::string Uri() { return _uri; }
    void SetUri(const std::string newuri) { _uri = newuri; }
    std::string Path() { return _path; }
    std::string Args() { return _args; }
    bool IsHasArgs() { return _isexec; }

    void Print()
    {
        std::cout << "请求行详细字段: " << std::endl;
        std::cout << "_req_method: " << _req_method << std::endl;
        std::cout << "_uri: " << _uri << std::endl;
        std::cout << "_http_version: " << _http_version << std::endl;

        std::cout << "请求报头: " << std::endl;
        for (auto &kv : _header_kv)
        {
            std::cout << kv.first << " # " << kv.second << std::endl;
        }

        std::cout << "空行: " << std::endl;
        std::cout << "_blank_line: " << _blank_line << std::endl;

        std::cout << "请求正文: " << std::endl;
        std::cout << "_body: " << _req_body << std::endl;
    }

private:
    std::string _req_line;                                   // 请求行
    std::vector<std::string> _req_header;                    // 请求报头
    std::unordered_map<std::string, std::string> _header_kv; // 请求报头的KV结构
    std::string _blank_line;                                 // 空行
    std::string _req_body;                                   // 请求正文: 内部可能会包含参数(POST请求)

    // 请求行中详细的字段
    std::string _req_method;   // 请求方法
    std::string _uri;          // 用户想要的资源路径: 内部可能会包含参数(GET请求) /login.hmtl  |  /login?xxx&yyy
    std::string _http_version; // http版本

    // 关于请求传参GET/POST相关的结构
    std::string _path;    // 路径
    std::string _args;    // 参数
    bool _isexec = false; // 执行动态方法
};

// 对于http, 任何请求都要有应答
class HttpResponse
{
public:
    HttpResponse() {}
    ~HttpResponse() {}

    // 通过requset结构体, 构建response结构体
    void Build(HttpRequset &req)
    {
        // 当用户输入:
        // 123.60.170.90:8080/      -> 默认访问 wwwroot/index.html
        // 123.60.170.90:8080/a/b/  -> 默认访问 wwwroot/a/b/index.html

        std::string uri = default_home_path + req.Uri(); // wwwroot/
        if (uri.back() == '/')
        {
            uri += first_page; // wwwroot/index.html
            req.SetUri(uri);
        }

        // 获取用户请求的资源
        _content = req.GetContent(uri);
        if (_content.empty())
        {
            _status_code = 404; // 用户请求的资源不存在!
            req.SetUri(page_404);
            _content = req.GetContent(page_404); // 注意: 需要读取404页面
        }
        else
        {
            _status_code = 200; // 用户请求的资源存在!
        }
        _status_code_desc = CodeToDesc(_status_code);
        _resp_body = _content;

        // 设置响应报头
        SetHeader("Content-Length", std::to_string(_content.size()));
        std::string mime_type = SuffixToDesc(req.Suffix());
        SetHeader("Content-Type", mime_type);
    }

    // 设置响应报头的KV结构
    void SetHeader(const std::string &k, const std::string &v)
    {
        _header_kv[k] = v;
    }

    void SetCode(int code)
    {
        _status_code = code;
        _status_code_desc = CodeToDesc(_status_code);
    }   

    void SetBody(const std::string &body)
    {
        _resp_body = body;
    }

    // 对http响应序列化
    void Serialize(std::string *response)
    {
        // 1. 求各个字段
        for (auto &header : _header_kv)
        {
            _resp_header.push_back(header.first + HeaderLineSep + header.second);
        }
        _http_version = http_version;
        _resp_line = _http_version + LineSep + std::to_string(_status_code) + LineSep + _status_code_desc + Sep;
        _blank_line = BlankLine;

        // 2. 开始序列化: 各个字段相加
        *response = _resp_line;
        for (auto &line : _resp_header)
        {
            *response += (line + Sep);
        }
        *response += _blank_line;
        *response += _resp_body;
    }

private:
    // 将 状态码 转化为 状态码描述
    std::string CodeToDesc(int code)
    {
        switch (code)
        {
        case 200:
            return "OK";
        case 404:
            return "Not Found";
        default:
            return std::string();
        }
    }

    // 将 文件后缀 转化为 文件类型
    std::string SuffixToDesc(const std::string &suffix)
    {
        if (suffix == ".html")
            return "text/html";
        else if (suffix == ".jpg")
            return "application/x-jpg";
        else
            return "text/html";
    }

private:
    std::string _resp_line;                                  // 响应行
    std::vector<std::string> _resp_header;                   // 响应报头
    std::unordered_map<std::string, std::string> _header_kv; // 响应报头的KV结构
    std::string _blank_line;                                 // 空行
    std::string _resp_body;                                  // 响应正文

    // 响应行中详细的字段
    std::string _http_version;     // http版本
    int _status_code;              // 状态码
    std::string _status_code_desc; // 状态码描述
    std::string _content;          // 返回给用户的内容: 响应正文
};
  1. HttpServer.hpp
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <unordered_map>

#include "TcpServer.hpp"
#include "HttpProtocol.hpp"

using namespace TcpServerModule;

using http_handler_t = std::function<void(HttpRequset &, HttpResponse &)>;

class HttpServer
{
public:
    HttpServer(int port)
        : _tsvr(std::make_unique<TcpServer>(port))
    {
    }

    ~HttpServer() {}

    void Register(std::string funcname, http_handler_t func)
    {
        _route[funcname] = func;
    }

    void Start()
    {
        _tsvr->InitServer([this](SockPtr sockfd, InetAddr client)
                          { return this->HanlerRequset(sockfd, client); });

        _tsvr->Loop();
    }

    bool SafeCheck(const std::string &service)
    {
        auto iter = _route.find(service);
        return iter != _route.end();
    }

    bool HanlerRequset(SockPtr sockfd, InetAddr client)
    {
        LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();

        // 1. 读取浏览器发送的http请求
        std::string http_requset;
        sockfd->Recv(&http_requset);

        // 2. 请求反序列化
        HttpRequset req;
        req.Deserialize(http_requset);
        
        // 3. 根据请求构建响应
        HttpResponse resp;
        if (req.IsHasArgs()) // 动态交互请求(含有参数): 登入, 注册... 
        {
            // GET 请求的参数在 URL 中
            // POST请求的参数在 body中
            std::string service = req.Path();
            if(SafeCheck(service))
            {
                _route[service](req, resp); // login
            }
            else
            {
                resp.Build(req);
            }
        }
        else // 请求一般的静态资源(不含参数): 网页, 图片, 视频...
        {
            resp.Build(req);
        }

        // 4. 响应序列化
        std::string http_response;
        resp.Serialize(&http_response);

        // 5. 发送响应给用户
        sockfd->Send(http_response);

        return true;
    }

private:
    std::unique_ptr<TcpServer> _tsvr;
    std::unordered_map<std::string, http_handler_t> _route; // 功能路由
};
  1. HttpServer.cc
cpp 复制代码
#include "HttpServer.hpp"
#include "Deamon.hpp"

using namespace LogModule;

// 登入功能
void Login(HttpRequset &req, HttpResponse &resp)
{
    // 根据 req 动态构建 resp: 
    // Path: /login
    // Args: name=zhangsan&password=123456
    LOG(LogLevel::DEBUG) << "进入登入模块: " << req.Path() << ", " << req.Args();

    // 1. 解析参数格式, 得到想要的参数
    std::string req_args = req.Args();

    // 2. 访问数据库, 验证是否是合法用户

    // 3. 登入成功
    // resp.SetCode(302);
    // resp.SetHeader("Location", "/"); // 登入成功后跳转到首页

    std::string body = req.GetContent("wwwroot/success.html");
    resp.SetCode(200);
    resp.SetHeader("Content-Length", std::to_string(body.size()));
    resp.SetHeader("Content-Type", "text/html");
    resp.SetHeader("Set-Cookie", "username=xzy&password=123456");
    resp.SetBody(body);
}

// 注册功能
void Register(HttpRequset &req, HttpResponse &resp)
{
    LOG(LogLevel::DEBUG) << "进入注册模块: " << req.Path() << ", " << req.Args();
}

// 搜索引擎功能
void Search(HttpRequset &req, HttpResponse &resp)
{
    LOG(LogLevel::DEBUG) << "进入注册模块: " << req.Path() << ", " << req.Args();
}

int main(int argc, char *argv[])
{
    // Deamon(false, false); // 守护进程

    if (argc != 2)
    {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    int port = std::stoi(argv[1]);

    std::unique_ptr<HttpServer> httpserver = std::make_unique<HttpServer>(port);

    // 服务器具有登入成功功能
    httpserver->Register("/login", Login);
    httpserver->Register("/register", Register);

    httpserver->Start();

    return 0;
}
  1. 前端代码
    点击跳转

  2. 运行操作

bash 复制代码
# 启动http服务器
xzy@hcss-ecs-b3aa:~$ ./httpserver 8888

浏览器输入:云服务器IP地址:端口号(例如:http://123.60.170.90:8888/)

效果如下:

相关推荐
想躺在地上晒成地瓜干23 分钟前
树莓派超全系列文档--(14)无需交互使用raspi-config工具其一
linux·树莓派·raspberrypi·树莓派教程
Shier833_Ww32 分钟前
目标识别与双目测距(1)环境搭建:Ubuntu+yolov5+pcl库
linux·yolo·ubuntu
无名之逆1 小时前
hyperlane:Rust HTTP 服务器开发的不二之选
服务器·开发语言·前端·后端·安全·http·rust
唐青枫1 小时前
Linux 历史命令操作教程
linux
愚润求学2 小时前
Linux基础指令(一)
linux·服务器·语法
struggle20252 小时前
AWS Bedrock 多代理蓝图存储库使用 CDK、Streamlit 和 LangFuse 运行 AWS Bedrock 多代理 AI 协作的蓝图
运维·人工智能·自动化·云计算·aws
IEVEl2 小时前
CentOS 7 安装 EMQX (MQTT)
linux·运维·centos
好多知识都想学2 小时前
Centos 7 搭建 jumpserver 堡垒机
linux
vortex53 小时前
深入理解 Linux 文件权限:从 ACL 到扩展属性,解剖底层技术细节与命令应用
linux·运维·服务器
Cyber4K3 小时前
《零基础实战:手把手教你用LNMP环境搭建Discuz论坛》
linux