首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【Linux 网络】理解并应用应用层协议:HTTP(附简单HTTP服务器C++代码)

【Linux 网络】理解并应用应用层协议:HTTP(附简单HTTP服务器C++代码)

作者头像
Yuzuriha
发布2026-01-14 19:27:05
发布2026-01-14 19:27:05
1260
举报
文章被收录于专栏:Linux网络Linux网络

前言: 上文我们学习到了什么是守护进程以及其原理【Linux网络】深入理解守护进程(Daemon)及其实现原理-CSDN博客 本文我们来认识应用层的协议:HTTP!

HTTP协议

虽然应用层协议通常可由程序员自定义,但在实际开发中,我们通常直接使用业界专家已经定义好且非常成熟的现成协议。HTTP(超文本传输协议)就是其中最重要、最好用的应用层协议之一。

HTTP是互联网世界的基石,它定义了客户端(如浏览器)与服务器之间进行通信的标准方式,主要用于交换或传输超文本数据(例如 HTML 文档)。

HTTP协议遵循标准的请求-响应(Request-Response)模型: 请求:客户端通过HTTP协议主动向服务器发送请求。 响应:服务器收到客户端发来的请求后进行处理,并将结果作为响应返回给客户端。

HTTP协议具有两个显著特点: 无连接:每一次请求都想要建立一个新的连接,处理完既断开 无状态:服务器不会保存客户端的状态信息。这意味着对于服务器而言,每一次请求都是全新的,它不具有记忆功能。


URL

URL(Uniform Resource Locator) 统一资源定位符。

我们常说的链接其实就是URL。

代码语言:javascript
复制
https://blog.csdn.net/huangyuchi/article/details/155441819

上面的链接由三部分组成:

协议方案名:http;分隔符 ://, 是固定的分隔格式。

服务器地址:blog.csdn.net。

资源在服务器中的地址:/huangyuchi/article/details/155441819 解释:这其实就是Linux系统下的路径,指向我们想要访问的资源! 注意:最开头的 '/' 并不会我们以为的Linux根目录!!! 而是一个叫做webroot的目录!所以实际路径会转化为:./webroot/huangyuchi/article/details/155441819


HTTP的请求与响应格式

请求格式

请求行:请求方法(GET) 空格 URI(资源地址) 空格 HTTP版本(HTTP/1.1) 换行符(\r\n)

请求报头:表示请求的属性。冒号分割的键值对;每组属性之间使用 \r\n 分隔,遇到空行表示Header部分结束。

请求正文:空行后面就是请求正文,请求正文允许为空!但如果有内容,在请求报头中就必须有 Content-Length属性的报头来标记正文长度。

响应格式

状态行:HTTP版本(HTTP/1.1) 空格 状态码 空格 状态码描述符 换行符(\r\n)

响应报头:表示响应的属性。冒号分割的键值对;每组属性之间使用 \r\n 分隔,遇到空行表示结束。

响应正文:空行后面就是响应正文,可以为空!但如果有内容,在响应报头中就必须有 Content-Length属性的报头来标记正文长度。


HTTP请求方法

简单介绍两个常用方法

GET方法

用途:用于URL指定的资源 例如:GET /index.html HTTP/1.1 特点:将指定资源解析后,由服务器返回响应内容给客户端

POST方法

用途:传输数据,其数据存放在请求正文中。常用于传输表单数据。 例如:POST /submit.cgi HTTP/1.1 特点:可以发送大量数据给服务器,并且数据都在请求体中。


状态码

响应格式状态行中表示当前状态的数字。

最常用:200 表示成功;404 表示资源不存在;302 表示重定向;

常用具体状态码速查表

状态码

英文名称

含义(通俗版)

常见场景

200

OK

成功

最常见的,表示你的请求服务器收到了,也没出问题,东西给你拿回来了。

301

Moved Permanently

永久重定向

比如网站换网址了,收藏夹里的旧链接会自动跳到新链接。

302

Found

临时重定向

比如你点“我的订单”,因为没登录,系统把你暂时跳到“登录页”。

304

Not Modified

资源未修改

这是一个缓存机制。服务器告诉你:“你本地存的那张图片没变,不用重新下载了,直接用吧”。

400

Bad Request

请求错误

通常是参数写错了。比如服务器要数字,你传了个字符串。

401

Unauthorized

未授权

“你是谁?” 表示你没登录,或者 Token 过期了。

403

Forbidden

禁止访问

“我知道你是谁,但我不让你进”。比如普通用户想进管理员后台。

404

Not Found

未找到

这个最出名。要么是你网址输错了,要么是那个页面被删了。

500

Internal Server Error

服务器内部错误

后端代码抛异常了(Bug),比如空指针、数据库连不上。完全是服务器的锅。

502

Bad Gateway

网关错误

通常是中间件(如 Nginx)虽然是好的,但后端的服务(如 Tomcat/Python)挂了或没响应。

理解重定向

重定向分为:302临时重定向 301永久重定向

重定向

首先,我们先理解一下什么叫做重定向。

重定向:简单来说,就是让客户端访问指定的地址时,转头访问另一个地址!

重定向主要用于:当一个资源更换位置的时候,为了保证之前的旧地址依然可以有效。对旧地址进行重定向到新地址。

临时重定向(302)

资源的临时转移,使用临时重定向!

举个例子:一个店面由于装修等缘故临时搬到另一个地方,在店门口贴上公告“由于装修,本店暂时搬到xxxx”。

对于临时重定向,客户端都是默认先访问旧地址,再根据新地址去访问对于资源。

永久重定向(301)

资源的永久转移,使用永久重定向!

再举个例子:一个店面由于要扩大直接搬到了一个更大的店面!在店门口贴上公告“本店从今日起,永久搬到xxxx”。

对于永久重定向来说,客户端会直接访问最新的地址!


HTTP报头

常用的报头如下

  1. Content-Type:数据的格式(JSON 还是表单?)
  2. Content-Length:数据的大小
  3. Host:你要访问的域名(区分虚拟主机)
  4. Cookie / Set-Cookie:登录状态和身份识别
  5. User-Agent:你是手机还是电脑,是谷歌浏览器还是 IE

关于Connection报头

HTTP中的 Connection 字段是HTTP报头的⼀部分,它主要用于控制和管理客户端与服务器之间的连接状态。

核心作用:

管理持久连接:connect字段用于管理持久连接(长连接)。持久连接允许客户端和服务器在请求/响应完成后不立即关闭TCP连接,以便于在同一连接上发送发送多个请求和接收多个响应。

持久链接:

在HTTP/1.1中,默认使用持久连接。当客户端和服务器都不明确关闭连接的时候,连接将保持打开状态。以便于后续请求和响应可以复用同一个连接。

在HTTP1.0中,默认使用的是非持久连接。如果想要实现持久连接,这想要在报头中显式的设置 Connect: keep-alive

Connect: keep-alive:表示希望进行持久连接。

Connect: close:表示请求or响应完成后,应该关闭TCP连接。


代码实现

首先,我们要知道HTTP协议的实现是基于TCP协议的。所以我们想要接收客户端的信息,就必要要通过TCP协议来接收信息。

前提代码

InetAddr.hpp
代码语言:javascript
复制
#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>

// 实现网络地址与主机地址的转换

class InetAddr
{
public:
    InetAddr() {}

    // 网络转主机
    InetAddr(struct sockaddr_in &addr)
        : _addr(addr)
    {
        _prot = ntohs(_addr.sin_port); // 网络地址转主机地址
        char buff[1024];
        inet_ntop(AF_INET, &addr.sin_addr, buff, sizeof(buff)); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP
        _ip = std::string(buff);
    }

    // 主机转网络(客户端)
    InetAddr(std::string ip, uint16_t prot)
        : _ip(ip),
          _prot(prot)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        //&addr.sin_addr 是一个指向 struct in_addr 的指针,其内存地址等价于 &(addr.sin_addr.s_addr)(因为结构体的起始地址就是第一个成员的起始地址)
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
        _addr.sin_port = htons(_prot);
    }

    // 主机转网络(服务端)
    InetAddr(uint16_t prot)
        : _prot(prot)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(prot);
        _addr.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
    }

    // 直接获取sockaddr_in
    struct sockaddr *Getaddr()
    {
        return (struct sockaddr *)&_addr;
    }

    socklen_t Len()
    {
        socklen_t len = sizeof(_addr);
        return len;
    }

    uint16_t prot()
    {
        return _prot;
    }

    std::string ip()
    {
        return _ip;
    }

    // 运算符重载
    bool operator==(InetAddr &addr)
    {
        return _prot == addr._prot && _ip == addr._ip;
    }

    std::string Getname()
    {
        return _ip + ':' + std::to_string(_prot);
    }

private:
    struct sockaddr_in _addr;
    uint16_t _prot;
    std::string _ip;
};
Log.hpp
代码语言:javascript
复制
// 实现日志模块

#pragma once
#include <iostream>
#include <sstream>    // 包含stringstream类
#include <filesystem> //C++17文件操作接口库
#include <fstream>
#include <sys/types.h>
#include <unistd.h>
#include "Mutex.hpp"
using namespace std;
using namespace MutexModule;

// 补充:外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制
//       内部类可以直接访问外部类的方法以及成员,没有限制

namespace LogModule
{
    const string end = "\r\n";

    // 实现刷新策略:a.向显示器刷新 b.向指定文件刷新

    // 利用多态机制实现
    // 包含至少一个纯虚函数的类称为抽象类,不能实例化,只能被继承
    class LogStrategy // 基类
    {
    public:
        //"=0"声明为纯虚函数。纯虚函数强制派生类必须重写该函数
        virtual void SyncLog(const string &message) = 0;
    };

    // 向显示器刷新:子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        void SyncLog(const string &message) override
        {
            // 加锁,访问显示器,显示器也是临界资源
            LockGuard lockguard(_mutex);
            cout << message << end;
        }

    private:
        Mutex _mutex;
    };

    // 向指定文件刷新:子类
    const string defaultpath = "./log";
    const string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const string &path = defaultpath, const string &file = defaultfile)
            : _path(path), _file(file)
        {
            LockGuard lockguard(_mutex);
            // 判断路径是否存在,如果不存在就创建对应的路径
            if (!(filesystem::exists(_path)))
                filesystem::create_directories(_path);
        }

        void SyncLog(const string &message) override
        {
            // 合成最后路径
            string Path = _path + (_path.back() == '/' ? "" : "/") + _file;
            // 打开文件
            ofstream out(Path, ios::app);
            out << message << end;
        }

    private:
        string _path;
        string _file;
        Mutex _mutex;
    };

    //

    // 日志等级
    // enum class:强类型枚举。1.必须通过域名访问枚举值 2.枚举值不能隐式类型转化为整型
    enum class LogLevel
    {
        DEBUG,   // 调试级
        INFO,    // 信息级
        WARNING, // 警告级
        ERROR,   // 错误级
        FATAL    // 致命级
    };

    //

    // 将等级转化为字符串
    string LevelToStr(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "DEBUG";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKOWN";
        }
    }

    // 获取时间
    string GetTime()
    {
        // time函数:获取当前系统的时间戳
        // localtime_r函数:将时间戳转化为本地时间(可重入函数,localtime则是不可重入函数)
        // struct tm结构体,会将转化之后的本地时间存储在结构体中
        time_t curr = time(nullptr);
        struct tm curr_time;
        localtime_r(&curr, &curr_time);
        char buffer[128];
        snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
                 curr_time.tm_year + 1900, // 年份是从1900开始计算的,需要加上1900才能得到正确的年份
                 curr_time.tm_mon + 1,     // 月份了0~11,需要加上1才能得到正确的月份
                 curr_time.tm_mday,        // 日
                 curr_time.tm_hour,        // 时
                 curr_time.tm_min,         // 分
                 curr_time.tm_sec);        // 秒

        return buffer;
    }

    //

    // 实现日志信息,并选择刷新策略
    class Logger
    {
    public:
        Logger()
        {
            // 默认选择显示器刷新
            Strategy = make_unique<ConsoleLogStrategy>();
        }

        void EnableConsoleLogStrategy()
        {
            Strategy = make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            Strategy = make_unique<FileLogStrategy>();
        }

        // 日志信息
        class LogMessage
        {
        public:
            LogMessage(const LogLevel &level, const string &name, const int &line, Logger &logger)
                : _level(level),
                  _name(name),
                  _logger(logger),
                  _line_member(line)
            {
                _pid = getpid();
                _time = GetTime();
                // 合并:日志信息的左半部分

                stringstream ss; // 创建输出流对象,stringstream可以将输入的所有数据全部转为为字符串
                ss << "[" << _time << "] "
                   << "[" << LevelToStr(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _name << "] "
                   << "[" << _line_member << "] "
                   << " - ";

                // 返回ss中的字符串
                _loginfo = ss.str();
            }

            // 日志文件的右半部分:可变参数,重载运算符<<

            // e.g. <<"huang"<<123<<"dasd"<<24
            template <class T>
            LogMessage &operator<<(const T &message) // 引用返回可以让后续内容不断追加
            {
                stringstream ss;
                ss << message;
                _loginfo += ss.str();

                // 返回对象!
                return *this;
            }

            // 销毁时,将信息刷新
            ~LogMessage()
            {
                // 日志文件
                _logger.Strategy->SyncLog(_loginfo);
            }

        private:
            string _time;
            LogLevel _level;
            pid_t _pid;
            string _name;
            int _line_member;
            string _loginfo; // 合并之后的一条完整信息

            // 日志对象
            Logger &_logger;
        };

        // 重载运算符(),便于创建LogMessage对象
        // 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新!
        // 其次局部对象也不能传引用返回!
        LogMessage operator()(const LogLevel &level, const string &name, const int &line)
        {
            return LogMessage(level, name, line, *this);
        }

    private:
        unique_ptr<LogStrategy> Strategy;
    };

    // 为了用户使用更方便,我们使用宏封装一下
    Logger logger;

// 切换刷新策略
#define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy();
#define Enable_File_LogStrategy() logger.EnableFileLogStrategy();
// 创建日志,并刷新
//__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号
#define LOG(level) logger(level, __FILE__, __LINE__) // 细节:不加;
};
Comment.hpp
代码语言:javascript
复制
#pragma once

// 禁止拷贝类
class NoCopy
{
public:
    NoCopy() {};
    // 拷贝构造
    NoCopy(const NoCopy &nocopy) = delete;
    // 赋值重载
    const NoCopy &operator=(const NoCopy &nocopy) = delete;
};

enum EXIT
{
    OK = 0,
    SOCK_ERR,
    BIND_ERR,
    LISTEN_ERR,
    ACCP_ERR,
    TCPSERVER_ERR,
    TCPCLIENT_ERR,
    CONN_ERR
};
Mutex.hpp
代码语言:javascript
复制
// 封装锁接口
#pragma once
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    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 *Get()
        {
            return &mutex;
        }

    private:
        pthread_mutex_t mutex;
    };

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

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

    private:
        // 为了保证锁的底层逻辑,锁是不能够拷贝的,并且也是没有拷贝构造函数的
        //  避免拷贝,应该引用
        Mutex &_Mutex;
    };
}
Socket.hpp
代码语言:javascript
复制
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <memory>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Comment.hpp"
using namespace LogModule;

const int BACK = 16;

namespace SocketModule
{
    // 模板方法模式
    // 基类大部分函数都是虚函数
    class Socket
    {
    public:
        virtual void SocketOrDie() = 0;
        virtual void BindOrDie(uint16_t prot) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual void ConnectOrDie(InetAddr &addr) = 0;
        virtual ssize_t Recv(string &buffer) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual void Close() = 0;

        void BuildTcpSocket(uint16_t prot, int backlog = BACK)
        {
            SocketOrDie();
            BindOrDie(prot);
            ListenOrDie(backlog);
        }
    };

    class TcpSocket : public Socket
    {
    public:
        TcpSocket() {}

        TcpSocket(int fd)
            : _sockfd(fd)
        {
        }

        void SocketOrDie() override
        {
            _sockfd = socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "socket error";
                exit(SOCK_ERR);
            }
        }

        void BindOrDie(uint16_t prot) override
        {
            InetAddr addr(prot);
            int ret = bind(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "bind error";
                exit(BIND_ERR);
            }
        }

        void ListenOrDie(int backlog) override
        {
            int ret = listen(_sockfd, backlog);
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "listen error";
                exit(LISTEN_ERR);
            }
        }

        // 客户端
        std::shared_ptr<Socket> Accept(InetAddr *client) override
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int fd = accept(_sockfd, (struct sockaddr *)&peer, &len);
            if (fd < 0)
            {
                LOG(LogLevel::ERROR) << "accept error";
                exit(ACCP_ERR);
            }

            // 将客户端信息带出
            InetAddr addr(peer);
            client = &addr;
            // accept成功后返回一个新的套接字,用于服务
            // 可以直接返回fd,也可以返回新对象(的指针)
            return std::make_shared<TcpSocket>(fd);
        }

        void ConnectOrDie(InetAddr &addr) override
        {
            int ret = connect(_sockfd, addr.Getaddr(), addr.Len());
            if (ret < 0)
            {
                LOG(LogLevel::ERROR) << "connect error";
                exit(CONN_ERR);
            }
        }

        ssize_t Recv(string &buffer) override
        {
            char str[4096 * 3];
            ssize_t n = recv(_sockfd, str, sizeof(str), 0);
            if (n)
            {
                // 首先,str并不是空白的,其空间可以被其他函数使用过,保存有垃圾数据
                // 其次,revc函数读取后,并不会主动的为我们读取到的信息末尾加上'\0'
                // 所以,我们必须自己显示的加上!
                // 不然当buffer+=的时候,str作为C风格的字符串,遇到'\0'才会停止
                // 导致程序就会不知道读到哪停,把后面的内存垃圾也一并收进去了!
                // 最终导致json串解析的时候失败!

                str[n] = 0;
                buffer += str; // 一定是+=,缓冲区信息是累加,而不是覆盖
            }
            return n;
        }

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

        void Close() override
        {
            close(_sockfd);
        }

        int Sockfd()
        {
            return _sockfd;
        }

    private:
        int _sockfd;
    };
}
TcpServer.hpp
代码语言:javascript
复制
#pragma once
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include "Socket.hpp"
using namespace SocketModule;

class TcpServer
{
    using Func_t = function<void(shared_ptr<Socket>, InetAddr &)>;

public:
    TcpServer(uint16_t prot)
        : _prot(prot),
          _isrunning(false)
    {
        // 完成tcp套接字的创建、绑定、监听
        ts.BuildTcpSocket(_prot);
    }

    void Start(Func_t func)
    {
        _func = func;
        _isrunning = true;
        while (_isrunning)
        {
            auto sock = ts.Accept(&_client);
            if (sock == nullptr)
                continue;

            pid_t pid = fork();
            if (pid == 0)
            {
                // 关闭不需要的描述符
                ts.Close();
                // 子进程
                if (fork() > 0)
                    return;

                // 孙子进程执行任务
                _func(sock, _client);
                return;
            }
            else if (pid > 0)
            {
                // 父进程
                // 关闭不需要的描述符
                sock->Close();
                // 子进程会立刻退出,不会阻塞等待
                wait(&pid);
            }
            else
            {
                ts.Close();
                LOG(LogLevel::ERROR) << "fork error";
                return;
            }
        }
        _isrunning = false;
    }

private:
    uint16_t _prot;
    TcpSocket ts;
    bool _isrunning;

    InetAddr _client;
    Func_t _func;
};

HTTP服务器模拟代码

HTTP.hpp
代码语言:javascript
复制
#pragma once
#include "TcpServer.hpp"
#include <memory>
#include "Tool.hpp"

const string space = " ";
const std::string linesep = ": ";

const string webroot = "./wwwroot";
const string homepage = "index.html";

class HttpRequest
{
public:
    // 不重要,请求的序列化由客户端实现:浏览器!
    std::string Serialize()
    {
        return std::string();
    }

    // 实现,将客户端发送过来的http字符串,反序列化,得到url
    bool Deserialize(std::string &reqstr)
    {
        // 1.获取请求行
        string req = Tool::ReadOneLine(reqstr);
        // 2.将请求行序列化
        ParseReqLine(req);
        // 3.合成正确的uri路径
        if (_uri == "/")
            _uri = webroot + _uri + homepage;
        else
            _uri = webroot + _uri;

        // bebug
        cout << "_method: " << _method << endl;
        cout << "_uri: " << _uri << endl;
        cout << "_version: " << _version << endl;

        return true;
    }

    // 将请求行按照空格分隔
    void ParseReqLine(std::string &reqline)
    {
        // GET / HTTP/1.1
        std::stringstream ss(reqline);
        ss >> _method >> _uri >> _version;
    }

    string Uri()
    {
        return _uri;
    }

private:
    std::string _method;
    std::string _uri;
    std::string _version;
};

class HttpResponse
{
public:
    HttpResponse()
        : _version("HTTP/1.1")
    {
    }

    // 不重要,应答的反序列化由客户端实现:浏览器!
    bool Deserialize(std::string &reqstr)
    {
        return true;
    }

    // 实现,将对应的资料序列化后,返回给客户端
    std::string Serialize()
    {
        std::string status_line = _version + space + std::to_string(_code) + space + _desc + Separator;
        std::string resp_header;
        for (auto &str : _headers)
        {
            resp_header += str.first + linesep + str.second + Separator;
        }

        return status_line + resp_header + _blankline + _text;
    }

    // 获取资源,添加报头
    void MakeResponse(string uri)
    {
        // 顺便设置状态行信息
        if (Tool::ReadFileContent(uri, _text))
        {
            // 获取资源成功
            SetCode(200);
        }
        else
        {
            // 获取资源失败
            SetCode(404);
            uri = "./wwwroot/404.html";
            Tool::ReadFileContent(uri, _text);
        }

        // 添加报头

        // 正文长度
        string filesize = to_string(Tool::FileSize(uri)); // 当以二进制方式读取时,不会存在'\n'没有被读取的情况。

        // string filesize = to_string(_text.size()); // 以字符串方式读取,会少读取一部分'\n',与FileSize()的返回值不一致
        _headers.insert(make_pair("Content-Length", filesize));
        // 正文类型
        string Suffic = Tool::Uri2Suffix(uri);
        _headers.insert(make_pair("Content-Type", Suffic));
    }

    void SetCode(int code)
    {
        // 设置状态码
        _code = code;
        switch (code)
        {
        case 200:
            _desc = "OK";
            break;
        case 404:
            _desc = "NOT FOUND";
            break;
        default:
            break;
        }
    }

    string text()
    {
        return _text;
    }

public:
    std::string _version;
    int _code;                                        // 404
    std::string _desc;                                // "Not Found"
    string _text;                                     // 资源
    unordered_map<std::string, std::string> _headers; // 报头

    string _blankline = "\r\n";
};

class HTTP
{
public:
    HTTP(uint16_t prot)
        : _tsp(make_unique<TcpServer>(prot))
    {
    }

    void HandlerHttpRquest(std::shared_ptr<Socket> &sock, InetAddr &client)
    {
        // 接收浏览器发送来的http请求字符串
        string str;
        sock->Recv(str);

        // 反序列化:http字符串
        HttpRequest req;
        req.Deserialize(str);

        // 序列化:将客户端要访问的资源返回
        HttpResponse rep;
        rep.MakeResponse(req.Uri());
        string ret = rep.Serialize();
        sock->Send(ret);
    }

    void Run()
    {
        _tsp->Start([this](std::shared_ptr<Socket> sock, InetAddr &client)
                    { this->HandlerHttpRquest(sock, client); });
    }

private:
    unique_ptr<TcpServer> _tsp;
};
Tool.hpp
代码语言:javascript
复制
#pragma once

// 工具类
#include <string>
#include <fstream>

const string Separator = "\r\n";

struct Tool
{
    // 将资源写入 text
    static bool ReadFileContent(std::string uri, std::string &text)
    {
        // version1:以字符串方式读取
        //  std::fstream in(uri);
        //  if (in.is_open())
        //  {
        //      std::string str;
        //      // getline 会自动去掉每一行的换行符 (\n)
        //      // 这会导致最终发送的信息的数量会少一点
        //      while (getline(in, str))
        //      {
        //          text += str;
        //      }
        //      in.close();
        //      return true;
        //  }
        //  return false;

        // version2:以二进制方式读取,更好!
        // 更兼容照片、视频等资源
        std::ifstream in(uri, std::ios::binary); // 以二进制模式打开文件
        int filesize = FileSize(uri);
        if (filesize < 0)
            return false;
        if (in.is_open())
        {
            text.resize(filesize);
            in.read(&text[0], filesize);
            return true;
        }

        return false;
    }

    // 获取请求行
    static string ReadOneLine(string &reqstr)
    {
        int pos = reqstr.find(Separator);
        string ret = reqstr.substr(0, pos);
        reqstr.erase(0, pos + Separator.size());
        return ret;
    }

    // 获取文件大小
    // 该读取方式是不会忽略掉'\n'的
    // 当ReadFileContent使用字符串发的方式读取时,其读取上来的大小会与下面得到的大小不一致,导致网页无法加载!
    static int FileSize(string filename)
    {
        std::ifstream in(filename, std::ios::binary); // 以二进制模式打开文件
        if (!in.is_open())
            return -1;
        in.seekg(0, in.end);       // 将读取指针直接跳转到文件的最末端
        int filesize = in.tellg(); // 获取当前读取指针的位置,既变相的获取文件大小
        in.close();

        return filesize;
    }

    // 获取资源后缀,返回对应类型
    static std::string Uri2Suffix(const std::string &targetfile)
    {
        // ./wwwroot/indext.html
        auto pos = targetfile.find('.');
        if (pos == string::npos)
        {
            return "text/html";
        }

        string ret = targetfile.substr(pos);
        if (ret == ".html" || ret == ".htm")
        {
            return "text/html";
        }
        else if (ret == ".png")
        {
            return "image/png";
        }
        else if (ret == ".jpg")
        {
            return "image/jpg";
        }
        else if (ret == ".mp4")
        {
            return "video/mp4";
        }
        else
        {
            return "";
        }
    }
};
Main.cc
代码语言:javascript
复制
#include "HTTP.hpp"

int main(int args, char *argv[])
{
    if (args != 2)
    {
        cout << "Please use: " << argv[0] << " prot";
        return 0;
    }

    int prot = stoi(argv[1]);
    unique_ptr<HTTP> up = make_unique<HTTP>(prot);
    up->Run();
}

代码实现注意要点

代码模拟http协议中,客户端:既浏览器。自己实现服务器端!

实现要点:

http协议的基于Socket的,所以我们可以直接复用之前封装好了的Socket.hpp

一. 实现反序列化:

客户端发送来的请求,需要我们自己来进行反序列化。反序列化后得到:uri(要访问资源的路径!)

判断uri合成可以正确找到资源的路径。当uri == /(web根目录:wwwroot)时访问默认页面:index.html,将uri合成为 ./wwwroot/index.html

uri != /时,将uri合成为指定路径 ./wwwroot/uri

二.实现序列化

当我们通过uri地址,找到并获取到了对应资源,要将资源返回给客户端,这样客户端才能够访问到对应的资源。而想要对客户端进行回应,要遵守响应结构。

其中实现过程中注意以下要点:

1.如果请求的资源中不仅包含网页资源,还是包含了图片、视频等资源。那么浏览器将会进行多次请求:第一次请求网页资源、第二次请求图片资源、第三次.....。依次请求请求资源知道请求完毕!

2.在MakeResponse中,对浏览器进行响应时。要添加两个报头:Content-Length,表示正文长度;Content-Type,表示正文的数据类型(html、png、MP4等)

3.在读取对应资源时,如果使用字符串方式读取不妥!因为无法正确读取:图片、视频等资源。应该使用二进制方式读取

4.注意读取资源的缓冲区大小,如果太小会导致数据不完整,客户端无法解析!

5.当要访问的资源不存在,应该构建404页面

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HTTP协议
  • URL
  • HTTP的请求与响应格式
    • 请求格式
    • 响应格式
  • HTTP请求方法
    • GET方法
    • POST方法
  • 状态码
    • 理解重定向
      • 重定向
      • 临时重定向(302)
      • 永久重定向(301)
  • HTTP报头
    • 关于Connection报头
  • 代码实现
    • 前提代码
      • InetAddr.hpp
      • Log.hpp
      • Comment.hpp
      • Mutex.hpp
      • Socket.hpp
      • TcpServer.hpp
    • HTTP服务器模拟代码
      • HTTP.hpp
      • Tool.hpp
      • Main.cc
    • 代码实现注意要点
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档