第七期我们聊了怎么自定义一个应用层协议。虽然嘴上说“协议是程序员自己定的”,但现实里,早有一批大佬把好用的应用层协议定义好了,摆在那儿供我们直接参考、直接使用。HTTP(超文本传输协议),就是其中最经典的一个。
从本文开始,连续多篇内容,我们就把HTTP协议掰开揉碎,好好讲一讲。
目录
一、初识HTTP:理解Web通信的基本协议
二、从URL入手:理解HTTP如何定位网络资源
2.1 URL中的协议:HTTP与HTTPS
2.2 域名:如何找到目标服务器
2.3 资源访问路径:服务器上的内容如何定位
2.3.1 什么是网络资源
2.3.2 URL中的路径:如何表示资源的位置
2.4 从输入网址到访问资源:基本通信流程
2.5 小结
三、HTTP请求与响应:深入理解报文格式
3.1 HTTP请求报文详解:客户端如何发送请求
3.1.1 请求报文的基本结构:请求行、请求头与消息体
3.1.2 深入理解HTTP请求报文的组织方式
3.1.2.1 HTTP如何区分报头与消息体
3.1.2.2 HTTP报文如何组装与解析:序列化和反序列化
3.2 HTTP响应报文详解:服务器如何返回结果
3.2.1 响应报文的基本结构:状态行、响应头与消息体
四、HTTP服务器实战:从协议解析到网页访问
4.1 Common.hpp(错误码与公共定义)
4.2 Log.hpp(日志)
4.3 InetAddr.hpp(IP 地址封装)
4.4 Socket.hpp(Socket 封装)
4.5 HttpProtocol.hpp(HTTP 请求解析与响应构建)
4.6 HttpServer.hpp(HTTP 服务器)
4.7 HttpServer.cc(主程序)
4.8 Makefile
4.9 准备一个测试页面
4.10 运行与验证
一、初识HTTP:理解Web通信的基本协议
先给一个课本上的定义:在互联网世界里,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。它规定客户端(比如浏览器)和服务器之间如何通信,用来交换或传输超文本,比如HTML文档。
HTTP是客户端与服务器通信的基础。客户端通过HTTP向服务器发送请求,服务器收到请求后进行处理,再返回响应。HTTP是一个无连接、无状态的协议:每次请求都像一次独立的对话,需要重新建立连接,服务器也不会保存客户端的状态信息。
二、从URL入手:理解HTTP如何定位网络资源
2.1 URL中的协议:HTTP与HTTPS
网址开头这个https,或者http,表示的是获取资源时采用的协议。成熟的协议,端口号往往是固定的,https走443,http走80,ssh走22。这也从侧面印证了一件事:0~1023这段端口号,不是谁想用就能用的。

2.2 域名:如何找到目标服务器
你可以简单地把域名理解成IP地址的“门牌号”。域名最终要解析成IP地址,访问的其实就是这家公司的服务器。那为什么不直接用IP?因为IP地址没有表意性,一串数字摆在那儿,谁知道它是谁家的服务器。域名不一样,一看名字,大概就能猜到这是干什么的。

2.3 资源访问路径:服务器上的内容如何定位
2.3.1 什么是网络资源
先来想一个问题:资源,到底是什么?
人的上网行为,说白了就两种,从远端拿数据下来,把本地数据传上去。 拿的、传的,可能是短视频、网页、图片、音频,也可能是任何一个文件。
那问题来了:你没获取它的时候,这些资源在哪儿?
答案:在Linux服务器内部。
以什么形式存在?一个文件,就是一份“资源”。 网页是文件,图片是文件,视频也是文件。网络那头,服务器里躺着的,就是一个一个的文件。所谓的“访问资源”,说到底,就是去服务器上取某个文件。
2.3.2 URL中的路径:如何表示资源的位置
回到这里,/2201_76111162?type=lately就可以理解为一份资源存放的路径。
最前面那个/,代表的是Web根目录。注意,Web根目录不是Linux的根目录,这里先埋个伏笔,后面再展开。
中间那些/,就是Linux的路径分隔符。顺着这条路径找下去,资源就是服务器下对应的那个文件。

![]()
2.4 从输入网址到访问资源:基本通信流程
首先,运营商和各级DNS体系为我们提供了域名解析服务。我们在浏览器里输入URL,浏览器会先去问域名服务器:这个域名对应哪个IP?域名服务器翻出一本庞大的“电话簿”,DNS记录表,在里面检索,把域名精准翻译成对应的IP地址。
拿到IP之后,浏览器再结合端口号(具体用哪个端口,取决于你采用的是HTTP还是HTTPS),就能访问对应的服务器,并顺着域名后面的资源访问路径,取回想要的那份资源。
去ping一下百度的网址,能看到类似这样的输出:
正在 Ping www.a.shifen.com [2409:8c54:870:187:0:ff:b0d9:bb1c] 具有 32 字节的数据:
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=551ms
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=529ms
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=352ms
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=597ms
方括号里那串2409:8c54:870:187:0:ff:b0d9:bb1c,就是IP地址。有了IP地址,再加上端口号,就能定位到目标服务器。而我们平时在浏览器里输入的,是“协议 + 域名”,DNS负责把域名翻译成IP,剩下的路,浏览器自己会走。
Tips:为什么这个IP地址看起来这么奇怪?
因为它是IPv6地址。我们平常看到的,大多是IPv4。
PS C:\\Windows\\System32\\WindowsPowerShell\\v1.0> ping www.baidu.com
正在 Ping www.a.shifen.com [2409:8c54:870:187:0:ff:b0d9:bb1c] 具有 32 字节的数据:
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=551ms
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=529ms
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=352ms
来自 2409:8c54:870:187:0:ff:b0d9:bb1c 的回复: 时间=597ms
2409:8c54:870:187:0:ff:b0d9:bb1c 的 Ping 统计信息:
数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
最短 = 352ms,最长 = 597ms,平均 = 507ms
PS C:\\Windows\\System32\\WindowsPowerShell\\v1.0>
历史补充:图中框出来的两个字段,曾经存在过,如今已经被废弃了。

2.5 小结
站在HTTP的角度看,“资源”本质上就是存放在Linux服务器特定路径下的一个文件。
我们在URL里看到的协议与端口号(http/https加端口)、域名(映射到具有唯一性的IP地址),以及路径(目标机器上某个特定文件的位置),这三样东西凑在一起,就构成了全网唯一的文件定位。
所以,超文本传输协议的本质,就是用来传文件的。而它底层,依然是基于Socket实现的网络通信。
Tips:urlencode和urldecode(补充内容)
我们试着搜索一下hello: //@world。
可以看到,搜索框里输入的特殊字符,到了URL里就变成了%20、%3A、%2F、%40这样一串东西。

像 :、/、@以及空格这些字符,在URL里都有特殊用途或分隔含义。要是让它们直接出现在参数里,客户端或服务器解析URL时就会犯迷糊,轻则解析出错,重则直接失败。

为了避免歧义,客户端(比如浏览器)在向服务器发请求之前,会自动把URL里的特殊字符转成十六进制形式。这个转码过程,叫urlencode。服务器收到请求后,再把它还原成原始字符,这个解码过程,叫urldecode。
浏览器负责编码,服务器负责解码,这种配合模式,正是典型的B/S(Browser/Server)架构。我们以前写的,是C/S架构。

转义规则:把需要转码的字符转成十六进制,从右往左取4位(不足4位直接处理),每2位算一组,前面加上%,编码成%XY的格式。
三、HTTP请求与响应:深入理解报文格式
3.1 HTTP请求报文详解:客户端如何发送请求
3.1.1 请求报文的基本结构:请求行、请求头与消息体
拿到一个HTTP请求报文,我们来拆开看看。

-
首行:[方法] + [url] + [版本]。三个部分,一句话交代清楚“用什么方式、访问哪个资源、用的哪个 HTTP 版本”。
-
Header:请求的各种属性,一堆冒号分割的键值对。每组属性之间用\\r\\n隔开;碰到一个空行,Header部分就到此为止。
-
Body:空行后面的内容,全归Body。Body允许是空字符串。如果Body存在,Header里会有一个Content-Length属性,专门标明Body有多长(这个后面细讲)。
HTTP的底层是TCP,这一点,等后面写代码的时候,自然就能体会到。
把上面这个报文抽象一下,就能得到下面这样一个结构图:

3.1.2 深入理解HTTP请求报文的组织方式
整个HTTP请求报文,放在网络里传输的时候,本质上就是一段多行的字符串。没什么玄乎的,就是一串字符,按规矩排好队而已。
3.1.2.1 HTTP如何区分报头与消息体
靠一个空行。HTTP协议拿\\r\\n当分隔符,把报头和有效载荷一刀切开。解析报文的时候,系统按行往下读,一旦读到一行光秃秃的、只有换行符的空行,就知道:报头到此为止,再往后的内容,全是有效载荷(Body)。
就像一封信,正文写完了,落款和信封信息之间空一行,读信的人一看空行,就明白“前面是信的内容,后面是寄件信息”。HTTP用的是同一个思路,一个空行,干净利落。
3.1.2.2 HTTP报文如何组装与解析:序列化和反序列化
非常直接,甚至可以说朴素。
它不依赖任何复杂的第三方库,就是拿特殊字符当刻刀,对字符串做拼接和切割。发送端,把内存里的结构化请求数据,按规范一段一段拼成字符串,这就是序列化。接收端,再对着这些特殊字符,把字符串重新拆开、提取,还原成结构化对象,这就是反序列化。
没有Protobuf那套二进制编码的复杂机制,也没有JSON库的层层解析。HTTP的序列化和反序列化,就是最原始的字符串操作,简单、直白,但足够管用。
3.2 HTTP响应报文详解:服务器如何返回结果
3.2.1 响应报文的基本结构:状态行、响应头与消息体
拿到一个 HTTP 响应报文,照样拆开看。

-
首行:[版本号] + [状态码] + [状态码解释]。服务器一开口,先报三件事:用的哪个HTTP版本、这次请求的结果是几号状态码、这个状态码又是什么意思。
-
Header:响应头,一堆冒号分割的键值对。每组之间用\\r\\n隔开,碰到空行,Header部分就收尾了。
-
Body:空行后面的内容,全归Body。Body允许是空字符串。如果Body存在,Header里会有一个Content-Length属性,标明它有多长。服务器要是返回了一个HTML页面,那页面的内容,就安安静静躺在Body里。


四、HTTP服务器实战:从协议解析到网页访问
下面把一套可直接编译运行的极简HTTP服务器完整写出来。代码沿用之前的Socket封装思路,重点放在HTTP请求解析 和HTTP响应构建上。
4.1 Common.hpp(错误码与公共定义)
#pragma once
#define SOCKET_ERR 1
#define BIND_ERR 2
#define LISTEN_ERR 3
#define ACCEPT_ERR 4
4.2 Log.hpp(日志)
#pragma once
#include <iostream>
#include <string>
#include <sstream>
namespace LogModule
{
const std::string CAGE = "\\n";
enum class LEVEL
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
inline std::string LevelToString(LEVEL level)
{
switch (level)
{
case LEVEL::DEBUG: return "DEBUG";
case LEVEL::INFO: return "INFO";
case LEVEL::WARNING: return "WARNING";
case LEVEL::ERROR: return "ERROR";
case LEVEL::FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
inline void Log(LEVEL level, const std::string &msg)
{
std::cout << "[" << LevelToString(level) << "] " << msg << CAGE;
}
}
#define LOG(level) LogModule::Log(level, std::string(__FILE__) + ":" + std::to_string(__LINE__) + " ")
4.3 InetAddr.hpp(IP 地址封装)
#pragma once
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string>
#include <cstring>
namespace InetAddrModule
{
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
InetAddr(const std::string &ip, uint16_t port)
: _ip(ip), _port(port)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
}
void SetAddr(const struct sockaddr_in &addr)
{
_addr = addr;
_port = ntohs(_addr.sin_port);
char ipbuffer[64];
inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ipbuffer;
}
std::string Ip() const { return _ip; }
uint16_t Port() const { return _port; }
struct sockaddr_in *GetSockaddrPtr() { return &_addr; }
socklen_t GetSockLine() const { return sizeof(_addr); }
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
}
4.4 Socket.hpp(Socket 封装)
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
using namespace InetAddrModule;
namespace SocketModule
{
const int DEFAULT_BACKLOG = 128;
class TcpSocket
{
public:
TcpSocket(int sockfd = -1) : _sockfd(sockfd) {}
~TcpSocket() {}
void CreateSocket()
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LEVEL::FATAL) << "套接字创建失败";
exit(SOCKET_ERR);
}
}
bool BindSocket(InetAddr &server)
{
int n = ::bind(_sockfd, (struct sockaddr *)server.GetSockaddrPtr(),
server.GetSockLine());
if (n == -1)
{
LOG(LEVEL::FATAL) << "绑定套接字失败";
exit(BIND_ERR);
}
return true;
}
bool ListenSocket(int backlog = DEFAULT_BACKLOG)
{
int n = ::listen(_sockfd, backlog);
if (n == -1)
{
LOG(LEVEL::FATAL) << "监听套接字失败";
exit(LISTEN_ERR);
}
return true;
}
std::shared_ptr<TcpSocket> AcceptSocket(InetAddr &client)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sockfd = ::accept(_sockfd, (struct sockaddr *)&peer, &len);
if (sockfd < 0)
{
LOG(LEVEL::ERROR) << "获取连接失败";
return nullptr;
}
client.SetAddr(peer);
return std::make_shared<TcpSocket>(sockfd);
}
int Recv(void *buf, size_t len)
{
return ::recv(_sockfd, buf, len, 0);
}
int Send(const void *buf, size_t len)
{
return ::send(_sockfd, buf, len, 0);
}
int GetSockFd() { return _sockfd; }
void Close()
{
if (_sockfd >= 0)
{
::close(_sockfd);
_sockfd = -1;
}
}
private:
int _sockfd;
};
}
4.5 HttpProtocol.hpp(HTTP 请求解析与响应构建)
这是整个服务器的核心:解析请求、构建响应。
#pragma once
#include <string>
#include <sstream>
#include <unordered_map>
#include <vector>
#include <cctype>
namespace HttpModule
{
// ———- HTTP 请求结构 ———-
struct HttpRequest
{
std::string method; // GET / POST
std::string url; // 资源路径
std::string version; // HTTP/1.0 或 HTTP/1.1
std::unordered_map<std::string, std::string> headers; // 请求头
std::string body; // 请求正文
};
// ———- HTTP 响应结构 ———-
struct HttpResponse
{
std::string version = "HTTP/1.1";
int status_code = 200;
std::string status_text = "OK";
std::unordered_map<std::string, std::string> headers;
std::string body;
};
// ———- 工具:去首尾空白 ———-
inline std::string Trim(const std::string &s)
{
size_t begin = 0;
while (begin < s.size() && std::isspace(static_cast<unsigned char>(s[begin])))
{
++begin;
}
size_t end = s.size();
while (end > begin && std::isspace(static_cast<unsigned char>(s[end – 1])))
{
–end;
}
return s.substr(begin, end – begin);
}
// ———- 工具:字符串转小写 ———-
inline std::string ToLower(const std::string &s)
{
std::string ret = s;
for (char &c : ret)
{
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return ret;
}
// ———- 序列化:HttpResponse → 字符串 ———-
inline std::string SerializeResponse(const HttpResponse &resp)
{
std::ostringstream oss;
// 1. 状态行
oss << resp.version << " " << resp.status_code << " " << resp.status_text << "\\r\\n";
// 2. 响应头
for (const auto &kv : resp.headers)
{
oss << kv.first << ": " << kv.second << "\\r\\n";
}
// 3. 空行
oss << "\\r\\n";
// 4. 正文
oss << resp.body;
return oss.str();
}
// ———- 反序列化:字符串 → HttpRequest ———-
// 返回值:true 解析成功;false 解析失败
inline bool DeserializeRequest(const std::string &raw, HttpRequest &req)
{
// 1. 找到头部与正文的分界空行
size_t pos = raw.find("\\r\\n\\r\\n");
std::string header_part;
if (pos == std::string::npos)
{
// 没有空行,说明只有头部,没有正文
header_part = raw;
req.body.clear();
}
else
{
header_part = raw.substr(0, pos);
req.body = raw.substr(pos + 4);
}
// 2. 按行切分头部
std::istringstream iss(header_part);
std::string line;
// 3. 第一行:请求行
if (!std::getline(iss, line))
{
return false;
}
if (!line.empty() && line.back() == '\\r')
{
line.pop_back();
}
std::istringstream line_ss(line);
if (!(line_ss >> req.method >> req.url >> req.version))
{
return false;
}
// 4. 后续行:请求头
while (std::getline(iss, line))
{
if (!line.empty() && line.back() == '\\r')
{
line.pop_back();
}
if (line.empty())
{
continue;
}
size_t colon = line.find(':');
if (colon == std::string::npos)
{
continue;
}
std::string key = ToLower(Trim(line.substr(0, colon)));
std::string value = Trim(line.substr(colon + 1));
req.headers[key] = value;
}
return true;
}
// ———- 工具:根据状态码生成状态文本 ———-
inline std::string StatusText(int code)
{
switch (code)
{
case 200: return "OK";
case 404: return "Not Found";
case 400: return "Bad Request";
case 405: return "Method Not Allowed";
case 500: return "Internal Server Error";
default: return "Unknown";
}
}
// ———- 构建响应 ———-
inline HttpResponse MakeResponse(int code, const std::string &body,
const std::string &content_type = "text/html; charset=utf-8")
{
HttpResponse resp;
resp.status_code = code;
resp.status_text = StatusText(code);
resp.headers["Content-Type"] = content_type;
resp.headers["Content-Length"] = std::to_string(body.size());
resp.headers["Connection"] = "close";
resp.body = body;
return resp;
}
}
4.6 HttpServer.hpp(HTTP 服务器)
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <fstream>
#include <sstream>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
#include "Socket.hpp"
#include "HttpProtocol.hpp"
using namespace SocketModule;
using namespace HttpModule;
namespace ServerModule
{
class HttpServer
{
public:
HttpServer(uint16_t port = 8080, const std::string &webroot = ".")
: _port(port), _webroot(webroot)
{
}
void Init()
{
InetAddr server("0.0.0.0", _port);
_listensock = std::make_shared<TcpSocket>();
_listensock->CreateSocket();
// 端口复用,避免重启时 bind 失败
int opt = 1;
setsockopt(_listensock->GetSockFd(), SOL_SOCKET, SO_REUSEADDR,
&opt, sizeof(opt));
_listensock->BindSocket(server);
_listensock->ListenSocket();
}
void Start()
{
signal(SIGCHLD, SIG_IGN);
LOG(LEVEL::INFO) << "HTTP 服务器启动,端口:" + std::to_string(_port);
while (true)
{
InetAddr client;
auto conn = _listensock->AcceptSocket(client);
if (!conn)
{
continue;
}
LOG(LEVEL::INFO) << "客户端连接:" + client.Ip() + ":" + std::to_string(client.Port());
pid_t id = fork();
if (id == 0)
{
_listensock->Close();
HandleConnection(conn);
conn->Close();
exit(0);
}
conn->Close();
}
}
private:
// 处理单个 HTTP 连接
void HandleConnection(std::shared_ptr<TcpSocket> conn)
{
std::string raw;
char buf[4096];
// 读取请求:简单处理,读一次或多次,直到缓冲区里出现 \\r\\n\\r\\n
while (true)
{
int n = conn->Recv(buf, sizeof(buf));
if (n <= 0)
{
return;
}
raw.append(buf, n);
// 头部接收完整后,再检查是否已收到 Content-Length 指定的正文
size_t head_end = raw.find("\\r\\n\\r\\n");
if (head_end != std::string::npos)
{
HttpRequest tmp;
if (DeserializeRequest(raw, tmp))
{
size_t content_length = 0;
auto it = tmp.headers.find("content-length");
if (it != tmp.headers.end())
{
content_length = static_cast<size_t>(std::stoul(it->second));
}
size_t body_start = head_end + 4;
if (raw.size() – body_start >= content_length)
{
break;
}
}
}
// 防止恶意请求把缓冲区撑爆
if (raw.size() > 1024 * 1024)
{
return;
}
}
// 解析请求
HttpRequest req;
if (!DeserializeRequest(raw, req))
{
std::string frame = SerializeResponse(MakeResponse(400, "<h1>400 Bad Request</h1>"));
conn->Send(frame.c_str(), frame.size());
return;
}
LOG(LEVEL::INFO) << "请求:" + req.method + " " + req.url + " " + req.version;
// 构建响应
HttpResponse resp = Route(req);
// 序列化并发送
std::string frame = SerializeResponse(resp);
conn->Send(frame.c_str(), frame.size());
}
// 简单的路由:只处理 GET,其他方法返回 405
HttpResponse Route(const HttpRequest &req)
{
if (req.method != "GET")
{
return MakeResponse(405, "<h1>405 Method Not Allowed</h1>");
}
// 根路径默认返回首页
std::string path = req.url;
if (path == "/" || path.empty())
{
path = "/index.html";
}
// 简单的路径安全校验,防止 ../ 目录穿越
if (path.find("..") != std::string::npos)
{
return MakeResponse(400, "<h1>400 Bad Request</h1>");
}
// 拼接本地文件路径
std::string filepath = _webroot + path;
std::ifstream ifs(filepath, std::ios::binary);
if (!ifs.is_open())
{
return MakeResponse(404, "<h1>404 Not Found</h1>");
}
std::ostringstream oss;
oss << ifs.rdbuf();
std::string body = oss.str();
// 根据后缀推断 Content-Type
std::string content_type = GuessContentType(path);
return MakeResponse(200, body, content_type);
}
std::string GuessContentType(const std::string &path)
{
auto ends_with = [&](const std::string &suffix) {
return path.size() >= suffix.size() &&
path.compare(path.size() – suffix.size(), suffix.size(), suffix) == 0;
};
if (ends_with(".html") || ends_with(".htm")) return "text/html; charset=utf-8";
if (ends_with(".css")) return "text/css; charset=utf-8";
if (ends_with(".js")) return "application/javascript; charset=utf-8";
if (ends_with(".png")) return "image/png";
if (ends_with(".jpg") || ends_with(".jpeg")) return "image/jpeg";
if (ends_with(".gif")) return "image/gif";
if (ends_with(".txt")) return "text/plain; charset=utf-8";
return "application/octet-stream";
}
private:
std::shared_ptr<TcpSocket> _listensock;
uint16_t _port;
std::string _webroot; // Web 根目录
};
}
4.7 HttpServer.cc(主程序)
#include "HttpServer.hpp"
#include <memory>
// 用法:./http_server [端口号] [Web根目录]
int main(int argc, char *argv[])
{
uint16_t port = 8080;
std::string webroot = ".";
if (argc >= 2)
{
port = static_cast<uint16_t>(std::stoi(argv[1]));
}
if (argc >= 3)
{
webroot = argv[2];
}
std::unique_ptr<ServerModule::HttpServer> server =
std::make_unique<ServerModule::HttpServer>(port, webroot);
server->Init();
server->Start();
return 0;
}
4.8 Makefile
.PHONY: all
all: http_server
http_server: HttpServer.cc HttpServer.hpp Socket.hpp InetAddr.hpp HttpProtocol.hpp Common.hpp Log.hpp
g++ -o $@ $< -std=c++17
.PHONY: clean
clean:
rm -f http_server
4.9 准备一个测试页面
在Web根目录下放一个index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>简易HTTP服务器</title>
</head>
<body>
<h1>Hello, HTTP Server!</h1>
<p>这是一个极简HTTP服务器返回的页面。</p>
</body>
</html>
4.10 运行与验证
编译:
make
启动服务器,把当前目录作为Web根目录:
./http_server 8080 .
浏览器访问:
http://127.0.0.1:8080/
或者用curl验证:
curl -v http://127.0.0.1:8080/
输出大致如下:
< HTTP/1.1 200 OK < Content-Type: text/html; charset=utf-8 < Content-Length: 245 < Connection: close < <!DOCTYPE html> <html> …
如果这篇文章对你有帮助,别忘了点赞、收藏、关注三连支持。你的每一次反馈,都是我继续硬核输出的最大动力。
网硕互联帮助中心




评论前必须登录!
注册