一、认识 URL
平时我们俗称的'网址'其实就是说的 URL。
域名会通过 DNS 被解析为服务器的 IP 地址。但是现在的服务器不允许用户直接通过 IP 地址请求服务。
二、urlencode 和 urldecode
像 / ? : 等这样的字符,已经被 url 当做特殊意义理解了。所以这些字符不能随意出现。 例如,某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
转义的规则如下: 将需要转码的字符转为 16 进制,然后从右到左,取 4 位 (不足 4 位直接处理),每 2 位做一位,前面加上%,编码成%XY 格式。
'+'被转义成了'%2B'
urldecode 就是 urlencode 的逆过程。
三、HTTP 协议格式(使用抓包工具)
3.1 安装并使用抓包工具
这里我们使用软件 Fiddler 进行抓包,刚刚安装的软件无法对 HTTPS 进行抓包,如果需要对 HTTPS 进行抓包,需要进行以下操作。
接下来我们就可以看一下 HTTP 协议的格式了。
3.2 HTTP 协议格式
将上面的内容进行提取就是下面图片中的内容了。这里我只提取了部分内容。
3.2.1 HTTP 请求
- 提到请求报文来说,我们需要解决以下问题:
- 报头和有效载荷是如何分离的
- 有效载荷如何交付的
- 对于 HTTP 请求报文来说,我们需要解决以下问题: 3. HTTP 是如何做的读取到完整的报文的 4. HTTP 是如何进行反序列化的
- 回答问题
- HTTP 的报头和有效载荷是通过空行进行分离的
- HTTP 是顶层协议,所以不需要解决向上交付的问题
- 首先读取到空行,说明报头已经读取完整,报头中有一个字段为 Content-Length,再读取 Content-Length 字节大小的内容,即可将请求正文的读取完整,结合起来就将整个报文读取完整
- 正文部分可以根据正文的类型进行反序列化,根据类型的不同,方式有很多种,这里先不考虑。其他部分则是通过\r\n反序列化即可。
3.2.1.1 资源 URL 路径
资源 URL 路径就是图片、网页、音频等资源的路径。
在服务器上,通常会使用一个特定的目录,来保存 HTTP 服务器的所有资源。
就如下图中,我这个简易的 HTTP 服务器中,就将主页和图片资源都放在了 wwwroot 这个目录下。web 根目录是 web 服务器存储网站文件的顶级目录,由于 HTTP 服务器是 web 服务器的子集,所以我这里的 wwwroot 目录就可以被当做 web 根目录。
当用户使用的网站并没指定 URL,则 URL 为 \ ,此时用户访问的就是 web 根目录下的默认文件(通常为首页)。
当用户使用的网站并指定了 URL,则请求中的 URL 就是用户指定的 URL。
3.2.1.2 请求方法(Method)
下面就是 HTTP 可以使用的方法,但是最常用的还是 GET 和 POST 方法。
GET:可以用来获取资源,也可以用来上传数据(参数)。 POST:可以用来上传数据(参数)。
在一些场景下,我们需要将数据上传给服务器,例如登录、注册、搜索等,我们通过 GET/POST 再结合 HTML(HTML 表单)来实现这样的场景。
下面我实现了一个简单的登录页面,当我输入了用户名和密码后,再点击提交,发现我提交的信息作为了参数被拼接到了 URL 中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
</head>
<body>
<form action="login.html">
用户名:<br>
<input type="text" name="name" value="chineseperson04"><br>
密码:<br>
<input type="text" name="password"><br><br>
<input type="submit" value="登录"></form>
<a href=>回到首页
在 HTML 表单中我们还可以设置提交表单时使用的 HTTP 请求方法。
下面我就设置了 HTTP 请求方法为 GET,当我提交参数后,参数就直接拼接到了 URL 中了。
这里我设置 HTTP 请求方法为 POST,再次提交参数后,发现参数并没有拼接到 URL 中,而是保存到了 HTTP 的正文部分了。
当我默认没有设置 HTTP 请求方法,则 HTTP 请求方法默认为 GET。
到这里我们就知道 GET 方法是直接将参数拼接到 URL 中,而 POST 方法是将参数保存到正文中。
这时候有的人就会认为 POST 方法比 GET 方法安全,实际上两者都不安全,因为 HTTP 是明文传输,虽然 POST 方法无法从 URL 中读取到参数,但是不代表无法被抓包,通过下图我们也可以看出来,所以 POST 方法比 GET 方法只是私密性好一些,并没有更安全。
3.2.1.3 Location 头字段(重定向相关)
Location 头字段:服务器在重定向响应中通过 Location 头字段提供新 URL。
例如状态码中的301就代表着永久重定向,举个例子:搜索引擎可以爬取网页中的 URL 关键字,关键字又对应着无数个网络连接。当一个用户通过网页访问一个网站时,这个网站由于改变了域名导致老网站过期了,服务器给用户的响应中状态码为 301,并且 Location 字段中保存着新网站的 URL,这时候用户会直接跳转到新网站,然后再过一段时间后还可以使搜索引擎中该网站 URL 更新。
再例如状态码中的302和307代表临时重定向,这有什么用呢?在生活中,当我们想使用某一个刷题网站时,服务器检测到用户未登录则会设置状态码为 302 或 307,并设置 Location 为登录页面的 URL,该网站就会跳转到登录页面,当我们登录完毕后,服务器检测到用户登录完毕后,则会设置状态码为 302 或 307,并设置 Location 为首页页面的 URL,然后再次跳转到首页。临时重定向最主要的功能就是让用户跳转到目标网页。
3.2.2 HTTP 响应
3.2.2.1 状态码和状态码描述
状态码:表示网页服务器超文本传输协议响应状态的 3 位数字代码 状态码描述:状态码对应的简短文本说明
下面是一些常见的状态码和状态码描述:
| 状态码 | 状态码描述 |
|---|---|
| 200 - OK | 代表一切正常,服务器处理客户端请求,并返回客户端请求的资源 |
| 204 - No Content | 与 200 基本相同,但是并不返回给客户端任何内容 |
| 301 - Moved Permanently | 永久重定向,请求资源已经不在,需要新的 URL 重新访问 |
| 302 - Found | 临时重定向,请求资源还在,但是暂时需要使用新的 URL 重新访问 |
| 304 - Not Modified | 缓存重定向,没有跳转的含义,表示资源还在,重定向已存在的缓存文件,也就是告诉客户端可以继续使用缓存资源 |
| 307- Temporary Redirect | 临时重定向,类似 302,但明确要求客户端保持请求方法不变。 |
| 400 - Bad Request | 客户端报文有错误,但是是笼统的错误 |
| 403 - Forbidden | 客户端报文没有错误,但是服务器禁止访问资源 |
| 404 - Not Found | 客户端请求的资源,在服务器上不存在 |
| 500 - Internal Server Error | 服务器发生错误,是个笼统的错误码 |
| 501 - Not Implemented | 客户端请求的资源,服务器目前还不支持 |
| 502 - Bad Gateway | 服务器运行正常,但访问后端服务器发生错误 |
| 503 - Service Unavailable | 服务器繁忙,无法进行响应 |
3.2.3 请求与响应共有字段
3.2.3.1 HTTP 协议版本
注意:请求与响应中的 HTTP 协议版本的定义是不同的。
请求中的 HTTP 协议版本:浏览器支持的 HTTP 协议 响应中的 HTTP 协议版本:服务器支持的 HTTP 协议
请求声明客户端上线,响应声明服务器实现。版本匹配通过协商机制确保兼容性,这样可以让不同的客户端分别有不同的服务。
3.2.3.2 Content-Type
Content-Type 在请求与响应中指的都是描述正文内容的类型。
- 请求中描述的是客户端发送的数据格式
- 响应中描述的是服务器返回的数据格式
关于 Content-Type 对应文件后缀,我这里列举一些常见的,其他的大家可以到 HTTP content-type 网站中了解。
| Content-Type | 文件后缀 |
|---|---|
| text/html | .html / .htm |
| application/json | .json |
| image/jpeg | .jpg / .jpeg |
| image/png | .png |
| application/pdf | |
| text/plain | .txt |
3.2.4 Cookie 和 Session
3.2.4.1 Cookie
首先我们需要知道 HTTP 是无连接和无状态的,无状态就是 HTTP 协议在设计上不会保留客户端与服务器之间的交互状态,这就导致了服务器并不知道后序的请求是否来自同一个用户。
例如:用户想要在某刷题网站上刷题,这时候就需要用户登录,登录完毕后,用户回到了首页,此时用户想要完成第一道题目,这时候服务器不知道这个用户已经登录过了,需要让用户再次登录,当用户完成第一题,需要完成第二道题时,服务器又不认识这个用户了,又需要用户再次登录,多次登录势必会导致用户不想再次使用了。
如何解决用户需要多次登录的问题呢?这里就要提到 Cookie 了。
当用户首次登录成功以后,服务器会在响应报头中添加一个或多个 SetCookie 字段,当浏览器得到响应后,会将 Cookie 信息保存起来,当用户再次访问该网站时,浏览器会自动将 Cookie 信息添加到请求报头中,而服务器得到请求报头后,会自动根据请求报头中的 Cookie 信息进行认证,认证通过则将用户请求的资源发送给浏览器。
下面我使用 Edge 浏览器来演示如何看到 Cookie,并且删除 Cookie 后发生什么。
这里我以 B 站为例,当我将 Cookie 删除后,当我再次访问 B 站时,就需要重新登录了。如果在登录状态下,再次访问则不需要登录。
但是仅仅这样让浏览器保存 Cookie 可能会导致用户的敏感信息泄露,例如下图,我向我的 HTTP 服务器发送请求,浏览器中确实保存了 Cookie 信息,但是很容易的就被抓包了。这样就可能导致别人拿着用户的 Cookie 信息,登录用户账号,所以 Cookie 信息不能保存在用户的浏览器中,下面的 Session 中会讲到。
3.2.4.2 Session
上面我们提到了浏览器保存 Cookie 信息会使得用户的敏感信息很容易就被抓包,所以我们就让服务器保存 Cookie 信息,当用户首次登录成功以后,服务器会创建一个Session(会话)来保存 Cookie 信息,并且使用一种方法在 MySQL 中生成一个唯一的 Sessionid,将 Sessionid 添加到响应报头中,浏览器就会只会保存 Sessionid,当用户再次访问的时候,浏览器会自动建 Sessionid 添加到请求报头中,服务器会自动根据 Sessionid 在服务器中查找并认证,认证通过则将用户请求的资源发送给浏览器。
虽然浏览器中的 Sessionid 会被抓包,别人也可以使用 Sessionid 来登录用户的账号,但是却无法抓取用户的敏感信息了。
如何减少别人登录用户的账号呢?大家可以去查一下,可以设置 Session 的时长,到时间后,Session 就自动失效,需要用户再次使用账号密码进行登录,也可以匹配登录账号的地理位置,当位置相隔过大时,就让 Session 失效,让用户再次使用账号密码进行登录。
一个服务器中一定会有很多用户同时登录,那服务器中就会存在很多的 Session(Sessionid),所以服务器一定需要对 Session 进行管理,管理就需要先描述再组织。(先描述)定义一个 Session 类,将一个用户的 Cookie 信息保存起来,(再组织)定义一个 SessionManager 类,使用某种数据结构将所有的 Session 管理起来,SessionManager 中需要包含对 Session 的增删查改的方法。
四、实现一个简单的 HTTP 服务器
整体 HTTP 服务器文件结构
4.1 Socket.hpp(封装套接字)
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#define CONV(addrptr) (struct sockaddr*)addrptr
enum { Socket_err = 1, Bind_err, Listen_err };
const static int defalutsockfd = -1;
const int defalutbacklog = 5;
class Socket {
public:
virtual ~Socket() {};
virtual void CreateSocketOrDie() = 0;
virtual void BindSocketOrDie(uint16_t port) = 0;
= ;
= ;
= ;
= ;
= ;
= ;
= ;
= ;
= ;
:
{
();
();
(port);
(defalutbacklog);
}
{
();
(serverip, serverport);
}
{
(sockfd);
}
};
: Socket {
:
( sockfd = defalutsockfd) : _sockfd(sockfd) {}
~() {};
{
_sockfd = ::(AF_INET, SOCK_STREAM, );
(_sockfd < ) (Socket_err);
}
{
addr;
(&addr, , (addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = (port);
len = (addr);
n = ::(_sockfd, (&addr), len);
(n < ) (Bind_err);
}
{
n = ::(_sockfd, backlog);
(n < ) (Listen_err);
}
{
client;
(&client, , (client));
len = (client);
fd = ::(_sockfd, (&client), &len);
(fd < ) ;
buffer[];
(AF_INET, &client.sin_addr, buffer, len);
*clientip = buffer;
*clientport = (client.sin_port);
Socket* s = (fd);
s;
}
{
server;
(&server, , (server));
server.sin_family = AF_INET;
(AF_INET, serverip.(), &server.sin_addr);
server.sin_port = (serverport);
len = (server);
n = (_sockfd, (&server), len);
(n < ) ;
;
}
{ _sockfd; }
{ _sockfd = sockfd; }
{
(_sockfd > defalutsockfd) {
(_sockfd);
}
}
{
inbuffer[size];
n = (_sockfd, inbuffer, (inbuffer) - , );
(n > ) {
inbuffer[n] = ;
} {
;
}
buffer += inbuffer;
;
}
{
(_sockfd, send_string.(), send_string.(), );
}
{
opt = ;
(_sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, (opt));
}
:
_sockfd;
};
4.2 HttpProtocol.hpp(自定义 HTTP 协议)
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<sstream>
#include<fstream>
const std::string BlankSep = "\r\n";
const std::string SpaceSep = " ";
const std::string wwwroot = "./wwwroot";
const std::string homepage = "index.html";
class HttpRequest {
public:
HttpRequest() : _request_blank(BlankSep), _path(wwwroot) {};
bool Getline(std::string &request, std::string &line) {
auto pos = request.find(BlankSep);
if (pos == request.size()) return false;
line = request.substr(0, pos);
request.erase(0, pos + BlankSep.size());
return true;
}
// 反序列化
void Deserialize(std::string &request) {
std::string line;
bool flag = Getline(request, line);
_request_line = line;
() {
flag = (request, line);
(flag && line.()) {
_request_content = request;
;
} (flag && !line.()) {
_request_hander.(line);
} {
;
}
}
}
{
;
in >> _method >> _url >> _version;
(_url == ) _path += ( + homepage);
_path += _url;
}
{
pos = _request_line.();
(pos != std::string::npos) _suffix = _request_line.(pos);
}
{
();
();
}
{
std::cout << << _request_line << std::endl;
( line : _request_hander) {
std::cout << line << std::endl;
}
std::cout << _request_blank << std::endl;
std::cout << << _request_content << std::endl;
std::cout << << _method << << _url << << _version << std::endl;
}
{
;
(!in.()) ;
in.(, in.end);
filesize = in.();
in.(, in.beg);
std::string content;
content.(filesize);
in.((*)content.(), filesize);
in.();
content;
}
{ (_path); }
{ (wwwroot + + ); }
{ _url; }
{ _path; }
{ _suffix; }
~() {};
:
std::string _request_line;
std::vector<std::string> _request_hander;
std::string _request_blank;
std::string _request_content;
std::string _method;
std::string _url;
std::string _version;
std::string _path;
std::string _suffix;
};
{
:
() : _response_blank(BlankSep), _method(), _code(), _desc() {};
{ _code = code; }
{ _desc = desc; }
{
_response_line = _method + SpaceSep + std::(_code) + SpaceSep + _desc + BlankSep;
}
{ _response_hander.(line); }
{ _response_content = content; }
{
std::string response_str = _response_line;
(& line : _response_hander) {
response_str += line;
}
response_str += _response_blank;
response_str += _response_content;
response_str;
}
~() {};
:
std::string _response_line;
std::vector<std::string> _response_hander;
std::string _response_blank;
std::string _response_content;
std::string _method;
_code;
std::string _desc;
};
4.3 TcpServer.hpp(服务端封装)
#pragma once
#include"Socket.hpp"
#include<string>
#include<functional>
#include<pthread.h>
using func_t = std::function<std::string(std::string &)>;
class TcpServer;
class ThreadDate {
public:
ThreadDate(TcpServer *tser_this, Socket *socket) : _this(tser_this), _socket(socket) {}
public:
TcpServer *_this;
Socket *_socket;
};
class TcpServer {
public:
TcpServer(uint16_t port, func_t handler_request) : _port(port), _listensock(new TcpSocket()), _handler_request(handler_request) {
_listensock->BuildListenSocketMethod(_port);
}
static void* HandlerRequest(void* arg) {
pthread_detach(pthread_self());
ThreadDate *th = (ThreadDate *)arg;
std::string inbufferstream;
// 1、读取报文
if (th->_socket->Recv(inbufferstream, 4096)) {
// 2、调用函数处理报文
std::string send_string = th->_this->_handler_request(inbufferstream);
// 3、发送
th->_socket->Send(send_string);
}
th->_socket->();
th->_socket;
th;
;
}
{
() {
std::string clientip;
clientport;
Socket *NewSocket = _listensock->(&clientip, &clientport);
std::cout << << NewSocket->() << << clientip << << clientport << std::endl;
pid;
ThreadDate *td = (, NewSocket);
(&pid, , HandlerRequest, td);
}
_listensock->();
}
~() {}
:
TcpSocket *_listensock;
_port;
:
_handler_request;
};
4.4 wwwroot/index.html(主页)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h2>欢迎来到我的主页!</h2>
<a href="http://47.109.128.33:8888/login.html">登录</a>
</body>
</html>
4.5 wwwroot/login.html(登录页面)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录页面</title>
</head>
<body>
<form action="login.html" method="post">
用户名:<br>
<input type="text" name="name" value="chineseperson04"><br>
密码:<br>
<input type="text" name="password"><br><br>
<input type="submit" value="登录"></form>
< =>回到首页
4.6 wwwroot/404.html(404 页面)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 页面未找到</title>
<style>
body { margin: 0; padding: 0; font-family: 'Arial', sans-serif; background: linear-gradient(135deg, #ff7e5f, #feb47b); color: #fff; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; }
.container { text-align: center; animation: fadeIn 2s ease-in-out; }
h1 { font-size: 10rem; margin: 0; animation: float 3s ease-in-out infinite; }
p { : ; : -; }
{ : inline-block; : ; : ; : ; : ; : (, , , ); : none; : ; : all ease; }
{ : (, , , ); : (); }
float { , { : (); } { : (-); } }
fadeIn { { : ; : (); } { : ; : (); } }
404
哎呀!页面走丢了...
返回首页
4.7 Main.cpp(服务端)
#include"TcpServer.hpp"
#include"HttpProtocol.hpp"
#include<iostream>
#include<unistd.h>
#include<string>
#include<string.h>
#include<memory>
#include<fstream>
using namespace std;
string CodeToType(int code) {
switch (code) {
case 200: return "OK"; break;
case 204: return "No Content"; break;
case 301: return "Moved Permanently"; break;
case 302: return "Found "; break;
case 304: return "Not Modified"; break;
case 307: return ; ;
: ; ;
: ; ;
: ; ;
: ; ;
: ; ;
: ; ;
: ; ;
: ; ;
}
}
{
(suffix == || suffix == ) ;
(suffix == || suffix == ) ;
(suffix == ) ;
;
}
{
HttpRequest req;
code = ;
string desc = ;
req.(request);
req.();
req.();
std::string content = req.();
(content.()) {
code = ;
desc = ();
content = req.();
}
HttpResponse resp;
resp.(code);
resp.(desc);
resp.();
std::string http_content_length = + std::(content.()) + ;
resp.(http_content_length);
std::string http_content_type = + (req.()) + ;
resp.(http_content_type);
resp.(content);
string response_str = resp.();
response_str;
}
{
cout << proc << << endl;
}
{
(argc != ) {
(argv[]);
();
}
serverport = (argv[]);
unique_ptr<TcpServer> up = <TcpServer>(serverport, handler_http_request);
up->();
;
}
本文介绍了 HTTP 协议的基础知识,包括 URL 编码、请求响应格式、状态码及 Cookie Session 机制,并通过 C++ 代码演示了如何从零实现一个简单的 HTTP 服务器。内容涵盖资源路径解析、GET/POST 方法区别、重定向原理以及 Socket 封装细节,适合希望深入理解网络编程原理的开发者阅读。


