【Linux】应用层协议 HTTP

news2025/3/31 17:14:04

应用层协议 HTTP

  • 一. HTTP 协议
    • 1. URL 地址
    • 2. urlencode 和 urldecode
    • 3. 请求与响应格式
  • 二. HTTP 请求方法
    • 1. GET 和 POST (重点)
  • 三. 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 (重点)

特性GETPOST
用途用于请求 URL 指定的资源提交数据到服务器
数据位置参数附加在 URL 中参数放在请求体(Body)中
数据可见性URL 中明文显示,不安全数据不可见,相对安全
数据长度限制受限于 URL 长度(通常 ≤ 2048 字节)无限制(理论上)
常见场景搜索、浏览页面、获取 API 数据表单提交、上传文件、用户登录

在这里插入图片描述

  • GET 的参数:通过 ? 附加在 URL 后,多个参数用 & 分隔!
  • 浏览器默认使用 GET 发起请求(例如:直接输入 URL 或点击链接)
  • HTTP 协议本身是明文传输的,无论是 GET 还是 POST 方法,数据在网络中传输时都可能被抓包,需要 HTTPS 协议对数据进行加密!

三. HTTP 状态码

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

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

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

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

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

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

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

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

  • 当服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
  • 同样地,服务器也会在响应中添加一个 Location 头部来指定资源的新位置。浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。
  • 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:
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
httpserver:HttpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -rf httpserver
  1. Mutex.hpp
#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
#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
#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
#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
#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
#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
#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
#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
#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
#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. 运行操作

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

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

效果如下:

在这里插入图片描述

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

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

相关文章

数据可视化TensorboardX和tensorBoard安装及使用

tensorBoard 和TensorboardX 安装及使用指南 tensorBoard 和 TensorBoardX 是用于可视化机器学习实验和模型训练过程的工具。TensorBoard 是 TensorFlow 官方提供的可视化工具&#xff0c;而 TensorBoardX 是其社区驱动的替代品&#xff0c;支持 PyTorch 等其他框架。以下是它…

【Hugging Face 开源库】Diffusers 库 —— 扩散模型

Diffusers 的三个主要组件1. DiffusionPipeline&#xff1a;端到端推理工具__call__ 函数callback_on_step_end 管道回调函数 2. 预训练模型架构和模块UNetVAE&#xff08;Variational AutoEncoder&#xff09;图像尺寸与 UNet 和 VAE 的关系EMA&#xff08;Exponential Moving…

AWTK-WEB 快速入门(6) - JS WebSocket 应用程序

WebSocket 可以实现双向通信&#xff0c;适合实时通信场景。本文介绍一下使用 Javacript 语言开发 AWTK-WEB 应用程序&#xff0c;并用 WebSocket 与服务器通讯。 用 AWTK Designer 新建一个应用程序 先安装 AWTK Designer&#xff1a; https://awtk.zlg.cn/web/index.html …

使用VSCODE导致CPU占用率过高的处理方法

1&#xff1a;cpptools 原因&#xff1a;原因是C/C会在全局搜索文件&#xff0c;可以快速进行跳转&#xff1b;当打开的文件过大&#xff0c;全局搜索文件会占用大量CPU&#xff1b; 处理方法&#xff1a; 1&#xff1a;每次只打开小文件夹&#xff1b; 2&#xff1a;打开大文…

【力扣hot100题】(004)盛水最多的容器

现在能这么快做出来纯粹是因为当时做的时候给我的印象实在太深了。 犹记得这题是当年开启我用CSDN记录leetcode日记历史的开端。 总之印象太深了不会都不行啊&#xff01;&#xff01;记得当年是想到用各种动态规划回溯等等等等最终发现是最简单贪心和双指针。 解法也是非常简…

用Deepseek写扫雷uniapp小游戏

扫雷作为Windows系统自带的经典小游戏&#xff0c;承载了许多人的童年回忆。本文将详细介绍如何使用Uniapp框架从零开始实现一个完整的扫雷游戏&#xff0c;包含核心算法、交互设计和状态管理。无论你是Uniapp初学者还是有一定经验的开发者&#xff0c;都能从本文中获得启发。 …

Eclipse IDE for ModusToolbox™ 3.4环境通过JLINK调试CYT4BB

使用JLINK在Eclipse IDE for ModusToolbox™ 3.4环境下调试CYT4BB&#xff0c;配置是难点。总结一下在IDE中配置JLINK调试中遇到的坑&#xff0c;以及如何一步一步解决遇到的问题。 1. JFLASH能够正常下载程序 首先要保证通过JFLASH(我使用的J-Flash V7.88c版本)能够通过JLIN…

修改git在提交代码时的名称

在git中&#xff0c;如果想修改提交代码作者的名字&#xff0c;可以进行以下操作&#xff1a; 1.在桌面或者文件夹内右击鼠标&#xff0c;点开Git Bash here。 2.进入后&#xff0c;通过git config user.name 回车查看当前名称。 3.通过git config --global user.name "…

【Linux】深入解析Linux命名管道(FIFO):原理、实现与实战应用

本文承接上文匿名管道&#xff1a;【Linux】深度解析Linux进程间通信&#xff1a;匿名管道原理、实战与高频问题排查-CSDN博客 深入探讨Linux进程间通信&#xff08;IPC&#xff09;&#xff0c;以匿名管道为核心&#xff0c;详细阐述其通信目的、实现前提及机制。涵盖数据传输…

第十四届蓝桥杯省赛电子类单片机学习记录(客观题)

01.一个8位的DAC转换器&#xff0c;供电电压为3.3V&#xff0c;参考电压2.4V&#xff0c;其ILSB产生的输出电压增量是&#xff08;D&#xff09;V。 A. 0.0129 B. 0.0047 C. 0.0064 D. 0.0094 解析&#xff1a; ILSB&#xff08;最低有效位&#xff09;的电压增量计算公式…

vim的一般操作(分屏操作) 和 Makefile 和 gdb

目录 一. vim的基本概念 二. vim基础操作 2.1 插入模式 aio 2.2 [插入模式]切换至[正常模式] Esc 2.3[正常模式]切换至[末行模式] shift ; 2.4 替换模式 Shift R 2.5 视图&#xff08;可视&#xff09;模式 (可以快速 删除//注释 或者 增加//注释) ctrl v 三&…

Apache Shiro 统一化实现多端登录(PC端移动端)

Apache Shiro 是一个强大且易用的Java安全框架&#xff0c;提供了身份验证、授权、密码学和会话管理等功能。它被广泛用于保护各种类型的应用程序&#xff0c;包括Web应用、桌面应用、RESTful服务、移动端应用和大型企业级应用。 需求背景 在当今数字化浪潮的推动下&#xff…

NAT—地址转换(实战篇)

一、实验拓扑&#xff1a; 二、实验需求&#xff1a; 1.实现内网主机访问外网 2.实现外网客户端能够访问内网服务器 三、实验思路 1.配置NAT地址池实现内网地址转换成公网地址&#xff0c;实现内网主机能够访问外网。 2.配置NAT Sever实现公网地址映射内网服务器地址&…

用HTML和CSS生成炫光动画卡片

这个效果结合了渐变、旋转和悬浮效果的炫酷动画示例&#xff0c;使用HTML和CSS实现。 一、效果 二、实现 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport&quo…

FPGA_YOLO(三)

上一篇讲的是完全映射&#xff0c;也就是block中的所包含的所有的卷积以及归一&#xff0c;池化卷积 举例总共6个等都在pl侧进行处理&#xff08;写一个top 顶层 里面conv 1 bn1 relu1 pool1 conv1*1 conv 2 bn2 relu2 pool2 conv1*1 ....总共6个 &#xff09;&#xff0c;…

旅游CMS选型:WordPress、Joomla与Drupal对比

内容概要 在旅游行业数字化转型进程中&#xff0c;内容管理系统&#xff08;CMS&#xff09;的选择直接影响网站运营效率与用户体验。WordPress、Joomla和Drupal作为全球主流的开源CMS平台&#xff0c;其功能特性与行业适配性存在显著差异。本文将从旅游企业核心需求出发&…

全面适配iOS 18.4!通付盾加固产品全面升级,护航App安全上架

引言&#xff1a; 苹果官方新规落地&#xff01; 自2025年4月24日起&#xff0c;所有提交至App Store Connect的应用必须使用Xcode 16或更高版本构建&#xff0c;否则将面临审核驳回风险&#xff01;Beta版iOS 18.4、iPadOS 18.4现已推出&#xff0c;通付盾iOS加固产品率先完成…

一台电脑最多能接几个硬盘?

在使用电脑时&#xff0c;硬盘空间不够是许多用户都会遇到的问题。无论是摄影师、剪辑师等需要大量存储空间的专业人士&#xff0c;还是游戏玩家、数据备份爱好者&#xff0c;都可能希望通过增加硬盘来扩展存储容量。然而&#xff0c;一台电脑究竟最多能接多少个硬盘&#xff1…

【玩转全栈】---- Django 基于 Websocket 实现群聊(解决channel连接不了)

学习视频&#xff1a; 14-11 群聊&#xff08;一&#xff09;_哔哩哔哩_bilibili 目录 Websocket 连接不了&#xff1f; 收发数据 断开连接 完整代码 聊天室的实现 聊天室一 聊天室二 settings 配置 consumer 配置 多聊天室 Websocket 连接不了&#xff1f; 基于这篇博客&…

如何快速解决django报错:cx_Oracle.DatabaseError: ORA-00942: table or view does not exist

我们在使用django连接oracle进行编程时&#xff0c;使用model进行表映射对接oracle数据时&#xff0c;默认表名组成结构为&#xff1a;应用名_类名&#xff08;如&#xff1a;OracleModel_test&#xff09;&#xff0c;故即使我们库中存在表test&#xff0c;运行查询时候&#…