跳到主要内容Linux 网络编程实战:HTTP 协议解析与 C++ 服务器实现 | 极客日志C++大前端
Linux 网络编程实战:HTTP 协议解析与 C++ 服务器实现
HTTP 协议基础结构、请求响应格式详解。基于 C++ Socket 模拟 HTTP 服务器的完整流程,涵盖线程池模型、文件读取及 Cookie 会话管理。适合系统编程学习者参考。
静心2 浏览 URL 基础
URL(统一资源定位符)是我们在浏览器中访问资源的地址。当参数中包含特殊字符如 ?、/ 或 + 时,系统会自动进行转义处理。

例如,+ 符号会被转义为 %2b,这是为了确保传输过程中数据的完整性。
HTTP 协议格式
请求格式
一个标准的 HTTP 请求由三部分组成:请求行、请求报头和正文。

- 请求行:包含方法、URL 和版本号,以
\r\n 结尾。
- 请求报头:每行一对键值对,同样以
\r\n 结尾。
- 空行:必须存在,用于区分报头和正文。
- 正文:内容长度通常由报头中的
Content-Length 指定。
HTTP 常用方法
常用的请求方法包括 GET 和 POST,它们分别对应不同的业务场景。

响应格式
响应报文结构与请求类似,包含状态行、响应报头和响应正文。


常见状态码
理解状态码对于调试至关重要:
- 200 (OK):请求成功。
- 404 (Not Found):资源未找到。
- 403 (Forbidden):禁止访问。
- :重定向。
302 (Redirect)
504 (Bad Gateway):网关超时。
Content-Type:数据类型(如 text/html)。
Content-Length:Body 的长度。
Host:客户端告知服务器请求的资源所在主机及端口。
User-Agent:声明用户的操作系统和浏览器版本。
Referer:当前页面是从哪个页面跳转过来的。
Location:搭配 3xx 状态码使用,指示客户端下一步访问地址。
Cookie:用于在客户端存储少量信息,常配合 Session 实现会话功能。
模拟实现 HTTP 服务器
接下来我们动手写一个简单的 HTTP 服务器。目录结构大致如下:
为了清晰起见,我们将代码拆分为几个模块:Socket 封装、日志工具、服务器核心逻辑以及前端页面。
Socket 封装 (socket.hpp)
这部分负责底层的网络通信,包括创建 socket、绑定端口、监听和接受连接。
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <string.h>
#include <unistd.h>
using namespace std;
Log lg;
const int backlog = 10;
enum { SocketErr = 2, BindErr, ListenErr };
class Sock {
public:
Sock() {}
~Sock() {}
void Socket() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0) {
lg(Fatal, "socket error, strerror: %s, errno: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Bind(uint16_t port) {
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0) {
lg(Fatal, "Bind error, strerror: %s, errno: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen() {
if (listen(sockfd_, backlog) < 0) {
lg(Fatal, "Listen Error, strerror: %s, errno: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* ip, uint16_t* port) {
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if (newfd < 0) {
lg(Warning, "accept error, strerror: %s, errno: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*ip = ipstr;
*port = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const string& ip, const uint16_t& port) {
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if (n < 0) {
cerr << "Connect to" << ip << ":" << port << endl;
return false;
}
return true;
}
void Close() {
close(sockfd_);
}
int Fd() {
return sockfd_;
}
private:
int sockfd_;
};
日志工具 (log.hpp)
为了方便调试,我们需要一个简单的日志类来记录运行状态。
#pragma once
#include <iostream>
#include <stdlib.h>
#include <time.h>
#include <stdarg.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define Logfile "log.txt"
class Log {
public:
Log() {
printmethod = Screen;
path = "./log/";
mkdir(path.c_str(), 0777);
}
void Enable(int method) {
printmethod = method;
}
string levelToString(int level) {
switch (level) {
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "Unknown";
}
}
void operator()(int level, const char* format, ...) {
time_t t = time(nullptr);
struct tm* ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printlog(level, logtxt);
}
void printlog(int level, const string& logtxt) {
switch (printmethod) {
case Screen: cout << logtxt << endl; break;
case Onefile: printOnefile(Logfile, logtxt); break;
case Classfile: printClassfile(level, logtxt); break;
}
}
void printOnefile(const string& failname, const string& logtxt) {
string _failname = path + failname;
int fd = open(_failname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0) return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassfile(int level, const string& logtxt) {
string _failname = Logfile;
_failname += ".";
_failname += levelToString(level);
printOnefile(_failname, logtxt);
}
~Log() {}
private:
int printmethod;
string path;
};
服务器核心逻辑 (server.hpp)
这里定义了 HTTP 服务器的主体,包括请求解析、线程管理和文件读取。
#pragma once
#include <iostream>
#include "socket.hpp"
#include <string>
#include <pthread.h>
#include <fstream>
#include <vector>
#include <sstream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unordered_map>
std::string blank_line = "\r\n";
std::string wwwroot = "./wwwroot";
std::string homepage = "index.html";
class HttpServer;
class ThreadData {
public:
ThreadData(int sockfd, HttpServer* hs) : _sockfd(sockfd), _hs(hs) {}
public:
int _sockfd;
HttpServer* _hs;
};
class HttpResponse {
public:
void Deserialize(std::string req) {
while (true) {
std::size_t pos = req.find(blank_line);
if (pos == string::npos) break;
std::string temp = req.substr(0, pos);
if (temp.empty())
break;
vc.push_back(temp);
req.erase(0, pos + blank_line.size());
}
text += req;
}
void Parse() {
std::stringstream ss(vc[0]);
ss >> method >> url >> version;
file_path = wwwroot;
if (url == "/" || url == "index.html") {
file_path += "/";
file_path += homepage;
} else {
file_path += url;
}
}
void DebugPrint() {
for (auto line : vc) {
std::cout << line << std::endl;
std::cout << "------------" << std::endl;
}
std::cout << "method: " << method << std::endl;
std::cout << "url: " << url << std::endl;
std::cout << "version: " << version << std::endl;
std::cout << "file_path: " << file_path << std::endl;
std::cout << "text: " << text << std::endl;
}
public:
vector<string> vc;
string text;
std::string method;
std::string url;
std::string version;
std::string file_path;
};
class HttpServer {
public:
HttpServer(uint16_t port) : _port(port) {}
void start() {
_listen.Socket();
_listen.Bind(_port);
_listen.Listen();
lg(Info, "server create success");
for (;;) {
std::string clientip;
uint16_t port;
int sockfd = _listen.Accept(&clientip, &port);
lg(Info, "get a newfd:%d", sockfd);
pthread_t tid;
ThreadData* td = new ThreadData(sockfd, this);
pthread_create(&tid, nullptr, ThreadRun, td);
}
}
static std::string ReadHtmlContent(const std::string& htmlpath) {
std::ifstream in(htmlpath);
if (!in.is_open()) return "404";
std::string content;
std::string line;
while (getline(in, line)) {
content += line;
}
in.close();
return content;
}
static void HandlerHttp(int sockfd) {
char buffer[10240];
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
buffer[n] = 0;
HttpResponse rep;
rep.Deserialize(buffer);
rep.Parse();
rep.DebugPrint();
std::string text = ReadHtmlContent(rep.file_path);
std::string response_line = "HTTP/1.0 200 OK\r\n";
std::string response_header = "Content-Length: ";
response_header += std::to_string(text.size());
std::string response = response_line;
response += response_header;
response += "\r\n";
response += blank_line;
response += text;
send(sockfd, response.c_str(), response.size(), 0);
}
close(sockfd);
}
static void* ThreadRun(void* args) {
pthread_detach(pthread_self());
ThreadData* td = static_cast<ThreadData*>(args);
HandlerHttp(td->_sockfd);
close(td->_sockfd);
delete td;
return nullptr;
}
Sock _listen;
uint16_t _port;
};
启动入口 (server.cc)
#include <iostream>
#include "server.hpp"
using namespace std;
int main(int argc, char* argv[]) {
if (argc != 2) exit(1);
HttpServer* svr = new HttpServer(stoi(argv[1]));
svr->start();
return 0;
}
前端页面示例
为了让服务器有东西可返回,我们需要准备一些 HTML 文件放在 wwwroot 目录下。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>首页</title></head>
<body>
<h1>这个是我们的首页</h1>
<a href="http://127.0.0.1:8080/hello.html">到第二张网页</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>第二页</title></head>
<body>
<h1>第二张图片</h1>
<a href="http://127.0.0.1:8080">回到首页</a>
</body>
</html>
编译并运行后,打开浏览器输入 IP:端口号 即可看到效果。通过抓包工具(如 Wireshark 或 Xshell)可以观察到实际的网络交互过程。
可以看到域名和路径的变化,这就是 HTTP 重定向和路由的基本原理。
Cookie 机制
为什么第一次访问网站需要登录,第二次却不用?这是因为浏览器帮我们记住了账户信息,存储在 Cookie 中。服务器会为每个用户分配一个 ID,每次请求都携带这个 Cookie 和 ID,服务端据此确认身份。
在我们的服务器代码中加入 Cookie 设置非常简单,只需在响应头中添加 Set-Cookie 字段:
response_header += "Set-Cookie: name=haha&&password=123456";
response_header += "\r\n";
注意这里修正了拼写错误(passward -> password),确保数据准确。设置完成后,刷新浏览器查看开发者工具中的 Cookie 栏,就能发现刚才设置的键值对。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online