应用层协议 HTTP
- [一. HTTP 协议](#一. HTTP 协议)
-
- [1. URL 地址](#1. URL 地址)
- [2. urlencode 和 urldecode](#2. urlencode 和 urldecode)
- [3. 请求与响应格式](#3. 请求与响应格式)
- [二. HTTP 请求方法](#二. HTTP 请求方法)
-
- [1. GET 和 POST (重点)](#1. GET 和 POST (重点))
- [三. HTTP 状态码](#三. HTTP 状态码)
- [四. HTTP 常见报头](#四. 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 响应中,可能会看到类似于以下的头部信息:
bash
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n
HTTP 状态码 302(临时重定向):
- 当服务器返回 HTTP 302 状态码时,表示请求的资源临时被移动到新的位置。
- 同样地,服务器也会在响应中添加一个 Location 头部来指定资源的新位置。浏览器会暂时使用新的 URL 进行后续的请求,但不会缓存这个重定向。
- 例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:
bash
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
bash
httpserver:HttpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -rf httpserver
- Mutex.hpp
cpp
#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
cpp
#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
cpp
#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; // 进程pid
std::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
cpp
#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
cpp
#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
cpp
#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; // 主机序列: IP
uint16_t _port; // 主机序列: 端口号
};
- TcpServer.hpp
cpp
#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);
}
// 父进程关闭sockfd
sockfd->Close();
waitpid(id, nullptr, 0); // 子进程直接退出, 父进程无需阻塞等待
}
_isrunning = false;
}
private:
std::unique_ptr<Socket> _listensockp;
bool _isrunning;
tcphandler_t _handler;
int _port;
};
}
- HttpProtocol.hpp
cpp
#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.6
void 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.1
void 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=123456
auto pos = _uri.find("?");
if (pos != std::string::npos) // 存在?带参数
{
_isexec = true;
_path = _uri.substr(0, pos);
_args = _uri.substr(pos + 1);
}
else // 不存在?不带参数
{
_isexec = false;
}
}
}
}
// 返回请求的资源: uri
std::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.jpg
auto pos = _uri.rfind(".");
if (pos == std::string::npos)
return std::string(".html");
else
return _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&yyy
std::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.html
std::string uri = default_home_path + req.Uri(); // wwwroot/
if (uri.back() == '/')
{
uri += first_page; // wwwroot/index.html
req.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";
else
return "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
cpp
#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; // 功能路由
};
cpp
#include "HttpServer.hpp"
#include "Deamon.hpp"
using namespace LogModule;
// 登入功能
void Login(HttpRequset &req, HttpResponse &resp)
{
// 根据 req 动态构建 resp:
// Path: /login
// Args: name=zhangsan&password=123456
LOG(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;
}
-
前端代码
点击跳转 -
运行操作
bash
# 启动http服务器
xzy@hcss-ecs-b3aa:~$ ./httpserver 8888
浏览器输入:云服务器IP地址:端口号(例如:http://123.60.170.90:8888/)
效果如下:
