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

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

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

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

HTTP协议

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

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

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

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


URL

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

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

https://blog.ZEEKLOG.net/huangyuchi/article/details/155441819

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

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

        服务器地址:blog.ZEEKLOG.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 表示重定向;

常用具体状态码速查表

状态码英文名称含义(通俗版)常见场景
200OK成功最常见的,表示你的请求服务器收到了,也没出问题,东西给你拿回来了。
301Moved Permanently永久重定向比如网站换网址了,收藏夹里的旧链接会自动跳到新链接。
302Found临时重定向比如你点“我的订单”,因为没登录,系统把你暂时跳到“登录页”。
304Not Modified资源未修改这是一个缓存机制。服务器告诉你:“你本地存的那张图片没变,不用重新下载了,直接用吧”。
400Bad Request请求错误通常是参数写错了。比如服务器要数字,你传了个字符串。
401Unauthorized未授权“你是谁?” 表示你没登录,或者 Token 过期了。
403Forbidden禁止访问“我知道你是谁,但我不让你进”。比如普通用户想进管理员后台。
404Not Found未找到这个最出名。要么是你网址输错了,要么是那个页面被删了。
500Internal Server Error服务器内部错误后端代码抛异常了(Bug),比如空指针、数据库连不上。完全是服务器的锅。
502Bad Gateway网关错误通常是中间件(如 Nginx)虽然是好的,但后端的服务(如 Tomcat/Python)挂了或没响应。

理解重定向

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

重定向

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

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

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

临时重定向(302)

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

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

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

永久重定向(301)

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

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

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


HTTP报头

常用的报头如下

Content-Type:数据的格式(JSON 还是表单?)Content-Length:数据的大小Host:你要访问的域名(区分虚拟主机)Cookie / Set-Cookie:登录状态和身份识别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

#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

// 实现日志模块 #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

#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

// 封装锁接口 #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

#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

#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

#pragma once #include "TcpServer.hpp" #include <memory> #include "Tool.hpp" const string; 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

#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

#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页面

Read more

【Linux网络系列】从底层寻址到应用穿透:深度剖析 ARP、ICMP 协议与 FRP 内网穿透、内网打洞、代理机制

【Linux网络系列】从底层寻址到应用穿透:深度剖析 ARP、ICMP 协议与 FRP 内网穿透、内网打洞、代理机制

🔥 本文专栏:Linux网络 🌸作者主页:努力努力再努力wz 💪 今日博客励志语录:我们无法预知每一个选择的对错,但我们可以赋予每一个选择正确的意义。 引入 那么在之前的内容中,我已经介绍了应用层协议、传输层协议以及网络层协议。而这篇博客将正式进入 TCP/IP 体系结构中的最后一层——数据链路层协议。 我们知道,当一个 IP 数据包从当前主机发送出去之后,接下来便会进入外部网络,也就是运营商所构成的路由器网络。随后,该数据包会在运营商的多个路由器之间被逐跳转发,最终到达目标主机。 对于网络中间的路由器节点来说,当其接收到一个数据帧时,首先会对数据帧的帧头和帧尾进行校验(例如通过 FCS 等机制验证帧的完整性)。如果校验没有问题,随后才会将其中的 IP 数据包递交给网络层协议栈进行处理。具体来说,就是从 IP 报文中提取目标 IP 地址,并根据该地址查询本地的路由表,从而确定下一跳设备的 IP 地址以及对应的出口接口。 在获取到下一跳设备的 IP 地址以及出口接口之后,路由器会将该 IP

By Ne0inhk
WSL2 + Ubuntu 22.04 全流程安装与避坑指南(适配 D 盘)

WSL2 + Ubuntu 22.04 全流程安装与避坑指南(适配 D 盘)

适用于:Windows 10/11 用户 目标:在 D 盘上部署完整可联网的 Ubuntu 22.04 + GPU 支持的开发环境 作者:jiahao(实际踩坑实践) 更新时间:2025-10 📋 一、前置条件检查 * Windows 已升级到 21H2 或更高版本; * 已安装 NVIDIA 显卡驱动 ≥ 510(支持 CUDA 12); * Windows 已安装 PowerShell 7+; * 本机具备管理员权限; * 可联网。 🧹 二、彻底清理旧版 WSL 环境 很多用户安装不干净是因为旧版本遗留。执行以下命令可完全重置。 1️⃣ 查看已安装发行版 wsl --list --all 如果看到:

By Ne0inhk
Linux 进程间通信之管道基础解析 —— 匿名管道的原理与实现

Linux 进程间通信之管道基础解析 —— 匿名管道的原理与实现

🔥草莓熊Lotso:个人主页 ❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》 ✨生活是默默的坚持,毅力是永久的享受! 🎬 博主简介: 文章目录 * 前言: * 一. 进程间通信基础认知 * 1.1 进程间通信的核心目的 * 1.2 进程间通信的发展与分类 * 二. 管道的基础概念 * 2.1 管道的定义 * 2.2 管道的核心特性(最后总结部分的图片里更全点,可以着重看那个) * 三. 匿名管道的创建与 API * 3.1 匿名管道的创建函数 * 3.2 匿名管道的简单使用示例 * 四. 基于 fork 的匿名管道跨进程通信 * 4.1 fork 共享管道的核心原理 * 4.2

By Ne0inhk