目录
一、http协议含义
在上一篇文章协议定制中,我们自己写了一份简单的协议定制。但是写起来也是比较麻烦的。虽然实际中存在着各种各样的场景,但也存在一些常见的场景,那么有没有人针对这些常见的场景,写出一份在这些常见下通用的协议软件呢?答案是有的,其中最典型的就是http/https协议。其中http协议就是指“超文本传输协议”。
在这些协议里面,都需要包含通信连接、协议定制、序列化反序列化等各项内容。只不过它做的比较隐蔽,大家不太容易发现罢了。
二、 认识URL
URL,其实就是大家日常所说的网址。在一个网址中,一般最少都会包含三个内容,即采用的协议、域名和文件路径:
这里的https,指的就是采用https协议;域名是为了方便看的,这个域名会被解析为一个ip地址,标识一台linux机器;而后面的路径则是对应的数据在该机器下保存的路径。在访问网址时,就是拿着这个字符串,到对应的主机上的对应路径上拉取数据呈现给用户。
那此时有人就会感到奇怪了,因为大家知道,虽然可以用ip标识一台主机,但是访问主机时是需要通过ip + port访问的啊,为什么这里没有端口号呢?其实是有的,只是在网址中一般将这个端口号忽略了,因为有些端口号是众所周知的,是与它所提供的服务强相关的。例如在浏览器里面,浏览器结合你所使用的协议就知道你要访问的端口号,在http中这个端口号一般是80,而https中这个端口号则一般是443。
那么上面的“/”是什么呢?problemset后面的“/”大家应该都知道,就是“路径分隔符”。那么域名后面的"/"呢?对于这个目录,大家不能将其简单的理解为linux下的根目录。因为它其实是“web根目录”,一般而言,它可以是linux下的任意一个目录。
那么为什么网址后面会有文件路径呢?其实是因为大家在网络中看到的一切东西,其实都是“资源文件”。既然是资源文件,那么就必须要被保存起来,因此这些资源就被保存在对应的服务器的磁盘上,而要对这些资源进行管理,也就需要有文件结构。因此,当我们要拿取资源时,也就需要通过对应的文件路径寻找对应的资源了。
在上文中说了hhtp协议其实就叫做“超文本传输协议”。其原因就是文件资源有非常多的种类,而http协议可以解决所有文件资源的传输问题,所以就被叫做“超文本传输协议”。
三、urlencode与urldecode
在上文中说了,url中存在一些特殊字符被url当做特殊意义理解,例如=、:、/、?等字符。但如果我们的搜索内容中就出现了这些特殊字符,http如何对这些参数进行区分呢?其实很简单,当某个参数中需要带有这些特殊字符的时候,会先对这些特殊字符进行转义。
1. urlencode
转义的规则很简单,就是将需要转码的字符转为16进制,然后从右到左,取四位(不足4位直接处理),每2位看做一位,再在前面加上%,编码成%XY格式。
为了看到这一现象,大家可以在自己的浏览器中搜索如带有/、?等特殊字符的内容。例如我现在在浏览器中搜索“abc?=/”,然后将它的网址拿取出来:
不同浏览器上得到的内容可能有所不同,但其核心内容基本都是一致的。例如上面的这个网址,我在bing上搜索就是如上内容。在这里面?其实就是一个分隔符,?后面就是你发给远端服务器进行搜索的内容。有人可能会很奇怪,明明只是搜索“abc?=/”,怎么这里面会多出这么多东西。其实是因为在搜索时,浏览器会将要搜索的数据发给服务器,而服务器所需要的参数并不仅仅是你要搜索什么,还包括其他各种参数。
在上面的网址中,虽然后面的很多人我们看不懂,但是里面有一个内容大家应该是能看懂的:
上图中圈出来的部分,前面的“abc”就是我们搜索的内容的前缀,而后面的%3F、%3D和%2F刚刚好是三个参数,不就刚好对应我们搜索的内容中所带有的特殊字符么。这也就印证了上面说的,如果url请求中包含了一些特殊字符,那么这些特殊字符就会被通过特定的方式进行编码,以避免干扰对url的解析。
2. urldecode
通过上面的例子可以发现,虽然http中会对url请求中的特殊字符进行编码,但是如果不是特殊字符,就原封不动的填充上去。
那这里就有一个问题,虽然普通内容可以直接使用,但特殊字符是被编码过的,是无法直接使用的。因此,在服务器收到url请求后,就需要自己对这些数据进行解码然后再使用。这一过程就和序列化与反序列化一样,发送端将处理过的数据发送给接收端,接收端在使用这些数据之前都需要先对这份数据进行解析,拿到有效数据。
上面所说的编码和解码过程一般是不需要自己写的,一方面是大家使用的语言会自行处理,另一方面是网络上存在很多的encode源码,需要时直接拿过来用就行了。
四、http协议响应与请求格式
1. http协议请求格式
http请求是按行来划分的。而这些行又可以分为四块。
第一块一般只有一行,其中包括http请求的方法,请求的url和请求的http版本。常见的http版本有1.0、1.1等版本。这一块一般叫做“请求行”。
第二块则包含多行,里面包含了http请求的各种属性,例如目标主机、浏览器版本、链接方式等内容。一般是“name: value\r\n”的格式。这一块一般叫做“请求报头”。
第三块则只有"\r\n",被称为“空行”。
第四块中保存的就是用户要向服务器提交的参数。这一块被叫做“请求正文”。请求正文和上面的三个部分有所不同,上面的三个部分是在http请求中是必须存在内容的,而请求正文部分却可以为空。基本格式如下图:
2.http协议响应格式
http协议的相应格式也分为四个板块。
第一个板块中也是一行,其中包括http版本、状态码和状态码描述,也是一行。被叫做“状态行”。这里出现了“状态码”,大家可能不太明白是什么东西。状态码,简单来讲就是表明你这次请求的结果是否正确,常见的有200、400、302、307、500等。
这些状态码大家可能没有见过,但是还有一个常见的状态码404,想必大家都见过。当我们访问了错误的不存在的页面是,就会看到404这个状态码。当然,有些时候大家可能并看不到这个状态码,而是出现一些其他内容来告诉你这个页面不存在,那是因为这些页面都是经过处理的。而状态码描述就是描述状态码表示的含义的,例如404的状态码描述就是“Not Found”。
第二个板块的内容和http请求一样,也是由多行内容构成的,格式也是“name: value\r\n”。它里面的内容和http请求中请求报头中的很多内容都一样,但是也存在一些独有的内容。这个板块被称为“响应报头”。
第三个板块是由“\r\n”组成,被称为“空行”。
第四个板块中则是服务器向客户端返回的资源,其中包括html/css/js等资源,甚至还可能存在图片、视频等资源。这些资源会由浏览器进行解释然后呈现给用户。这一块的内容就被称为“响应正文”。
整体格式可以看成下图所示:
3. http请求实际形式
3.1 程序准备
上面说了http请求和http响应的格式,但是我们仅仅知道格式,并没有看到它的实际模样是怎么样的。因此在这里, 我们就可以写一个简单的程序来获取它的http请求,实际看一下它们的样子。
为方便测试,这里就直接使用在前几章写好的服务端程序,在里面添加部分内容来得到对应的数据。
首先准备好一个服务端,然后在这个服务端中写一个handlerHttp函数,该函数用于解析命令。
正常来讲,在这个函数中首先需要确保获取一个完整的请求,然后对这个请求反序列化并执行对应任务,最后将执行完任务所得到的数据序列化并发送给客户端。但这些步骤在上一章协议控制中已经讲解过了,这里就不再做了,而是假设该服务端收到的就是一个完整的请求,将这个请求直接打印出来。
有了上面这个函数后,准备httpRequest和httpResponse两个类,这两个类可以用来存储请求和响应的内容。
在这两个类中可以定义不同的成员变量来获取请求和响应的不同内容,这里也就不这么做了,而是将数据直接存放到一个缓冲区内。
最后,可以再准备一个回调函数,用于表示该请求要执行的任务,在目前并没有什么任务需要执行,所以可以在这个函数中将请求的内容直接打印出来:
做好这些准备工作后,就可以向该服务端发起请求了。
3.2 浏览器发起请求
有人可能会奇怪,这里怎么只有服务端程序,而没有客户端程序呢?其实是因为在这个场景下没必要写客户端,我们的浏览器天然就是一个可以访问不同服务端的客户端,因此直接将浏览器当做客户端,然后向服务端发起请求即可。
连接方法也很简单,直接在浏览器的网址中输入你的服务器的公网ip+port即可:
如果你输入ip和port后却无法访问或显示访问超时,那么大概率就是你的云服务器或轻量级服务器没有打开对应的端口,公网ip对外是拒绝访问的。出现这种情况只需要去你购买服务器的官网下打开端口即可。
如果可以访问,就会出现如下反馈:
原因很简单,因为在服务端的程序中我们并没有向客户端返回数据,而仅仅是打印了请求。此时再看linux中的服务端,就可以看到如下内容:
这其实就是一份完整的http请求。至于上面的start和end则是我们自己写的,不用管。
3.3 请求行内容
首先来看请求行。根据上面的请求格式可知,这份请求中的第一行就是“请求行”。里面包括请求方法,url和协议版本:
在这里面的"GET"就是请求方法,浏览器中默认的请求方法都是GET的。
“/”就是url,是一个web根目录。至于这里是根目录的原因很简单,因为在这次请求中,我们并没有指定要请求哪个资源,此时,web server会返回一个默认的首页。例如我们打开b站,此时我们并没有向b站中请求指定资源,此时返回的就是b站的首页:
“HTTP/1.1”是协议版本,表示使用的是http1.1版本。那么为什么请求行中会包括http协议版本呢?其实是因为在后续客户端与服务端交互时,http请求会交换双方的协议版本。以b站为例,假设b站有1亿用户,这一亿用户手中的客户端原本都是1.0版本的。但是在某一天b站要更新协议版本为1.1。但是在客户收到更新通知时,并不是每一个用户都会立刻升级,那有些人就是不想升级客户端。假设在这1亿人中有5千万人升级了客户端,其他人没有升级,那么这些没有升级的人就无法使用新客户端的功能。此时就需要有这个协议版本区分新老客户端,让新客户端的人使用其中的新功能,让老客户端的人使用老客户端原有的功能而不接触到新功能。
3.4 请求报头内容
在请求行下面的内容就是请求报头。
其中第一行Host,表示的就是这份请求要发给哪一个服务端。有人可能会奇怪,这份请求不是直接服务端了么,为什么还要带这个内容。这是因为http请求其实是可能被代理的,就是说在某些情况下,这份请求可能不会直接由指定的服务端接收,而是让其他服务端接收,接收到该请求的服务端再将请求发送给指定服务端。简单来说就是在指定的客户端和服务端之间存在中转站。
第二行的Connection中是“keep-alive”,表示支持长链接,这里不再多说。
第三行表示协议升级,这里的升级并不是指协议版本升级。大家应该知道,http协议是cs或bs模式的,是基于请求和响应的。这就意味着只有当客户端主动向服务端发起请求,服务端才会对客户端发起响应。但是现实中存在这么一种情况,那就是客户端什么都没有做,而服务端主动向客户端发消息。当然,这个部分大家不用关心,未来很少会用到。
第四行"User-Agent",表示就是客户端的信息。
例如里面的Windows后面的内容,就表示客户端是一台windows主机,版本是win64和x64的。再后面还有一个Chrome,表示我们使用的浏览器就是Chrome的。
那么这个信息有什么用呢?以主机为例。在这里使用的主机是windows的,所以当我们在搜索一些软件,如微信时,它默认的下载链接就是windows的:
如果你用你的安卓手机去搜索,那么它的下载链接就应该默认是安卓版的:
这个功能其实就是靠保存的客户端信息完成的。
第五行的“Accept”中是支持的资源格式,如html、xml等。
第六行的Accept-Encoding指的是支持压缩。
最后一行则是支持的编码,zh-CN和enUS就分别指中文编码和英文编码。
至于空行和请求正文就不再多说。
4. http响应实际格式
http响应是需要由我们自己来构建的。在这里,我们就可以自己形成响应行、响应报文、空行和响应报文。在这里的响应报文,我们就可以直接返回一个网页。
要实现一个简单的网页是很简单的,大家可以去搜索“w3cschool html教程”,里面就有如何编写网页。这里不再多讲。因为这不是我们的主要任务。
修改Get函数中的内容如下:
bool Get(const httpRequest &req, httpResponse &resp)
{
std::cout << "--------------------------http start--------------------------------" << std::endl;
std::cout << req._inbuffer;
std::cout << "---------------------------http end---------------------------------" << std::endl;
std::string respline = "HTTP/1.1 200 OK\r\n";//响应行
std::string respheader = "Content-Type: text/html\r\n";
std::string respblank = "\r\n";//空行
std::string body = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"<<title>我的首页</title></head><body><h1>我是网站的首页<h1></body></html>";
resp._outbuffer += respline;
resp._outbuffer += respheader;
resp._outbuffer += respblank;
resp._outbuffer += body;
return true;
}
在上面的代码中,respheader中的内容是告诉浏览器,服务端返回的是什么资源。而body中的内容就是一个网页里面的内容。
准备好上面的代码后,就可以使用telnet工具了。该工具可以用于远端登录。使用起来也很简单,输入“telnet ip port”命令远端登录:
出现上述界面说明登录成功。然后按下“ctrl ]”组合键,再按下enter,此时就可以准备拿取响应了。输入“GET / HTTP/1.1”:
此时再在你的浏览器下访问你的服务端就可以得到如下内容:
此时就成功的向服务端返回了一个网页。
5. 服务器与网页分离
在上面的程序中,虽然我们向浏览器返回了一个网页,但是这个网页的内容是通过字符串构建的。看起来非常杂乱且不美观。因此,实际上的网页并不是这样生成的,而是要专门建一个web根目录,在这个根目录下保存网页文件和相关资源。
6. web根目录
在上文中说过,url就是要从哪个路径下拉取资源,如果url为“/”就表示从web根目录下拉取资源。那么这个web根目录究竟是什么呢?有人可能以为它就是linux的根目录,其实这个理解是错误的。web根目录就是一个wwwroot目录,该目录下就保存了各类网页文件和各种资源。web根目录其实可以是任意目录,因为可以通过配置文件修改web根目录指向的位置。
五、实现一个简单的网页
有了上面的知识,再结合已经写好的代码, 就可以写一个简单的网页了。因为我们的主要目标并不是学习如何制作网页,所以这里就只是制作一点最基础最简单网页用于返回。
1. 准备web根目录和默认主页
因为在这个程序里面,当浏览器发起请求后,我们首先需要有一个web根目录,然后返回一个默认主页。因此创建web根目录和默认主页。同时,为了防止访问错误的路径,再准备一个404网页用以应对:
如果不想自己慢慢写,可以在网页文件中直接输入“!”然后按下enter键,编译器会自动添加形成网页的基本内容,我们只需要修改title中的内容和添加正文即可。
2. 获取请求行和url
前文说过,请求中的url就是要从哪个路径下获取资源。因此,我们要能够拿到这个url。
首先写一个getline函数用于获取请求中的请求行。因为在这个程序里面我们已经将请求内容存放进了httprequest类中的_inbuffer缓冲区,所以直接从这个缓冲区中读取数据,再按照分隔符拿到第一行即可:
再先写一个parse函数,用于拿到请求行和解析出里面的内容:
在这里使用了<sstream>头文件中的stringstream类,该类可以将传入的字符串按照空格进行切分。当然,在这之前还需要在httprequest类中添加几个成员函数:
为了方便看到是否成功,大家可以在Get函数中添加如下打印内容:
运行服务端,然后从浏览器向服务端发起请求:
可以看到,成功拿到了我们想要的数据。有人可能会感到奇怪,明明在这里只是访问了首页,为什么这里的url却有“favicon.ico”字段呢?其实这就是一个图标。就是在网页名字旁边的那个图标:
上面的程序中,仅仅只是访问了首页,但如果未来我们想访问其他页面呢?所以我们还要添加web默认路径。在这个默认路径中,获取到浏览器要访问的路径,并去拉取资源:
3. 读取网页内容
网页准备好后,就需要让程序能将网页中的资源读走。因此,写一个readFile函数用于读取文件。读取方式很简单,直接按行读取就行了。
readFile函数准备好后,就修改Get函数,让其获取文件内容:
此时,就可以进行测试了。在浏览器中访问你的服务端:
可以正常运行。然后再访问一个不存在的路径:
此时就返回的是404页面。
4. 页面跳转
在其他网页中,大家都应该见过点击一个字符,然后就跳转到另一个页面的情况。其实其本质就是从一个web目录中跳转到另一个web目录中拉取资源。在这里也可以实现这一功能。
先在web根目录下创建一个test目录,再在这个目录里面创建两个html文件:
然后再在首页中添加如下内容:
运行服务端,从浏览器发起请求:
此时就会出现如上页面。点击图片和视频:
此时就实现了页面跳转的功能。
如果想返回首页,在a.html和b.html中也添加上面的字段,然后将路径修改为"/"即可:
5. 上传图片
在访问别人的网页时,很多网页中都是有图片的,我们也可以尝试给我们的网页中上传图片。
注意,在网页中,一个用户看到的网页结果可能是多个资源组合而成。简单来讲,假设你的这个网页中有10张图片,那么这个网页就需要将这10张图片从服务端全部一一加载过来。因此,要获取一张完整的网页效果,我们的浏览器一定会发起多次http请求。这也就意味着一旦网页中的资源过多,那么打开这个网页的速度就会被拖慢。所以,在网页中不宜放过多过大的资源。
首先可以创建一个“image”文件夹,准备一张照片,将该照片放到对应的目录下:
然后再在默认首页中将图片添加进去:
此时就已经可以加载图片了。但是,由于图片的格式与html的不同,所以最好添加对应的Content-Type。由于这里存在html文件和png文件,所以可以写一个suffixToDesc函数来根据传输资源的后缀选择不同的Content-Type:
由于这里需要用到后缀来识别资源,所以可以在httprequest类中再添加一个_suffix变量来保存后缀:
然后再在类内函数parse中获取后缀:
当然,因为需要传输图片资源,我们最好也带上一个“Content-Length”字段将正文长度填入到响应中:
在这之后还有很重要的一步,那就是修改readFile函数。在readFile函数中,是用getline函数来获取文件内容的。但是要注意,getline只能获取文本内容,而图片资源是二进制的,如果继续用getline函数获取,就会出现错误。因此,要修改readFile函数人,让它以二进制的方式读取数据:
在传参时,要将缓冲区的大小传入。这里需要提前开辟空间,开辟的空间是文件大小+1,为了防止出现文件填满,没有位置填充\0的情况。
做好这些工作,就可以运行服务端,然后用浏览器发起请求了:
但是大家可能会出现浏览器图片加载不出来的情况。这可能是因为图片的占用有点大,也可能是因为vscode本身资源占用就大,无法返回图片资源等原因造成的。这种情况不用担心,因为当前我们并不是学网页制作,而是了解一下如何制作网页,可以看到有加载图片的情况就行,不必过多纠结。
如果确实想看到图片加载出来的场景,可以到网上找一张图片,复制它的图片链接进行访问:
通过这种方式获取图片大概率可以成功。
六、http请求方法
在上面的程序中,用的一直都是GET方法,但是http请求的方法其实并不止这一种,一共存在三种方法。
1. 获取资源
获取资源的请求方法想必不用再多说了,在上面的程序中一直使用的GET方法就是获取资源的方法。
2. 上传资源
获取资源的GET方法大家已经了解了,那么如果是想上传资源呢?
要上传资源,就需要使用到“from表单”,浏览器会自动将from表单中的内容转换GET/POST方法请求。例如我们在登录一些网站的时候,就需要在一个输入框中输入你的账号和密码,这其实就是在向服务端上传资源,使用的方法也是form表单。大家可以在你的浏览器上找一个登陆页面,然后打开你的浏览器的开发者工具,上面就有详细的http代码:
制作形式如下:
为了更方便的看到form表单的效果,我们也可以自行制作一个简单的form表单。如果不会写,可以直接到w3cschool上直接复制:
上面的代码中,input type表示要上传的资源的类型;name表示表格名字,因为在服务端提取参数的时候,大家可以看成是以kv形式提取的,所以就要有着个name来对应这个表格;value是预设内容。
这里面的action就是你要将这份提取出来的数据提交到哪里,可以填某个服务,也可以填某个url,甚至可以填一个不存在的路径。当然,你也可以在action后面加上method,用于指定用哪种方法上传资源。
在这里,就使用这份复制过来的代码,修改一下填充的内容即可:
用浏览器访问该服务端:
可以看到,此时就成功显示出了一个登陆界面。这里的密码中显示“·”的原因是,在这个form表单中的密码的input type中填的是password,这个字段会在显示form表单时将对应表格中的内容隐藏。
3. GET和POST方法的区别
3.1 GET方法的提取
在上面的程序中,我们用的是GET方法获取浏览器中上传的资源,但是上文中说了,POST也可以用于获取资源。那么这两个方法有什么区别呢?
大家可以在这个表单中随便填数据,然后点击登录。再去看一下跳转的网址:
在当前程序中,我们使用的是GET方法获取,当点击登录后跳转到了404页面,这是因为我在form表单中设置的提交位置是不存在的。这不重要,重要的是大家可以看看网址里面的内容,里面直接将我们刚刚在form表单中填写的数据显示在的网址里面。
然后再来看程序打印的http请求:
可以看到,在打印的http请求中,url里面填充的内容就是我们刚刚输入的账号和密码。这里显示特殊字符的原因是账号里面填写的是中文字符,中文字符会被解析然后替换成特殊字符。
3.2 POST方法的提取
为了看到GET与POST的区别,我们将form表单的提取方法修改为POST:
再次从浏览器发起请求:
在更换POST方法再去登录后可以看到,在网址中不再有我们刚刚填的内容。再来看一下程序打印的http请求:
可以看到,与GET将数据填充到url中不同,POST是直接将浏览器中提取出来的数据当做了正文来处理
3.3 区别
通过上面的实例就可以知道,GET和POST方法的区别很简单,就是在在浏览器提取数据将数据传输给服务端时,GET是通过url传递参数;POST方法则是通过正文传递参数。
因此,通过POST方法提交的数据,用户一般是看不到的,私密性更好。但是要注意,这里的私密性并不等于安全性。虽然POST方法提交的数据用户无法看懂,但是在网络中,通过GET和POST方法提交的数据的安全性都是没有保障的,要保障安全,还需要通过其他手段进行加密。
GET方法的url传参除了没有私密性以外,还有一个坏处。那就是通过url传参,势必导致这个参数不能太大太长。例如上传一张照片,这些照片都是二进制的,如果用url传参,就会导致url非常的长,可读性很差,而且还可能出现一些其他问题。而POST方法的正文传参则没有这个忧虑。
3.4 url的作用
看到这里,不知道大家有没有一个疑惑,那就是url到底可以用来做什么。在获取资源时好说,url就是一个文件或一份资源的路径,url就是用来标识要获取哪个文件或资源的。除了标识文件或资源的路径,其实它还有一个作用,就是选择某项服务。
例如网页中的搜索:
在域名后面可以看到“/search”,这个就可以看成表示某种服务。因为大家知道,在请求行到达服务端后,是需要解析出url的,那么在解析出url后就可以根据url的内容选择去执行某项特定的服务:
更甚至,我们可以将制定的url与执行方法填入到unordered_map中。当有请求到达时,就直接拿着url到这个unordered_map中找对应的方法并执行。
3.5 其他方法
http中除了GET和POST,其实还有其他方法:
但是这些方法基本很少用到,所以也就不再多讲。
七、http状态码
1. 普通的状态码
http状态码一般分为5个大类,分别是1~5开头的,表示的含义各不相同,但总的来讲是某一个大类的情况。
以1开头的状态码,就是表示你的请求正在被服务器处理,但是可能需要一段时间。例如你要从远端服务器下载一些数据,这些数据可能需要一定的时间,此时服务端就可能会给你返回一个1开头的状态码,告诉你服务器正在处理该请求。
以2开头的状态码就不多说了,就是表示你的请求被正常接收并处理了,例如在上面的程序中的响应报头里面就一直是填的200,表示运行正常。
以4开头的是客户端错误。例如我们在京东中访问一个不存在的web路径,此时就是客户端访问的路径有问题,和服务端无关,返回了404。
以5开头的状态码表示服务器错误。大家知道,客户端连接服务端之后是需要执行创建进程或线程,向OS申请空间以完成某些操作等等内容,如果这些内容里面出现问题,例如申请空间失败,这才是服务器错误,会返回一个5开头的状态码。
对于这些状态码大家不用去记,也不用特别关心。因为在某些情况下,程序员写的状态码可能并不会严格遵守状态码的含义,例如5开头的状态码,如果服务器真的出现服务器错误,很多公司也不会返回5开头的状态码,而是返回4开头的状态码。一个是因为将服务器出错直接告诉用户有点丢公司面子,另一个就是为了防止某些人根据返回的状态码对服务器进行恶意攻击。例如有人发现给服务器发送某些数据时服务器会出bug让服务器崩溃,这时就可能有人故意一直发送这类数据攻击服务器。
并且现在的浏览器其实已经非常智能了,浏览器在返回状态码时并不是根据服务器返回的状态码来的,而是根据出现的问题来返回的,因此在绝大多数情况下,浏览器返回的状态码其实与程序员在服务端中设置的状态码无关,只与当前的情况有关。
2. 3开头的状态码
在上文中大家应该发现并没有说3开头的状态码的使用场景。从上面的状态码图中可以知道,3开头的状态码表示重定向。以3开头的状态码很多,例如301、302、307等等,它们都表示重定向。这里并不会一一讲解,因为这不并不是重点。所以在这里就简单介绍两个状态码,301和307。
2.1 重定向的含义
首先来了解一下什么是重定向。在http中的重定向其实就是指“页面跳转”。大家浏览器中访问页面时,应该都遇到过这种情况:你在某个浏览器网站上登录你的账户,这个网页在你登录成功后会重新加载一遍,加载完后就是你登录后的页面。这其实就是重定向。
简单来讲就是,当客户端向服务器发起请求后,服务器在收到这份请求后,会向客户端返回一个响应,在这个响应里面就携带了3开头的状态码,例如301或307,并且还有一个新的url。当客户端接收到这个响应后,客户端通过识别这个状态码就知道需要进行重定向,因此客户端就拿着返回的这个新的url重新向另一个位置发起请求。
2.2 临时重定向
重定向中又分为临时重定向和永久重定向。首先来看永久重定向。
永久重定向,举个例子。假设现在有一个叫“www.a.com”的网站,维护这个网站的公司或团体觉得这个网站写的不太行,里面的很多架构也过时了,因此想废弃掉这个网站使用一个新的叫做“www.b.com”的网站。但是在公司的用户中,有很多人都习惯用老网站,并不想用新网站,此时就可以进行一次永久重定向。即用户还是使用老网站的网址,但是当用户访问这个网址的时候,服务端会返回新的url,客户端接收到后就会自动使用新的url跳转到新的网站中。
在永久重定向中,会更新用户保存的书签,例如大家打开浏览器时在浏览器中显示的网站,在永久重定向中就会更新保存的这些书签:
2.3 临时重定向
临时重定向大家也应该很常见,例如我们在登录某些网站后,要临时跳转到某些页面,这其实就是在进行临时重定向。还有一种就是广告跳转,例如我们打开某些网页后,你一点进去它就会跳转到另一个广告商的网站上,例如在手机上打开b站后有一个开屏广告,你不小心点一下就会跳转到某个产品的网页中。这其实也是临时重定向。
2.4 实现一个重定向
要实现一个重定向其实也很简单。要实现重定向需要使用到“Location”字段。首先将响应行中的状态码修改为307,状态表示修改为“Temporary Redirect”。然后再在响应报头中添加“Location”字段,并在该字段中添加你要重定向到的网址:
重新服务器并用浏览器访问:
访问网址:
此时就直接跳转到了该网页上。
八、长连接
1. 长连接的概念
上文中说过,大家看到的一张网页,其实是由多个资源组成的。这就导致浏览器要生成一张网页,就需要多次向服务器发起http请求拿到对应的资源。
假设一张网页中有100个资源,这些资源都存放在不同的位置,此时浏览器要生成这张网页,就需要向服务器发起100次http请求。同时http是基于tcp的,而tcp是要求建立连接的,这就会导致服务器中需要频繁创建连接,很明显不合理。
针对这一问题,便有了“长连接”。
长连接是需要客户端和服务端都支持的,当打开一张网页时,该网页中的所有资源都是通过这一条连接来传递资源的。同时因为在请求和响应中都是有各自的报头的,报头中就有正文长度,且没份响应也有各自的分隔符。通过这种方式,就可以让浏览器拿到一份完整的响应进而获取资源生成网页了。
注意,这里的长连接只能在一张网页内生效。如果你打开了其他网页,那么这些网页会重新向服务器发起连接请求。即一张网页对应一个长连接。
要看到当前的客户端和服务端是否支持长连接,其实这一标识在http请求中就已经有了:
在请求中有一个Connection字段,当该字段的内容是“keep-alive”时,就表示客户端和服务端都支持长连接。而字段内容是“close”时,则表示不支持长链接。
九、http周边会话保持
1. http周边会话保持概念
首先大家要知道,严格来讲http中是没有会话保持这一概念的。因为http协议的含义是“超文本传输协议”,这就意味着http协议只需要保证资源能够正常传输就可以了。而会话保持这一概念的诞生是在http的长期使用中发现需要才加上的。
在现实中,大家应该都在浏览器上登录过自己的账号,例如在b站看视频的时候,大家可能就会登录自己的账号:
大家在登录一次自己的账号后都会发现,如果把这个页面关闭,乃至把整个浏览器关闭,在你下一次在同一个浏览器下打开同一个界面的时候,你的账号依然是登录状态,并不需要你重登录。虽然这个现象大家已经习以为常了,但是大家有没有想过为什么呢?其实,实现这一功能的就是http周边会话保持。
同时大家要知道,就如上文中所说,http本身是无状态的。即在正常情况下,http不会保留你的登录信息,在你每次打开这些需要登录的页面时,你都需要重新登录。大家可以试想一下,你打开一个视频网站,在这个网站中你每打开一个视频都会打开一个新的网页,而这些新的网页在你打开后都需要你重新登录一遍。这种情况对于用户来说非常的麻烦。
因此,虽然http是无状态的,但是用户需要能够保持自己长期在线。由此便提供了会话保持,让用户一经登录后,就可以在整个网站中随意跳转,按照自己的身份随意访问了。
2. 会话保持的实现方式
2.1 本地保存信息
实现会话保持的第一种方法,就是在本地保存登录信息。
当大家在浏览器上第一次登录某个网站时,浏览器会将你的账号和密码保存在本地。当你下一次打开同一个网页时,浏览器就会自动推送登录信息给服务器。
当你想要访问该网站上的其他网页时,服务器都会做一次身份验证。大家在平时可能对这个身份验证感觉不到,举个例子,假设你有一个腾讯视频的账号,当你想要看一部腾讯视频中的vip电影时,你在浏览器上点击该电影进行网页跳转时,服务器中会自动对你做一次身份验证,判断你是不是vip。这就意味着,凡是访问对网页访问有权限要求的网页时,在登录之前,都会做一次身份认证。
由此,通过上面的客户端保存登录信息推送给服务器和服务器中做身份验证的机制,当我们以后要访问同一个已经登录过的网站时,都会由客户端推送登录信息,服务器进行身份验证以实现自动登录,无需用户重复登录的状态。
2.2 cookie
这种保存用户的账号等信息的技术,就叫做“cookie”。打开你的浏览器,也可以看到cookie:
大家也可以打开自己浏览器的清除数据页面,在这个页面中就会有如下信息:
可以看到,这里有一种数据就是cookie,并且下面提示你清除该数据会导致你从大多数站点退出登录,也就进一步验证了上面说的浏览器会在cookie中保存用户的登录信息了。
cookie的保存又分为文件级别的保存和内存级别的保存。
文件级别的保存,简单来讲就是将你的信息保存在文件中。当你在一个网站登录后这份cookie就会被保存在文件里,因此,就算你将这个浏览器关闭后重新启动并打开同一个网站,那么你依然处于登录状态。
内存级别的保存,简单来讲就是将cookie保存在内存中。大家要知道,浏览器是一个软件,当一个浏览器启动后,必然会生成一个对应的进程。cookie保存在内存中,就意味着你的登录信息是随进程的生命周期的。当你登录一个网站后,如果你将浏览器关闭后重新启动,并打开同一个网站时,你依然需要重新输入账号和密码。例如大家登录自己的学校官网,或者说四六级网站时,如果你不小心将浏览器关闭了,当你重新启动浏览器打开同一个页面时需要你重新登录,就是因为它的cookie是保存在内存中的。
cookie以文件保存还是以内存保存,是可以配置的。对于那些频繁访问的网站,最好以文件保存;对于那些偶尔访问的网站,就可以以内存保存。
2.3 本地保存账号密码的安全问题
如果采用在本地保存账号密码的方式,这就意味着当我们首次登录一个网站时,服务器会验证这个登录信息,如果可以登录,就会将账号信息返回回来,让服务器保存。当你下一次打开同一个网站的时候,浏览器就会自动将账号信息推送给服务器,服务器再进行身份验证并把对应的资源返回回来。
大家这样看可能觉得没有什么问题,但是这里面存在一个严重的网络安全问题。不知道大家知不知道木马是什么东西。这里简单介绍一下,木马和普通的病毒是有区别的。病毒是破坏你的计算机,对你的计算机做暴力修改导致你的计算机崩溃或出现各种问题。木马则不同,木马本身并不是以破坏计算机为目的,而是以窃取计算机中的数据为目的。也就是说,如果你的计算机中了木马,其实你的计算机中并不会出现问题,因为木马并不是要破坏你的计算机,相反,木马更希望你的计算机能够正常运行,因为这样木马才能够从你的计算机中窃取各类数据。
大家可以设想一下,假设浏览器中直接将你的账号和密码保存在本地,如果有一天你的计算机中了木马但你却不知道,那么这个木马就会悄无声息的窃取你的数据,包括你的浏览器中的cookie数据。
如果有不法分子通过这种手段拿到了你的cookie数据,那么这个人只需要将这份cookie数据放到对应的浏览器中并打开对应的页面,服务器在收到浏览器推送的登录信息后就会自动登录你的账号,此时他就可以非法使用你的账号执行各种操作。
同时大家要知道,你的qq和微信等社交软件在你打开后会自动登录的原因和上面是类似的,但如果都是将登录信息保存在了本地,发起请求时软件自动推送登录信息。假设这个木马盗取的不是你的网站账号,而是你的诸如qq、微信等社交工具的账号,就可能出现各种问题。
2.4 远端保存信息
通过上文中大家知道,如果将用户的账号和密码直接保存在本地,就很可能会被不法分子窃取。针对这一问题,便有了一个新的实现方案,即在服务器中保存用户的认证信息。
当一个用户在浏览器上第一次登陆某个网站时,浏览器会将用户的账号和密码传输给服务器,服务器中接收到这份数据后,就会形成一个session文件,在这个session文件中就保存了用户形成的认证信息和一些其他痕迹。但是大家知道,一个服务器中会被大量的客户端请求,要为每一个账户都形成一个session文件,就意味着服务器中必然会存在大量的session文件。因此,服务器中为了区分这些session文件,就会为这些session文件起一个唯一的名称,叫做session id。
当服务器保存好用户的认证信息形成session文件后,服务器就会将session id返回给浏览器,浏览器将这份session id保存起来,未来用户打开同一个网站时,浏览器就会将这个session id发送给服务器,服务器比对session id然后拿到进行身份认证,然后返回对应的数据即可。
通过这种方式,就大大改善了用户信息的泄漏问题。看到这里有人可能就会疑惑了,这仅仅是将账号信息保存在了云端而已,但还是要从浏览器发起请求啊,那些不法分子依然可以通过盗取session id来进行非法访问啊。这个结论其实没有问题,不法分子确实可以通过盗取session id来非法登录。但是这里有一个不同的点,那就是账户信息的保存从个人移交到了企业上。企业与个人的一个大区别就是,企业拥有足够的能力来找出攻击者,进而使用法律来保护自己。而普通人受限于本身的知识水平问题,当被不法分子盗取信息后,是很难找到对方的,更不提维权的事。因此,很多不法分子都并没有那么大的胆量对企业发起攻击盗取用户信息。
另一个大区别就是,企业中保存了大量的用户信息,这些信息都是非常重要的,这也就导致企业中会有配套的网络安全人员对网络安全进行维护。因此不法分子要攻破一个企业的防火墙是很困难的,并不是说像电视剧里面那样敲几下键盘就能攻破的,毕竟企业养的网络安全人员也不是吃干饭的,越是大型的企业其防火墙就越难攻破。
那现在大家可能还有一个问题。上面都仅仅是说不法分子很难突破企业的防火墙直接拿到用户信息,但是他们依然可以盗取个人用户的session id啊,如果这个session id被盗取,那不法分子不依然可以非法登录吗?
这说的也没错,但是要注意,这里不法分子拿到的仅仅是session id,而不是详细的用户信息,这就意味着当他们拿着这个session id去登录时,是需要经过服务器认证鉴权的。在服务器认证之前,其实就可以采取一些其他的措施来防范这些问题。例如以地区为划分,前1分钟你在成都登录,后一分钟你的登录地点就转变为了北京,此时服务器的检测机制就可能判定你的session id泄漏,于是服务器拒绝登录请求,在不法分子这边要求输入账号和密码登录,而在用户那边则强制用户下线重新登录。但不法分子并没有你的账号和密码,只有session id,这也就起到了防止非法登录的效果。而强制用户下线重新登录,其实就是让用户重新发送账号和密码形成一个新的session id以供用户使用,此时不法分子手中的session id也就报废了。当然,实际上的识别方案是很复杂的,并不是向上文说的那么简单,但都是同一个道理。
要注意,上面的问题虽然有很多解决方案,但无论是哪种解决方案,都是无法彻底解决问题的,只能缓解。因为世界上毕竟有这么多的网民,总有人会因为各种奇奇怪怪的原因让不法分子知道自己的账号和密码。同时当前世界上的所有程序其实都是存在漏洞,有被攻破的可能性的。因此,从目前来看,这类问题只能通过各种方法缓解,而无法彻底解决。
大家在未来也最好不要去随意找软件漏洞进行攻击,因为有些软件中会故意放几个漏洞,如果有人通过这些漏洞发起攻击,开发商那边就会收到这次攻击信息,反向溯源追踪攻击者,到线下来给你几个大嘴巴子。因此大家最好不要去尝试做一些不好的事。
2.5 实际查看cookie
上面一直在说cookie中会保存信息用以会话保持,那我们能不能实际看到cookie中的内容呢?答案是可以的。在以前写的Get函数中添加如下内容:
运行服务端,并在浏览器上访问并点击网址旁边的标志:
看到这个界面后再点击上图中圈出来的位置:
再打开网址下面的内容,就可以找到cookie,这里显示的内容就是在Get函数中添加的cookie。当然,实际cookie内容的形成并不是这么简单的,这里只是为了方便演示,随便写了一串数字。
大家应该也注意到了上图中有一个到期时间,其实这个到期时间也是可以设置的,有兴趣的话大家可以网上搜索一下怎么设置,其实很简单,就是在Set-Cookie中新增字段,这里就不再演示了。
十、两个http中常用的工具
1. postman
postman是一个用于模拟浏览器的工具,可以用于程序的网络测试。要创建一个连接,直接点击中间图中圈出来的“+”号即可:
点击后在弹出的页面中的GET的输入框中输入你的网址并点击send:
成功建立连接后,就可以看到如下内容了。在这里面可以直接查看响应返回的正文内容、cookies和报头等等信息。
如果你的网页中有需要传输的数据,你也可以在GET下面的body中输入你要传输的内容。这里我的网页中只有一个form表单,所以就是向form-urlencoded中写数据。再次点击send:
然后再查看linux中的打印信息:
可以看到,在postman中填入的数据就被写入到了请求中了。这里填在正文位置是因为获取方法为POST
2. fiddler
fiddler是一个本地网络抓包工具,可以将网络中传输的数据进行截获、重发等工作。在这里就简单介绍一下。
当fiddler下载好后,fiddler就已经设置好了浏览器的代理,所以基本如果只是简单的使用,是不需要配置什么东西的。
当我们打开fiddler以后,可能在左边的框内看到很多连接信息。大家可以点击左上角的x中的remove all清空:
为了方便测试,在进行http请求时,大家可以在浏览器中只访问自己写的程序的网址。运行完后就可以看到如下界面:
这就是这次服务端和客户端的http请求与响应的资源了。点击任意一个你想要查看的连接,然后点击下图圈出来的位置,就可以看到该请求或响应的内容了:
在下面的中你也可以查看该网页中的各类信息:
至于其他的使用,这里就不再多讲,有兴趣的话可以到网上查询其他相关的使用资料,在这里只是介绍一下。
fiddler的原因其实很简单,要使用fiddler,就需要将它启动。当fiddler启动后,它其实就相当于是以某种方式劫持了浏览器,你可以看成在你的浏览器发送请求时,并不是直接发送给服务器,而是先将请求发送给fiddler,然后fiddler再将请求发送给服务器。
既然请求都需要经过fiddler来传输了,fiddler当然也就可以抓取这份请求了。