應用層協議 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)
前置知識:
- 我的數據給別人,別人的數據給我,就是 IO 操作,也就是說:上網的行為就是 IO
- 請求的資源:圖片,視頻,音頻,文本,本質就是文件。
- 先要確認我要的資源在那一臺服務器上(IP 地址),在什么路徑下(文件路徑)
- URL 中的 “/” 不一定是根目錄,它是 Web 根目錄,二者不一樣。
- 為什么沒有端口號?在成熟的應用層協議中,默認存在固定的端口號,HTTP 的默認端口號是80
2. urlencode 和 urldecode
像 / ? : 等這樣的字符,已經被 url 當做特殊意義理解了,因此這些字符不能隨意出現,比如,某個參數中需要帶有這些特殊字符,就必須先對特殊字符進行轉義,轉義的規則如下:
將需要轉碼的字符轉為 16 進制,然后從右到左,取 4 位(不足 4 位直接處理),每 2 位做一位,前面加上%,編碼成%XY 格式,例如:
3. 請求與響應格式
HTTP 請求:
- 首行:[請求方法] + [url] + [版本]
- Header:請求的屬性,冒號分割的鍵值對。每組屬性之間使用\r\n 分隔,遇到空行表示 Header 部分結束。
- Body:空行后面的內容都是 Body,Body 允許為空字符串,如果 Body 存在,則在Header 中會有一個 Content-Length 屬性來標識 Body 的長度。
HTTP 響應:
- 首行:[版本號] + [狀態碼] + [狀態碼解釋]
- Header:請求的屬性,冒號分割的鍵值對,每組屬性之間使用\r\n 分隔,遇到空行表示 Header 部分結束。
- Body:空行后面的內容都是 Body,Body 允許為空字符串,如果 Body 存在,則在 Header 中會有一個 Content-Length 屬性來標識 Body 的長度,如果服務器返回了一個 html 頁面, 那么 html 頁面內容就是在 body 中。
基本的應答格式:
二. HTTP 請求方法
方法 | 說明 | 支持的 HTTP 協議版本 |
---|---|---|
GET | 獲取資源 | 1.0、1.1 |
POST | 傳輸實體主體 | 1.0、1.1 |
PUT | 傳輸文件 | 1.0、1.1 |
HEAD | 獲取報文首部 | 1.0、1.1 |
DELETE | 刪除文件 | 1.0、1.1 |
OPTIONS | 詢問支持的方法 | 1.1 |
TRACE | 追蹤路徑 | 1.1 |
CONNECT | 要求用隧道協議連接代理 | 1.1 |
LINK | 建立和資源之間的聯系 | 1.0 |
UNLINK | 斷開鏈接關系 | 1.0 |
GET 和 POST 是 HTTP 協議中最常用的兩種請求方法,用于客戶端與服務器之間的數據交互。
1. GET 和 POST (重點)
特性 | GET | POST |
---|---|---|
用途 | 用于請求 URL 指定的資源 | 提交數據到服務器 |
數據位置 | 參數附加在 URL 中 | 參數放在請求體(Body)中 |
數據可見性 | URL 中明文顯示,不安全 | 數據不可見,相對安全 |
數據長度限制 | 受限于 URL 長度(通常 ≤ 2048 字節) | 無限制(理論上) |
常見場景 | 搜索、瀏覽頁面、獲取 API 數據 | 表單提交、上傳文件、用戶登錄 |
- GET 的參數:通過 ? 附加在 URL 后,多個參數用 & 分隔!
- 瀏覽器默認使用 GET 發起請求(例如:直接輸入 URL 或點擊鏈接)
- HTTP 協議本身是明文傳輸的,無論是 GET 還是 POST 方法,數據在網絡中傳輸時都可能被抓包,需要 HTTPS 協議對數據進行加密!
三. HTTP 狀態碼
狀態碼 | 類別 | 說明 |
---|---|---|
1XX | Informational(信息性狀態碼) | 接收的請求正在處理 |
2XX | Success(成功狀態碼) | 請求正常處理方式 |
3XX | Redirection(重定向狀態碼) | 需要進行附加操作以完成請求 |
4XX | Client Error(客戶端錯誤狀態碼) | 服務器無法處理請求 |
5XX | Server Error(服務器錯誤狀態碼) | 服務器處理錯誤請求 |
最常見的狀態碼,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)
狀態碼 | 狀態碼描述 | 應用樣例 |
---|---|---|
100 | Continue | 上傳大文件時,服務器告訴客戶端可以繼續上傳 |
200 | OK | 訪問網站首頁,服務器返回網頁內容 |
201 | Created | 發布新文章,服務器返回文章創建成功的信息 |
204 | No Content | 刪除文章后,服務器返回“無內容”表示操作成功 |
301 | Moved Permanently | 網站換域名后,自動跳轉到新域名;搜索引擎更新網站鏈接時使用 |
302 | Found 或 See Other | 用戶登錄成功后,重定向到用戶首頁 |
304 | Not Modified | 瀏覽器緩存機制,對未修改的資源返回 304 狀態碼 |
400 | Bad Request | 填寫表單時,格式不正確導致提交失敗 |
401 | Unauthorized | 訪問需要登錄的頁面時,未登錄或認證失敗 |
403 | Forbidden | 嘗試訪問你沒有權限查看的頁面 |
404 | Not Found | 訪問不存在的網頁鏈接 |
500 | Internal Server Error | 服務器崩潰或數據庫錯誤導致頁面無法加載 |
502 | Bad Gateway | 使用代理服務器時,代理服務器無法從上游服務器獲取有效響應 |
503 | Service Unavailable | 服務器維護或過載,暫時無法處理請求 |
以下是僅包含重定向相關狀態碼的表格:
狀態碼 | 狀態碼描述 | 重定向類型 | 應用樣例 |
---|---|---|---|
301 | Moved Permanently | 永久重定向 | 網站換域名后,自動跳轉到新域名;搜索引擎更新網站鏈接時使用 |
302 | Found 或 See Other | 臨時重定向 | 用戶登錄成功后,重定向到用戶首頁 |
307 | Temporary Redirect | 臨時重定向 | 臨時重定向資源到新的位置(較少使用) |
308 | Permanent Redirect | 永久重定向 | 永久重定向資源到新的位置(較少使用) |
- HTTP 狀態碼 301(永久重定向)和 302(臨時重定向)都依賴 Location 選項。以下是關于兩者依賴 Location 選項的詳細說明:
HTTP 狀態碼 301(永久重定向):
- 當服務器返回 HTTP 301 狀態碼時,表示請求的資源已經被永久移動到新的位置。
- 在這種情況下,服務器會在響應中添加一個 Location 頭部,用于指定資源的新位置。這個 Location 頭部包含了新的 URL 地址,瀏覽器會自動重定向到該地址。
- 例如,在 HTTP 響應中,可能會看到類似于以下的頭部信息:
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 服務器
- Makefile
httpserver:HttpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -rf httpserver
- 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; // 使用引用: 互斥鎖不支持拷貝};
}
- 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;};
}
- 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; // 進程pidstd::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()
}
- 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;
}
- 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);}}
}
- 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; // 主機序列: IPuint16_t _port; // 主機序列: 端口號
};
- 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);}// 父進程關閉sockfdsockfd->Close();waitpid(id, nullptr, 0); // 子進程直接退出, 父進程無需阻塞等待}_isrunning = false;}private:std::unique_ptr<Socket> _listensockp;bool _isrunning;tcphandler_t _handler;int _port;};
}
- 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.6void 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.1void 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=123456auto pos = _uri.find("?"); if (pos != std::string::npos) // 存在?帶參數{_isexec = true;_path = _uri.substr(0, pos);_args = _uri.substr(pos + 1);}else // 不存在?不帶參數{_isexec = false;}}}}// 返回請求的資源: uristd::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.jpgauto pos = _uri.rfind(".");if (pos == std::string::npos)return std::string(".html");elsereturn _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&yyystd::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.htmlstd::string uri = default_home_path + req.Uri(); // wwwroot/if (uri.back() == '/'){uri += first_page; // wwwroot/index.htmlreq.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";elsereturn "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; // 返回給用戶的內容: 響應正文
};
- 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; // 功能路由
};
- 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=123456LOG(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;
}
-
前端代碼
點擊跳轉 -
運行操作
# 啟動http服務器
xzy@hcss-ecs-b3aa:~$ ./httpserver 8888
瀏覽器輸入:云服務器IP地址:端口號(例如:http://123.60.170.90:8888/)
效果如下: