本篇博客由 CSDN@先搞面包再谈爱 原创,转载请标注清楚,请勿抄袭。
本篇主要讲解http协议,讲解主要内容有URL、HTTP格式、Web目录、HTTP请求方法、HTTP响应状态码、HTTP报头介绍,还有两个小工具。
协议就是指网络的规定,传输数据的时候双方以什么格式进行传输,这篇讲的是应用层协议HTTP,也就是应用层在采用HTTP协议时,应该以什么样的格式来传输数据。
有了我前面编写协议的代码的经历后再去看所谓的应用层,其实就是程序员基于网络编程之上编写的具体逻辑,做的很多动作其实都是和文本处理有关的,也就是协议的分析与处理。
我上一篇中网络计算器的例子(上一篇博客链接:【网络】用代码讲解协议 + 序列化和反序列化 + 守护进程 + jsoncpp),其实真正涉及到业务的也就是其中的Calculate函数,接收到请求后去处理才叫业务,有点难度的是那些文本处理的地方,不过有现成的工具json用,也就使得整体的难度下降了不少。上一篇中的协议是自定义协议,本片中讲的是http协议。
HTTP协议,也叫超文本传输协议,名字听起来很酷炫。
虽然我们说, 应用层协议是我们程序猿自己定的,但是很多大佬们已经定义了一些现成的, 又非常好用的应用层协议,其中就包含本篇要讲的HTTP协议。
先来说url。
有的同学可能第一次听说这个URL,啥是URL呢?
俗称链接、网址。
就比如说百度的首页链接就是: www.baidu.com
这就是URL。
URL有固定格式,我来拿出来一个简单的链接分析分析。
看这个:https://legacy.cplusplus.com/reference/string/
这个其实是cplusplus的网站,专门用来查阅C++中的一些接口的,学C++的同学应该很熟悉。
https://legacy.cplusplus.com/reference/string/
URL开头为协议名称,这里就是https,本篇要讲的是http,不过现在主流的协议已经是https了,感觉http开头的都好难找了,但是学习http还是很有必要的,后面我也会讲https的,屏幕前的你先不要着急,先学好http再去看https。
https://后面的legacy.cplusplus.com为域名,其实就是IP,我们程序员写服务器的时候IP用的是纯数字的,但是纯数字用起来对普通的老百姓不是很友好,所以在纯数字外面套了一层,就变成了一堆字符,域名解析后就是IP地址,域名解析成IP这个工作是浏览器自动做的,我们不需要操心。
域名下来是端口,但是这个URL中没有显示。
这里要说一点,我们所请求的网络服务,对应的端口号对于客户端来说是众所周知的。这里浏览器就是客户端,也就是说浏览器是有共识的,如果URL没有显示给出端口号,就会按照不同协议的默认端口号去找,比如HTTP默认的端口号是80,HTTPS的默认端口号是443。故端口号是可以省略的,我前面写的服务器大部分运行的时候端口号是8080,那么请求服务的时候显示给出就行。等会我也会说怎么用的,先别急。
- 这里域名后面的/叫做web根目录。也就是https://legacy.cplusplus.com后面的/。
我们平时上网的行为对于服务器来说有哪些?
就两点:
- 想要获取什么
- 想要上传什么
.当然,二者可以混合。
比如说逛淘宝、刷抖音,这就叫做获取东西,将服务器中的资源获取到本地,抖音上的视频刚开始并不在你的手机上,而是在服务端。注册、登录、上传视频,提交给服务端,再比如说搜索,把关键字提交给服务端,让服务端帮我搜索,这就叫上传。我们想要获取的资源可能是一张图片、一个视频、一段文字,这些东西统称为资源,在这些资源没有被你拿到的时候,这个资源在服务器上,后端的服务器大多是Linux的(centos、redhat等)。
一个服务器上可能存在很多资源,在Linux上都是以文件表示的,比如说请求一张图片,那么就在代码中打开图片对应的文件,加载到内存中,然后再通过网络发送出去,发送后再关掉文件就行了。
所以请求资源拿到本地就是让服务器进程打开你要访问的文件,读取该文件并通过网络发给客户端。打开某个文件的前提是先找到这个文件,而Linux中使用路径来唯一标识一个文件的。
那么再来看看这个网址:https://legacy.cplusplus.com/reference/string/,去掉前面的版本和域名,剩下的是/reference/string/,这里就是一个路径,第一个/是web根目录(注意web根目录和系统的根目录可一定不是一个东西),web根目录可以在写代码的时候指定,一般是一个相对的目录,不会给成系统的根目录,那么从这个url就可以看出,后面的/就是路径分隔符,和Linux中的路径分隔符一样。
所以url整个格式就是:
协议名称://server ip[:port]/a/b/c/d.html
这里我给:port加上[ ],意思就是可以省略端口号。
server ip(服务端IP):标定全网中唯一的机器。
port(端口号):标定某个机器上的唯一进程
/a/b(路径):标定客户想要访问的资源路径
c.html(文件名):标定客户想要访问资源的文件名。
其中/a/b/c/d.html就可以标定客户要的资源。
这样的话,请求一个资源时,首先确定在哪一台机器上(server ip),在这台机器上的哪个路径下(/a/b),在这个路径下的文件的名字是什么(c.html),这样就能在全网中定位到客户想要的资源,并且让这台机器上的某一个服务把资源交给客户(port)。
故URL用来定位互联网中唯一的一个资源,全称叫做UniformResourceLocator,就是统一资源标定符。所以URL就表示了客户端想访问什么资源。
在全球范围内的所有资源,只要给定一个URL,就能访问该资源,这种获取资源的方式称之为www,也就是WorldWideWeb,也就是万维网。
URL后面可能还会带参,比如说我最开始给的网址,这个问题后面再说。还有web根目录后面的路径也是后面再说。
像 / ? : 等这样的字符,已经被url当做特殊意义理解了,因此这些字符不能随意出现。
如果用户想要在URL中包含用户本身用来作为特殊字符的字符,URL在形成的时候浏览器会自动给我们进行编码(encode),会不让我们搜索的东西包含特殊符号的,不信的话,我这里用三个关键词进行搜索,一个?,一个helloworld,一个/,我这里把三个搜索出来的结果网址给出:
其中wd后面跟的就是搜索的关键词,这里?和/都被转了。所以一般服务器在接收到之后,需要将特殊字符进行转回。
浏览器转换的规则如下:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。
比如这里?的ASCII转成十六进制就是3F,然后取出这两位作为一位,然后加上%就行。汉字也会采用同样的规则,比如我搜一个你好:
就转成了这。
这里涉及到编码,不做详细讨论了。
有在线的编码解码网站,网上也有相应的源码,如果后面写服务器的时候想用直接cv就行。
http是基于响应的,看图:
这种基于请求和响应的模式称为cs模式(client - server模式),交互的就是双方的报文,http协议是应用层的协议,底层采用的是TCP,所以在http正式进行request和response之前就已经完成了三次握手的过程,也就是已经建立好了连接,双方才可正式通信。
http整体格式,单纯在报文角度,http可以是基于行的文本协议,什么意思呢?
就是按行收发,想我前一篇写的那个网络计算器,发送的时候每一行的最后面加上\r\n,接收的时候就按照\r\n来读。
http request格式一般可以为三部或四部分,格式如下:
解释一下。
请求行:由请求方法、请求URL、协议版本三个字段组成,三个字段之间一般有一个空格来做为分隔符,最后面加上\r\n。
请求报头:由多个请求属性组成。请求报头中包含本次请求的属性,如本次请求用的是长连接还是短连接,本次请求中期望的编码格式是什么,发起这次请求的客户端信息等等。
空行里面只包含了\r\n。
请求正文,可以省略。
虽然上面的图是按照\r\n划分而画出来的,但是可以把\r\n看成字符,那么通信时就是client给server发送大量数据流,所以看待http请求的时候把它当做线性结构就行:
把上面按照行来分的看成草图,这里的一整行来看更有助于我们在写代码时去理解,前一篇中网络计算器中的协议就由\r\n组成的,不过这里http更复杂一些。
直接看图:
和http request很相似。也是四个部分。
当服务端接收到一个http请求,对该请求进行解析处理,提取到对应响应信息后,构建响应,就可以把响应发给服务端。
这里依旧可以把上面的每一行合到一块看成一整个大字符串。
在读取时就按行读取状态行、各个响应报头等等。
请求和响应当中都包含了协议版本,如何理解?
1. 不同版本的HTTP协议具有不同的特性和限制。例如,HTTP 1.0协议默认使用非持久连接,而HTTP 1.1协议则默认使用持久连接,并支持分块传输编码等特性。因此,在编写HTTP请求或响应时,正确指定协议版本可以帮助确保通信双方使用相同的协议特性,避免因协议版本不匹配而导致的问题。
2. 协议版本还可以用于向后兼容。例如,如果一个服务器只支持HTTP 1.0协议,但收到了一个使用HTTP 1.1协2议的请求,那么服务器仍然可以解析和处理该请求,只是无法使用该请求中可能包含的HTTP 1.1特有的特性。
http是如何区分报头和有效载荷的?(有效载荷就是正文(无论是请求正文还是响应正文),报头就是正文上面的部分。)
通过中间的空行来区分,也就是只有\r\n的那一行。接收到数据后按行读取,一行一行读,一定能把报头读完,直到读到空行时,接下来读的就是正文。
如何得知正文大小?
在报头属性中,也就是一堆Key:Value字段,有一个Content-Length,比如说53,这个就代表了正文的大小,读取到正文时直接读53个字节就行了。
那么以上就是对于http的宏观理解,所以前面讲的网络计算器在这里就能体现出来,首先http协议序列化和反序列化的方式很简单,一个报文实际上是有多行数据的,但这里把多行数据按照\r\n为分隔符全部合并在一起就将多个字符串变成了一个字符串,发给对方后再以行为单位进行读取,读到空行时再根据属性读取正文就行。
下面就写点代码,来演示演示http。
其实没有新的接口,还是前面TCP中的那些接口,只不过客户端变成了浏览器。刚刚说Http这种基于请求和响应的模式称作cs模式,其实也可以说是bs模式(browser - server),也就是浏览器 - 服务端模式,直接用浏览器发送请求。
我就直接把代码给出来了,里面的接口我前面博客中已经讲过了,这里就不再详细讲了。
对于Socket相关接口的封装(Sock.hpp):
#pragma once
#include "LogMessage.hpp" // 这个是一个日志,后面给出
#include
#include
#include
#include
#include
#include
#include
#include
// 对套接字相关的接口进行封装
class Sock
{
private:
const int gBackLog = 20;
public:
// 1. 创建套接字
int Socket()
{
/*先AF_INET确定网络通信*/ /*这里用的是TCP,所以用SOCK_STREAM*/
int listenSock = socket(AF_INET, SOCK_STREAM, 0);
// 创建失败返回-1
if(listenSock == -1)
{
LogMessage(FATAL, _F, _L, "server create socket fail");
exit(2);
}
LogMessage(DEBUG, _F, _L, "server create socket success, listen sock::%d", listenSock);
// 创建成功
return listenSock;
}
// 2. bind 绑定IP和port
void Bind(int listenSock, uint16_t port, const std::string& ip = "0.0.0.0")
{
sockaddr_in local; // 各个字段填充
memset(&local, 0, sizeof(local));
// 若为空字符串就绑定当前主机所有IP
local.sin_addr.s_addr = inet_addr(ip.c_str());
local.sin_port = htons(port);
local.sin_family = AF_INET;
/*填充好了绑定*/
if(bind(listenSock, reinterpret_cast<sockaddr*>(&local), sizeof(local)) < 0)
{
LogMessage(FATAL, _F, _L, "server bind IP+port fail :: %d:%s", errno, strerror(errno));
exit(3);
}
LogMessage(DEBUG, _F, _L, "server bind IP+port success");
}
// 3. listen为套接字设置监听状态
void Listen(int listenSock)
{
if(listen(listenSock, gBackLog/*后面再详谈listen第二个参数*/) < 0)
{
LogMessage(FATAL, _F, _L, "srever listen fail");
exit(4);
}
LogMessage(NORMAL, _F, _L, "server init success");
}
// 4.accept接收连接 输出型参数,返回客户端的IP + port
int Accept(int listenSock, std::string &clientIp, uint16_t &clientPort)
{
/*客户端相关字段*/
sockaddr_in clientMessage;
socklen_t clientLen = sizeof(clientMessage);
memset(&clientMessage, 0, clientLen);
// 接收连接
int serverSock = accept(listenSock, reinterpret_cast<sockaddr*>(&clientMessage), &clientLen);
// 对端的IP和port信息
clientIp = inet_ntoa(clientMessage.sin_addr);
clientPort = ntohs(clientMessage.sin_port);
if(serverSock < 0)
{
// 这里没连接上不能说直接退出,就像张三没有揽到某个客人餐馆就不干了,所以日志等级为ERROR
LogMessage(ERROR, _F, _L, "server accept connection fail");
return -1;
}
else
{
LogMessage(NORMAL, _F, _L, "server accept connection success ::[%s:%d] server sock::%d", \
clientIp.c_str(), clientPort,serverSock);
}
return serverSock;
}
// 因为这里不需要客户端,所以就没有对connect进行封装
};
服务器再对于上面的sock进行封装(HttpServer.hpp):
#include "Sock.hpp"
#include
#include
#include
using func_t = std::function<void(int)>;
class HttpServer
{
public:
HttpServer(uint16_t port, func_t func)
: _listenSock(-1)
, _func(func)
{
// 创建套接字
_listenSock = _sock.Socket();
assert(_listenSock != -1);
// 绑定IP + port
_sock.Bind(_listenSock, port);
// 设置listen状态
_sock.Listen(_listenSock);
}
// 启动服务器
void Start()
{
signal(SIGCHLD, SIG_IGN);
std::cout << "start" << std::endl;
signal(SIGCHLD, SIG_IGN);
while(1)
{
// 获取客户端IP + port
std::string clientIp;
uint16_t clientPort;
// 接收数据
int serverSock = _sock.Accept(_listenSock, clientIp, clientPort);
if(serverSock < 0)
{
std::cout << "accept fail" << std::endl;
continue;
}
if(fork() == 0)
{// 子进程
// 关闭不需要的文件描述符
close(_listenSock);
_func(serverSock);
close(serverSock);
exit(0);
}
// 关闭不需要的文件描述符
close(serverSock);
}
}
~HttpServer()
{
if(_listenSock >= 0) close(_listenSock);
}
private:
int _listenSock;
Sock _sock;
func_t _func;
};
日志(LogMessage.hpp):
#pragma once
#include
#include
#include
#include
#include
#include
// 文件名
#define _F __FILE__
// 所在行
#define _L __LINE__
enum level
{
DEBUG, // 0
NORMAL, // 1
WARING, // 2
ERROR, // 3
FATAL // 4
};
std::vector<const char*> gLevelMap = {
"DEBUG",
"NORMAL",
"WARING",
"ERROR",
"FATAL"
};
#define FILE_NAME "./log.txt"
void LogMessage(int level, const char* file, int line, const char* format, ...)
{
#ifdef NO_DEBUG
if(level == DEBUG) return;
#endif
// 固定格式
char FixBuffer[512];
time_t tm = time(nullptr);
// 日志级别 时间 哪一个文件 哪一行
snprintf(FixBuffer, sizeof(FixBuffer), \
"<%s>==[file->%s] [line->%d] ----------------------------------- time:: %s", gLevelMap[level], file, line, ctime(&tm));
// 用户自定义格式
char DefBuffer[512];
va_list args; // 定义一个可变参数
va_start(args, format); // 用format初始化可变参数
vsnprintf(DefBuffer, sizeof DefBuffer, format, args); // 将可变参数格式化打印到DefBuffer中
va_end(args); // 销毁可变参数
// 往显示器打
printf("%s\t=\n\t=> %s\n\n\n", FixBuffer, DefBuffer);
// 往文件中打
// FILE* pf = fopen(FILE_NAME, "a");
// fprintf(pf, "%s\t==> %s\n\n\n", FixBuffer, DefBuffer);
// fclose(pf);
}
服务器(Httpserver.cpp):
#include "HttpServer.hpp"
#include
void ProcessRoutine(int serverSock)
{
char buff[10240];
// 接收任务,这里先简单进行接收等会还会改
ssize_t ret = recv(serverSock, buff, sizeof(buff) - 1, 0);
if(ret > 0)
{
buff[ret] = 0;
std::cout << buff << "--------------" << std::endl;
}
// 执行任务并回应
}
void Usage(const std::string &fileName)
{
std::cout << "\nUsage:\n" << fileName << "port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
std::unique_ptr<HttpServer> httpServer(new HttpServer(atoi(argv[1]), ProcessRoutine));
httpServer->Start();
return 0;
}
这里服务器只是简单的接受客户端发送来的东西,已经可以接受http请求了,我现在在浏览器上输入我的服务器IP + port就能发请求:
浏览器这边是用不了的,但是服务端能够接受到请求,打印结果如下:
这里其实就是上面的这张图:
再来看:
一行一行拆开看:
第一行的GET / HTTP/1.1就是请求行。其中GET就是请求方法,/是请求URL,默认情况下来说请求什么资源会自动填一个/代表web根目录,但并不是要把web根目录下的所有文件发给客户端,而是要请求默认网页,web服务器要有其首页,等会再细说。
剩下的都是KV,也就是key:Value。
Host代表这次http请求所要请求的目标,冒号后面的就是服务端的IP + 端口号。
connection后面的keep-alive表示支持长连接,后面再说。
Cache-Control表示控制缓存行为。例如,“no-cache”表示不应使用缓存的响应,而“max-age=3600”表示响应可以在缓存中保存3600秒。
Upgrade-Insecure表示升级的,也不考虑。
User-Agent代表发起这次请求的客户端信息,像这里我用的是我电脑上的浏览器,os是Windows的,后面还有相关的信息。
Accept指这次请求我们可以接收什么类型的数据,比如html网页、xml、apng图片格式等,语言中英文都支持、也支持编码。
最下面一行是空行,这里没有正文,所以这里的http请求有三部分构成。
现在用浏览器已经发起了一个请求,这里服务器已经可以读到请求了,可是无响应,这里就写一个最简单的响应。前面说了把请求和响应看成字符串就行了,所以返回时响应一个字符串就行。
下面就来构建一个http响应。
首先就是状态行,就返回一个HTTP/1.1 200 OK\r\n。说一下为啥,HTTP不区分大小写,但是简易大写,正常情况下状态码为200,如果没找到就是404,404应该都见过,OK就是200对应的描述。
报头也没讲,就不给了,直接给空就行了,现在的浏览器已经很牛了,有些东西少了人家可以自动识别出来,所以直接加一个\r\n当做空行就行。
然后正文不需要太复杂,如下:
然后发送回去就行:
此时运行起来服务器,浏览器再发送请求就能获得到如下页面:
很成功。
这里会接收到两次请求:
第一个是web根目录。
第二个是web根目录下的favicon.ico。这是一个图标信息,但是我这里没有搞图标,先不说。
来认识一下url中web路径是啥。
如果我在浏览器中给出如下请求:
IP:8080/a/b/c/d.html
那么服务器就会接收到如下报文:
这里GET /a/b/c/d.html HTTP/1.1意思就是获取根目录下的a目录下的b目录下的c目录下的d.html资源。
若我请求的时候后面没有加上/a/b/c/d.html就会默认获取web根目录/,也就是下面这样:
这里的web根目录不是Linux系统的根目录,我们可以自定义web根目录,比如在服务器同路径下创建一个wwwroot目录文件,那么就可以让请求时的web根目录定为这个wwwroot:
然后在代码中打开wwwroot中的一个文件并将其中的数据读出来再发送过去就行了。
平时不会像我前面代码中那样直接让httpResponse直接+=一个html语句,这种方式太矬了。应该是打开一个html文件,然后把文件中的内容发过去。
在根目录中搞一个首页文件,如果用户请求的是根目录,那么就把这个首页文件发给浏览器就行,如果请求的是其他文件就直接打开其他的文件然后发回来就行。让用户根据目录去找资源,找到了就给,找不到就返回404。
那么如何实现打开指定文件呢?
前面我们已经可以获取到客户端发来的请求了,比如说刚刚的:
我们可以提取出这里报文中的第一样,然后这样就能提取出来GET /a/b/c/d.html HTTP/1.1。
然后再根据这一行提取出其中的/a/b/c/d.html,这样就能提取出来文件的位置,然后就好说了,根据这个路径打开对应的文件,然后把文件中的内容交给httpResponse的正文再发给客户端即可。
首先是实现一个接口能提取出报文中的每一行:
在接收到报文后,将报文的每一行提取出来,这里每一行按照\r\n分割,那么就给sep赋值为\r\n:
运行:
提取成功。
然后提取出第一行中的以空格为间隔的三个字段,就根据刚刚提取出来的第一行再以空格分割提一次就行:
运行:
很成功。
再来试一下加上路径的:
成功。
其实这样搞还给搞麻烦了,可以写的更简单点,不过没关系这样写也行,想改的同学自行改改。很简单。
那么就好说了,这里的firstLine[1]就能得到文件路径和文件名。
【注】这里将默认首页名的文件给为index.html,如果用户请求的是web根目录就返回这个文件。
然后就能打开文件了:
运行服务器后进行请求。
总共三个测试的:
在VScode下配置一下html相关的东西,创建html文件,进入后输入! + tab键就可以自动补齐如下内容:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
body>
html>
三个文件内容如下:
index
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方丈的测试title>
head>
<body>
<h2>一个简单的首页测试h2>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
body>
html>
test
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方丈的另一个测试title>
head>
<body>
<h1>test测试h1>
body>
html>
a/b/c/d.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>方丈的多目录测试title>
head>
<body>
<h2>一个简单的多目录测试h2>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
<p>hello 方丈!!!!p>
body>
html>
默认根目录:
指定默认首页:
wwwroot下的test.html:
多路径下:
都是成功的。
再来看一个没有的文件:
来看看我这里搞的web目录:
首页在wwwroot下,以首页为入口,在站内各个子网页进行跳转就是在wwwroot下的各个子目录下来回跳转,在请求时会把网页信息构成http的一部分返回给客户。
如果想要在网页中插入图片,可以这样:
<img src="https://ts1.cn.mm.bing.net/th/id/R-C.d31cd6663d7f03e9f3262501c6b93c5d?rik=3SbWFNxtZzQJSg&riu=http%3a%2f%2fpic.bizhi360.com%2fbbpic%2f20%2f6320.jpg&ehk=dbSxYupjjj1OlCFHBaPN2vMAAmRMqGGeKKg7y4ope0g%3d&risl=&pid=ImgRaw&r=0" alt="这是一张图片">
这里src后面跟的是一张图片的路径,也就是知名哪一张图片,但是我这里没有图片,就直接
在网上随便搞了一张玫瑰花的链接放这了。
如果图片如果没显示就会显示alt中的文字。
这里测试结果就是这样:
到这里web根目录还有url算是讲完了。
前面说了平时上网的行为有拿数据和提交数据。
不管是APP还是浏览器,向服务端发起请求,二者在用户这边都是一个进程。同样的服务端也是一个进程,一般以守护进程的方式在跑,所以拿数据和交数据本质上就是进程间通信,不过在HTTP这里需要更具体的通信方案。
一般浏览器/APP在拿数据的时候就是GET方法。提交数据时一般用的是GET/POST两种方法:
这里HTTP请求方法了解这两个就够了,也有其他的方法,但是不是很重要,先看看都有啥:
在一般的HTTP应用场景中,80% ~ 90%的请求方法就这两个,其他方法要么被服务端禁用,要么服务端不支持,因为服务器(尤其是web服务器),会将资源直接推送给客户,而客户也有好坏之分,所以直接应对客户的服务器要以最小的成本提供基本服务之外,还要减少无用服务的暴露,进而减少自己服务器出现漏洞的可能性,所以一般服务器能关的方法都关了,虽然支持某些方法,但是这些方法没有那么轻易被拿到。
这里用命令行演示一下直接用GET方法来获取百度的首页面:
直接用telnet就行,发送一个只有请求行和空行的报文,然后就能获取如下数据:
上面就是百度给我发回来的报文,但是下面正文中开起来很乱,因为这里html语句是给浏览器看的,不是给我们看的,特殊字符也要占用空间,而浏览器不需要这些特殊字符,所以在发送出去的时候需要做压缩,将不需要的字符就直接去掉,减少网络传输的成本,所以看起来很乱,我这里也可以通过浏览器来看到这些代码:
这里浏览器会自动做格式化显示。
右边就是相关的前端代码,如果你也想看的话,可以找浏览器右上角的设置:
上面用红色框框起来的按键可以让你鼠标指向哪里就显示哪里的前端代码。
如果要让我刚刚写的服务器能进行客户端GET和POST提交的测试,就需要用到表单。
那么表单是啥呢?
是前端的东西,就是输入框 + 按键,很简单。作用就是收集用户的数据并把用户的数据交给服务器,用户就直接在输入框中输入后点一下提交的按钮就把数据推给服务器了。推送的时候用户输入的数据会转换成HTTP协议的一部分(等会说是哪一部分)。
下面来看看表单相关的前端代码长啥样:
<form name="input" action="/a/b/notexist.html" method="POST">
Username: <input type="text" name="user"> <br/>
Password: <input type="password" name="pwd"> <br/>
<input type="submit" value="登录">
form>
先看看长啥样,再来细讲。
我这里把这个东西加到我首页中:
长这样:
再把代码拿出来:
<form name="input" action="/a/b/notexist.html" method="POST">
Username: <input type="text" name="user"> <br/>
Password: <input type="password" name="pwd"> <br/>
<input type="submit" value="登录">
form>
第一行中
name为表单名,这里就是input。
action是提交给的目标,这里就是要将表单中的数据交给web目录下的/a/b/notexist.html这个文件。也可以给绝对路径,就是把整个网站的URL都给action
method就是提交方法,这里给的就是POST,html对大小写不敏感,所以也可给小写。也可以给GET,这里先以post演示。
第二行中,看尖括号里的
输入的类型为text,也就是文本类型,等会输入啥就是啥。
标签名为user。
是显示的时候换行。
看我输入:
我此时按下登录:
会出现404,因为我这里并没有写这个文件。
打码好麻烦,下面的我就不打码了。
服务端接收到的:
可以看到,报文正文中就存放了提交的数据。
现在我再将method改为GET:
运行后,网页的:
服务端接收的:
可以看到GET和POST二者有什么区别吗?
我来对比一下,先说网页:
用GET会把刚刚输入的东西加到URL当中,比如说这里我刚刚输入的用户名是zhangsan,密码是123456ab,直接会在URL当中显示出来。但是POST不会,还是在action中的连接。
我这里把GET的URL拿出来:
http://43.138.118.133:8080/a/b/notexist.html?user=zhangsan&pwd=123456ab
注意看,?右边是要提交的参数,也就是刚刚输入的东西,?左边是要把参数提交给谁。
如果我在百度上进行搜索,关键词为html教程:
https://www.baidu.com/s?wd=html%E6%95%99%E7%A8%8B&rsv_spt=1&rsv_iqid=0xe8f0cc900000858b&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_enter=1&rsv_dl=tb&rsv_sug3=15&rsv_sug1=4&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=html%25E6%2595%2599%25E7%25A8%258B&rsp=5&inputT=2740&rsv_sug4=3591
其中就会显示出我搜索的内容,也就是?右边的wd=html%E6%95%99%E7%A8%8B,后面的东西是其他的参数。?左边是一个s,s是search的首字母,也就是要交给web根目录下的s服务。
name是用来提参的时候提出来user=什么和pwd=什么,在构建请求的时候就被称作变量的变量名,内容就填在=后面。
所以在请求的时候若用的是GET方法,浏览器会自动把表单中的字段拆出来构建出name=val这样的值,如果有多个参数的话就用&符号作为分隔符来构建出一个长字符串作为请求的参数提交给服务器,当点击登录后会将参数回显到URL的输入框中。
再来说服务器:
结合刚刚讲的东西,URL不一样,很正常,因为GET时会将参数在URL中回显。
但是最大的区别是这里GET的正文是空的,但POST的中文是user=zhangsan&pwd=123456ab,也就是刚刚输入的内容。
所以GET和POST的最大区别如下:
我们这里是先获取的表单,而表单就是静态页面,所以是客户端先通过GET方法把资源从服务器中拿下来,然后才根据表单把数据提交给服务器,提交的时候要么用GET,通过URL传参,要么用POST通过正文传参。
GET方法会回显用户的私密信息,就算是小白也能直接看到,若被看到后被人利用了,那就不太好。
POST方法通过正文提交参数,不会回显,一般私密性有保证,注意私密性可不是安全,私密性仅仅是让一般人看不到,通过POST在网络中传送时没有进行加密、解密动作,那么这些数据照样可以被别人拿到,所以对数据进行加密解密才是安全。
GET方法传参时都传不太重要的数据,比如百度搜索时就不管,提交就回显。
像登录注册等就一般是POST方法,所以GET、POST最主要的区别就是传参方式,其他不谈(比如URL大小有限制,这一点也取决于协议版本,不说),提交数据不想回显就用POST。还有一个不成文规定,若上传东西比较大,比如说用网盘传视频,一般用POST,不建议用GET,因为GET通过URL传输,按行读取时若参数中包含了换行等特殊字符就可能会不好处理,解析起来不方便,而用POST传,包头中有Content-Length标明正文长度,所以可以更方便的进行读取。
其实可以不用说的,其他的没啥大用。
来说说HEAD,我再用一次telnet:
状态码是302(状态码等会再说),是一个重定向的状态码。反正没咋成功。
再看看别的:
OPTIONS也给我重定向了。
其他的就不演示了,像DELETE方法,用户不可能删除服务器上的东西,不可能给你放开的,目前就懂GET和POST方法就行,后面这几个其他的知道有就行。
状态码就是一个表明某次请求的一个数字,就像进程退出码一样,每个数字含义不同。
状态码常见的有5种,看图:
nXX就表示n开头的状态码。
1开头的,这类状态码表示请求已被接收,继续处理。例如,100 Continue表示客户端应继续其请求,因为服务器已准备接收数据。这类状态码通常不会直接返回给最终用户。不过目前我这里没有这种场景,请求都很短。
2开头的比如说200,就是成功。状态码和状态码描述要匹配,200就是OK。
3开头的叫重定向,就是进入一个页面后要进行附加操作,比如进入一个网站先登录,登录后就会跳转到主页,这个等会详谈。
4开头的为客户端错误状态码,最经典就是404(NotFound),403(Forbidden),403比较少见,比如说公司内网可能较为私密,访问会被拒绝。
404为什么是客户端的错?
因为客户端在申请一个根本不存在的资源。打个比方,你这天回到家中,管你爹要钱,张口就要了一个亿,你觉得可能吗?根本不可能,正常人谁能挣那么多,就算这一个亿在王健林眼里只是一个小目标,但是王思聪要一个亿也不能说给就给啊。所以说是客户端在进行非法的请求。
5开头的为服务器错误码。比如说来了一个任务创建一个线程/进程去执行但是创建失败了,或者申请空间失败了,就是服务器内部错误,但是一般不会直接给用户显示出来服务器错误,因为正常情况下公司并不想暴露服务器的错误给用户。
这里知道最常见的200、404等等就行了,不知道的如果遇到了可以到网上查一下,能查到的。但是状态码和状态码描述符可能对不上,不必担心这一点,因为没有一个绝对的标准,不同公司浏览器标准可能不相同,这是因为早年的各个浏览器公司竞争的时候导致的,谁也不服谁,不同公司定的标准也就可能不一样了。
重点讲一下这里的重定向状态码。
挑出来三个说说:301(Moved Permanently,永久移动),302(Found,临时移动),307(Temporary Redirect,临时重定向)。
这三个相对来说重要一点。301的永久移动,也可说是永久重定向;图中302和307的描述一样,就当做一个来看吧,都看作临时重定向。
那么什么是永久重定向和临时重定向呢?
举个生活中的例子。
比如你学校东门,有一家川菜馆,里面的菜很好吃,生意挺好的,但是有一天学校东门的马路开始施工了,为期大约三个月,此时饭馆老板觉得施工会影响就餐环境,所以决定先临时将店面搬到北门,此时搬走了后需要在原来东门饭店门口贴一张告示,先告诉同学们饭店搬到北门了,此时可以有两种情况:
那么第二种情况可以在干了一个月后将原先东门门口的告示改改,改成本店由于业务需要已移至北门,此店已关闭,以后想要在本店就餐的同学去北门即可。但是第一种就不需要了,干完接着回东门。
针对这两种情况,顾客在三个月后的访问策略也是不同的,三个月后:
那么这里的第一种情况就是临时重定向,不会影响用户的后续请求策略,该去东门的还是去东门,而永久重定向会影响后续请求策略,想去原先东门饭店吃饭的必须去北门。
再说回来http:
临时重定向:浏览器不会缓存当前域名的解析记录
永久重定向:浏览器会缓存永久重定向的DNS解析记录。
意思就是你前后两次登录的页面是否会相同,比如说你登录一个网站,进入到登录的页面输入完后就会进入主页面,但是下一次登陆的时候还是登录页面,不会说直接是已经登录的主页,除非你选了什么自动登录。再比如说书签,你看小说看到哪了都会有一个书签,你这次从第十五页开始看,看到了第36页后退出,浏览器就会记录下来你现在读到了36页,就会添加一个书签(重定向,更新了一下缓存),然后下一次你再次打开小说网站开始看时就是从第36页开始。
再来拿出刚刚我用OPTIONS方法获取到百度服务器发来的内容:
我把里面的Location框柱了,这里其实就是重定向的网址,进入看看:
就是这个。
那么我也可以给我的服务器中添加一个重定向的功能,就直接重定向到csdn的官网吧:
这里就是如果用web目录打开一个不存在的文件,不返回404,而是重定向一下,直接重定向到csdn的官网:
搞了个动图,有点糊,但是能演示出来:
我来专门搞一个404的网页,如果没找到就跳转到这个网页中:
整理重定向可以用这里相对的路径(加不加./都可),这里是相对于根目录的路径。也可以直接给个URL,跟刚刚重定向CSDN的一样:
都可以重定向到这个页面:
看看服务器接收到的:
其实中间应该再加上这个:
也就是服务器发给浏览器的。
整个流程大致就是这样:
来看看永久重定向:
也能重定向成功:
服务器接收:
中间也是这个:
但是可惜我这里服务器中没有写对应的代码逻辑,所以永久重定向演示不出来。重新进入登录页面还是会让登录。就不多说了。
其实刚刚在细节那里讲了一部分。
就下面这些:
Content-Type: 数据类型(text/html等)
Content-Length: Body的长度
Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
User-Agent: 声明用户的操作系统和浏览器版本信息;
referer: 当前页面是从哪个页面跳转过来的;
location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
常见的报头就这些。
比如说第二个Content-Length,刚刚已经说过了,就是正文长度。我也可以在服务器代码中添加上:
但是浏览器上也看不到浏览器接受了啥:
用telnet测试能看到:
再来说Content-Type,这个是数据类型的意思,也就是正文数据类型。正文数据也是分类的,服务器返回一个资源,这个资源可以是这里的网页,也可是图片、视频、音频等等,这些资源都是文件,但是在不同类型的文件都有着不同的后缀。服务端在接收到客户端的请求后,会根据所请求的文件的后缀来确定相应的正文类型,这里的网页后缀就是.html,其对应的正文类型就是text/html,代码中加上这个Content-Type:
就不看网页了,还是那个页面,也看不到接收到的字段,还是用telnet看:
成功。
正文类型,也可以查出来,这里给一个链接:常见 content-type对应表。
比如里面:
如果说我想让浏览器以纯文本的形式获取服务端发送的数据,那我就将Contest_Type改成test/plain就行,就是上面的第三格:
然后获取浏览器解析出来的就是纯文本,所以就会产生下面的页面:
这个字段中包含有客户端浏览器和os的信息,有什么用呢?
比如说你现在百度一下微信,百度就会根据你请求报文中的User-Agent字段来给你推荐微信是PC端的还是移动端的,是Windows的还是Mac的:
不同的设备首个推送的不一样。
这个字段是获取当前页面是由哪个页面跳转来的:
比如刚刚获取一个不存在的页面,就会跳转到404.html页面,是点了登录之后进行重定向的,所以就会返回登录时候的页面,也就是主页面。
剩下的就cookie字段得要详细讲讲。
报头中的cookie字段: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能。
初学者肯定听不懂,先不说的这么高深。
讲之前得先说点别的。
http协议的特点有什么?
说一下第三点:
http不会记录用户是否登陆过某网页,但是实际上我们在使用的时候一般网站会记录下我的登录状态,当我登陆了之后关闭浏览器再次进入某个网页并不会让我进行登录操作,而是直接进入登录状态下的主页面。
比如说京东登录页面和主页面是两个网页,首次进入的时候登录就行了,后面再次进入就不需要再输入密码啥的登录了。但这里说http不会记录用户是否登陆过某网页,那么当登录后主页面是如何得知当前是否为登录状态呢?
HTTP不记录登录状态,并不代表客户端/浏览器不会提供这样的服务。HTTP只是关心你要传什么资源,资源大小是什么等等这样的数据,将这些数据在服务端和客户端之间快速转发才是它的任务,与其说HTTP是一个超文本传输协议,不如说就是一个文件传输协议,超文本就是能传的类型多一点(如视频、图片啥的),所以HTTP只要负责网络功能就行。至于浏览器画面怎么渲染,用户是否登录,并不是协议操心的。所以说是浏览器/客户端记录的,并不是协议本身记录的。
客户端/浏览器在发送登录请求后,服务器进行用户名、密码相关的认证,认证结束后就将同样的信息(用户名、密码)返回到客户端/浏览器,而浏览器(后面就只说浏览器了)会通过文件保存这些数据,而保存这些数据的文件就是Cookie文件。Cookie可以是本地磁盘文件,也可以是内存文件(暂时)。
如果是内存文件的话,登录后跳转到相关网站页面就会保存你的登录状态(不关闭所有浏览器的前提下),如果关闭所有的浏览器再次进入相关页面还要重新登录。
如果是本地文件的话,下次登录的之后就不用再登录了,点击链接就是登录的主页面。不过有一个过期时间,比如说半个月、一个月等等。
如果没有过期时间就是内存级Cookie文件,如果有过期时间就是本地文件。
当有了Cookie文件后,后续登录的时候就直接按照Cookie文件中用户曾经输入过的用户名和密码来进行自动登录验证,不需要再让用户手动输入用户的用户名和密码了。
整个流程大致如下:
那么客户端是如何把信息提交给服务器的,服务端又是如何将信息返回给客户端的呢?
就是通过HTTP报头中的Cookie和Set-Cookie来实现的。
Cookie是客户端发给服务端。Set-Cookie是服务端发给客户端。
浏览器URL左边有一个锁,点击这个锁就可以看到Cookie文件,我这里用B站演示一下:
点击后显示出:
这里Cookie中有很多文件,因为一个公司可能不止一台服务器,在进行网页的访问时可能会在服务器间来回跳转。
其中这些Cookie文件就包含了我的登录信息(加密后的),如果我选中这些文件并删除,那么我再次打开B站就要重新登录了。
但是如果直接用Cookie文件保存明文的用户名、密码登私密信息的做法有点危险,用户大多都是小白,不懂这方面的知识,如果不小心下载了木马,黑客就可以通过Cookie文件获取到用户的私密信息,比如说用户名和密码等等:
非常危险。
所以得改改,目前主流的行为是客户端首次发起登录请求后,报文带着用户名、密码到服务端进行验证,验证通过后,会在服务端生成一个与用户数据相关的唯一ID(session ID),然后再到服务端响应的报文中就带着这个唯一ID还有session文件的到期时间等信息返回给客户端,私密信息全部保存在服务端这边,由专业的安全人员进行保护,这样比第一种方法更安全一点。当浏览器的cookie文件中有了session ID后下一次再进行登录请求时服务端就会通过session ID进行验证。
整个流程就是这样:
但是很遗憾,安全是相对的,如果用户还是下载了木马,还是会被黑客盗走Cookie文件中的数据,这样黑客还是能通过session ID来以用户身份进行登录。不过好在这里用户的私密信息是放在服务端的,黑客只能拿走Cookie中的session ID。不过这种情况也有应对策略,如当一个用户的ID在短暂时间内发生改变,比如说一个河南的用户账号被一个荷兰的黑客盗走了,能盗走的也只是session ID,而且此时用户IP是在短暂的几秒钟就发生改变的,坐火箭也没这么快,所以此时服务端就可以做出判断,直接让session ID失效,并且给河南的用户发一条短信,此时黑客和用户就都登不上了,只要用户改一下密码就行,密码一改原先服务端的验证就过不了了,只能用新的用户名和密码登录。
刚刚说了Cookie不光是在浏览器中有,客户端也有的,想一想你手机上的qq上一次手动输入账号和密码是啥时候,应该很久了吧。
一般情况下,本地磁盘上的Cookie相对来说更容易被盗取,因为它们存储在硬盘上的文件中,可以被恶意软件或黑客轻易地获取。而内存中的Cookie虽然也存在被攻击的风险,但相对来说更加安全,因为它们只存在于计算机的内存中,不会像硬盘上的Cookie那样长期存储。此外,现代操作系统和浏览器都采取了一系列措施来保护内存中的Cookie,例如使用加密算法、限制访问权限等。
所以说不要随便点击什么链接,指不定一点啥东西就泄漏了,如果想要下载软件就到正版官网去下载,不要在非正规渠道搞事。
一般被骗钱是本人转的,而不是黑客直接单方面操作的,现在涉及到金钱的东西,认证还是很严格的,大部分是被骗了但不知道自己被骗了,从头到尾都是本人在进行相关操作的。
只要在HttpResponse中添加一个Cookie字段就行了。
此时服务端就可以给客户端发送这个Cookie了。
浏览器链接上后:
就OK了。
这里到期写的是浏览会话结束时,也就是说我浏览器关了Cookie就没了,所以这里是一个内存级的Cookie文件。
同时浏览器也会给客户端发回一个Cookie:
因为这里又一次请求了网页资源。
我这里写的这点代码可算不上会话管理,得添加一些逻辑,比如登录要分两种验证方式(输入密码验证和session id验证)等等。
再说刚刚的B站中的Cookie文件,其中一定是有一个session ID的,如果删掉那个session ID也就不能登陆了。
最后再说一个报头中的Connection。
开始那里说过了,Connection是一个表示长连接还是短连接的。
Connection: keep-alive就是长连接。
Connection: close就是短连接。
长连接和短连接是网络编程中使用的两种不同的通信方式。
长连接是指在客户端和服务器之间建立一个持久化的连接,可以在任何时间点进行数据传输和接收,而不需要每次都进行连接和断开。这种方式的优点是稳定性高、可靠性强、数据传输准确无误,但缺点是消耗资源较多。
短连接则相反,是在客户端和服务器之间建立一次短暂的连接,完成一次通信后就会立即关闭连接。在短连接中,客户端和服务器需要每次都进行连接、数据传输和关闭,没有持久化的连接。例如,浏览器中的HTTP请求就是一种短连接。
每次进行连接的时候都要经过TCP的三次握手,断开的时候还要经过四次挥手,等等会有时间消耗。所以读取多的操作最好是从头到尾连接一次,就用长连接,不然老是连上断开又连又断,耗时。
只需要读取一次就用短连接,不然长连接会占着资源不放。
总的来说,长连接适用于需要频繁通信的场景,而短连接适用于一次性通信的场景。
介绍两个工具,一个是Postman,一个是Fiddler。
Postman能向目标服务器构建http请求,而且能选择是POST还是GET,send后就能发送,还能获取到服务器发过来的的报文:
Fiddler是一个抓包的工具,能够抓到http请求的报文,也能够抓到服务端响应的报文。甚至账号、密码等私密数据也能抓出来:
其实这里Fiddler原理很简单。
我们没有开Fiddler的时候会直接发到对端,当打开Fiddler时,不管是报文从本机中出去还是从服务端进来都要经过Fiddler,所以Fiddler就能得到所有的HTTP请求,还有服务端的响应,也就是下图:
都是经过它的,那么它就肯定能抓到喽。
所以说HTTP是不安全的,安全的是下一篇要讲的HTTPS。
到此结束。。。