目录
[1.1 HTTP的请求与响应格式](#1.1 HTTP的请求与响应格式)
[1.2 URL](#1.2 URL)
[1.3 HTTP的常用请求方法](#1.3 HTTP的常用请求方法)
[1.3.1 GET](#1.3.1 GET)
[1.3.2 POST](#1.3.2 POST)
[1.4 HTTP的常见报头](#1.4 HTTP的常见报头)
[1.5 HTTP的常用状态码及其描述](#1.5 HTTP的常用状态码及其描述)
[1.6 HTTP协议的介绍](#1.6 HTTP协议的介绍)
[2、Hello Http](#2、Hello Http)
[2.1 大致思路](#2.1 大致思路)
[2.2 Http.hpp](#2.2 Http.hpp)
[2.3 Util.hpp](#2.3 Util.hpp)
[2.5 Tcpserver.hpp](#2.5 Tcpserver.hpp)
[2.6 示例及完整代码](#2.6 示例及完整代码)
1、HTTP协议
1.1 HTTP的请求与响应格式

- HTTP版本 :不同的版本 提供 不同的服务。
- HTTP协议如何做到报头和有效载荷的分离 ?以空行为分隔符。
- HTTP协议如何序列化和反序列化 ?成熟的 HTTP协议,不依赖于任何第三方库 ,直接序列化为字节流 ,再反序列化 。为什么不是字符流 ?因为**'\0'可能不是结尾**。
- 传输 交给传输层TCP/UDP ,HTTP是应用层 ,只是解析请求和应答。
1.2 URL

- URL,Uniform Resource Locator,统一资源定位符。
- HTTP是基于TCP的协议;HTTPS是HTTP经过加密 的协议;HTTPS,下一节讲解。
- 域名 :对应着服务器的IP。
- 端口号 :HTTP默认80****端口号 ,HTTPS默认443****端口号。
- 路径 :定位服务器里的 "具体文件 / 功能 "。/s是百度 "搜索功能" 的专属路径标识。
- 参数 (可选):向服务器传递 "具体需求 "。wd 是百度 "搜索关键词 " 的参数名 ,天气 就是你要搜索的内容。
- 注意:
- 浏览器不写 https://,www.,端口号 ,会自动补全。
- 像 / : ?,这样的符号,被当作是特殊字符 ,如需使用 ,需要进行转义 。UrlEncode编码/UrlDecode解码
1.3 HTTP的常用请求方法
- 访问资源(数据),无非就是从远端拿 (GET)数据 ;将本地数据 ,上传 (POST)到远端 。其实就是I/O。
1.3.1 GET
- 用于请求URL指定的资源。
- 可以通过URL的方式 ,提交参数,进行交互式的动态服务。
- 注意:
- 在URL中,参数在?的后面 ,以key=value的显示,可用&连接多个参数,但是不建议太长。
- 因为客户端发起请求前 ,已经通过 TCP 连接到了服务器的 IP 和端口 ,所以请求报文中的URL只剩下了路径+参数。
1.3.2 POST
-
向服务器提交数据 ,让服务器对数据进行处理。
-
可以通过请求正文的方式 ,提交参数 ,不显示 ,可以传长数据。
-
注意:
-
GET,POST,无论显不显示参数,都不安全,因为,如Fiddler,可以抓取报文信息。所以HTTPS,进行加密。
1.4 HTTP的常见报头
- Content-Type: 数据类型 (text/html等)。根据请求资源的后缀 ,指定数据类型,才能正确显示。
- Content-Length: 正文的长度。知道了有效载荷的长度。
- Connection: 是否支持长连接 (一条连接,可以发更多的请求)。keep-alive是支持长连接 ;close****不支持长连接。
- Host: 客户端告知服务器 ,所请求的资源是在哪个主机的哪个端口上。
- User-Agent: 声明用户的操作系统和浏览器版本信息。
- Referer: 当前页面是从哪个页面跳转过来的。
- Location: 搭配3xx状态码(重定向 )使用,告诉客户端接下来要去哪里访问。
- Cookie: 用于在客户端存储少量信息 ,session****对这些信息进行了加密 ,但是整体的加密程度有限。如:账号密码经过session加密放到Cookie中,但是黑客依然可以使用这些加密的信息进行登录。
1.5 HTTP的常用状态码及其描述
-
1xx:信息性状态码(请求已接收,继续处理)
- 100 Continue:服务器已接收请求头部,允许客户端继续发送请求体(常用于客户端发送大型数据前的 "预请求",如文件上传)。
-
2xx:成功状态码(请求已被成功处理)
- 200 OK:最常见的成功状态,请求被正常处理并返回响应体(如网页、接口数据等)。
-
3xx:重定向状态码(需要客户端进一步操作)
- 301 Moved Permanently:永久重定向。请求的资源已被永久迁移到新地址,客户端应更新本地缓存的 URL(如域名更换,旧域名跳转到新域名)。
- 302 Found:临时重定向。资源临时迁移到新地址,客户端下次请求仍应使用原 URL(如网站维护时临时跳转到通知页)。
-
4xx:客户端错误状态码(请求存在错误,服务器无法处理)
- 400 Bad Request:请求格式错误(如参数缺失、格式不正确,服务器无法解析)。 401 Unauthorized:未认证。请求需要身份验证(如登录),但客户端未提供或验证失败(常见于需要登录的页面)。
- 401 Unauthorized(未认证):请求需要身份验证(如登录),但客户端未提供有效凭证(如 token 过期、未携带 cookie)。场景:未登录时访问需要权限的接口(如/api/orders),服务器返回 401,提示用户登录。
- 403 Forbidden:服务器拒绝处理。客户端已认证,但没有权限访问该资源(如普通用户访问管理员后台)。 404 Not Found:最常见的错误之一,请求的资源不存在(如网址输错、页面已删除)。
- 404 Not Found:服务器无法找到请求的资源(URL 不存在)。
-
5xx:服务器错误状态码(服务器处理请求时出错)
- 一般不设置错误状态码 ,因为企业不能让别人"乘虚而入"。
-
注意:
-
状态码 :必须严格遵循标准,不能自定义。
-
状态描述 :可以自定义 ,但建议遵循标准。
-
为什么浏览器不显示状态码和状态描述?普通用户不需要关心 "404""Not Found" 这些技术符号,浏览器会把它们翻译成 "页面加载成功""页面找不到" 等易懂信息;而开发者可以通过工具随时查看原始的状态码和描述,用于调试问题。
1.6 HTTP协议的介绍
- HTTP是为了获取"资源"而采取的协议。
- HTTP,HyperText Transfer Protocol, "超文本传输协议"。因为正文传的是字节,显示器以对应的数据类型(Content-Type)显示。
- HTTP,无连接 (传输层TCP来连接,HTTP是应用层),无状态(不保存客户端的信息,所以报头里使用了Cookie保存了一些信息)。
2、Hello Http
2.1 大致思路
- 浏览器 作为客户端 ,向服务器 (myhttp )发起请求 ,服务器 (myhttp )向浏览器发送应答。
- 客户端 (浏览器)默认发送**'/'** ,访问首页 。如果访问其他页面 ,就临时重定向到404页面。后续可以添加更多服务。
2.2 Util.hpp
- 一些工具类的方法。
cpp
#pragma once
#include <fstream>
#include <string>
class Util
{
public:
static bool ReadOneLine(std::string &bigstr, std::string *out, const std::string &sep_line /*"\r\n"*/)
{
auto pos = bigstr.find(sep_line);
if (pos == std::string::npos)
return false;
*out = bigstr.substr(0, pos);
bigstr.erase(0, pos + sep_line.size());
return true;
}
static int FileSize(const std::string &file_name)
{
// 按文本方式读, 读到'\0'可能会结束, 所以按照二进制的方式读
std::ifstream in(file_name, std::ios::binary);
if (!in.is_open())
return -1;
in.seekg(0, in.end); // 将文件读指针移动到文件末尾
int file_size = in.tellg(); // 获取文件的字节数
in.seekg(0, in.beg); // 将指针移回文件开头
in.close();
return file_size;
}
static bool ReadFileContent(const std::string &file_name, std::string *out)
{
int file_size = FileSize(file_name);
if (file_size > 0)
{
std::ifstream in(file_name, std::ios::binary);
if (!in.is_open())
return false;
out->resize(file_size);
in.read(out->data(), file_size); // C++11后 data() 可返回 char*(非const)
in.close();
return true;
}
return false;
}
};
2.3 Http.hpp
- 如何读到完整的请求?
-
- 空行 -> 读取到请求行 +请求报头;
-
- 请求报头 中的Content-Length -> 请求正文。
cpp
#pragma once
#include "Socket.hpp"
#include "TcpServer.hpp"
#include "Util.hpp"
#include "Log.hpp"
#include <string>
#include <unordered_map>
const static std::string sep_space = " ";
const static std::string sep_line = "\r\n";
const static std::string sep_kv = ": ";
const static std::string wwwroot = "./wwwroot";
const static std::string home_page = "/home.html";
const static std::string page_404 = "/404.html";
class HttpRequest
{
public:
void ParseReqLine(std::string &req_line)
{
// GET / HTTP/1.1
std::stringstream ss(req_line);
ss >> _method >> _url >> _http_version;
}
bool Deserialize(std::string& req_str)
{
// 请求行
std::string req_line;
bool res = Util::ReadOneLine(req_str, &req_line, sep_line);
if(!res)
return false;
ParseReqLine(req_line);
LOG(LogLevel::DEBUG) << "reqline: " << req_line;
if(_url == "/")
_url = wwwroot + home_page; // "./wwwroot/home.html";
else
_url = wwwroot + _url; // "./wwwroot/..."
// 假设没有?后面的参数...
LOG(LogLevel::DEBUG) << "_method: " << _method;
LOG(LogLevel::DEBUG) << "_url: " << _url;
LOG(LogLevel::DEBUG) << "_http_version: " << _http_version;
// 请求报头 请求正文 ...
return true;
}
std::string GetUrl()
{
return _url;
}
private:
std::string _method;
std::string _url;
std::string _http_version;
std::unordered_map<std::string, std::string> _headers;
std::string _blank_line;
std::string _text;
};
class HttpResponse
{
public:
HttpResponse()
:_blank_line(sep_line)
,_http_version("HTTP/1.1")
{}
// 实现: 成熟的http,应答做序列化,不要依赖任何第三方库!
std::string Serialize()
{
std::string status_line = _http_version + sep_space + std::to_string(_code) + sep_space + _code_desc + sep_line;
std::string headers;
for(auto& header : _headers)
{
headers += header.first + sep_kv + header.second + sep_line;
}
return status_line + headers + _blank_line + _text;
}
void SetCode(int code)
{
_code = code;
switch(code)
{
case 200:
_code_desc = "OK";
break;
case 301:
_code_desc = "Found";
break;
case 404:
_code_desc = "Not Found";
break;
default:
break;
}
}
void SetHeaders(const std::string& key, const std::string& value)
{
auto it = _headers.find(key);
if(it == _headers.end())
_headers.insert({key,value});
}
// 默认访问的是.htm/.html文件
void SetTargetFile(const std::string& file)
{
_target_file = file;
}
void SetText(const std::string text)
{
_text = text;
}
bool MakeResponse(uint16_t port)
{
// 浏览器要访问图标, 忽略他
if(_target_file == "./wwwroot/favicon.ico")
{
LOG(LogLevel::INFO) << "用户请求图标: " << _target_file << "忽略他";
return false;
}
bool res = Util::ReadFileContent(_target_file, &_text);
if(!res)
{
LOG(LogLevel::WARNING) << "没有文件: " << _target_file << " , 404";
SetCode(302); // 没有这个文件, 重定向到404
SetHeaders("Location","http://124.221.189.63:"+std::to_string(port)+"/404.html");
}
else
{
LOG(LogLevel::WARNING) << "读取文件: " << _target_file;
SetCode(200); // 成功
int file_size = Util::FileSize(_target_file);
SetHeaders("Content-Length", std::to_string(file_size));
}
return true;
}
private:
std::string _http_version;
int _code;
std::string _code_desc;
std::unordered_map<std::string, std::string> _headers;
std::string _blank_line;
std::string _text;
// 其他属性
std::string _target_file;
};
class Http
{
public:
Http(uint16_t port)
:_tsvrp(std::make_unique<TcpServer>(port))
,_port(port)
{}
void HandleHttpRequest(std::shared_ptr<Socket>& sockfd, const InetAddr& client)
{
LOG(LogLevel::DEBUG) << "收到新连接,准备读取请求";
std::string http_req;
int n = sockfd->Recv(&http_req); // 大概率一次读到完整的请求报文
if(n > 0)
{
std::cout << "##########################" << std::endl;
std::cout << http_req;
std::cout << "##########################" << std::endl;
HttpRequest req;
HttpResponse resp;
req.Deserialize(http_req);
resp.SetTargetFile(req.GetUrl());
if(resp.MakeResponse(_port))
{
sockfd->Send(resp.Serialize());
}
}
}
void Start()
{
_tsvrp->Start([this](std::shared_ptr<Socket>& sockfd, const InetAddr& client){
this->HandleHttpRequest(sockfd, client);
});
}
private:
std::unique_ptr<TcpServer> _tsvrp;
uint16_t _port;
};
2.4 Main.cc
cpp
#include "Http.hpp"
#include <memory>
// ./myhttp server_port
int main(int argc, char* argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " server_port" << std::endl;
exit(USAGE_ERROR);
}
Enable_Console_Log_Strategy();
uint16_t server_port = std::stoi(argv[1]);
std::unique_ptr<Http> httpsvrp = std::make_unique<Http>(server_port);
httpsvrp->Start();
return 0;
}
2.5 Tcpserver.hpp
cpp
#pragma once
#include "Common.hpp"
#include "Log.hpp"
#include <memory>
using namespace LogModule;
const static int default_sockfd = -1;
const static int default_backlog = 16;
class Socket
{
public:
virtual void SocketOrDie() = 0; // = 0, 不需要实现
virtual void BindOrDie(uint16_t port) = 0;
virtual void ListenOrDie(int backlog) = 0;
virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) = 0;
virtual std::shared_ptr<Socket> Accept(InetAddr* client) = 0;
virtual int Recv(std::string* out) = 0;
virtual int Send(const std::string& in) = 0;
virtual void Close() = 0;
public:
void BuildTcpServer(uint16_t port)
{
SocketOrDie();
BindOrDie(port);
ListenOrDie(default_backlog);
}
void BuildTcpClient(std::string &server_ip, uint16_t server_port)
{
SocketOrDie();
ConnectOrDie(server_ip,server_port);
}
};
class TcpSocket : public Socket
{
public:
TcpSocket(int sockfd = default_sockfd)
: _sockfd(sockfd)
{
}
virtual void Close() override
{
if (_sockfd != default_sockfd)
::close(_sockfd); // ::表示调用 全局作用域 中的 close 函数
}
virtual void SocketOrDie() override
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket error!";
exit(SOCKET_ERROR);
}
LOG(LogLevel::INFO) << "socket success, socket: " << _sockfd;
}
virtual void BindOrDie(uint16_t port) override
{
InetAddr local(port);
int n = ::bind(_sockfd, CONST_CONV(local.Addr()), local.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind error!";
exit(BIND_ERROR);
}
LOG(LogLevel::INFO) << "bind success, socket: " << _sockfd;
}
virtual void ListenOrDie(int backlog) override
{
int n = ::listen(_sockfd, default_backlog);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen error!";
exit(LISTEN_ERROR);
}
LOG(LogLevel::INFO) << "listen success, sockfd: " << _sockfd;
}
virtual void ConnectOrDie(std::string &server_ip, uint16_t server_port) override
{
InetAddr server(server_ip, server_port);
int n = ::connect(_sockfd, CONST_CONV(server.Addr()), server.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "connect error!";
exit(CONNECT_ERROR);
}
LOG(LogLevel::INFO) << "connect success, sockfd: " << _sockfd;
}
virtual std::shared_ptr<Socket> Accept(InetAddr* client) override
{
std::cout << std::endl;
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int fd = ::accept(_sockfd, CONV(addr), &len);
if (fd < 0)
{
LOG(LogLevel::WARNING) << "accept failed";
return nullptr;
}
client->SetAddr(addr);
LOG(LogLevel::INFO) << "accept success, client: " << client->StringAddr();
return std::make_shared<TcpSocket>(fd); // 这个server的sockfd就可以调用Recv和Send方法。
}
virtual int Recv(std::string* out) override
{
char buf[1024*8];
ssize_t n = ::recv(_sockfd,buf,sizeof(buf)-1,0);
if(n > 0)
{
buf[n] = 0;
*out += buf; // += 可能要不断的读
}
return n;
}
virtual int Send(const std::string& in) override
{
return ::send(_sockfd,in.c_str(),in.size(),0);
}
private:
int _sockfd; // 既可以是listen_sockfd,也可以是sockfd,复用代码。
};
