虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用。HTTP(超文本传输协议) 就是其中之一。
在互联网世界中,HTTP(HyperText Transfer Protocol,超文本传输协议)是一个至关重要的协议。听起来好像是那么回事,实际上超文本传输协议指的是不仅仅可以传输文本,还可以传输图片、音频、视频等文件。它定义了客户端(如浏览器)与服务器之间如何通信,以交换或传输超文本(如 HTML 文档)
GET
方法,没有头部信息,只能传输纯文本(HTML)。
HTTP 协议经历了多个版本的演进,每个版本都引入了重要的改进:
HTTP/0.9(1991 年)
GET
方法。
HTTP/1.0(1996 年,RFC 1945)
GET
、POST
、HEAD
)。
HTTP/1.1(1997 年,RFC 2068;1999 年更新,RFC 2616)
Connection: keep-alive
),允许复用 TCP 连接。
Transfer-Encoding: chunked
)。
Host
头部)。
Cache-Control
头部)。
HTTP/2(2015 年,RFC 7540)
HTTP/3(2022 年,RFC 9114)
HTTP 协议的本质是从服务器获取文件资源。资源可以是:
无论是哪种资源,它们最终都存储在服务器的某个路径下,而 HTTP 协议则负责根据客户端的请求,定位并返回这些资源。
GET
:获取资源。
POST
:提交数据。
PUT
:更新资源。
DELETE
:删除资源。
HEAD
:获取资源的元数据。
200 OK
:请求成功。
404 Not Found
:资源未找到。
500 Internal Server Error
:服务器内部错误。
Content-Type
:资源的媒体类型(如 text/html
)。
Cache-Control
:缓存控制指令。
Authorization
:身份验证信息。
🔥 我们常说的“网址”,其实就是 URL(Uniform Resource Locator 统一资源定位符),用于标识互联网上的资源,一个网址通常包含如下部分:
举个例子:https:://news.qq.com/rain/a/2025
我们从网络上获取的文字,图片,音视频等等,这些信息本质上都是资源。那么在我们获取这些信息之前,这些文件存储在哪个位置?大多数在Linux服务器中,而在Linux当中,一切皆文件,所以 网络上获取的所有资源本质上都是文件!
而我们从网络中获取数据本质上就是从 Linux服务器当中获取文件,而每个文件都是有路径的,所以找到一个文件直接通过文件路径即可访问资源。而我们能够找到对应文件的 前提是我们能够找到对应的服务器。 而要想 找到一个服务器就必须要知道该服务器的 IP[ + PORT] ,而IP[PORT] + 服务器中的文件路径 也就在网络中标识了唯一的文件(资源)
这里IP后面为什么要给PORT打上括号呢?
在最上方那张图当中,有一点过时了,实际上现在登录信息时,是不需要用户身份信息验证的。并且片段标识符早期可以将图片进行轮播所用,但是现在运用的同样少了,所以这张图实际上应该如下所示:
明明 成熟的应用层协议都是与 端口号强关联的,但是这里的 url 却没有端口号
当我们在某度的搜索框内搜索东西的时候,比如我们搜索C++,浏览器将在资源路径后面出现一个 wd=关键字信息
(不同的浏览器解释可能不同,有些浏览器就不会使用wd),如果你仔细观察,其实wd后面跟着的就是我们想要索引的关键字。而这些关键字有时候会发现跟我们在搜索框内搜索的不同:
我们在随便搜索一个问题时,我们会在资源路径后面看到一大堆的字符串,其中包含不少的特殊字符,比如 ‘?’,‘/’, ‘:’ 等字符,而 这些字符实际上有特殊含义的,已经被 url 当做特殊意义理解了。因此这些字符不能随意出现. 比如, 某个参数中需要带有这些特殊字符, 就必须先对特殊字符进行转义。
由上面的例子我们可以看出,其实浏览器对特殊字符的转换是有规则的,在 URL 中,一些特殊字符(如 /, ?, : 等)具有特定的含义,因此不能直接出现在请求的参数中。这些字符在传递时需要进行 URL 编码。编码后的字符以 %XY 的形式表示,其中 XY 是字符的 16 进制表示。
编码规则如下:
解码是将 %XY 转回原始字符。
比如:在上面百度搜索那
虽然我们可以从头开始实现这个编码和解码过程,但实际上现成的编码和解码函数已经非常成熟,可以直接使用。我们可以在网上查找相关的 URL 解码源码,作为工程师使用即可。
wd -- 解释说明
在 URL 中,wd 通常是 查询参数(query parameter) 的一部分,具体含义取决于上下文和网站的实现。它通常用于传递用户输入的关键字或搜索词,尤其是在搜索引擎或站内搜索功能中。
请求行(首行):[版本号] + [状态码] + [状态码解释]
请求报头(Header)
空行
请求正文(Body)
一个完整的HTTP请求报文包括上述四大块,并通过TCP连接发送到服务器。
HTTP响应的格式与请求格式非常相似,也分为四部分。
状态行 响应的第一行是状态行,同样由三部分组成:
响应报文
空行:与请求格式一样,响应报文后也跟着一个空行。 响应正文
这四部分构成了HTTP响应报文,并通过TCP连接发送回客户端。
基本框架,先把我们之前文章写的 Common.hpp、InetAddr.hpp、Log.hpp、Mutex.hpp 导入
InetAddr.hpp 部分修改如下:
Common.hpp 部分修改如下:
Makefile
httpserver:HttpServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -rf httpserver
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include "Log.hpp"
#include "Mutex.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace SocketModule
{
using namespace LogModule;
class Socket;
using SockPtr = std::shared_ptr<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 Accepter (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;
// 提供一个创建 listensockfd 的固定套路
void BuildTcpSocketMethod(int port)
{
SocketOrDie();
SetSocketOpt();
BindOrDie(port);
ListenOrDie();
}
class TcpSocket : public Socket
{
public:
TcpSocket(): _sockfd(gdefaultsockfd)
{}
TcpSocket(int sockfd): _sockfd(sockfd)
{}
virtual ~TcpSocket()
{}
virtual void SocketOrDie() override // 创建成功/死亡
{
_sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if(_sockfd < 0)
{
LOG(LogLevel::ERROR) << "socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "socket create success: " << _sockfd;
}
virtual void SetSocketOpt() override
{
}
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::ERROR) << "bind error";
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind success: " << _sockfd;
return true;
}
virtual bool ListenOrDie() override
{
if(_sockfd == gdefaultsockfd) return false;
int n = ::listen(_sockfd, gbacklog);
if(n < 0)
{
LOG(LogLevel::ERROR) << "listen error";
exit(LISTEN_ERR);
}
LOG(LogLevel::DEBUG) << "listen success: " << _sockfd;
return true;
}
// 拿到 1. 文件描述符 2. client info(客户端信息)
virtual SockPtr Accepter(InetAddr *client) override
{
if(!client) return nullptr;
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newsockfd = ::accept(_sockfd, CONV(&peer), &len);
if(newsockfd < 0)
{
LOG(LogLevel::WARNING) << "accept error";
return nullptr;
}
client->SetAddr(peer, len);
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];
auto size = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
if(size > 0)
{
buffer[size] = 0;
*out = buffer;
}
return size;
}
virtual int Send(const std::string &in) override
{
auto size = ::send(_sockfd, in.c_str(), in.size(), 0);
return size;
}
virtual int Fd() override
{
return _sockfd;
}
private:
int _sockfd;
};
}
#pragma once
#include <iostream>
#include <memory>
#include <functional>
#include <sys/wait.h>
#include "Socket.hpp"
namespace TcpServerModule
{
using namespace SocketModule;
using namespace LogModule;
using tcphandler_t = std::function<bool(SockPtr, InetAddr)>;
// 它只负责进行流式 IO,不对协议做任何处理 -- 相当于只负责读写就行
class TcpServer
{
public:
TcpServer(int port)
: _listensockp(std::make_unique<TcpSocket>()),
_running(false),
_port(port)
{}
void InitServer(tcphandler_t handler)
{
_handler = handler;
_listensockp->BuildTcpSocketMethod(_port);
}
void Loop() // 循环
{
_running = true;
while(_running)
{
// 1. Accept
InetAddr clientaddr;
auto sockfd = _listensockp->Accepter(&clientaddr);
if(sockfd == nullptr) continue;
// 2. IO处理
LOG(LogLevel::DEBUG) << "get a new client, info is: " << clientaddr.Addr();
// sockfd->Recv();
// sockfd->Send();
pid_t id = fork();
if(id == 0)
{
_listensockp->Close();
if(fork() > 0) exit(0);
_handler(sockfd, clientaddr); // 回调
exit(0);
}
sockfd->Close();
waitpid(id, nullptr, 0);
}
_running = false;
}
~TcpServer()
{
_listensockp->Close();
}
private:
// 一定要有一个 Listensock
std::unique_ptr<Socket> _listensockp;
bool _running;
tcphandler_t _handler;
int _port;
};
}
#pragma once
#include <iostream>
#include <string>
#include "TcpServer.hpp"
using namespace TcpServerModule;
const std::string Sep = "\r\n";
const std::string BlankLine = Sep;
class HttpServer
{
public:
HttpServer(int port)
: _tsvr(std::make_unique<TcpServer>(port))
{
}
void Start()
{
_tsvr->InitServer([this](SockPtr sockfd, InetAddr client){
return this->HandlerHttpRequest(sockfd, client);
});
_tsvr->Loop();
}
// 就是我们处理http的入口
bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
{
LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
return true;
}
~HttpServer() {}
private:
std::unique_ptr<TcpServer> _tsvr;
};
#include "HttpServer.hpp"
int main()
{
auto httpserver = std::make_unique<HttpServer>(8080);
httpserver->Start();
return 0;
}
结果运行如下:
打开浏览器进行搜索,相当于用浏览器作客户端 -- B/S 模式
搜索格式:http:://云服务器ip:端口号/,然后结果如下:
结果如下:
我们可以发现,客户端发来的请求报头一共有11行,其中最后一行为空行,但实际上它们就只有一行,只不过每一行的末尾都有 \r\n 将它们连接到一起。我们之前说过,无论是什么协议,都要做到 将报头 和 有效载荷进行分离,同样,http协议也是如此。
在上图中,空行以上实际上全部都是有效载荷的部分,空行之后就是正文,只不过这里我们没有做任何处理,所以空行下面就啥也没有。
如果这里无法访问的话,我们就需要去服务器上开放一下端口号的,这个需要注意一下
这里说一下:网页不会写的话,可以直接要 AI 跑一个,或者可以搜一下 w3cschool html教程
现在我们基本可以知道 我们处理 Http 的入口基本都在 HandlerHttpRequest 中,那么我们需要改一下我们之前的代码,因为那个直接写 HTML 代码在string 中,还是过于冗长,而且不方便,因此修改如下:
// 就是我们处理http的入口
bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
{
LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
std::string http_request;
sockfd->Recv(&http_request); // 在 Http 这里暂时不做报文完整性处理
HttpRequest req;
req.Deserialize(http_request);
return true;
}
Common.http 修改如下:
HttpProtocol.hpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <unordered_map>
#include "Common.hpp"
#include "Log.hpp"
const std::string Sep = "\r\n";
const std::string LineSep = " "; // 行分隔符
const std::string HeaderLineSep = ": "; // 属性分隔符
const std::string BlankLine = Sep;
const std::string defaulthomepage = "wwwroot"; // 自定义的 web 根目录
const std::string http_version = "HTTP/1.0";
const std::string page404 = "wwwroot/404.html";
const std::string firstpage = "index.html";
using namespace LogModule;
// B/S 模式 -- 由浏览器当客户端了
class HttpRequest
{
public:
HttpRequest(){}
~HttpRequest(){}
// GET /favicon.ico HTTP/1.1
// Host: 1.12.51.69:8080
// Connection: keep-alive
// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 SLBrowser/9.0.5.12181 SLBChan/105 SLBVPV/64-bit
// Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
// Referer: http://1.12.51.69:8080/
// Accept-Encoding: gzip, deflate
// Accept-Language: zh-CN,zh;q=0.9
void Deserialize(std::string &request_str)
{
std::cout << "########################" << std::endl;
std::cout << "之前: request:\n" << request_str;
if(ParseOneLine(request_str, &_req_line, Sep)) // _req_line 在这里被初始值
{
// 提取请求行中的详细字段
ParReqLine(_req_line, LineSep);
}
std::cout << "之后: request:\n" << request_str;
Print();
std::cout << "########################" << std::endl;
}
void Print()
{
std::cout << "_method: " << _method << std::endl;
std::cout << "_uri: " << _uri << std::endl;
std::cout << "_version: " << _version << std::endl;
}
std::string Uri()
{
return _uri;
}
void SetUri(const std::string newuri)
{
_uri = newuri;
}
private:
// 从 HTTP 请求行中提取方法(_method)、URI(_uri)和版本(_version),并将 _uri 与一个默认主页路径(defaulthomepage)拼接
void ParReqLine(std::string &_req_line, const std::string &Sep)
{
(void)Sep;
std::stringstream ss(_req_line); // 流式分隔
ss >> _method >> _uri >> _version;
}
std::string _req_line; // 首行
std::vector <std::string> _req_header; // 请求报头
std::string _blank_line; // 空行
std::string _body; // 请求正文
// 在反序列化的过程中,细化我们解析出来的字段
std::string _method;
std::string _uri;
std::string _version;
std::unordered_map<std::string, std::string> _headerkv;
};
// 对于 http 任何请求都要有应答
class HttpResponse
{
public:
HttpResponse(): _verion(http_version), _blank_line(Sep)
{}
~HttpResponse()
{}
};
如果这次我们在请求的后面加上一些路径,比如: /a/b/c/d.html,结果如下:
在http请求报头当中,存在一个 key为 Content-Length value为 正文长度 的key: value请求行,如果发送的请求当中没有正文,那么这个字段也就不会存在。HttpRequest 类修改如下:
bool ParseHeader(std::string& request_str)
{
std::string line;
while (true)
{
bool r = ParseOneLine(request_str, &line, Sep);
if (r && !line.empty())
{
_req_header.push_back(line);
}
else if (r && line.empty()) // 读到了 空行
{
_blank_line = Sep;
break;
}
else return false; // 解析失败
}
return true;
}
void Deserialize(std::string& request_str)
{
if (ParseOneLine(request_str, &_req_line, Sep)) // 1. 解析请求报头
{
// 提取请求行详细字段
ParReqLine(_req_line, LineSep);
ParseHeader(request_str); // 2. 报头继续请求
_body = request_str; // 3. 获取正文
}
}
void Print()
{
std::cout << "_method: " << _method << std::endl;
std::cout << "_uri: " << _uri << std::endl;
std::cout << "_version: " << _version << std::endl;
for (auto& line : _req_header) {
std::cout << line << std::endl;
}
std::cout << "_blank_line: " << _blank_line << std::endl;
std::cout << "_body: " << _body << std::endl;
}
然后再在 HttpServer.cc 那加个 Print 打印即可,此时我们要的东西是可以正常获得的就可以出来了
但是我们的解析还是不够彻底,还需要继续打散,因此我们这里就需要用到 哈希 来进行
bool ParseHeaderkv()
{
std::string key, value;
for (auto& header : _req_header)
{
// Connection: keep-alive -> 以 ": " 分割字符串为 Kv
if (SplitString(header, HeaderLineSep, &key, &value))
{
_headerkv.insert(std::make_pair(key, value));
}
}
return true;
}
bool ParseHeader(std::string& request_str)
{
std::string line;
while (true)
{
bool r = ParseOneLine(request_str, &line, Sep);
if (r && !line.empty())
{
_req_header.push_back(line);
}
else if (r && line.empty()) // 读到了 空行
{
_blank_line = Sep;
break;
}
else return false; // 解析失败
}
ParseHeaderkv();
return true;
}
void Print()
{
std::cout << "_method: " << _method << std::endl;
std::cout << "_uri: " << _uri << std::endl;
std::cout << "_version: " << _version << std::endl;
for (auto& kv : _headerkv) {
std::cout << kv.first << " # " << kv.second << std::endl;
}
std::cout << "_blank_line: " << _blank_line << std::endl;
std::cout << "_body: " << _body << std::endl;
}
Common.hpp 下的 SplitString实现如下:
// Connection: 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;
}
此时结果如下:
🚩 URI是客户端访问服务器的资源路径,当服务器端收到客户端发来的请求时,如果没有访问任何资源路径,那么默认就是 ‘/’ ,而默认的’/’ 需要有一个默认的首页,以后只要对服务器进行访问,如果没有访问具体的服务器资源,就会默认跳转到给出的默认首页,通常默认首页是一个 index.html 文件。而这些文件,通常存在于一个名为 wwwroot 的目录结构下,服务器默认访问资源路径实际上就是wwwroot目录下的 index.html文件。
后续我们在资源访问的时候,都需要默认带上 wwwroot或者我们自定义的 web 根目录,如下:
void ParReqLine(std::string& _req_line, const std::string& Sep)
{
(void)Sep;
std::stringstream ss(_req_line); // 流式分隔
ss >> _method >> _uri >> _version;
_uri = defaulthomepage + _uri; // defaulthomepage 全局定义的web根目录
}
运行如下:
因此我们可以知道所有的请求都会被转化到 从 web 根目录下找的内容,而不是从 Linux 根目录下找了,这个很关键,后面我们的 网页请求链接 就会设计到这个
继续往后,我们如果想要得到文件内容的话
std::string GetContent()
{
std::string content;
std::ifstream in(_uri);
if(!in.is_open()) return std::string();
std::string line;
while(std::getline(in, line))
{
content += line;
}
in.close();
return content;
}
响应 HttpResponse 类实现如下:
// 对于 http 任何请求都要有应答
class HttpResponse
{
public:
HttpResponse(): _verion(http_version), _blank_line(Sep)
{}
~HttpResponse()
{}
void Build(HttpRequest &req)
{
_content = req.GetContent();
if(_content.empty())
{
// 用户请求的资源并不存在
_status_code = 404;
req.SetUri(page404);
_content = req.GetContent();
}
else
{
_status_code = 200;
}
LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();
_status_desc = Code2Desc(_status_code); // 和状态码强相关
}
void Serialize(std::string *resp_str)
{
// 首行: 版本+ 空格 + 状态码 + 空格 + 状态码描述 + 换行符
_resp_line = _verion + LineSep + std::to_string(_status_code) + LineSep + _status_desc + Sep;
_body = _content; // 正文
// 序列化
*resp_str = _resp_line;
for(auto &line : _resp_header)
{
*resp_str += (line + Sep);
}
*resp_str += _blank_line;
*resp_str += _body;
}
private:
std::string Code2Desc(int code)
{
switch(code)
{
case 200: return "OK";
case 404: return "Not Found";
default: return std::string();
}
}
private:
// 必备要素
std::string _verion;
int _status_code; // 状态码
std::string _status_desc; // 状态码描述
std::unordered_map<std::string, std::string> _header_kv;
std::string _resp_line;
std::vector<std::string> _resp_header;
std::string _blank_line;
std::string _body;
std::string _content;
};
然后我们再用AI 写个 404 界面(404.html),将其放在 wwwroot 目录下,方便我们等下使用,然后,此时 Http 入口函数修改如下:
// 就是我们处理http的入口
bool HandlerHttpRequest(SockPtr sockfd, InetAddr client)
{
LOG(LogLevel::DEBUG) << "HttpServer: get a new client: " << sockfd->Fd() << " addr info: " << client.Addr();
std::string http_request;
sockfd->Recv(&http_request); // 在 Http 这里暂时不做报文完整性处理
HttpRequest req;
req.Deserialize(http_request);
HttpResponse resp;
resp.Build(req);
std::string resp_str;
resp.Serialize(&resp_str);
sockfd->Send(resp_str);
return true;
}
结果如下:
浏览器此时也可以正常显示出对应的 HTML 代码所对应的 页面了
然后我们再对 HttpResponse 做修改,如下:
void Build(HttpRequest& req)
{
std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
if (uri.back() == '/')
{
uri += firstpage; // wwwroot/index.html
req.SetUri(uri);
}
_content = req.GetContent();
LOG(LogLevel::DEBUG) << "content length: " << _content.size();
if (_content.empty())
{
// 用户请求的资源并不存在
_status_code = 404;
req.SetUri(page404);
_content = req.GetContent();
}
else
{
_status_code = 200;
}
LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();
_status_desc = Code2Desc(_status_code); // 和状态码强相关
}
void Serialize(std::string* resp_str)
{
// 首行: 版本+ 空格 + 状态码 + 空格 + 状态码描述 + 换行符
_resp_line = _verion + LineSep + std::to_string(_status_code) + LineSep + _status_desc + Sep;
_body = _content; // 正文
// 序列化
*resp_str = _resp_line;
for (auto& line : _resp_header)
{
*resp_str += (line + Sep);
}
*resp_str += _blank_line;
*resp_str += _body;
}
此时进入我们的服务器链接,就可以看到默认主页了,如下:
然后我们再在我们的链接之后,输入我们登录界面对应的 login.html,则跳转如下:
网页页面之间跳转的功能,是通过链接来进行切换页面的,如下图:
然后对应应答再做一些修改,如下:
void Build(HttpRequest& req)
{
std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
if (uri.back() == '/'){
uri += firstpage; // wwwroot/index.html
req.SetUri(uri);
}
_content = req.GetContent();
LOG(LogLevel::DEBUG) << "content length: " << _content.size();
if (_content.empty()){
// 用户请求的资源并不存在
_status_code = 404;
req.SetUri(page404);
_content = req.GetContent();
}
else{
_status_code = 200;
}
LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();
_status_desc = Code2Desc(_status_code); // 和状态码强相关
if(!_content.empty()){
SetHeader("Content-Length", std::to_string(_content.size()));
}
for (auto& header : _header_kv){
_resp_header.push_back(header.first + HeaderLineSep + header.second);
}
}
// ...
void SetHeader(const std::string& k, const std::string& v){
_header_kv[k] = v;
}
为了好看我们把图片也导入,如下:
但是其实服务器是访问了图片的,如下:
因此我们还要对我们的代码进行修改,如下:
此时图片就可以正常显示了,如下:
然后由于图片一般在代码中是二进制的形式显示,因此我们需要设置 HTTP 响应头中的 Content-Type
字段,用于将文件后缀映射到对应的 MIME 类型描述。这个功能在 Web 开发中非常常见,通常用,如下:
现在我们的 服务器-- 网页 代码就基本写好了
什么是网站?站在我们程序员的角度来说,网站其实就是一堆特定的目录和文件所构成的目录结构!前端程序员并不需要管后端的各种服务是怎么实现的,只需要将wwwroot里的内容做好,进行页面之间的跳转即可。
🔥 Content-Type 表示资源的数据类型,资源类型必须得提前在代码当中定义好,如果你拿着一个js文件返回给浏览器,但是却告诉浏览器这个文件是html文件,这就可能会导致一些无法预测的问题,不过浏览器还是很聪明的,如果你发来的是js文件,那么浏览器不会解释为html文件的,但是文件类型有非常多,浏览器不能保证每次都能帮你纠正错误,在服务器的代码中,我们有必要将文件后缀做解析,发送正确的文件后缀,至于如何做,我们可以使用 Content-Type对照表 来定位文件类型:
但是这里有个问题:明明请求的 服务器和端口号就是我,为啥 host 需要把我的 ip 和 端口号带上,这里其实是因为 服务器 作为了 代理服务器,如下:
但是对于公司而言,它不可能只有一台服务器,那么我们把这么多台提供资源的 http 服务器 称为(集群或者机房),此时就需要在我的 Http 服务器内维护一张资源服务器的清单 / 列表,然后从这里获取服务器信息,此时代理服务器就需要使用策略动态选择后端服务器,维护了每个后端服务器的压力,此时就做了 转发 和 负载均衡
上面我们在 Code2Desc 那用到了状态码,这里我们补充一下 Http 状态码知识:
最常见的状态码, 比如 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 为代表 HTTP 状态码 301(永久重定向)和 302(临时重定向)都依赖 Location 选项。以下 是关于两者依赖 Location 选项的详细说明:
HTTP 状态码 301(永久重定向):
例如,在 HTTP 响应中,可能会看到类似于以下的头部信息:
HTTP/1.1 301 Moved Permanently\r\n
Location: https://www.new-url.com\r\n
HTTP 状态码 302(临时重定向):
例如,在 HTTP 响应中,可能会看到类似于以下的头部信息
HTTP/1.1 302 Found\r\n
Location: https://www.new-url.com\r\n
我们对上面代码做些修改,如下:
void Build(HttpRequest& req)
{
#define TestRedir 1
#ifdef TestRedir
_status_code = 302;
_status_desc = Code2Desc(_status_code); // 和状态码强相关
SetHeader("Location", "https://www.qq.com/");
for (auto& header : _header_kv)
{
_resp_header.push_back(header.first + HeaderLineSep + header.second);
}
#else
std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
if (uri.back() == '/')
{
uri += firstpage; // wwwroot/index.html
req.SetUri(uri);
}
_content = req.GetContent();
LOG(LogLevel::DEBUG) << "content length: " << _content.size();
if (_content.empty())
{
// 用户请求的资源并不存在
_status_code = 404;
req.SetUri(page404);
_content = req.GetContent();
}
else
{
_status_code = 200;
}
LOG(LogLevel::DEBUG) << "客户端在请求:" << req.Uri();
_status_desc = Code2Desc(_status_code); // 和状态码强相关
if (!_content.empty())
{
SetHeader("Content-Length", std::to_string(_content.size()));
}
std::string mime_type = Suffix2Desc(req.Suffix());
SetHeader("Content-Type", mime_type);
for (auto& header : _header_kv)
{
_resp_header.push_back(header.first + HeaderLineSep + header.second);
}
#endif
}
此时结果如下:
然后再做点修改,
此时访问结果如下:
总结:无论是 HTTP 301 还是 HTTP 302 重定向,都需要依赖 Location 选项来指定资 源的新位置。这个 Location 选项是一个标准的 HTTP 响应头部,用于告诉浏览器应该 将请求重定向到哪个新的 URL 地址。
在发送报文或者相应报文的报头当中,都在一个叫做 Connection
的字段,HTTP 中的 Connection 字段是 HTTP 报文头的一部分,它主要 用于控制和管理客户端与服务器之间的连接状态。
HTTP 中的 Connection 字段是 HTTP 报文头的一部分,它主要用于控制和管理客户端与服务器之间的连接状态
核心作用
持久连接(长连接)
语法格式
字段名 | 含义 | 样例 |
---|---|---|
Accept | 客户端可接受的响应内容类型 | Accept:text/html,application/xhtml+xml,application/xml;q=0.9, image/webp,image/apng,*/*;q=0.8 |
Accept-Encoding | 客户端支持的数据压缩格式 | Accept-Encoding: gzip, deflate, br |
Accept- Language | 客户端可接受的语言类型 | Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 |
Host | 请求的主机名和端口号 | Host: www.example.com:8080 |
User-Agent | 客户端的软件环 境信息 | User-Agent: Mozilla/5.0 (Windows NT10.0; Win64; x64)AppleWebKit/537.36 (KHTML, likeGecko)Chrome/91.0.4472.124Safari/537.36 |
Cookie | 客户端发送给服 务器的 HTTP cookie 信息 | Cookie: session_id=abcdefg12345;user_id=123 |
Referer | 请求的来源 URL | Referer:http://www.example.com/previous_page.html |
Content-Type | 实体主体的媒体类型 | Content-Type: application/x-www-form-urlencoded (对于表单提交) 或Content-Type: application/json (对于JSON 数据) |
Content-Length | 实体主体的字节大小 | Content-Length: 150 |
Authorization | 认证信息,如用户名和密码 | Authorization: BasicQWxhZGRpbjpvcGVuIHNlc2FtZQ== (Base64 编码后的用户名:密码) |
Cache-Control | 缓存控制指令 | 请求时:Cache-Control: no-cache 或Cache-Control: max-age=3600; 响应时:Cache-Control: public, max-age=3600 |
Connection | 请求完后是关闭还是保持连接 | Connection: keep-alive 或 Connection: close |
Date | 请求或响应的日期和时间 | Date: Wed, 21 Oct 2023 07:28:00 GMT |
Location | 重定向的目标URL(与 3xx 状态码配合使用) | Location: http://www.example.com/new_location.html (与 302 状态码配合使用) |
Server | 服务器类型 | Server: Apache/2.4.41 (Unix) |
Last-Modified | 资源的最后修改 时间 | Last-Modified: Wed, 21 Oct 2023 07:20:00 GMT |
ETag | 资源的唯一标识符,用于缓存 | ETag: "3f80f-1b6-5f4e2512a4100" |
Expires | 响应过期的日期和时间 | Expires: Wed, 21 Oct 2023 08:28:00GMT |
GET 方法:
POST 方法:
PUT 方法(不常用):
HEAD 方法:
DELETE 方法(不常用):
OPTIONS 方法:
以上这些方法一般都不是由后端代码来完成的,不过如果想要处理这些请求后端也可以处理,一般这些都属于前端页面的请求方法,我们可以通过 HTML表单 来获取简单的前段页面,而以上方法中最重要的莫过于 GET 和 POST方法了。
其中GET方法我们如何理解呢?就拿简单的登录页面来说,我们登录页面实际上在前端代码中,就是一个form表单,我们对我们上面的代码进行修改,方便理解:
void Build(HttpRequest& req)
{
std::string uri = req.Uri(); // wwwroot/ wwwroot/a/b/
if (uri.back() == '/')
{
uri += firstpage; // wwwroot/index.html
req.SetUri(uri);
}
LOG(LogLevel::DEBUG) << "----- 客户端在请求:" << req.Uri();
req.Print();
LOG(LogLevel::DEBUG) << "--------------------------";
_content = req.GetContent();
// LOG(LogLevel::DEBUG) << "content length: " << _content.size();
if (_content.empty())
{
// 用户请求的资源并不存在
_status_code = 404;
req.SetUri(page404);
_content = req.GetContent();
}
else
{
_status_code = 200;
}
_status_desc = Code2Desc(_status_code); // 和状态码强相关
if (!_content.empty())
{
SetHeader("Content-Length", std::to_string(_content.size()));
}
std::string mime_type = Suffix2Desc(req.Suffix());
SetHeader("Content-Type", mime_type);
for (auto& header : _header_kv)
{
_resp_header.push_back(header.first + HeaderLineSep + header.second);
}
}
把我们的方法换成 GET,如下:
然后登录输入账号密码,如下:
如果我们再将方法换成 POST,如下:
因此我们可以知道 GET 通常获取网页内容,是通过url来上传资源,而POST方法上传参数,是以正文形式进行参数上传的, content-length + request body 传参,POST 传参更私密,但是并不是更安全
静态内容
GET /index.html
index.html
文件内容。
动态内容
GET /user?id=123
上面完整代码:服务器-网页