【Linux】應用層協議 HTTP

應用層協議 HTTP

  • 一. HTTP 協議
    • 1. URL 地址
    • 2. urlencode 和 urldecode
    • 3. 請求與響應格式
  • 二. HTTP 請求方法
    • 1. GET 和 POST (重點)
  • 三. HTTP 狀態碼
  • 四. HTTP 常見報頭
  • 五. 手寫 HTTP 服務器

HTTP(超文本傳輸協議)是一種應用層協議,用于在萬維網上進行超文本傳輸。它是現代互聯網的基礎協議之一,主要用于瀏覽器和服務器之間的通信,用于請求和響應網頁內容。HTTP協議是無連接的、無狀態的,基于請求-響應模型。

  • 無連接:客戶端和服務器之間不需要建立長期的連接,每個請求/響應對完成后,連接即被關閉。
  • 無狀態:請求/響應對都是獨立的,服務器不會保存客戶端請求之間的任何狀態信息。

一. HTTP 協議

1. URL 地址

平時我們俗稱的 “網址” 其實就是說的 URL(Uniform Resource Locator),“統一資源定位符”

例如:https://news.qq.com/rain/a/20250326A01C0V00

  • news.qq.com:域名,公網 IP 地址。
  • rain/a/20250326A01C0V00:服務器路徑下的文件(html、css、js)

前置知識:

  1. 我的數據給別人,別人的數據給我,就是 IO 操作,也就是說:上網的行為就是 IO
  2. 請求的資源:圖片,視頻,音頻,文本,本質就是文件。
  3. 先要確認我要的資源在那一臺服務器上(IP 地址),在什么路徑下(文件路徑)
  4. URL 中的 “/” 不一定是根目錄,它是 Web 根目錄,二者不一樣。
  5. 為什么沒有端口號?在成熟的應用層協議中,默認存在固定的端口號,HTTP 的默認端口號是80

2. urlencode 和 urldecode

像 / ? : 等這樣的字符,已經被 url 當做特殊意義理解了,因此這些字符不能隨意出現,比如,某個參數中需要帶有這些特殊字符,就必須先對特殊字符進行轉義,轉義的規則如下:

將需要轉碼的字符轉為 16 進制,然后從右到左,取 4 位(不足 4 位直接處理),每 2 位做一位,前面加上%,編碼成%XY 格式,例如:

在這里插入圖片描述

3. 請求與響應格式

HTTP 請求:

在這里插入圖片描述

  • 首行:[請求方法] + [url] + [版本]
  • Header:請求的屬性,冒號分割的鍵值對。每組屬性之間使用\r\n 分隔,遇到空行表示 Header 部分結束。
  • Body:空行后面的內容都是 Body,Body 允許為空字符串,如果 Body 存在,則在Header 中會有一個 Content-Length 屬性來標識 Body 的長度。

在這里插入圖片描述

HTTP 響應:

在這里插入圖片描述

  • 首行:[版本號] + [狀態碼] + [狀態碼解釋]
  • Header:請求的屬性,冒號分割的鍵值對,每組屬性之間使用\r\n 分隔,遇到空行表示 Header 部分結束。
  • Body:空行后面的內容都是 Body,Body 允許為空字符串,如果 Body 存在,則在 Header 中會有一個 Content-Length 屬性來標識 Body 的長度,如果服務器返回了一個 html 頁面, 那么 html 頁面內容就是在 body 中。

在這里插入圖片描述

基本的應答格式:

在這里插入圖片描述

二. HTTP 請求方法

方法說明支持的 HTTP 協議版本
GET獲取資源1.0、1.1
POST傳輸實體主體1.0、1.1
PUT傳輸文件1.0、1.1
HEAD獲取報文首部1.0、1.1
DELETE刪除文件1.0、1.1
OPTIONS詢問支持的方法1.1
TRACE追蹤路徑1.1
CONNECT要求用隧道協議連接代理1.1
LINK建立和資源之間的聯系1.0
UNLINK斷開鏈接關系1.0

GET 和 POST 是 HTTP 協議中最常用的兩種請求方法,用于客戶端與服務器之間的數據交互。

1. GET 和 POST (重點)

特性GETPOST
用途用于請求 URL 指定的資源提交數據到服務器
數據位置參數附加在 URL 中參數放在請求體(Body)中
數據可見性URL 中明文顯示,不安全數據不可見,相對安全
數據長度限制受限于 URL 長度(通常 ≤ 2048 字節)無限制(理論上)
常見場景搜索、瀏覽頁面、獲取 API 數據表單提交、上傳文件、用戶登錄

在這里插入圖片描述

  • GET 的參數:通過 ? 附加在 URL 后,多個參數用 & 分隔!
  • 瀏覽器默認使用 GET 發起請求(例如:直接輸入 URL 或點擊鏈接)
  • HTTP 協議本身是明文傳輸的,無論是 GET 還是 POST 方法,數據在網絡中傳輸時都可能被抓包,需要 HTTPS 協議對數據進行加密!

三. HTTP 狀態碼

狀態碼類別說明
1XXInformational(信息性狀態碼)接收的請求正在處理
2XXSuccess(成功狀態碼)請求正常處理方式
3XXRedirection(重定向狀態碼)需要進行附加操作以完成請求
4XXClient Error(客戶端錯誤狀態碼)服務器無法處理請求
5XXServer Error(服務器錯誤狀態碼)服務器處理錯誤請求

最常見的狀態碼,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)

狀態碼狀態碼描述應用樣例
100Continue上傳大文件時,服務器告訴客戶端可以繼續上傳
200OK訪問網站首頁,服務器返回網頁內容
201Created發布新文章,服務器返回文章創建成功的信息
204No Content刪除文章后,服務器返回“無內容”表示操作成功
301Moved Permanently網站換域名后,自動跳轉到新域名;搜索引擎更新網站鏈接時使用
302Found 或 See Other用戶登錄成功后,重定向到用戶首頁
304Not Modified瀏覽器緩存機制,對未修改的資源返回 304 狀態碼
400Bad Request填寫表單時,格式不正確導致提交失敗
401Unauthorized訪問需要登錄的頁面時,未登錄或認證失敗
403Forbidden嘗試訪問你沒有權限查看的頁面
404Not Found訪問不存在的網頁鏈接
500Internal Server Error服務器崩潰或數據庫錯誤導致頁面無法加載
502Bad Gateway使用代理服務器時,代理服務器無法從上游服務器獲取有效響應
503Service Unavailable服務器維護或過載,暫時無法處理請求

以下是僅包含重定向相關狀態碼的表格:

狀態碼狀態碼描述重定向類型應用樣例
301Moved Permanently永久重定向網站換域名后,自動跳轉到新域名;搜索引擎更新網站鏈接時使用
302Found 或 See Other臨時重定向用戶登錄成功后,重定向到用戶首頁
307Temporary Redirect臨時重定向臨時重定向資源到新的位置(較少使用)
308Permanent Redirect永久重定向永久重定向資源到新的位置(較少使用)
  • HTTP 狀態碼 301(永久重定向)和 302(臨時重定向)都依賴 Location 選項。以下是關于兩者依賴 Location 選項的詳細說明:

HTTP 狀態碼 301(永久重定向):

  • 當服務器返回 HTTP 301 狀態碼時,表示請求的資源已經被永久移動到新的位置。
  • 在這種情況下,服務器會在響應中添加一個 Location 頭部,用于指定資源的新位置。這個 Location 頭部包含了新的 URL 地址,瀏覽器會自動重定向到該地址。
  • 例如,在 HTTP 響應中,可能會看到類似于以下的頭部信息:
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n

HTTP 狀態碼 302(臨時重定向):

  • 當服務器返回 HTTP 302 狀態碼時,表示請求的資源臨時被移動到新的位置。
  • 同樣地,服務器也會在響應中添加一個 Location 頭部來指定資源的新位置。瀏覽器會暫時使用新的 URL 進行后續的請求,但不會緩存這個重定向。
  • 例如,在 HTTP 響應中,可能會看到類似于以下的頭部信息:
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n

總結:無論是 HTTP 301 還是 HTTP 302 重定向,都需要依賴 Location 選項來指定資源的新位置。這個 Location 選項是一個標準的 HTTP 響應頭部,用于告訴瀏覽器應該將請求重定向到哪個新的 URL 地址。

  • 爬蟲原理:模擬瀏覽器向目標網站發送 HTTP/HTTPS 請求,獲取服務器返回的 HTML/XML 頁面內容,從當前頁面提取所有 URL,加入待爬隊列(避免重復抓取,通過 URL 去重),將提取的數據存入數據庫/文件/內存中。
  • 搜索引擎:核心功能是從互聯網上獲取信息并為用戶提供精準的搜索結果,而這一過程的基礎正是爬蟲能力

四. HTTP 常見報頭

  • Content-Type:數據類型(例如:text/html)
  • Content-Length:正文的長度。
  • Host:客戶端告知服務器,所請求的資源是在哪個主機的哪個端口上。
  • User-Agent:聲明用戶的操作系統和瀏覽器版本信息。
  • Referer:當前頁面是從哪個頁面跳轉過來的。
  • Location:搭配 3XX 狀態碼使用,告訴客戶端接下來要去哪里訪問。
  • Set-Cookie:用于在客戶端存儲少量信息。通常用于實現會話(session)的功能。

五. 手寫 HTTP 服務器

  1. Makefile
httpserver:HttpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -rf httpserver
  1. Mutex.hpp
#pragma once#include <pthread.h>namespace MutexModule
{class Mutex{Mutex(const Mutex &m) = delete;const Mutex &operator=(const Mutex &m) = delete;public:Mutex(){::pthread_mutex_init(&_mutex, nullptr);}~Mutex(){::pthread_mutex_destroy(&_mutex);}void Lock(){::pthread_mutex_lock(&_mutex);}void Unlock(){::pthread_mutex_unlock(&_mutex);}pthread_mutex_t *LockAddr() { return &_mutex; }private:pthread_mutex_t _mutex;};class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex; // 使用引用: 互斥鎖不支持拷貝};
}
  1. Socket.hpp
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdlib>#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"using namespace LogModule;const int gdefaultsockfd = -1;
const int gbacklog = 8;namespace SocketModule
{class Socket;using SockPtr = std::shared_ptr<Socket>;// 模版方法模式// 基類: 規定創建Socket方法class Socket{public:virtual ~Socket() = default;virtual void SocketOrDie() = 0;virtual void SetSocketOpt() = 0;virtual bool BindOrDie(int port) = 0;virtual bool ListenOrDie() = 0;virtual SockPtr AcceptOrDie(InetAddr *client) = 0;virtual void Close() = 0;virtual int Recv(std::string *out) = 0;virtual int Send(const std::string &in) = 0;virtual int Fd() = 0;// 提供創建TCP套接字的固定格式void BuildTcpSocketMethod(int port){SocketOrDie();SetSocketOpt();BindOrDie(port);ListenOrDie();}};class TcpSocket : public Socket{public:TcpSocket(int sockfd = gdefaultsockfd): _sockfd(sockfd){}virtual ~TcpSocket() {}virtual void SocketOrDie() override{_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);if (_sockfd < 0){LOG(LogLevel::DEBUG) << "socket error";exit(SOCKET_ERR);}LOG(LogLevel::DEBUG) << "socket success, sockfd: " << _sockfd;}virtual void SetSocketOpt() override{// 保證服務器在異常斷開之后可以立即重啟, 不會存在bind error問題!int opt = 1;::setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));}virtual bool BindOrDie(int port) override{if (_sockfd == gdefaultsockfd)return false;InetAddr addr(port);int n = ::bind(_sockfd, addr.NetAddr(), addr.NetAddrLen());if (n < 0){LOG(LogLevel::DEBUG) << "bind error";exit(BIND_ERR);}LOG(LogLevel::DEBUG) << "bind success, sockfd: " << _sockfd;return true;}virtual bool ListenOrDie() override{if (_sockfd == gdefaultsockfd)return false;int n = ::listen(_sockfd, gbacklog);if (n < 0){LOG(LogLevel::DEBUG) << "listen error";exit(LISTEN_ERR);}LOG(LogLevel::DEBUG) << "listen success, sockfd: " << _sockfd;return true;}// 返回: 文件描述符 && 客戶端信息virtual SockPtr AcceptOrDie(InetAddr *client) override{struct sockaddr_in peer;socklen_t len = sizeof(peer);int newsockfd = ::accept(_sockfd, CONV(&peer), &len);if (newsockfd < 0){LOG(LogLevel::DEBUG) << "accept error";return nullptr;}client->SetAddr(peer);return std::make_shared<TcpSocket>(newsockfd);}virtual void Close() override{if (_sockfd == gdefaultsockfd)return;::close(_sockfd);}virtual int Recv(std::string *out) override{char buffer[1024 * 8];int n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);if(n > 0){buffer[n] = 0;*out = buffer;}return n;}virtual int Send(const std::string &in) override{int n = ::send(_sockfd, in.c_str(), in.size(), 0);return n;}virtual int Fd() override{return _sockfd;}private:int _sockfd;};
}
  1. Log.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"namespace LogModule
{using namespace MutexModule;// 獲取系統時間std::string CurrentTime(){time_t time_stamp = ::time(nullptr); // 獲取時間戳struct tm curr;localtime_r(&time_stamp, &curr); // 將時間戳轉化為可讀性強的信息char buffer[1024];snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",curr.tm_year + 1900,curr.tm_mon + 1,curr.tm_mday,curr.tm_hour,curr.tm_min,curr.tm_sec);return buffer;}// 日志文件: 默認路徑和默認文件名const std::string defaultlogpath = "./log/";const std::string defaultlogname = "log.txt";// 日志等級enum class LogLevel{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string Level2String(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::INFO:return "INFO";case LogLevel::WARNING:return "WARNING";case LogLevel::ERROR:return "ERROR";case LogLevel::FATAL:return "FATAL";default:return "NONE";}}// 3. 策略模式: 刷新策略class LogStrategy{public:virtual ~LogStrategy() = default;// 純虛函數: 無法實例化對象, 派生類可以重載該函數, 實現不同的刷新方式virtual void SyncLog(const std::string &message) = 0;};// 3.1 控制臺策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy() {}~ConsoleLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << std::endl;}private:Mutex _mutex;};// 3.2 文件級(磁盤)策略class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname): _logpath(logpath), _logname(logname){// 判斷_logpath目錄是否存在if (std::filesystem::exists(_logpath)){return;}try{std::filesystem::create_directories(_logpath);}catch (std::filesystem::filesystem_error &e){std::cerr << e.what() << std::endl;}}~FileLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string log = _logpath + _logname;std::ofstream out(log, std::ios::app); // 以追加的方式打開文件if (!out.is_open()){return;}out << message << "\n"; // 將信息刷新到out流中out.close();}private:std::string _logpath;std::string _logname;Mutex _mutex;};// 4. 日志類: 構建日志字符串, 根據策略進行刷新class Logger{public:Logger(){// 默認往控制臺上刷新_strategy = std::make_shared<ConsoleLogStrategy>();}~Logger() {}void EnableConsoleLog(){_strategy = std::make_shared<ConsoleLogStrategy>();}void EnableFileLog(){_strategy = std::make_shared<FileLogStrategy>();}// 內部類: 記錄完整的日志信息class LogMessage{public:LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger): _currtime(CurrentTime()), _level(level), _pid(::getpid()), _filename(filename), _line(line), _logger(logger){std::stringstream ssbuffer;ssbuffer << "[" << _currtime << "] "<< "[" << Level2String(_level) << "] "<< "[" << _pid << "] "<< "[" << _filename << "] "<< "[" << _line << "] - ";_loginfo = ssbuffer.str();}~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}template <class T>LogMessage &operator<<(const T &info){std::stringstream ssbuffer;ssbuffer << info;_loginfo += ssbuffer.str();return *this;}private:std::string _currtime;  // 當前日志時間LogLevel _level;       // 日志水平pid_t _pid;            // 進程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()
}
  1. Common.hpp
#pragma once#include <iostream>
#include <string>#define Die(code)   \do              \{               \exit(code); \} while (0)#define CONV(v) (struct sockaddr *)(v)enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};bool ParseOneLine(std::string &str, std::string *out, const std::string &sep)
{auto pos = str.find(sep);if (pos == std::string::npos)return false;*out = str.substr(0, pos);str.erase(0, pos + sep.size());return true;
}// Connection: keep-alive
// 解析后: key = Connection; value = keep-alive
bool SplitString(const std::string &header, const std::string sep, std::string *key, std::string *value)
{auto pos = header.find(sep);if (pos == std::string::npos)return false;*key = header.substr(0, pos);*value = header.substr(pos + sep.size());return true;
}
  1. Deamon.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>#define ROOT "/"
#define devnull "/dev/null"void Deamon(bool ischdir, bool isclose)
{// 1. 守護進程一般要屏蔽一些特定的信號signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 2. 成為非組長進程: 創建子進程if (fork())exit(0);// 3. 建立新會話setsid();// 4. 每一個進程都有自己的CWD, 是否將其修改為根目錄if (ischdir)chdir(ROOT);// 5. 脫離終端: 將標準輸入、輸出重定向到字符文件"/dev/null"中if (isclose){::close(0);::close(1);::close(2);}else{// 建議這樣!int fd = ::open(devnull, O_WRONLY);if (fd > 0){::dup2(fd, 0);::dup2(fd, 1);::dup2(fd, 2);::close(fd);}}
}
  1. InetAddr.hpp
#pragma once#include <iostream>
#include <string>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "Common.hpp"class InetAddr
{
private:// 端口號: 網絡序列->主機序列void PortNetToHost(){_port = ::ntohs(_net_addr.sin_port);}// IP: 網絡序列->主機序列void IpNetToHost(){char ipbuffer[64];::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));_ip = ipbuffer;}public:InetAddr() {}InetAddr(const struct sockaddr_in &addr): _net_addr(addr){PortNetToHost();IpNetToHost();}InetAddr(uint16_t port): _port(port), _ip(""){_net_addr.sin_family = AF_INET;_net_addr.sin_port = ::htons(_port);_net_addr.sin_addr.s_addr = INADDR_ANY;}~InetAddr() {}bool operator==(const InetAddr &addr) { return _ip == addr._ip && _port == addr._port; }struct sockaddr *NetAddr() { return CONV(&_net_addr); }socklen_t NetAddrLen() { return sizeof(_net_addr); }std::string Ip() { return _ip; }uint16_t Port() { return _port; }std::string Addr() { return Ip() + ":" + std::to_string(Port()); }void SetAddr(sockaddr_in &client){_net_addr = client;PortNetToHost();IpNetToHost();}private:struct sockaddr_in _net_addr;std::string _ip; // 主機序列: IPuint16_t _port;  // 主機序列: 端口號
};
  1. TcpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <sys/wait.h>#include "Socket.hpp"
#include "InetAddr.hpp"using namespace SocketModule;
using namespace LogModule;using tcphandler_t = std::function<bool(SockPtr, InetAddr)>;namespace TcpServerModule
{class TcpServer{public:TcpServer(int port): _listensockp(std::make_unique<TcpSocket>()), _isrunning(false), _port(port){}~TcpServer(){_listensockp->Close();}void InitServer(tcphandler_t handler){_listensockp->BuildTcpSocketMethod(_port);_handler = handler;}void Loop(){_isrunning = true;while (_isrunning){// 1. 獲取連接: 獲取網絡通信sockfd && 客戶端的InetAddr clientaddr;auto sockfd = _listensockp->AcceptOrDie(&clientaddr);if (sockfd == nullptr)continue;LOG(LogLevel::DEBUG) << "get a new client info is: " << clientaddr.Addr();// 2. IO處理pid_t id = fork();if (id == 0){// 子進程關閉listensockfd_listensockp->Close();if (fork() > 0)exit(0); // 子進程直接退出// 孫子進程進行IO處理_handler(sockfd, clientaddr);exit(0);}// 父進程關閉sockfdsockfd->Close();waitpid(id, nullptr, 0); // 子進程直接退出, 父進程無需阻塞等待}_isrunning = false;}private:std::unique_ptr<Socket> _listensockp;bool _isrunning;tcphandler_t _handler;int _port;};
}
  1. HttpProtocol.hpp
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <sstream>
#include <fstream>#include "Common.hpp"const std::string Sep = "\r\n";
const std::string LineSep = " ";
const std::string HeaderLineSep = ": ";
const std::string BlankLine = "\r\n";const std::string default_home_path = "wwwroot"; // 瀏覽器的請求的默認服務器路徑
const std::string http_version = "HTTP/1.0";     // http的版本
const std::string page_404 = "wwwroot/404.html"; // 404頁面
const std::string first_page = "index.html";     // 首頁// 瀏覽器/服務器模式(B/S): 瀏覽器充當客戶端, 發送請求; 輸入: 123.60.170.90:8080
class HttpRequset
{
public:HttpRequset() {}~HttpRequset() {}// 瀏覽器具有自動識別http請求的能力, 可以充當客戶端// 瀏覽器發送的http請求(序列化數據)如下:// GET /favicon.ico HTTP/1.1// Host: 123.60.170.90:8080// Connection: keep-alive// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0// Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8// Referer: http://123.60.170.90:8080/// Accept-Encoding: gzip, deflate// Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.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;          // 返回給用戶的內容: 響應正文
};
  1. HttpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <unordered_map>#include "TcpServer.hpp"
#include "HttpProtocol.hpp"using namespace TcpServerModule;using http_handler_t = std::function<void(HttpRequset &, HttpResponse &)>;class HttpServer
{
public:HttpServer(int port): _tsvr(std::make_unique<TcpServer>(port)){}~HttpServer() {}void Register(std::string funcname, http_handler_t func){_route[funcname] = func;}void Start(){_tsvr->InitServer([this](SockPtr sockfd, InetAddr client){ return this->HanlerRequset(sockfd, client); });_tsvr->Loop();}bool SafeCheck(const std::string &service){auto iter = _route.find(service);return iter != _route.end();}bool HanlerRequset(SockPtr sockfd, InetAddr client){LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();// 1. 讀取瀏覽器發送的http請求std::string http_requset;sockfd->Recv(&http_requset);// 2. 請求反序列化HttpRequset req;req.Deserialize(http_requset);// 3. 根據請求構建響應HttpResponse resp;if (req.IsHasArgs()) // 動態交互請求(含有參數): 登入, 注冊... {// GET 請求的參數在 URL 中// POST請求的參數在 body中std::string service = req.Path();if(SafeCheck(service)){_route[service](req, resp); // login}else{resp.Build(req);}}else // 請求一般的靜態資源(不含參數): 網頁, 圖片, 視頻...{resp.Build(req);}// 4. 響應序列化std::string http_response;resp.Serialize(&http_response);// 5. 發送響應給用戶sockfd->Send(http_response);return true;}private:std::unique_ptr<TcpServer> _tsvr;std::unordered_map<std::string, http_handler_t> _route; // 功能路由
};
  1. HttpServer.cc
#include "HttpServer.hpp"
#include "Deamon.hpp"using namespace LogModule;// 登入功能
void Login(HttpRequset &req, HttpResponse &resp)
{// 根據 req 動態構建 resp: // Path: /login// Args: name=zhangsan&password=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;
}
  1. 前端代碼
    點擊跳轉

  2. 運行操作

# 啟動http服務器
xzy@hcss-ecs-b3aa:~$ ./httpserver 8888

瀏覽器輸入:云服務器IP地址:端口號(例如:http://123.60.170.90:8888/)

效果如下:

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/75840.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/75840.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/75840.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【活動回顧】StarRocks Singapore Meetup #2 @Shopee

3 月 13 日&#xff0c;StarRocks 社區在新加坡成功舉辦了第二場 Meetup 活動&#xff0c;主題為“Empowering Customer-Facing Analytics”。本次活動在 Shopee 新加坡辦公室舉行&#xff0c;吸引了來自 Shopee、Grab 和 Pinterest 的專家講師以及 50 多位參會者。大家圍繞電商…

Retinexformer:基于 Retinex 的單階段 Transformer 低光照圖像增強方法

開頭發點牢騷&#xff1a;本來做的好好都都要中期了&#xff0c;導師怎么突然給我換題目啊。真是繃不住了......又要從頭開始學了&#xff0c;唉&#xff01; 原論文鏈接&#xff1a;Retinexformer: One-stage Retinex-based Transformer for Low-light Image Enhancement 低光…

后端——AOP異步日志

需求分析 在SpringBoot系統中&#xff0c;一般會對訪問系統的請求做日志記錄的需求&#xff0c;確保系統的安全維護以及查看接口的調用情況&#xff0c;可以使用AOP對controller層的接口進行增強&#xff0c;作日志記錄。日志保存在數據庫當中&#xff0c;為了避免影響接口的響…

flink廣播算子Broadcast

文章目錄 一、Broadcast二、代碼示例三.或者第二種(只讀取一個csv文件到廣播內存中)提示:以下是本篇文章正文內容,下面案例可供參考 一、Broadcast 為了關聯一個非廣播流(keyed 或者 non-keyed)與一個廣播流(BroadcastStream),我們可以調用非廣播流的方法 connect(),…

Redis 和 MySQL雙寫一致性的更新策略有哪些?常見面試題深度解答。

目錄 一. 業務數據查詢&#xff0c;更新順序簡要分析 二. 更新數據庫、查詢數據庫、更新緩存、查詢緩存耗時對比 2.1 更新數據庫&#xff08;最慢&#xff09; 2.2 查詢數據庫&#xff08;較慢&#xff09; 2.3 更新緩存&#xff08;次快&#xff09; 2.4 查詢緩存&#…

SRT協議

SRT&#xff08;Secure Reliable Transport&#xff09;是一種開源的視頻傳輸協議&#xff0c;專為高丟包、高延遲網絡環境設計&#xff0c;結合了UDP的低延遲和TCP的可靠性&#xff0c;廣泛應用于直播、遠程制作、視頻會議等場景。 定位&#xff1a;SRT協議的官方C/C實現庫&am…

“征服HTML引號惡魔:“完全解析手冊”!!!(quot;表示雙引號)

&#x1f6a8;&#x1f4e2; "征服HTML引號惡魔&#xff1a;“完全解析手冊” &#x1f4e2;&#x1f6a8; &#x1f3af; 博客引言&#xff1a;當引號變成"惡魔" &#x1f631; 是否遇到過這種情況&#xff1a; 寫HTML時滿心歡喜輸入<div title"他…

npm install 卡在創建項目:sill idealTree buildDeps

參考&#xff1a; https://blog.csdn.net/PengXing_Huang/article/details/136460133 或者再執行 npm install -g cnpm --registryhttps://registry.npm.taobao.org 或者換梯子

c++中cpp文件從編譯到執行的過程

C 文件從編寫到執行的過程可以分為幾個主要階段&#xff1a;編寫代碼、預處理、編譯、匯編、鏈接和運行。以下是每個階段的詳細說明&#xff1a; 1. 編寫代碼 這是整個過程的起點。程序員使用文本編輯器&#xff08;如 VSCode、Sublime Text 或其他 IDE&#xff09;編寫 C 源…

PROE 與 STL 格式轉換:開啟 3D 打印及多元應用的大門

在 3D 設計與制造的復雜生態中&#xff0c;將 PROE 格式轉換為 STL 格式絕非無端之舉&#xff0c;而是有著深厚且多元的現實需求作為支撐。 一、文件格式介紹? &#xff08;一&#xff09;PROE 格式? PROE 作為一款參數化設計軟件&#xff0c;采用基于特征的參數化建模技術…

開發中后端返回下劃線數據,要不要統一轉駝峰?

先說結論。看情況&#xff01;&#xff01;&#xff01;&#xff01; 前端 主要用 JS/TS 建議后端返回 camelCase&#xff0c;減少前端轉換成本。后端 主要是 Python/Go 建議保持 snake_case&#xff0c;前端做轉換。但是團隊統一風格最重要&#xff01;如果統一返回駝峰就駝峰…

docker pull時報錯:https://registry-1.docker.io/v2/

原文&#xff1a;https://www.cnblogs.com/sdgtxuyong/p/18647915 https://www.cnblogs.com/OneSeting/p/18532166 docker 換源&#xff0c;解決連接不上的問題。 編輯以下文件&#xff0c;不存在則創建&#xff1a; vim /etc/docker/daemon.json {"registry-mirrors&qu…

Pytorch學習筆記(十二)Learning PyTorch - NLP from Scratch

這篇博客瞄準的是 pytorch 官方教程中 Learning PyTorch 章節的 NLP from Scratch 部分。 官網鏈接&#xff1a;https://pytorch.org/tutorials/intermediate/nlp_from_scratch_index.html 完整網盤鏈接: https://pan.baidu.com/s/1L9PVZ-KRDGVER-AJnXOvlQ?pwdaa2m 提取碼: …

基礎算法02——冒泡排序(Bubble Sort)

冒泡排序&#xff08;Bubble Sort&#xff09; 冒泡排序&#xff1a;是一種簡單的排序算法&#xff0c;其基本思想是通過重復遍歷要排序的列表&#xff0c;比較相鄰的元素&#xff0c;并在必要時&#xff08;即前面的數比后面的數大的時候&#xff09;交換它們的位置&#xff…

RestTemplate遠程調用接口方式

1.Post(body空參) 也就是說需要給一個空的json 代碼: String getDeviceUrl this.MOVABLE_URL "detected-data/getMachineLists"; // 遠程調用 RestTemplate restTemplate new RestTemplate(); restTemplate.getMessageConverters().set(1,new StringHttpMessageC…

ar頭顯和眼鏡圖像特效處理

使用一個線程從攝像頭或者其他設備循環讀取圖像數據寫入鏈表&#xff0c;另一個線程從鏈表循環讀取數據并做相應的特效處理&#xff0c;由于寫入的速度比讀取的快&#xff0c;最終必然會因為寫入過快導致線程讀寫一幀而引發沖突和數據幀正常數據幀被覆蓋。最好使用共享內存&…

mysql--socket報錯

錯誤原因分析 MySQL 服務未運行&#xff08;最常見原因&#xff09; 錯誤中的 (2) 表示 “No such file or directory”&#xff0c;即 /tmp/mysql.sock 不存在這通常意味著 MySQL 服務器根本沒有啟動 socket 文件路徑不匹配 客戶端嘗試連接 /tmp/mysql.sock但 MySQL 服務器可…

labview加載matlab數據時報錯提示:對象引用句柄無效。

1. labview報錯提示 labview加載mat數據時報錯提示&#xff1a;對象引用句柄無效。返回該引用句柄的節點可能遇到錯誤&#xff0c;并沒有返回有效的引用句柄。該引用句柄所指的存儲可能在執行調用之前已關閉。報錯提示如下&#xff1a; 這是由于labview缺少matlab MathWorks導…

面試計算機操作系統解析(一中)

判斷 1. 一般來說&#xff0c;先進先出頁面置換算法比最近最少使用頁面置換算法有較少的缺頁率。&#xff08;?&#xff09; 正確答案&#xff1a;錯誤解釋&#xff1a;FIFO&#xff08;先進先出&#xff09;頁面置換算法可能導致“Belady異常”&#xff0c;即頁面數增加反而…

如何防御TCP洪泛攻擊

TCP洪泛攻擊&#xff08;TCP Flood Attack&#xff09;是一種常見的分布式拒絕服務&#xff08;DDoS&#xff09;攻擊手段&#xff0c;以下是其原理、攻擊方式和危害的詳細介紹&#xff1a; 定義與原理 TCP洪泛攻擊利用了TCP協議的三次握手過程。在正常的TCP連接建立過程中&a…