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

前言:

上文我们学习到了什么是守护进程以及其原理【Linux网络】深入理解守护进程(Daemon)及其实现原理-CSDN博客

本文我们来认识应用层的协议:HTTP!


HTTP协议

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

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

HTTP协议遵循标准的请求-响应(Request-Response)模型:

请求: 客户端通过HTTP协议主动向服务器发送请求。

**响应:**服务器收到客户端发来的请求后进行处理,并将结果作为响应返回给客户端。

HTTP协议具有两个显著特点:

无连接: 每一次请求都想要建立一个新的连接,处理完既断开

**无状态:**服务器不会保存客户端的状态信息。这意味着对于服务器而言,每一次请求都是全新的,它不具有记忆功能。


URL

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

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

css 复制代码
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

cpp 复制代码
#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

cpp 复制代码
// 实现日志模块

#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

cpp 复制代码
#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

cpp 复制代码
// 封装锁接口
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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页面

相关推荐
电子小子洋酱2 小时前
Linux驱动开发学习笔记(更新中)
linux·笔记·单片机
HinsCoder2 小时前
【技能拾遗】——手机远程控制方案(中继服务器版)
服务器·阿里云·adb·智能手机·远程控制·wireguard
AI即插即用2 小时前
即插即用系列 | MICCAI EM-Net:融合 Mamba 与频域学习的高效 3D 医学图像分割网络
网络·人工智能·深度学习·神经网络·学习·计算机视觉·视觉检测
行思理2 小时前
Linux多PHP如何切换系统默认PHP版本
linux·运维·php
捧 花2 小时前
Go Web 中 WebSocket 原理与实战详解
网络·后端·websocket·网络协议·http·golang·web
Kiri霧2 小时前
Go 切片表达式
java·服务器·golang
编程乐学(Arfan开发工程师)2 小时前
渗透测试基础详解:通往 CISP-PTE 的第一步
网络·安全·web安全
jimy12 小时前
ps aux|grep pid 和 ps -p pid 的区别
java·linux·开发语言
weixin_437546332 小时前
注释文件夹下脚本的Debug
java·linux·算法