协议。网络协议的简称,网络协议是通信计算机双方必须共同遵守的一组约定,比如怎么建立连接,怎么互相识别等。
为了使数据在网络上能够从源头到达目的,网络通信的参与方必须遵守相同的规则,我们称这套相同的规则为协议(protocol),而协议最终都需要通过计算机语言的方式表达出来。只有通信计算机双方都遵守相同的协议,计算机之间才能互相通信交流。
通俗来讲,协议就是要保证网络通信的双方、能够相互对上号。比如两个人传递纸条通过相互指定的暗号。
应用层协议
应用层协议位于网络协议栈的最顶层,负责定义应用程序之间的通信规则和数据交换方式。应用层协议允许不同设备上的不同设备上的应用程序能够相互交换数据和信息,使得互联网上的各种应用能够正常运行。
应用层协议定义了数据交换的格式、数据的编码和解码方法、通信的语义以及错误处理等规则。这些协议通常与特定的应用程序密切相关,例如Web浏览器使用HTTP协议来获取网页内容。
HTTP(Hyper Text Transfer
Protocol)又叫做超文本传输协议,是一个简单的请求-响应协议,HTTP通常运行在TCP之上。
虽然应用层协议大多是程序猿自己定的,但实际上,有大佬们定义了一些现成的、又非常好用的应用层协议,供我们直接参考使用。HTTP协议就是其中之一。
URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。
一个URL大致有以下部分组成:
协议方案名
http:// 表示的是协议名称,表示请求时使用的协议,通常使用的是HTTP协议或者安全的HTTPS协议。HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。
登录信息
usr:pass表示的是登录认证信息,包括登录用户的用户名和密码。虽然登录认证信息可以在URL中体现出来,但绝大多数URL的这个字段都是被省略的,因为登录信息可以通过其他方案交付给服务器。
服务器地址
www.example.jp 表示的是服务器地址,也叫做域名,常见的比如www.baidu.com等。
域名等价于IP,用于标识一台主机在全网中的唯一性,域名实际还会做解析,解析之后就是服务器的IP地址,用域名不用IP主要是因为域名用起来方便,且更适合用户来看。
域名会和特定的IP地址做映射,比如我们可以用ping命令查看www.baidu.com的IP地址。
如果用户看到的是IP地址,那么用户在访问这个网站之前并不知道这个网站是干什么的,但是用户如果看到的是www.baidu.com这个域名,就知道是哪家公司等信息了。因此域名具有更好的自描述性。
实际我们可以认为域名和IP地址是等价的,在计算机当中使用的时候既可以用域名,也可以用IP地址。但URL呈现出来是让用户看的,*因此URL当中是以域名的形式表示服务器地址的。
服务器端口号
80表示的是服务器端口号,HTTP协议和套接字编程一样都是位于应用层的,在进行套接字编程时我们需要给服务器绑定对应的IP和端口,而这里的应用层协议同样也需要有明确的端口号。
服务器端口号在大部分情况下都可以省略,因为协议和端口号是强绑定的!所有的网络服务都有对应的端口号,这是已经确定好的。
浏览器是根据我们的协议来知晓服务器的端口号的,一般HTTP协议对应服务器的端口号为80,HTTPS对应服务器的端口号是443等。
带层次的文件路径
/dir/index.htm 表示的就是要访问资源的所在路径,访问服务器的本质目的是获取服务器上的某种资源,通过前面的域名和端口已经能够找到对应的服务器进程了,此时要做的就是指明该资源所在的路径。
比如我们打开了浏览器输入百度的域名之后,此时浏览器就帮我们获取到了百度的首页。
当我们发起网页请求时,本质是获得一张网页信息,然后浏览器对这张网页信息进行解释,最后就呈现出来对应的网页。
此外我们还会向服务器请求视频、音频、网页、图片等资源。HTTP之所以叫做超文本传输协议,而不叫做文本传输协议,就是因为有很多资源实际并不是普通的文本资源。
因此在URL就有这样一个字段,用于表示要访问的资源所在的路径。此外我们可以看到,这里的路径分隔符是 / ,而不是 \ ,这也就证明了实际很多服务器都是部署在Linux上的!
查询字符串
uid=1表示的是请求时提供的额外的参数,这些参数是以键值对的形式,通过 & 符号分割开的。
片段标识符
ch1表示的是片段标识符,是对资源的部分补充。
URLencode介绍
现在网络中有许多在线编码工具,这里我分享一个:在线编码工具
举个例子,当我们在百度中搜索C++时,由于 + 号在URL中也是特殊符号,而 + 字符转为十六进制后的值为 0x2B ,因此一个 + 就会被编码成一个 %2B。
我们打开上面我分享的解码工具,进行URLdecode(解码的意思)。
实际当服务器拿到对应的URL之后,也需要对编码后的参数进行解码,此时服务器才能拿到你想传递的参数,解码实际就是编码的逆过程。
应用层常见的协议有HTTP和HTTPS,传输层常见的协议有TCP,网络层常见的协议是IP,数据链路层常见的协议是MAC数据帧。其中下三层是由操作系统或者驱动帮助我们完成的,它们主要负责通信细节。如果应用层不考虑下三层,它就可以认为自己是在和对方的应用层进行数据交互。
下三层负责是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要我们去定制协议,这里最典型的就是HTTP协议。
HTTP是基于请求和相应的应用层服务,作为客户端,你可以向服务器发起request,服务端接收到这个request之后,会对这个request做数据分析,得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,我们称之为cs或bs模式,其中c表示client,s表示server,b表示brower。
由于HTTP是基于请求和相应的应用层访问,因此我们必须要知道HTTP对应的请求格式和响应格式,这也是学习HTTP的重点。
其中,前面三部分一般是HTTP协议自带的,是由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请求进行打印。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
std::cerr << "socker error" << std::endl;
exit(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)
{
std::cerr << "bind error" << std::endl;
exit(2);
}
// 监听
if (listen(listen_sock, 5) < 0)
{
std::cerr << "listen error" << std::endl;
exit(3);
}
// 启动服务器
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
while (1)
{
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
if (fork() == 0) // 爸爸进程
{
close(listen_sock);
if (fork() > 0) exit(0);
// 孙子进程
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); // 获取HTTP请求
std::cout << "-----------------------------http request begin-------------------------" << std::endl;
std::cout << buffer << std::endl;
std::cout << "-----------------------------http request end-------------------------" << std::endl;
close(sock);
exit(0);
}
// 爷爷进程
close(sock);
waitpid(-1, nullptr, 0);
}
close(listen_sock);
return 0;
}
运行服务器之后,可以用浏览器进行访问,服务器会将收到的HTTP请求进行打印。
得到的结果如下:
说明:
其中请求行中的URL一般是不携带域名以及端口号的,因为在请求报头的Host字段当中会进行指明,请求行中的URL表示你要访问这个服务器下哪一路径的资源。如果浏览器在访问我们的服务器时要指明访问的资源路径,那么此时浏览器发起的HTTP请求当中的URL也会跟着变成该路径。
请求报头当中全部都是以 key:value 形式按行陈列的各种请求属性,请求属性陈列完之后紧接着的就是一个空行,空行后的就是本次HTTP请求的请求正文,此时请求正文为空字符串,因此这里有两个空行。
如何将HTTP相应的报头与有效载荷分离?
对于HTTP响应来讲,这里的状态行和响应报头就是HTTP的报头信息,而这里的响应正文就是HTTP的有效载荷。与HTTP请求相同,当应用层收到一个HTTP响应时,也是根据HTTP响应当中的空行来分离报头和有效载荷的。当客户端收到一个HTTP响应后,就可以按行进行读取,如果读取到空行则说明报头已经读取完毕。
构建HTTP响应给浏览器
服务端读取到客户端发来的HTTP请求后,需要对这个HTTP请求进行各种数据分析,然后构建成对应的HTTP响应发回给客户端。而我们的服务器连接到客户端之后,实际就只读取了客户端发来的HTTP请求就将连接断开了。
接下来我们可以构建一个HTTP请求给服务区,鉴于现在还没有办法分析浏览器发来的HTTP请求,这里我们可以给浏览器返回一个固定的HTTP响应。我们就将当前服务程序所在路径作为Web根目录,我们可以在该目录下创建一个html文件,然后编写一个简单的html作为服务器的首页。
当浏览器向服务器发起HTTP请求时,不管浏览器发来的是什么请求,我们都将这个网页响应给服务器,此时这个html文件的内容就一个放在响应正文当中,我们只需读取该文件当中的内容,然后将其作为响应正文即可。
int main()
{
// 创建套接字
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0)
{
std::cerr << "socker error" << std::endl;
exit(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)
{
std::cerr << "bind error" << std::endl;
exit(2);
}
// 监听
if (listen(listen_sock, 5) < 0)
{
std::cerr << "listen error" << std::endl;
exit(3);
}
// 启动服务器
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
while (1)
{
int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0)
{
std::cerr << "accept error" << std::endl;
continue;
}
if (fork() == 0) // 爸爸进程
{
close(listen_sock);
if (fork() > 0) exit(0);
// 孙子进程
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); // 获取HTTP请求
std::cout << "-----------------------------http request begin-------------------------" << std::endl;
std::cout << buffer << std::endl;
std::cout << "-----------------------------http request end-------------------------" << std::endl;
// 读取html文件内容
ifstream in("index.html");
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响应
std::string status_line = "http/1.1 200 OK\n"; // 状态行
std::string response_header = "Content-Length" + to_string(len) + "\n"; // 响应报头
std::string blank = "\n";
std::string response_text = file; // 响应正文
std::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);
}
close(listen_sock);
return 0;
}
当我们用浏览器访问服务器时,就会显示出对应的内容。
此外,我们通过telnet命令来访问服务器,也是能够得到这个HTTP响应的。
使用HTTP协议为什么要交互版本?
HTTP请求当中的请求行和HTTP响应当中的状态行都包含了HTTP的版本信息。其中HTTP请求是由客户端发送的,因此HTTP请求当中表明的是客户端的HTTP版本,而HTTP响应是由服务器发送的,因此HTTP响应表明的是服务器的HTTP版本。
客户端和服务器双方在进行通信时会交互HTTP版本,主要还是为了兼容性的问题。因为服务器和客户端使用的可能是不同的HTTP版本,为了让不同版本的客户端都能享受到对应的服务,此时就要求通信双方进行版本协商。
客户端在发起HTTP请求时告诉服务器自己使用的HTTP版本,此时服务器就可以根据客户端使用的HTTP版本,为客户端提供对应的服务,而不至于因为双方使用的HTTP版本不同而导致无法正常通信。因此为了保证良好的兼容性,通信双方需要交互一些各自的版本信息。
其中最常用的就是GET方法和POST方法
GET方法一般用户获取某种资源,而POST方法一般用于将资源上床给服务器。但实际我们上传数据时也有可能使用GET方法,比如百度提交数据时实际用的就是GET方法。
GET方法和POST方法都可以带参:
从GET方法和POST方法的传参形式可以看出,POST方法能传递更多的参数,因为URL的长度是有限制的,POST方法通过正文传参就可以携带更多的数据。
此外,使用POST方法传参更加私密,因为POST方法不会将你的参数回显到URL当中,此时也就不会被别人轻易看到。不能说POST方法比GET方法更安全,因为GET方法和POST方法实际都不安全,要做到安全只能通过加密来实现。
说明:
我们最常见的一些状态码有:200(OK),404(Not Found),403(Forbidden请求权限不够),302(Redirect),504(Bad Gateway)。
Redirection(重定向状态码)
重定向就是通过这种方法将各种网络请求重新定个方向转到其他位置,此时这个服务器相当于提供了一个引路的服务。
重定向又分为临时重定向和永久重定向,其中状态码301表示的就是永久重定向,而状态码302和307表示的是临时重定向。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的直接就是重定向之后的网站。而如果某个网站是临时重定向的,那么每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站。
临时重定向演示
进行临时重定向时需要用到Location字段,Location字段是HTTP报头当中的一个属性信息,该字段表明了你所要重定向到的目标网站。
我们这里要演示临时重定向,可以将HTTP响应当中的状态码改为307,然后跟上对应的状态码描述,此外,还需要在HTTP响应报头中添加Location字段,这个Location后面跟的就是你需要重定向到的网页,比如我们将其设置为百度搜索的首页。
// 构建HTTP响应
std::string status_line = "http/1.1 307 Temporary Redirect\n"; // 状态行
std::string response_header = "Location: https://www.baidu.com/\n"; // 响应报头
std::string blank = "\n"; // 空行
std::string response = status_line + response_header + blank; // 响应报文
// 响应HTTP请求
send(sock, response.c_str(), response.size(), 0);
这样,如果我们用浏览器访问服务器,就会自动跳转到百度搜索的首页了。
此时当我们用telnet命令登录我们的服务器器时,向服务器发起HTTP请求,此时服务器给我们的响应就是状态码307,响应报头当中Location字段对应的就是百度搜索的地址。
HTTP常见的Header如下:
Host
Host字段表明了客户端要访问的服务器的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口。但客户端不就是访问服务器吗?为什么客户端还要告诉服务器它要访问的服务对于的IP和端口?
因为有些服务器提供的是一种代理服务,也就是代理客户端向其他服务器发起请求,然后将请求得到的信息再返回给客户端。在这种情况下客户端就必须告诉代理服务器它要访问的服务对应的IP和端口,此时Host提供的信息就有效了。
User-Agent代表的是客户端对应的操作系统和浏览器的版本信息。
比如当我们用电脑下载某些软件时,它会自动向我们展示与我们操作系统相匹配的版本,这实际就是因为我们在向目标网站发起请求的时候,User-Agent字段当中包含了我们的主机信息,此时该网站就会向你推送相匹配的软件版本。
Referer
Referer代表的是你当前从哪一个页面跳转过来的,Referer记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页表之间的相关性。
Keep-Alive(长连接)
HTTP/1.0是通过request&response的方式来进行请求和响应的,HTTP/1.0常见的工作方式就是客户端和服务端先建立连接,然后客户端发起请求给服务端,服务器再对该请求进行响应,然后立马端口连接。
但如果一个连接建立后客户端和服务器只进行一次交互,就将连接关闭,就太浪费资源量,因此现在主流的HTTP/1.1是支持长连接的。所谓的长连接就是建立连接后,客户端可以不断地向服务器一次写入多个HTTP请求,而服务器在上层一次读取这些请求就行了,此时一条连接就可以传送大量的请求和响应,这就是长连接。
如果HTTP请求或响应报头当中的Connect字段对应的值是Keep-Alive,就代表支持长连接。
HTTP实际上是一种无状态协议,HTTP的每次请求/响应直接都是没有任何关系的,但你在使用浏览器的时候发现并不是这样的。
比如当你登录一次CSDN之后,就算你把CSDN网站关了甚至是重启电脑,再次打开CSDN,CSDN并没有要求你再次输入账号和密码,这实际上就是通过Cookie技术来实现的,点击浏览器当中锁的标志就可以看到对应网站的各种Cookie数据。
这些Cookie数据实际都是对应的服务器方写的,如果你将对应的某些Cookie删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时设置的Cookie信息。
Cookie是什么呢?
因为HTTP是一种无状态协议,如果没有Cookie的存在,那么每次我们要进行网页请求时都要重新输入账号和密码进行认证,这样太麻烦了。
比如你是某个视频网站的VIP,这个网站里面的VIP视频有很多,每次点击一个视频都要重新进行VIP身份认证,而HTTP不支持记录用户状态,那么我们就需要有一种独立技术来帮我们支持,这种技术目前已经内置到HTTP协议当中了,叫做Cookie。
当我们第一次登录某个网站时,需要输入我们的账号和密码进行身份认证,此时如果服务器经过数据对比之后判断你是一个合法的用户,那么为了让你后续在进行网页请求时不用重新输入账号和密码,此时服务器就会进行Set-Cookie的设置。(Set-Cookie也是HTTP报头当中的一种属性信息)。
当认证通过并在服务端进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时就会将这个Set-Cookie响应给浏览器。而浏览器收到响应之后会自动提取出Set-Cookie的值,将其保存在浏览器的Cookie的文件当中,此时相当于我的账号和密码信息保存在本地浏览器的Cookie文件当中。
从第一个登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个Cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的Cookie字段,而不会重新让你输入账号和密码了。
也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做Cookie技术。
内存级别和文件级别
Cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。Cookie文件可以分为两种,一种是内存级别的Cookie文件,一种是文件级别的Cookie文件。
Cookie被盗
如果浏览器当中保存的Cookie信息被非法盗取了,那么此时这个非法用户就可以用你的Cookie信息,以你的身份访问你曾经访问过的网站,我们将这种线程称为Cookie被盗取了。
比如你不小心点了某个链接,这个链接可能就是一个下载程序,当你点击之后它就会通过某种方式将程序下载到你本地,并且自动执行该程序,该程序会扫描你的浏览器当中的Cookie目录,把所有的Cookie信息通过网络的方式传送给恶意方,当恶意方拿到你的Cookie信息后就可以拷贝到它浏览器对应的Cookie目录当中,然后以你的身份访问你曾经访问过的网站。
SessionID的引入
单纯使用Cookie是不安全的,因为Cookie当中保存的是你的私密信息,一旦Cookie文件泄漏你的隐私信息也就泄漏。
所以当前主流的服务器还引入了SessionID这样的概念,当我们第一次登录某个网站输入账号和密码后,服务器认证成功之后还会在服务端生成一个对应的SessionID,这个SessionID与用户信息是不相关的。系统会将所有登录用户的SessionID值统一维护起来。
此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后自动提取出SessionID的值,将其保存在浏览器的Cookie文件当中。后续访问服务器时,对应的HTTP请求当中就会自动携带上这个SessionID。
而服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动认证成功了,然后就会正常处理你发来的请求,这就是当前主流的工作方式。
安全也只是相对的
引入SessionID之后,浏览器当中对应的Cookie文件保存的是SessionID,此时这个Cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,非法用户仍然可以盗取我的SessionID去访问我曾经访问过的服务器,相当于还是存在上面所说的问题。
这种办法虽然没有真正地解决安全问题,但是这种方法是相对安全的。互联网上是不存在绝对安全这样的概念的,任何安全都是相对的,就算你将发送到网络中的信息进行加密,也有可能被人破解。
不过在安全领域有一个准则:如果破解某个信息的成本已经远远大于破解之后获得的收益,那么可以说这个信息是安全的。
引入SessionID之后的好处
此时虽然SessionID可能被非法用户盗取,但服务器也可以使用各种各样的策略来保证用户账号的安全。
任何事情都有两面性,如果不是一些非法用户的存在,现在的服务器肯定是漏洞百出,只有双方不断进行对抗才能不断进步。