目录
HTTP简介
认识URL
协议方案名
登录信息(认证)
服务器地址
服务器端口号
带层次的文件路径
查询字符串
片段标识符
urlencode和urldecode
urlencode编码工具
HTTP协议格式
HTTP请求协议格式
如何将有效载荷跟HTTP报头进行分离?
获取浏览器的HTTP请求
HTTP响应协议格式
构建HTTP响应给浏览器
HTTP为什么要交互版本?
HTTP的方法
GET方法和POST方法
Postman演示GET方法和POST方法区别
TCP套接字演示GET和POST方法区别
HTTP的状态码
Redirection(重定向状态码)
临时重定向演示
HTTP常见的Header
Host
User-Agent
Referer
Keep-Alive(长连接)
Cookie和Session
Cookie
内存级别 & 文件级别
SessionID
套接字实验验证Cookie技术
HTTP简介
HTTP(Hyper Text Transfer Protocol)协议又叫超文本传输协议,是一个简单的请求 - 响应协议,HTTP通常运行于 TCP/UDP 之上。
虽然说应用层协议是我们程序猿自己定义的,但是,已经有很多大佬定义了一些现成的,又好用的应用层协议,比如HTTP协议。
认识URL
URL(Uniform Resource Lacator)统一资源定位符,也就是通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。
一个URL大致格式如下:
协议方案名
http:// 表示的是协议名称,表示在请求时需要使用的协议,通常使用的是HTTP或者是HTTPS协议,HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。
常见的应用层协议还有:DNS,FTP,TELNET,SMTP,POP3,SNMP,TFTP等。
登录信息(认证)
user:pass 表示登录认证信息,包括登录用的用户名和密码,虽然URL中登录信息可以体现出来,但是多数URL中这个字段都是被省略的,因为登录信息可以通过其他方式交付给服务器。
服务器地址
www.example.jp 表示服务器地址,也叫做域名。常见的域名比如:www.baidu.com等。
IP地址标识公网内的一台主机,但是IP地址本身并不适合给用户看。例如:用 ping 命令,获取一下 www.baidu.com 这个域名解析后的IP地址:
实际上,域名和IP地址是等价的,在计算机当中使用的时候即可以使用域名,也可以使用IP地址,但URL呈现出来的是让用户看到的,因此URL当中是以域名的形式表示服务器地址的。
服务器端口号
80 表示的是服务器的端口号,HTTP协议和套接字(socket编程)一样同样位于应用层,在套接字编程时需要给服务器绑定对应的IP和端口号,应用层协议也要绑定明确的端口号。
常见的协议对应的端口号:
HTTP - 80
HTTPS - 443
SSH - 22
因为常见的服务器与端口号之间是一一对应的,所以在使用某种协议时,不需要指明该协议的端口号,在URL中,端口号一般也被忽略。
带层次的文件路径
/dir/index.html 表示要访问的资源所在路径。访问服务器的目的就是获取服务器上的某种资源,通过前面的域名和端口号已经能够找到对应的服务器进程,此时要做的就是指明该资源的所在的路径。
查询字符串
uid = 1表示请求时额外的参数,这些参数是以键值对的形式,通过 & 符号分隔开来。比如:在百度中搜索栏中搜索对应的信息,可以看到在 url 的参数中有 wd = linux,等号后面的内容就是搜索的关键字。
因此在进行网络通信时,双方是可以通过 url 进行用户数据传送的。
片段标识符
ch1 表示片段标识符,是对资源的部分补充。比如:在查看图片的时候,url 中就会出现片段标识符,翻阅图片集时片段标识符就会发生变化。
urlencode和urldecode
像 / ? : 这样的字符,已经被 url 当作特殊意义理解了,因此这些字符不能随意出现,比如:某个参数中需要带有这些特殊字符,就必须先对特殊字符进行转义。
转义规则如下:
将需要转码的字符转为 16 进制,然后从右到左,取 4 位(不足4位的直接处理),每 2 位做一位,前面加上 %,编码成 %XY格式。
比如:在百度搜索栏中搜索 C++,因为 + 这个符号在 url 中被当作特殊符号处理,转成 16 进制后的值为 0x2B,因此 + 这个符号会被编码为 %2B:
其中,url 对符号做特殊处理外,也会堆中文进行编码。
urlencode编码工具
urlencode工具 <------
使用方法,比如:
在输入栏中输入想要 urlencode 的关键字,然后点击右下角的 urlencode 按键,就可以完成编码。
查看 urlencode 编码结果:
而 urldecode 就是 urlencode的逆过程。
HTTP协议格式
HTTP是基于请求和响应的应用层服务,作为客户端,可以向服务器发起request,服务器接收到这个request后,会对这个request做解析工作,得到要访问的资源,然后服务器构建response,完成一次HTTP请求。这种基于request和response的工作模式,成为 CS 或者 BS 模式。
HTTP请求协议格式
格式如下:
• 请求行:[请求方法] (post / get)+ [url] + [http版本]
• 请求报头:请求的属性,这些属性以 key:value 的形式,每组属性之间使用 \n 分隔。
• 空行:遇到空行表示请求报头结束。
• 请求正文:请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个 Content-Length 属性标识请求正文的长度。
前面三部分一般是http协议自带的,是由http协议自行设置的,请求正文一般是用户自己设置的相关信息,如果用户在请求时,没有信息要上传给服务器,此时请求正文为空字符串。
如何将有效载荷跟HTTP报头进行分离?
可以根据HTTP请求当中的空行来进行分离,当服务器接收到一个http请求后,按行进行读取,如果读取到空行部分就说明已经将报头读取完毕了,实际上空行就是HTTP协议中用来进行有效载荷与报头进行有效分离的。
如果将HTTP请求当成一个大的线性结构,每行的内容都是按照 \n 进行分隔开的,在按行读取的过程中,如果连续读到了两个 \n,就说明报头已经读取完毕了,后面的数据就是有效载荷了。
获取浏览器的HTTP请求
HTTP协议的底层通常使用的是传输层的TCP协议,因此可以使用套接字编写一个TCP服务器,然后用浏览器去访问这个服务器。因为服务器是直接用套接字进行读取浏览器发来的HTTP请求,没有对这个HTTP请求做任何的解析工作,可以直接将浏览器发来的HTTP请求进行输出查看,就可以看到HTTP请求的基本格式:
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstdlib>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main()
{
// 1、创建套接字
int list_socket = socket(AF_INET, SOCK_STREAM, 0);
if (list_socket < 0)
{
cerr << "create list_socket fail!" << endl;
exit(1);
}
// 2、绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(list_socket, (struct sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "bind list_socket fail!" << endl;
exit(2);
}
// 3、设置监听状态
if (listen(list_socket, 5) < 0)
{
cerr << "listen fail!" << endl;
exit(3);
}
// 4、启动服务器
struct sockaddr peer;
memset(&peer, 0, sizeof(peer));
socklen_t len = sizeof(peer);
for (;;)
{
// 获取新连接
int sock = accept(list_socket, (struct sockaddr *)&peer, &len);
if (sock < 0)
{
cerr << "accept sock fail!" << endl;
continue; // 继续获取
}
// 让子进程执行
if (fork() == 0)
{
close(list_socket);
// 继续让孙子进程去接受请求
if (fork() > 0)
{
exit(0);
}
char buffer[1024];
recv(sock, buffer, sizeof(buffer), 0); // 阻塞式读取
std::cout << "-------------------------- http request begin ----------------------------\n"<<endl;
std::cout << buffer << endl;
std::cout << "-------------------------- http request end ------------------------------\n"<<endl;
close(sock);
exit(0);
}
// 父进程
close(sock);
// 等待子进程回收
waitpid(-1, nullptr, 0);
}
return 0;
}
此时运行服务器,用浏览器进行IP + 端口号进行访问,在终端就可以接收到浏览器发来的HTTP请求了。
• 浏览器向服务器发起HTTP请求后,因为服务器没有对其进行响应,此时浏览器就会任务服务器没有收到请求,然后不断得向服务器发送HTTP请求,这也是为什么会多次显示HTTP请求的原因。
• 因为浏览器发起请求时默认使用的就是HTTP协议,因此在浏览器的url输入栏中输入网址不需要指明HTTP协议。
• 在请求的报头中的 url 显示的 /,不能称为云服务器商店根目录,而是web 根目录,可以是机器上的任意一个目录,可以随意指定。
如果浏览器在访问服务器时,指明要访问的资源路径,此时浏览器发起的HTTP请求当中的 url 也要变成该路径:
HTTP响应协议格式
格式如下:
• 状态行:[http版本] + [状态码] + [状态码描述]
• 响应报头:请求的属性,冒号分割的键值对,每组属性之间使用 \n 分隔
• 空行:遇到空行表示响应报头结束
• 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头当中会有一个 Content-Length 属性来标识响应正文的长度,如果服务器返回了一个html页面,那么html页面的内容就是在响应正文当中,图片,音频也一样。
构建HTTP响应给浏览器
现在还没有办法分析浏览器发送的HTTP请求的能力,只能给浏览器返回一个固定的HTTP响应,拿当前服务器程序所在的目录作为 web 根目录,编写一个简单的 html 页面作为主页面:
此时将这个 html 文件放在响应的正文中,只需要读取文件中的内容,然后作为响应正文进行返回即可:
// 发送响应
#define FILE "./index.html"
ifstream is(FILE);
if (is.is_open())
{
is.seekg(0, is.end);
int len = is.tellg();
is.seekg(0, is.beg);
char *file = new char[len + 1];
is.read(file, len);
is.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;
// 发送响应报文
send(sock, response.c_str(), response.size(), 0);
delete[] file;
}
此时再使用浏览器进行访问服务器,服务器就会将 html 页面进行返回给浏览器显示:
也可以使用 telnet 工具进行测试服务器,查看对应的HTTP响应:
HTTP为什么要交互版本?
http请求当中的请求行和http响应当中的状态行当中,都包含了http的版本信息,http请求表明的是客户端的http版本,http响应当中表明的是服务器的 http 版本。双方会交换http版本,主要还是为了兼容性,可能服务器与客户端使用的是不同的http版本,为了让不同版本的客户端都能享受对应的服务,此时需要双方进行版本协商。
如果双方使用的http版本不同的话,可能导致无法进行正常通信,为了保证良好的兼容性,双方需要交换一下http版本信息。
HTTP的方法
方法 | 说明 | 支持的http版本协议 |
GET | 获取资源 | 1.0、1.1 |
POST | 传输实体主体 | 1.0、1.1 |
PUT | 传输文件 | 1.0、1.1 |
HEAD | 获取报头首部 | 1.0、1.1 |
DELETE | 删除文件 | 1.0、1.1 |
OPTIONS | 询问支持的方法 | 1.1 |
TRACE | 追踪路径 | 1.1 |
CONNECT | 要求用隧道协议连接代理 | 1.1 |
LINK | 建立和资源之间的联系 | 1.0 |
UNLINE | 断开连接关系 | 1.0 |
一般最常使用的就是GET和POST方法。
GET方法和POST方法
GET方法一般用于获取某种资源信息,POST方法一般用于将数据上传给服务器,但实际上上传数据时有时候也用GET方法。
GET方法和POST方法都可以带参:
• GET方法通过 url 传参。
• POST方法通过正文进行传参。
因为 url 的长度是有限制的,POST通过正文传参就可以携带很多的数据,所有POST方法可以传递更多的参数,此外POST方法传参更加私密。因为不会把参数回显到 url 中。
Postman演示GET方法和POST方法区别
首先postman使用GET方法通过 url 进行传参,Params下面的值就代表 url 当中的携带的参数:
其次选择 POST方法通过正文进行传参,在Body下进行参数设置,选取 raw 方法进行传参,代表原始传参,就是输入什么样的参数,正文中就显示你原始的参数:
此时可以看到这HTTP请求的正文当中不再是空字符串,而是通过 postman进行正文传参的参数,所以在请求属性当中出现了 Connect-Length字段,表示响应正文的长度。
TCP套接字演示GET和POST方法区别
需要让浏览器通过提交参数的方法,进行演示。在之前写的 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>
<h1>hello world</h1>
<form method="Get" action="/a/b/c">
用户名:<br>
<input type="text" name="username">
<br>
密码:<br>
<input type="password" name="psw">
<br>
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
可以通过修改表单中的method属性指定参数的提交方法,后面的 action属性,表示把这个表单提交给服务器上的指定资源,此时用浏览器访问服务器:
此时通过 GET方法提交参数,提交的用户名以及密码信息就会回显到 url 中:
同时在服务器上也收到了浏览器发送到请求,并在 url 中显示刚刚提交的参数:
再将提交表单的方法method属性改为 POST方法,在提交参数的时候就会在正文中将这两个参数上传至服务器:
实际上GET方法和POST方法在传参时都是明文传输,所以都不安全,但是POST方法更私密。POST不会将参数回显到 url 中,是通过正文传递参数,相对私密。
HTTP的状态码
类别 | 原因短语 | |
1xx | Informational(信息性状态码) | 接收的请求正在处理 |
2xx | Success(成功状态码) | 请求正常处理完毕 |
3xx | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4xx | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5xx | Server Error(服务器错误状态码) | 服务器处理请求出错 |
常见的状态码:200(OK),404(Not Found),403(Forbidden - 禁止,请求权限不够),302(Redirect),504(Bad Gateway -错误网关)
Redirection(重定向状态码)
重定向就是通过各种方法将各种网络请求重新定个方向转移到其他位置,这个服务器相当于提供了一个引路的服务。
重定向又分为临时重定向和永久重定向,301表示永久重定向,302 和 307 表示临时重定向。临时重定向和永久重定向的本质是影响客户端的标签,决定客户端是否需要重新更新目标地址。
例如:某个网址是永久重定向,在第一次访问该网址时由路由器帮其进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时访问的直接就是重定向后的网址。
如果某个网址是临时重定向,每次访问该网站时,都需要浏览器进行重定向跳转到目标网址。
临时重定向演示
进行临时重定向时,需要用到报头属性中的 Location字段,该字段表示要重定向到的目标网址,并且需要把报头中的状态码改为 307,状态码描述进行修改,再在响应报头中添加 Location 属性:
string status_line = "http/1.1 307 Temporary Redirect\n";
string response_header = "Location:https://www.baidu.com/\n";
string blank = "\n";
string response = status_line + response_header + blank;
// 发送响应报文
send(sock, response.c_str(), response.size(), 0);
此时运行服务器,使用 telnet 工具进行测试,发起http请求时,返回的信息为:
如果使用浏览器来访问服务器,当浏览器收到这个http响应之后,会进行分析,查看到状态码是 307 时,就会提取 Location 后面的网址,继续对该网站发起请求,此时就完成了页面的跳转,也就是重定向功能:
此时,当浏览器进行访问时,立马就跳转到了百度的网页。
HTTP常见的Header
• Connect-Type:数据类型(text/html等)
• Connect-Length:正文的长度
• Host:客户端告知服务器,所请求的资源在哪个主机的哪个端空上
• User-Agent:声明用户的操作系统和浏览器版本
• Referer:当前页面是从哪个页面跳转过来的
• Location:搭配3xx状态码使用,告诉客户端接下来要跳转到哪个区访问
• Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能
Host
Host字段表明了客户端要访问的服务器IP和端口,有些服务器实际提供的也是一种代理服务,代替客户端向其他服务器发起请求,然后将请求的结果返回给客户端,这种情况下,客户端必须告诉代理服务器要访问的服务的IP和端口。
User-Agent
User-Agent代表的是客户端对应的操作系统和浏览器的版本信息。比如:在下载软件时,User-Agent字段填充的就是客户端主机的信息,此时网址吹推送相匹配的软件版本。
Referer
referer字段代表当前页面是从哪一个页面跳转过来的,记录上一个页面的好处就是方便回退,也为了了解当前页面与上一页面之间的相关性。
Keep-Alive(长连接)
HTTP/1.0 是通过request & response 的方式来进行请求和响应的,常见的工作模式就是客户端和服务器先建立连接,然后客户端发送请求给服务器,服务器再对请求做出分析,并对其做出相应的响应,然后端口连接。
如果一个连接建立后,客户端和服务器只进行一次交互,就将连接关闭,太浪费资源了,现在的HTTP/1.0 都支持长连接,比如建立好连接后,客户端可以不断得向服务器写入多个HTTP请求,服务器只需要读取这些请求即可,此时一条连接就可以传送大量的请求和响应。
在HTTP请求或者响应报头中有Connection字段填充的是 Keep-Alive ,说明支持长连接的。
Cookie和Session
HTTP实际上是一种无状态协议,HTTP的每次请求与响应之间没有任何关系,但是在使用浏览器的过程中并不是这样。
比如:当登录某个网站的时候,就算把网站关闭或者重启电脑,再次打开网站,发现该网站并没有要求再次输入账号密码,这种技术就是通过Cookie技术实现的:
这些Cookie数据实际都是服务器写的,如果将某些Cookie删除,就需要重新登录账号与密码了,删除的可能就是登录时所设置的Cookie信息。
Cookie
当第一次登录某个网站时,输入账号与密码后,服务器会经过数据对比判断是否是合法用户,为了后面的登录不再重新输入密码等信息,服务器会进行Set-Cookie设置,Set-Cookie也是HTTP报头当中的属性字段。
认证通过并在服务器进行Set-Cookie设置后,服务器在对浏览器进行HTTP响应时会将这个Set-Cookie响应给浏览器,浏览器接受响应后,自动提取Set-Cookie的值,保存在浏览器的Cookie文件中,相当于账号和密码信息保存在本地浏览器的Cookie文件中:
在第一次认证后,后续的认证都变成了自动认证的方式,这种技术就是Cookie技术。
内存级别 & 文件级别
Cookie就是浏览器中记录的一个小文件,分为两种,内存级别Cookie文件,文件级别Cookie文件。
• 当将浏览器关闭后,重新登录之前的网站,需要重新输入账号和密码的就是内存级别Cookie文件。
• 当将浏览器关闭后,重新登录之前的网站,不需要重新输入账号和密码的就是文件级别Cookie文件。
SessionID
当第一次登录某个网站后输入账号和密码后,服务器认证完成后还会生成对应的SessionID,这个ID与用户信息不相关,服务器会将所有登录用户的SessionID进行统一维护起来,然后将这个SessionID进行响应给浏览器,浏览器会自动提取SessionID的值,并保存在浏览器的Cookie文件中,后续访问该服务器是,会自动携带这个SessionID,服务器就会提取这个SessionID,到对应的集合中进行对比,对比成功,说明这个用户是一个曾经登录过的用户,自动认证成功,之后会正常处理发来的请求:
跟Cookie一样,SessionID保存在Cookie文件中,同样也面临着被盗取的可能性,但是之前 Cookie 中保存的是账号和密码信息,现在 Cookie 文件中保存的是服务器根据账号和密码生成的SessionID,至少不会造成密码信息的泄露问题,但是非法用任然可以拿着盗取的SessionID去访问曾经的网站。
服务器也有很多策略来保护账号的安全:
• 如果非法用户拿着SessionID进行登录时,IP地址发生了很大的变化,服务器就会立马识别这个账号发生了异常登录现象,服务器就会清除对应的SessionID,这时就需要重新输入账号和密码等信息重新进行登录。
• 当非法用户要进行某些高权限的操作时,系统会再次提醒重新输入密码等信息进行验证,而非法用户短时间内无法得到密码。
• SessionID也有过期策略,比如有些SessionID一个小时就失效了,此时非法用户得到了也没有什么用。
套接字实验验证Cookie技术
给HTTP响应报头当中添加 Set-Cookie 字段,当浏览器再次访问服务器时,就会携带 Cookie 信息:
// 发送响应
#define FILE "./index.html"
ifstream is(FILE);
if (is.is_open())
{
is.seekg(0, is.end);
int len = is.tellg();
is.seekg(0, is.beg);
char *file = new char[len + 1];
is.read(file, len);
is.close();
// 构建http响应报头
string status_line = "http/1.1 307 Temporary Redirect\n";
string response_header = "Content-Length:" + to_string(len) + "\n";
response_header += "Set-Cookie:hello HTTP,See you\n";
string blank = "\n";
string response_text = file;
string response = status_line + response_header + blank + response_text;
// 发送响应报文
send(sock, response.c_str(), response.size(), 0);
delete[] file;
此时第二次进行访问服务器时,就能查看到 Cookie 的内容了:
通过Fiddler抓包工具获取浏览器HTTP第二次请求中的Cookie信息: