文章目录:
- 什么是HTTP?
- 认识URL
- urlencode 和 urldecode
- HTTP 协议请求和响应格式
- HTTP 请求(Request)
- HTTP 响应(Response)
- HTTP 请求方法
- HTTP 的状态码
- 常见的Header
什么是HTTP?
HTTP(超文本传输协议)是一套用于在网络上传输文件(如:文本、图像、声音、视频和其它多媒体)的规则。一旦用户打开 web 浏览器,它们就间接地使用了 HTTP。HTTP 是一个运行在 TCP/IP 协议套件之上的应用协议,TCP/IP 协议套件构成了互联网的基础。它是互联网上应用最广泛的协议之一,用于在客户端和服务器之间传输数据。
HTTP 协议的版本有多个,最常见的是 HTTP/1.1 和 HTTP/2。HTTP/1.1 是目前广泛使用的版本,而 HTTP/2 在性能和效率方面有一些改进,例如支持多路复用、头部压缩等。
认识URL
URL(Uniform Resource Locator)是统一资源定位符的缩写,用于指定和定位互联网上的资源位置,用于指定和定位互联网上的资源位置。它是因特网上用于标识和定位资源的常用标识方法,通常被称为网址。
URL 由多个部分组成,包括协议、主机名、端口号、路径、查询参数和片段标识符等,一个典型的 URL 格式如下:
协议://主机名[:端口号]/路径?查询参数#片段标识符
接下来对 URL 的各部分进行解释:
协议方案名 |
http:// URL 的第一部分是协议方案名,它指示浏览器请求资源时必须使用的协议(协议是在计算机网络中交换或传输数据的一组方法)。通常网站的协议是 HTTPS 或 HTTP(它的不安全版本)。寻址网页需要这两种协议中的一种,但浏览器也知道如何处理其它协议,比如 milto:(打开邮件客户端),所以其它协议也是可以的。
登录信息 |
user:pass 通常用于标识登录认证信息,包括登录用户的用户名和密码。然而,出于安全性考虑,绝大多数的 URL 中的这个字段都被省略了,以避免再明文中传输敏感的认证凭据。
服务器地址 |
www.example.jp:80 它通过字符模式 : // 与协议名方案分开。如果存在,服务器地址包括域(例如:www.example.jp)和端口(如:80),用冒号分隔。
域指示请求的是哪个 Web 服务器。通常这是一个域名,但是也可以使用IP地址(但这种情况很少见,因为使用起来不太方便)。我们可以通过 ping
命令,查看某个网站的IP地址,如下所示,获取 www.qq.con
这个域名解析之后的IP地址:
通过域名,可以方便的看出该网站是干什么的,因此 Web 服务器大多都使用域名。
端口标识用于访问 Web 服务器上资源时使用的技术 “门”。如果 Web 服务器使用 HTTP 协议的标准端口(HTTP 为80,HTTPS 为443)来提供资源访问,则通常会省略端口。否则,端口是必需的。
注意:
协议和主机的分隔符是 “://”。冒号将协议与 URL 的下一部分分隔开来,而双斜杠 “//” 表示 URL 的下一部分是主机信息。
不适用主机信息的 URL 的一个实例是邮件客户端(mailto:foobar)。它包含一个协议,但不适用主机组件。因此,冒号后面不跟着两个斜杠,冒号仅作为协议和邮件地址之间的分隔符。
带层次的文件路径 |
/dir/index.htm 是 Web 服务器上资源的路径。在 Web 早期,这样的路径表示 Web 服务器上的物理文件位置。而如今,它大多是由 Web 服务器处理的抽象概念,没有任何物理实体存在。
现代的 Web 服务器可以根据需要动态生成内容,将路径视为一种抽象的标识符,而不仅仅是指向实际的文件。路径可以映射到数据库查询、API 调用、动态生成的页面或其它处理逻辑,以满足客户端请求并提供相应的响应。
实例:当我们打开浏览器输入腾讯官网的域名(qq.com)之后,浏览器就在对应的服务器上找到相应的资源,浏览器对其进行解析并返回腾讯网的首页给我们。
当我们发起网页请求时,本质是服务器返回对应的网页信息,然后由浏览器进行解析,然后呈现出相关的网页。
这种资源被称为网页资源。除了该资源,我们还可以通过 HTTP 协议请求和传输各种类型的资源,例如:视频、音频、文本、图片等。事实上,HTTP 协议称为超文本传输协议,而不仅仅是文本传输协议,正是因为它支持传输多种类型的资源。
在资源路径中。路径字段使用 “/” 作为分隔符,这表明该服务是部署在 Linux 系统上的,市场上的大部分服务也是如此。而 “\” 是在 Windows 中被用作路径分隔符。
查询字符串 |
uid=1
是提供给 Web 服务器的额外参数。这些参数是以 & 分隔的键/值对列表。Web 服务器可以在返回资源之前使用这些参数做一些额外的工作。每个 Web 服务器都有自己关于参数的规则,要知道特定 Web 服务器是否正在处理参数,唯一可靠的方法是询问 Web 服务器的所有者。
当我们搜索 cplusplus 官网时,此时的 URL 中就有许多对参数,其中有一个 q=cplusplus
就是我们搜索的关键词。
片段标识符 |
ch1 是指向资源本身的标识符。标识符表示资源内部的一种 “书签” ,它高数浏览器在特定的 “书签” 位置显示内容的指示。例如,在 HTML 文档中,浏览器将滚动到定义标识符的位置;在视频或音频文件中,浏览器将尝试跳转到标识符所代表的时间点。它不会随请求一起发送到服务器。
示例:如果一个网页由多个章节,每个章节都有唯一的 ID,可以使用片段标识符将浏览器定位到特定的章节。例如,URL 可能是 http://example.com/page.html#section2
,浏览器将滚动到页面中 ID 为 “section 2” 的部分。
urlencode 和 urldecode
在 URL 中,像 / ? :
等这样的字符被视为具有特殊意义的字符。因此这些字符不能随意出现。当某个参数中需要带有这些特殊字符时,就必须先对特殊字符进行转移。
为了进行转义处理,可以使用 urlencode
和 urldecode
这两个函数。它们对 URL 中的特殊字符进行编码和解码,确保这些字符在 URL 中能够正确传递和解析。
转移的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上 %
,编码成 %XY
格式。
示例:当在浏览器中搜索 c++
时,由于 URL 中的 + 字符被视为特殊字符,需要对其进行编码以确保在 URL 中正确传输。+
字符转化为16进制的值是 0x2B
,因此 +
字符被编码为 %2B
。
在线编码解码工具:http://www.urlencode.com.cn/
对 c++
进行编码:
对编码进行解码:
注意:网上的 urlencode 和 urldecode 在线转换工具对于同一个字符串解析出的结果不一定一样。因为它们可能使用过的是不同的算法或规则。
HTTP 协议请求和响应格式
客户端和服务器之间的交互称为消息。HTTP 消息是请求或响应。客户端设备向服务器提交 HTTP 请求,服务器通过客户端发送 HTTP 响应进行回答。
HTTP 请求(Request)
HTTP 请求。这是指客户端设备,如互联网浏览器,向服务器询问加载网站所需的信息。请求向服务器提供所需的信息,以便调整其对客户端设备的响应。每个 HTTP 请求都包含编码数据,其信息如下:
- 具体版本的 HTTP 遵循的是 HTTP 协议,并主要右 HTTP 和 HTTP/2 两个版本。
- URL(统一资源定位符)指向网络上的资源,它是一个标识资源位置的字符串,包含协议类型(如:HTTP 或 HTTPS)、服务器地址、端口号和资源路径。
- HTTP 方法。这指示了请求期望在其响应中从服务器接收到的特定操作。
- HTTP 请求头。包含了一些数据,例如浏览器类型、请求所需的数据类型。它还可以包括 cookie,用于在请求中携带之前从处理请求的服务器发送的信息。
- HTTP 正文是请求中可选的信息,服务器可能需要这些信息,例如用户表单数据、短文本响应和文件上传等。
- 首行:[方法] + [url] + [版本];
- Header:请求的属性,冒号分隔的键值对;每组属性之间使用 \n 分隔;遇到空行表示 Header 部分结束。
- Body:空行后面的内容都是 Body,Body 允许为空字符串。如果 Body 存在,则在 Header 中会有一个 Content-Length 属性来标识 Body 长度。
怎样将 HTTP 请求的报头与有效载荷进行分离?
在 HTTP 请求中,报头(Headers)和有效载荷(Payload)是通过空行 \r\n
来进行分隔的。因此,当服务器收到一个 HTTP 请求后,就可以按行进行读取,当读取到空行则说明报头读取完成,后续的内容即为有效载荷。
下列代码是一个简单的 TCP 服务器的基本框架,并且在处理 HTTP 请求时打印请求内容并输出,代码如下:
void handlerHttpRequest(int sock)
{
cout << "----------------------------------------" << endl;
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
cout << buffer << endl;
cout << "----------------------------------------" << endl;
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port), ip_(ip), listenSock_(-1) { quit_ = false; }
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5) < 0)
{
exit(3);
}
}
void loop()
{
signal(SIGCHLD, SIG_IGN);
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接,accept的返回值是一个新的socket fd
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
cerr << "accept error..." << endl;
// 获取连接失败,继续获取
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
// 5. 提供服务,echho -> 小写 -> 大写
// 5.1 v1版本-- 多进程版本 - 父进程打开的文件描述符会被子进程继承
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
close(listenSock_); // 建议
if (fork() > 0)
exit(0);
handlerHttpRequest(serviceSock);
exit(0);
}
close(serviceSock);
wait();
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
int listenSock_;
uint16_t port_;
std::string ip_;
bool quit_; // 安全退出
};
运行该服务器,使用浏览器进行访问。此时我们的服务器会收到浏览器发送的 HTTP 请求,并将该请求内容打印输出。
当浏览器向服务器发起 HTTP 请求后,如果服务器没有对请求进行相应,浏览器可能会认为请求没有成功,并尝试重新请求。因此,服务器运行的时候会一直打印请求数据。
大多数浏览器默认使用 HTTP 协议进行网页请求。如果未指定具体的协议,则浏览器会自动添加 HTTP 协议。
关于 URL 中的 “/”,它表示的是 Web 根目录,而不是云服务器上面的根目录。Web 根目录是在服务器上指定用于存储 Web 应用程序的目录。该目录中的文件和文件夹通常对外可访问,并通过 URL 中的路径来定位。
HTTP 请求中的 URL 中通常不携带域名和端口号。因为在 HTTP 请求的报头中的 “Host” 字段会指定要访问哪个域名和端口号,而请求行中的 URL 表示要访问服务器上哪个路径下的资源。
HTTP 响应(Response)
HTTP 的响应分为以下部分:
状态行:
HTTP 响应的开始行称为状态行,包含以下信息:[状态行] + [状态码] + [状态码解释]
- 协议版本,通常是 HTTP/1.1;
- 状态码,指示请求的成功或失败。常见的状态码是 200、404;
- 状态文本,简短的、纯信息的文本描述,帮助人们理解 HTTP 的信息。
典型的状态行是这样的:HTTP/1.1 404 Not Found。
Header
响应的 HTTP 报头遵循与任何其它报头相同的结构:一个不区分大小写的字符串,后跟一个冒号(:)和一个值,其结构取决于报头的类型。整个报头,包括它的值,都显示为一行。
许多不同的报头可以出现在响应中。这些可以分为几组:
- 一般报头,如:
Via
,适用于整个消息。 - 响应报头,如:
Vary
和Accept-Ranges
,提供了关于服务器的额外信息,这些信息不适合在状态行中显示。 - 表示报头,如:
Content-Type
,描述消息数据的原始格式和应用的任何编码(仅在消息具有正文时出现)。
Body
响应的最后部分是主体。并非所有的响应都有主体:对于能够充分回答请求而无需相应有效载荷的状态码(如:201 Created 或 204 Content),通常不包括主体。
主体可以分为三类:
- 单资源主体:由一个已知长度的单个文件组成,由两个头部字段定义:
Content-Type
和Content-Length
。 - 单资源主体:有一个未知长度的单个文件组成,以块(chunks)编码,其中
Transfer-Encoding
设置为chunked
。 - 多资源主体:由多部份主体组成,每个部分包含不同的信息。这种情况相对少见。
如何根据 HTTP 请求构建 HTTP 响应给客户端?
在服务器收到客户端发送的 HTTP 请求之后,服务器需要解析 HTTP 请求,从中提取出请求行、请求头和请求主体等信息。然后处理请求,即根据请求的 URL、方法和其它相关信息,服务器执行相应的逻辑处理。最后,构建 HTTP 响应请求。HTTP 响应请求应该由响应行、响应头、响应主体构成。将构建完成的 HTTP 响应信息发送给客户端。
下面,我们构建一个 HTTP 响应发送给客户端(浏览器)。由于分析 HTTP 请求比较复杂,我们这里直接演示一个固定的 HTTP 响应示例,主要是大致理解。为了简化示例,我们将当前程序所在的路径作为我们的 Web 根目录,在该目录下创建一个名为 index.html
的前端页面作为服务器首页,如下所示:
当浏览器向该服务器发起请求时,不管浏览器发送的什么请求,服务器仅需返回 index.html
网页即可。此时,该网页的内容应该出现在响应正文中。代码如下所示:
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
string getPath(string http_request)
{
size_t pos = http_request.find(CRLF);
if (pos == string::npos)
return "";
string request_line = http_request.substr(0, pos);
// GET /a/b/c http/1.1
ssize_t first = request_line.find(SPACE);
if (first == string::npos)
return "";
ssize_t second = request_line.rfind(SPACE);
if (second == string::npos)
return "";
string path = request_line.substr(first + SPACE_LEN, second - (first + SPACE_LEN));
if (path.size() == 1 && path[0] == '/')
path += HOME_PAGE;
return path;
}
std::string readFile(const std::string &recource)
{
std::ifstream in(recource, std::ifstream::binary);
if (!in.is_open())
return "404";
std::string content;
std::string line;
while (std::getline(in, line))
content += line;
in.close();
return content;
}
void handlerHttpRequest(int sock)
{
cout << "----------------------------------------" << endl;
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if (s > 0)
cout << buffer << endl;
string path = getPath(buffer);
// path = "/a/b/index.html";
// resource = "./wwwroot"; // 我们的web根目录
// resourct = "./wwwroot/a/b/index.html";
// 文件在哪里? - 在请求的请求行中,第二个字段就是你要访问的文件
// 如何读取?
std::string recource = ROOT_PATH;
recource += path;
string html = readFile(recource);
ssize_t pos = recource.rfind(".");
string suffix = recource.substr(pos);
// 开始响应
std::string response;
response = "HTTP/1.0 200 OK\r\n";
if (suffix == ".jpg")
response += "Content-Type: image/jpeg\r\n";
else
response += "Content-Type: text/html\r\n";
response += ("Content-Length: " + to_string(html.size()) + "\r\n");
response += "Set-Cookie: This is my cookie content;\r\n";
response += "\r\n";
response += html;
send(sock, response.c_str(), response.size(), 0);
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port), ip_(ip), listenSock_(-1) { quit_ = false; }
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5) < 0)
{
exit(3);
}
}
void loop()
{
signal(SIGCHLD, SIG_IGN);
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接,accept的返回值是一个新的socket fd
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
cerr << "accept error..." << endl;
// 获取连接失败,继续获取
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
// 5. 提供服务,echho -> 小写 -> 大写
// 5.1 v1版本-- 多进程版本 - 父进程打开的文件描述符会被子进程继承
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
close(listenSock_); // 建议
if (fork() > 0)
exit(0);
handlerHttpRequest(serviceSock);
exit(0);
}
close(serviceSock);
wait();
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
int listenSock_;
uint16_t port_;
std::string ip_;
bool quit_; // 安全退出
};
运行该服务,使用浏览器访问该服务,服务器就会将 index.html 文件响应给浏览器,然后浏览器将该文件解析并渲染到浏览器页面中。
需要注意的是,HTTP 响应报头的属性信息是以键值对的形式传递的,格式为 “属性名:属性值”。在构建 HTTP 响应时,根据需要添加适当的属性信息到相应报头中,以满足具体的需求和标准规范。
Content-Length 属性,确保填写正确的响应正文长度非常重要,以确保客户端能够正确接收和解析响应。如果长度不正确,可能会导致客户端无法完整接收响应或解析错误。
HTTP 请求方法
请求方法表示对由给定的请求URI标识的资源执行的操作。请求方法区分大小写,应始终以大写形式表示。下表列出了HTTP/1.1中支持的所有请求方法。
方法 | 描述 | 支持的HTTP协议版本 |
---|---|---|
GET | GET 方法用于使用给定的 URI 从指定的服务器检索信息。使用 GET 的请求应该只检索数据,不应该对其它数据产生其它影响。 | 1.0、1.1 |
POST | POST 请求用于向服务器发送数据,例如:使用 HTML 表单发送给客户信息、文件上传等。 | 1.0、1.1 |
PUT | PUT 用上传的内容替换目标资源的所有当前表示形式 | 1.0、1.1 |
HEAD | 与 GET 相同,但它只传输状态行和标题部分。 | 1.0、1.1 |
DELETE | 删除 URL 给出的目标资源的所有当前表示形式。 | 1.0、1.1 |
OPTIONS | 描述目标资源的通信选项 | 1.1 |
TRACE | 对目标资源执行一个消息环回测试。 | 1.1 |
CONNECT | 建立与目标资源的隧道连接。 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINK | 断开连接关系 | 1.0 |
请求 URI(Request-URI)是统一资源标识符(Uniform Resource Identifier),用于标识要英语请求的资源。以下是指定 URI 的最常用形式:
Request-URI = "*" | absoluteURI | abs_path | authority
说明:
"*"
:代表一个通配符,标识应用请求到所有资源。例如:OPTIONS * HTTP/1.1
。absoluteURI
:表示完整的 URI,包括方案(scheme)、主机(host)、端口(port)和路径(path)。例如:http://example.com/path
。abs_paht
:表示相对请求的 URI 路径。例如:/path
。authority
:表示在请求中提供的授权信息。它通常用于代理服务器等场景。例如:example.com
。
Request-URI 用于指定客户端希望与服务器交互的资源。根据具体的请求方法和 URI,服务器将确定应该采取的操作,并返回相应的响应。请求 URI 的格式和内容对于正确处理和解析 HTTP 请求是非常重要的。
HTTP 请求中最常用的就是 POST 和 GET 方法。
GET and POST
GET 方法是一个基本请求。负责显示资源的当前表示形式。例如:HTTP GET 方法用于显示网页或表单。参数以 URL 地址的形式传递。这种传参方式会导致 URL 的长度有一定限制,因此 GET 方法适合传递较少的参数。GET 只应该用于显示,而不是保持敏感数据。
POST 方法通常用于向服务器上传数据。与 GET 方法不同,POST 方法将请求参数包含在请求正文中。由于参数在正文中,POST 方法可以传递更多的参数,并且不受 URL 长度限制。POST 方法通常在想要添加新资源时使用。例如:创建新账户或发送文件。参数在主体中传递。
POST 方法相对于 GET 方法更具有一定的私密性,因为 POST 的参数在请求正文中,不在 URL 中。当然,也是相对安全一点,无论是 POST 方法还是 GET 方法,都不是安全的传输方式。要确保数据的安全性,需要使用加密等其它方式来保护数据的传输。
使用 Postman 来演示 GET 和 POST 的区别
GET 方法演示:
- 在 Postman 中选择 GET 请求方法。
- 在 URL 字段中输入服务器的地址。
- 在 Params 下设置请求参数,这相当于在 URL 中添加参数。
- 点击 Send 发送请求,可以观察到 URL 会随着参数的设置而变化。
POST 方法演示:
- 在 Postman 中选择 GET 请求方法。
- 在 URL 字段中输入服务器的地址。
- 在 Body 标签下选择 “raw” 选项。
- 在正文中输入需要传递给服务器的数据。
- 发送请求之后,可以看到 URL 不会随着参数的设置而变化,因为参数是通过正文传递的。
通过以上演示,可以演示 GET 方法和 POST 方法在传参上的区别。GET 方法通过 URL 传递参数,POST 将参数包含在请求的正文中。
HTTP 的状态码
服务器响应中的状态码(Status-Code)元素是一个由3位数字组成的整数,其中状态码的第一位数字定义了响应的类别,而最后两位数字没有任何的分类作用。第一个数字有5个可能的取值:
类别 | 说明 | |
---|---|---|
1XX | Informational(信息性状态码) | 服务器已接收到请求并正处理 |
2XX | Success(成功状态码) | 表示服务器成功处理了请求 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4XX | Client Error(客户端错误代码) | 指示客户端发送了错误的请求,服务器无法处理请求 |
5XX | Server Error(服务器错误代码) | 表示服务器在处理请求时遇到了错误 |
最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway)。
HTTP 状态码是可扩展的,HTTP 应用程序并不需要理解所有注册的状态码的含义。
Redirection(重定向状态码)
重定向状态码(Redirection status codes)是 HTTP 协议中的一类状态码,它通过各种方法将网络请求重定向到其它位置,此时的服务器相当于提供了一个引导服务。
在重定向中,可以区分为临时重定向和永久重定向,其中状态码(301)表示永久重定向,状态码(302、307)表示临时重定向。
临时重定向和永久重定向实际上影响客户端的缓存和更新行为,决定客户端需要更新目的地址。对于永久重定向,当首次访问网页时,浏览器会自动进行重定向,并且在之后的访问中直接访问重定向之后的网站。对于临时重定向,每次访问该网站时如果需要进行重定向,浏览器都会帮助它完成重定向跳转到目标网站。
为什么要有临时重定向和永久重定向,它们的作用是什么?
临时重定向对于临时的更改或暂时性的重定向非常有用。例如:当网站正在维护或临时更改网页内容时、还用于 A/B 测试等情况,其中不同版本的页面需要在用户之间进行轮流展示,评估哪个版本更优。
永久重定向对于更改网站架构、调整网页结构或更改域名时非常有用。永久重定向有助于确保搜索引擎原始页面的权重和排名传递给新的页面。这对于维持网站的搜索引擎优化(SEO)非常重要。
临时重定向演示
代码如下:
#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"
string getPath(string http_request)
{
size_t pos = http_request.find(CRLF);
if (pos == string::npos)
return "";
string request_line = http_request.substr(0, pos);
size_t first = request_line.find(SPACE);
if (first == string::npos)
return "";
size_t second = request_line.rfind(SPACE);
if (second == string::npos)
return "";
string path = request_line.substr(first + SPACE_LEN, second - (first + SPACE_LEN));
if (path.size() == 1 && path[0] == '/')
path += HOME_PAGE;
return path;
}
string readFile(const string &recource)
{
ifstream in(recource);
if (!in.is_open())
return "404";
string content;
string line;
while (getline(in, line))
content += line;
in.close();
return content;
}
void handlerHttpRequest(int sock)
{
cout << "---------------------------------------------------" << endl;
char buffer[10240];
ssize_t s = read(sock, buffer, sizeof buffer);
if (s > 0)
cout << buffer;
// 重定向
string response = "HTTP/1.1 302 Temporarily Moved\r\n";
// string response = "HTTP/1.1 301 Permanently Moved\r\n";
response += "Location: https://www.qq.com\r\n";
response += "\r\n";
send(sock, response.c_str(), response.size(), 0);
}
class ServerTcp
{
public:
ServerTcp(uint16_t port, const std::string &ip = "")
: port_(port), ip_(ip), listenSock_(-1)
{
quit_ = false;
}
~ServerTcp()
{
if (listenSock_ >= 0)
close(listenSock_);
}
public:
void init()
{
// 1. 创建socket
listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock_ < 0)
{
exit(1);
}
// 2. bind
// 2.1 填充服务器信息
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port_);
ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
// 3. 监听socket,为何要监听呢?tcp是面向连接的!
if (listen(listenSock_, 5) < 0)
{
exit(3);
}
}
void loop()
{
signal(SIGCHLD, SIG_IGN);
while (!quit_)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
// 4. 获取连接,accept的返回值是一个新的socket fd
int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
if (quit_)
break;
if (serviceSock < 0)
{
cerr << "accept error..." << endl;
// 获取连接失败,继续获取
continue;
}
// 4.1 获取客户端基本信息
uint16_t peerPort = ntohs(peer.sin_port);
std::string peerIp = inet_ntoa(peer.sin_addr);
// 5.1 v1版本-- 多进程版本 - 父进程打开的文件描述符会被子进程继承
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
close(listenSock_); // 建议
if (fork() > 0)
exit(0);
handlerHttpRequest(serviceSock);
exit(0);
}
close(serviceSock);
wait();
}
}
bool quitServer()
{
quit_ = true;
return true;
}
private:
int listenSock_;
uint16_t port_;
std::string ip_;
bool quit_; // 安全退出
};
此时,运行服务器并使用浏览器访问该服务器时,服务器将响应重定向之后的网址,浏览器收到重定向的网址之后就打开该网站,如下:
常见的Header
HTTP 中常见的 header 如下:
- Content-Type:数据类型(text/html)等;
- Content-Length:正文的长度;
- Host:客户端告知服务器,所请求的资源在哪个主机的哪个端口上;
- User-Agent:声明用户的操作系统和浏览器版本信息;
- Referer:当前页面是从哪个页面跳转过来的;
- Location:搭配 3XX 状态码使用,告诉客户端接下来需要去哪里访问;
- Cookie:用户在客户端存储少量信息,通常用于实现会话(session)的功能。
Host
在 HTTP 请求中,Host 字段的作用是指定客户端要访问的服务器的主机名和端口号。它是必需的,因为一个服务可能承载多个服务,每个服务都有唯一的主机名和端口号。
在一些情况下,某些服务器提供的是代理服务,它们充当客户端与服务器之间的中介。因此,客户端需要告知代理服务器它要访问的服务器的主机名和端口号,以便代理服务器正确地转发请求。
User-Agent
在 HTTP 请求中包含 User-Agent 字段是为了向服务器提供关于发起请求的客户端应用程序或用户代理信息。该字段允许服务器根据客户端的类型、版本和能力来进行适当的响应。使用 User-Agent 可以便于服务器更好的进行客户端识别、兼容性处理、统计和分析以及安全性考虑。
User-Agent 字段是可选的,客户端可以选择不发送该字段,或者发送自定义的 User-Agent 值。然而,为了服务器能更好的为客户端提供服务,尽量加上该字段。
Referer
Referer 是 HTTP 请求头部的一个字段,它记录了当前请求是从哪个请求跳转过来的,即上一个页面的 URL 。通过 Referer 字段,服务器可以获取到上一个页面的信息,从而进行一些相关的处理。Referer 字段主要用于跳转追踪、防盗链和安全性以及页面关联性。
Referer 字段也是可选的,浏览器或者客户端可以不发生该字段。在一些隐私敏感的场景中,有时会限制或清除 Referer 的发送,以保护用户的隐私。