文章目录
- 前言
- 一、HTTP的简要介绍
- 二、HTTP的逐步深入了解
- 总结
前言
我又回来了,前段时间看书沉淀去了,现在开始回来继续更新!
本篇我们将继续来结合实际代码来理解 HTTP 网络协议!
下面是一个实际的通信流程,就由此来引入吧~

一、HTTP的简要介绍
你肯定会想什么是 HTTP ,其实我们前面在讲 序列化和反序列化 的时候,你可能就在想是不是可以定制一个协议,其实还真可以,并且也确实有还不少这样已经成熟并被广泛应用的网络通信协议了,HTTP,本篇的主角,就是其中之一
(Hyper Text Transfer Protocol)中文名叫做超文本传输协议,底层协议是TCP,这是我们先要了解的一个大概知识
二、HTTP的逐步深入了解
认识URL
HTTP 作为一个应用层协议,它肯定有一个任务是解析 URL,所以我们先来看看什么是 URL
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。
一个URL大致由如下几部分构成:
- 协议方案名:http://表示的是协议名称,表示请求时需要使用的协议,通常使用的是 HTTP 协议或安全协议HTTPS
- 登录信息: usr:pass 表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在 URL 中体现出来,但绝大多数 URL 的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器( POST 请求方式,我们后面会讲)
- 服务器地址 : www.example.jp 表示的是服务器地址,也叫做域名,比如 www.alibaba.com , www.qq.com , www.baidu.com ,实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以使用域名,也可以使用IP地址(一个域名可以对应多个 IP)
- 服务器端口号:80表示的是服务器端口号。 HTTP 协议和套接字编程一样都是位于应用层的,在进行套接字编程时我们需要给服务器绑定对应的 IP 和端口,而这里的应用层协议也同样需要有明确的端口号(其实比较著名的协议是有默认端口号的,比如 HTTP 的80, HTTPS 的443, SSH 的20)
- 带层次的文件路径 : /dir/index.html 表示的是要访问的资源所在的路径。访问服务器的目的是获取服务器上的某种资源 ,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径,当我们发起网络请求的时候,本质上是获得了以下图的网页信息,再被浏览器解释,因此才有了所谓的呈现效果
同时我们还会向服务器请求别的资源,如视频、音频而不是只有简简单单的文本,所以 HTTP 才叫做超文本传输协议,另外我们可以看到,这里的路径分隔符是 / ,而不是 \ ,这也就证明了实际很多服务都是部署在 Linux 上的 - 查询字符串 : uid=1 表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过 & 符号分隔开的,比如说我们现在搜索 HTTP ,我们可以看到 URL 中有很多参数,其中就有一个 wd=HTTP
- 片段标识符:ch1表示的是片段标识符,是对资源的部分补充,这个没啥特别的,我都不太想讲
urlencode & urldecode
我们会发现 /?: 这些字符已经被 URL 当作特殊意义理解,所以如果我们搜索的时候还加入这些字符的话就要另外想办法,将需要转码的字符转为十六进制,然后从右到左,取4位(不足4位直接处理),每两位做一位,前面加上%,编码成%XY格式
比如当我们搜索C++时,由于+加号在URL当中也是特殊符号,而+字符转为十六进制后的值就是0x2B,因此一个+就会被编码成一个%2B,当然了,中文字符也会被编码
在线编码解码平台,可以尝试点击玩一玩
HTTP协议格式
应用层常见的协议有 HTTP 和 HTTPS ,传输层常见的协议有 TCP ,网络层常见的协议是 IP ,数据链路层对应就是 MAC 帧了。其中下三层是由操作系统或者驱动帮我们完成的,它们主要负责的是通信细节。如果应用层不考虑下三层,在应用层自己的心目当中,它就可以认为自己是在和对方的应用层在直接进行数据交互
我跟你打电话,我们用汉语交流,其实我说的话要编码到电话里面,再传输过去,再解码到你的电话被你听到,但是我仍然可以认为是我和你在直接进行对话

下三层负责的是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要我们去定制协议,这里最典型的就是 HTTP 协议。(也就是说下三层已经定死了,我们能决定的就是应用层)
HTTP 是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起 request ,服务器收到这个 request 后,会对这个 request 做数据分析,得出你想要访问什么资源,然后服务器再构建 response ,完成这一次 HTTP 的请求。这种基于 request & response 这样的工作方式,我们称之为 cs 或 bs 模式,其中 c 表示 client , s 表示 server , b 表示 browser 。
由于 HTTP 是基于请求和响应的应用层访问,因此我们必须要知道 HTTP 对应的请求格式和响应格式,这就是学习 HTTP 的重点。
HTTP请求协议格式
HTTP 请求协议格式如下:
HTTP请求由以下四部分组成:
- 请求行:[请求方法] + [url] + [http版本]
- 请求报头:请求的属性,这些属性都是以 key: value 的形式按行陈列的。
- 空行:遇到空行表示请求报头结束。
- 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个 Content-Length 属性来标识请求正文的长度。
其中,前面三部分是一般是 HTTP 协议自带的,是由 HTTP 协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。
如何将HTTP请求的报头与有效载荷进行分离?
当应用层收到一个 HTTP 请求时,它必须想办法将 HTTP 的报头与有效载荷进行分离。对于 HTTP 请求来讲,这里的请求行和请求报头就是 HTTP 的报头信息,而这里的请求正文实际就是 HTTP 的有效载荷。
我们可以根据 HTTP 请求当中的空行来进行分离,当服务器收到一个 HTTP 请求后,就可以按行进行读取,如果读取到空行则说明已经将报头读取完毕,实际 HTTP 请求当中的空行就是用来分离报头和有效载荷的。
如果将 HTTP 请求想象成一个大的线性结构,此时每行的内容都是用 \n 隔开的,因此在读取过程中,如果连续读取到了两个 \n ,就说明已经将报头读取完毕了,后面剩下的就是有效载荷了。
获取浏览器的 HTTP 请求
在网络协议栈中,应用层的下一层叫做传输层,而 HTTP 协议底层通常使用的传输层协议是 TCP 协议,因此我们可以用套接字编写一个 TCP 服务器,然后启动浏览器访问我们的这个服务器。
由于我们的服务器是直接用 TCP 套接字读取浏览器发来的 HTTP 请求,此时在服务端没有应用层对这个 HTTP 请求进行过任何解析,因此我们可以直接将浏览器发来的 HTTP 请求进行打印输出,此时就能看到 HTTP 请求的基本构成

看不懂的话我们来看看这个,你就明白了~

所以我们现在就来自己写一个 TCP 服务器,来看看这个 HTTP 协议的原貌到底是什么,来让我们有一个更加深刻的了解
掌握概念是永远没有用的,要自己真正敲代码,去感悟,只会背诵的话那跟孔乙己没啥区别
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
int main()
{
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "socket error!" << endl;
return 1;
}
// 绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error!" << endl;
return 2;
}
// 监听
if (listen(listen_sock, 5) < 0)
{
cerr << "listen error!" << endl;
return 3;
}
// 启动服务器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;)
{
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
{
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0)
{
// 爸爸进程
close(listen_sock);
if (fork() > 0)
{
// 爸爸进程
exit(0);
}
// 孙子进程
char buffer[1024];
// 读取HTTP请求
recv(sock, buffer, sizeof(buffer), 0);
cout << "--------------------------http request begin--------------------------" << endl;
cout << buffer << endl;
cout << "---------------------------http request end---------------------------" << endl;
close(sock);
exit(0);
}
// 爷爷进程
close(sock);
waitpid(-1, nullptr, 0); // 等待爸爸进程
}
return 0;
}
运行服务器程序后,然后用浏览器进行访问,此时我们的服务器就会收到浏览器发来的 HTTP 请求,并将收到的 HTTP 请求进行打印输出
另外以下几点你需要注意:
- 浏览器向我们的服务器发起 HTTP 请求后,因为我们的服务器没有对进行响应,此时浏览器就会认为服务器没有收到,然后再不断发起新的 HTTP 请求,因此虽然我们只用浏览器访问了一次,但会受到多次 HTTP 请求
- 由于浏览器发起请求时默认用的就是 HTTP 协议,因此我们在浏览器的 url 框当中输入网址时可以不用指明 HTTP 协议
- url 当中的 / 不能称之为我们云服务器上根目录,这个 / 表示的是 web 根目录,这个 web 根目录可以是你的机器上的任何一个目录,这个是可以自己指定的,不一定就是 Linux 的根目录
其中请求行当中的 url 一般是不携带域名以及端口号的,因为在请求报头中的 Host 字段当中会进行指明,请求行当中的 url 表示你要访问这个服务器上的哪一路径下的资源。如果浏览器在访问我们的服务器时指明要访问的资源路径,那么此时浏览器发起的 HTTP 请求当中的 url 也会跟着变成该路径
而请求报头当中全部都是以 key: value 形式按行陈列的各种请求属性,请求属性陈列完后紧接着的就是一个空行,空行后的就是本次 HTTP 请求的请求正文,此时请求正文为空字符串,因此这里有两个空行
HTTP响应协议格式
HTTP响应协议格式如下:
- 状态行:[http版本] + [状态码] + [状态码描述]
- 响应报头:响应的属性,这些属性都是以 key: value 的形式按行陈列的
- 空行:遇到空行表示响应报头结束
- 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个 Content-Length 属性来标识响应正文的长度。比如服务器返回了一个 html 页面,那么这个 html 页面的内容就是在响应正文当中的
如何将 HTTP 响应的报头与有效载荷进行分离
对于 HTTP 响应来讲,这里的状态行和响应报头就是 HTTP 的报头信息,而这里的响应正文实际就是 HTTP 的有效载荷。与 HTTP 请求相同,当应用层收到一个 HTTP 响应时,也是根据 HTTP 响应当中的空行来分离报头和有效载荷的。当客户端收到一个 HTTP 响应后,就可以按行进行读取,如果读取到空行则说明报头已经读取完毕
构建HTTP响应给浏览器
服务器读取到客户端发来的 HTTP 请求后,需要对这个 HTTP 请求进行各种数据分析,然后构建成对应的 HTTP 响应发回给客户端。而我们的服务器连接到客户端后,实际就只读取了客户端发来的 HTTP 请求就将连接断开了
接下来我们可以构建一个 HTTP 请求给浏览器,鉴于现在还没有办法分析浏览器发来的 HTTP 请求,这里我们可以给浏览器返回一个固定的 HTTP 响应。我们就将当前服务程序所在的路径作为我们的 web 根目录,我们可以在该目录下创建一个 html 文件,然后编写一个简单的 html 作为当前服务器的首页
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
int main()
{
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
cerr << "socket error!" << endl;
return 1;
}
// 绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8081);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
cerr << "bind error!" << endl;
return 2;
}
// 监听
if (listen(listen_sock, 5) < 0)
{
cerr << "listen error!" << endl;
return 3;
}
// 启动服务器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;)
{
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
{
cerr << "accept error!" << endl;
continue;
}
if (fork() == 0)
{
// 爸爸进程
close(listen_sock);
if (fork() > 0)
{
// 爸爸进程
exit(0);
}
// 孙子进程
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); // 读取HTTP请求
cout << "--------------------------http request begin--------------------------" << endl;
cout << buffer << endl;
cout << "---------------------------http request end---------------------------" << endl;
#define PAGE "wwwroot/index.html" // 网站首页
// 读取index.html文件
ifstream in(PAGE);
if (in.is_open())
{
in.seekg(0, in.end);
int len = in.tellg();
in.seekg(0, in.beg);
char* file = new char[len];
in.read(file, len);
in.close();
// 构建HTTP响应
string status_line = "http/1.1 200 OK\n"; // 状态行
string response_header = "Content-Length: " + to_string(len) + "\n"; // 响应报头
string blank = "\n"; // 空行
string response_text = file; // 响应正文
string response = status_line + response_header + blank + response_text; // 响应报文
// 响应HTTP请求
send(sock, response.c_str(), response.size(), 0);
delete[] file;
}
close(sock);
exit(0);
}
// 爷爷进程
close(sock);
waitpid(-1, nullptr, 0); // 等待爸爸进程
}
return 0;
}
因此当浏览器访问我们的服务器时,服务器会将这个 index.html 文件响应给浏览器,而该 html 文件被浏览器解释后就会显示出相应的内容

当然直接用 telnet 搞也是可以的

当然我一开始直接在浏览器输入 127.0.0.1:8081 当时怎么都访问不了,后面查询才发现原因在这里
简单说你在哪里执行命令,127.0.0.1 就指向哪台机器,你的服务器在云上,所以只有在云服务器内部测试 127.0.0.1 才有效
- 实际我们在进行网络请求的时候,如果不指明请求资源的路径,此时默认你想访问的就是目标网站的首页,也就是 web 根目录下的 index.html 文件
- 由于只是作为示例,我们在构建 HTTP 响应时,在响应报头当中只添加了一个属性信息 Content-Length ,表示响应正文的长度,实际 HTTP 响应报头当中的属性信息还有很多
HTTP请求和返回为什么要交互版本?
-
HTTP 请求当中的请求行和 HTTP 响应当中的状态行,当中都包含了 http 的版本信息。其中 HTTP 请求是由客户端发的,因此 HTTP 请求当中表明的是客户端的http版本,而HTTP响应是由服务器发的,因此 HTTP 响应当中表明的是服务器的 http 版本
-
客户端和服务器双方在进行通信时会交互双方 http 版本,主要还是为了兼容性的问题。因为服务器和客户端使用的可能是不同的 http 版本,为了让不同版本的客户端都能享受到对应的服务,此时就要求通信双方需要进行版本协商
-
客户端在发起 HTTP 请求时告诉服务器自己所使用的 http 版本,此时服务器就可以根据客户端使用的http版本,为客户端提供对应的服务,而不至于因为双方使用的 http 版本不同而导致无法正常通信。因此为了保证良好的兼容性,通信双方需要交互一下各自的版本信息
总结
受不了了,我先更新到这里!下篇马上就来!