应用层协议 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/)
效果如下: