计算机网络——应用层协议(2:http协议)

在这篇文章中自定义应用层协议,我曾介绍了应用层协议中需要我们开发人员自行制定应用层协议,而应用层协议又离不开结构化字段以及序列化和反序列化还有报头的封装。而在今天,我们有一种应用层协议是我们几乎人人都接触过的协议,它存在于电脑,平板和手机上。我们作为开发人员与这种应用层协议打交道的次数最多,因为浏览器使用的应用层协议就是http协议。
它就是http协议(当然也有https协议),那么本文章我就来简单的介绍关于http协议的一些知识。

1. url

在认识http协议之前,我们有必要认识url,也就是我们浏览网页的网址:
在这里插入图片描述
这就是一个比较全面的url的表示。
其中的登录信息在今天已经很少见到了,是因为今天的网站一般都会让你使用账号密码或者二维码的方式登录,再往后就是服务器地址加端口号,这时候就有人有疑问了,在网络中,要实现两端主机通信需要的是IP地址和端口号啊,这里怎么是一个什么www.example.jp然后加端口号啊?
其实这个就是一个IP地址,只不过这种叫做域名,我们访问这个网站时,浏览器会先向域名解析服务器发起请求,然后域名解析服务器给你返回对应的IP地址,之后浏览器才会拿着这个IP地址和端口号去访问对应的服务器。
而在端口号的后面有一个斜杠/从这里开始它就是一个路径,其中的/代表web根目录,后面就是相应的一个资源的路径,这个资源可以是图片、视频、音频、或者是html文件等等,这个路径以问号结尾(如果只有web根目录问号可省略),再往后就是查询的字符串,比如我们使用百度查询一个关键词 “IP地址” 时:
在这里插入图片描述
我们可以看到在问号的后面就会出现许多参数,而参数也有对应的值,这些参数之间通过 ‘&’ 隔开,再往后就是片段标识符这个不作为重点介绍。
不知道有没有人发现一个问题,那就是我刚开始提供的url和现在在百度中的url有两个很明显的区别:
一个就是它的协议方案名是https。
https其实就是加密的一种http协议,它可以保证一定的网络安全。https可以有效防止许多网络攻击,提高了用户在网站浏览、数据传输等方面的安全性和信任度。因此,在进行网站开发和数据传输时,推荐使用HTTPS协议来保障数据的安全。
还有一点那就是www.baidu.com的后面为什么没有端口号呢?
在这里我们所使用的协议(http/https)就已经表明了端口号,对于这种重要的常用的协议,它们所使用的端口号是固定的,不能轻易更改。其中http协议的端口号是80,https使用的端口号是443。
现在我们就知道了,我们使用浏览器输入人家的网址,其实也还是使用IP地址+端口号的方式进行网络通信。
我们上网的本质行为,也还是进程间通信。
而我们的上网行为也可分为两类:即获取资源和上传资源。
我们也能看到url中使用了很多的特殊字符作为一些字段的分隔符,那假如我们在百度的搜索栏中搜索的就是特殊字符呢?
在这里插入图片描述
我们发现他其中的一些搜索词发生了变化,‘/’ 变成了%2F, ‘@’ 变成了%40。我们的搜索关键词在成为url的一部分前,会先被进行url编码,这一点需要注意一下。

2. http

这里需要注意一下,下面的代码在实现过程中,只是为了能够说明将要讲述的一些知识的现象,代码中是会存在许多bug的。

a. 一个简单样例

既然http是一个应用层协议,并且浏览器使用的也是http协议的话,那么我们就可以让浏览器作为客户端来访问我们的服务器了,http协议传输层使用的是tcp协议,那么我们可以刚好用上自定义应用层协议中的代码,将其中的代码进行简单修改就可以了:
线程函数中,只让请求一次,我们也只响应一次:
在这里插入图片描述

ServerHandler函数中只进行简单的接收报文和返回一个特定字符串,返回的内容先不做介绍:
在这里插入图片描述
我们运行代码,然后再浏览器中输入我们机器的IP地址和这个进程的端口号就可以访问我们的服务了:
而我们的服务收到的浏览器的请求如下:
在这里插入图片描述

我们看到它有很多信息,而且是多行,但是我们知道使用TCP进行网络传输数据时,传输的都是字节流,所以这些所有的内容其实就是一行字符串,只不过这些字符串中,在上面的请求内容中的每一行中的结尾都有一个 “\r\n” 也就是换行的意思,所以我们打印出来才是多行,就如同我在自定义协议中自定义的协议一样,数据在网络中传输的时候时字节流,但是它们也有一定的格式,而http协议作为应用层协议,我们要认识它,其实也就是认识它在网络中传输所使用的结构化字段有哪些?代表的含义是什么?序列化和反序列化方案,以及如何保证我们在接收http发来的请求时,我们能保证这个请求是独立且完整的,这都是http作为应用层协议我们要考虑的。
而http作为网络层协议,我们也要应该认识它报头和有效载荷如何分离,以及有效载荷如何向上交付的问题(但是这里由于应用层已经是最顶层了,也不需要考虑)?那么话不多说,我们直接来认识,http协议在网络传输中的格式是什么?

b. http协议

关于http协议我们可以用一张表格来说明:
在这里插入图片描述

这就是一个http协议发出的请求内容,从逻辑上来分他有四部分,请求行,请求报头(请求行其实也算请求报头中的一部分),空行,和请求正文也就是有效载荷。
我们来大致介绍一下它们的含义:

请求行

请求行中有三个字段,分别是请求方法、请求资源路径、HTTP协议版本,它们中间以空格间隔,以\r\n结尾

请求方法:在实际开发中请求方法基本上只会用到两种GET/POST,虽然http有很多请求
方法。至于请求方法是什么,我们后面再细说
url:也叫请求资源路径,它主要是记录网页url中的这一部分:

在这里插入图片描述
从/开始到?结尾中间的字段,它指向的是我们服务器中的一个具体的路径下的某一个文件,也就是资源,当然他也可以携带后面的参数,也就是这一部分:
在这里插入图片描述
http协议版本:这个就是说明了我使用的就是这个版本的http协议我希望对端也就是响应端使用的也是这个协议。
http协议主要有三个版本,http/1.0、http/1.1、http/2.0,其中最常见的是http/1.1,http/2.0不做介绍,http/1.0主要做的是短连接,http/1.1做的是长连接。我们主要介绍http/1.0。

请求报头

在请求报头中有很多的属性行,这一部分有多少行是不确定的,看具体请求是什么,而这些属性行的格式都是:键:(空格)值\r\n,之后我们会介绍其中的属性。

空行

用来分隔报头和有效载荷的部分,这也就解决了http协议怎么将报头和报文分离。

有效载荷

有效载荷就是报文的内容了,它可以携带任何信息。

那么介绍完http请求报文的大致格式之后,我们现在只解决了如何分离报头和报文的问题,但是好象还有解决,http协议怎么保证对端收到的报文是独立且完整的。http协议是无法保证这一点的,但是http协议的请求报头中会有一个属性Content-Length,假如请求报文中,有有效载荷的话,该报文中的请求报头中就会携带这个属性,用来记录报文中有效载荷的字节长度,这样我们的在对端的机器就能根据Content-Length这个字段来判断自己是否收到了独立且完整的报文,假如报文中没有这一属,说明报文中没有有效载荷,只有报头,那么我们也能保证对端收到的报文是独立且完整的,那就是利用报文中的空行。当我们可以找到一个空行并且请求行中没有Content-Length字段我们就可以保证我们读到了一个完整且独立的报文了。

响应报文

刚介绍完请求报文,我们现在来介绍响应报文,响应报文大致与请求报文一致:
在这里插入图片描述
区别就是在响应行中,变为了http协议版本,状态码,和状态码描述,
对于状态码,状态码描述我们可能不清楚,但是我们一定见过一个状态码叫做404,一个状态码描述是 Not Found。
关于响应报头我在那个简单示例中也可以看到,我返回的httpresponse就是一个简单的响应报文,相应行中就是HTTP协议版本,状态码200和状态码描述ok。
我们也可以使用一些软件来验证,我们所说的响应报文是这样子的:
我们可以使用一些抓包软件来验证:现在我们再次使用我们的案例作为例子:
在这里插入图片描述
可以看到我们所返回的数据确实就是这个样子,我们的响应正文的内容,浏览器也能识别出是一个html代码,并将它渲染到浏览器上。
其实我们的响应正文就是响应给浏览器的各种资源,可以是网页、音频、图片、视频等等。
还有一个问题,那就是其实在我们使用浏览器访问服务器时,浏览器向服务器发送的是两次请求:
在这里插入图片描述
第二次请求我们看到它的url是一个web根目录+一个以.ico为后缀的文件,它其实是向我们请求一个小图标来作为网站的标识:
在这里插入图片描述
就是这个图片资源。

3. 认识http协议中的字段

我在上面说,http协议作为一个应用层协议,我们要认识它,其实也就是认识协议中的字段以及如何对它进行报头的封装与解包,以及序列化和反序列化。那么我们现在就开始认识http协议中的字段。

a. url

我们首先来认识请求行中的url即请求资源路径
我们可以再来看一眼在浏览器上访问我们的服务时,发个过来的请求是什么:
在这里插入图片描述
我们可以看到请求中url就是一个路径对应着一个资源文件,我们的浏览器就是向服务端访问这个文件,最后服务端给浏览器响应回这个资源文件,而其中url中开始的 ‘/’ 叫做web根目录,web根目录下放的就是我们所有浏览器向服务端请求的资源。我们现在在网址栏中访问www.baidu.com/:
在这里插入图片描述
我们发现,我们并没有在url中申请任何资源而只是输入了一个根目录,按理来说,他应该要不什么资源都不返回,要不就把web根目录下的资源都给我们响应过来,但是这样也是不合理的,所以我们一般规定,当访问网址时url中只有web根目录的话我们直接给它返回给一个首页网页。
那么为了实现这样的效果,我们就需要重新改造一下我们的代码了:
我们首先需要自己将http协议中的字段来进行反序列化,以便能够提取其中的结构化字段:
而前面我们也知道在浏览器发过来的请求中主要有四部分:请求行、请求报头、空行、请求正文,那么我们的请求类中的字段自然也是这几个:
在这里插入图片描述
而我们需要一个反序列化方法,其实也就是字符串操作:
在这里插入图片描述

我们再将它Debug打印一下:

在这里插入图片描述
ServerHandler函数中就该改变了:
在这里插入图片描述
在这里插入图片描述
有了请求自然也要响应:
响应字段我们先对请求字段做解析,看看请求字段要什么资源,然后我们给它返回即可:

既然要资源那就得把url给我们:
在这里插入图片描述
然后我们就可以开始在响应中给浏览器响应资源了:
在此之前我们需要,建立一个文件夹,然后将我们刚开始在代码中的响应content放到一个文件中:
在这里插入图片描述
而首页网页的命名一般都是index.html。
现在我们首先写一个当浏览器向服务端请求web根目录时,服务端给响应会首页的逻辑:
在这里插入图片描述
这里对于浏览器请求根目录来到服务端这里其实就是一个识别的问题,url是web根目录的话我们就把url变成请求一个首页,而对于其他资源我们只在前面加上我们的服务端的web目录。其实到现在我们也能知道web根目录是什么了,它其实就是服务端的一个目录罢了,这个目录负责存放浏览器需要的资源。
现在我们再次运行代码,发现我们的网站依然可以正确打开:
在这里插入图片描述
我们也可以通过telnet来查看服务端响应内容(telnet工具可以模拟HTTP请求,向HTTP服务器发送请求并查看返回的数据。):
在这里插入图片描述

可以看到,它的响应正文中确实是我们的网页。
而当我们的浏览器访问首页时,我们在远端改变index.html中的内容后,此时再刷新网页,内容就会更新,至此网页内容就可以和我们的服务进程没有关系了。除了网页之外,我们还可以传其他资源,比如图片:

在这里插入图片描述
现在我们在浏览器上申请这一张图片:
在这里插入图片描述
我们看到图片并没有加载出来,这实际上是因为我们的服务端中读取文件的方式不对:
在这里插入图片描述
这种读取方式只适合读取文本形式的文件,但是图片是二进制的,对于读取二进制文件,读取时会被字符末尾的 ‘\0’ 所影响文件的内容,所以我们这里以读取二进制的方式读取图片文件:
在这里插入图片描述
在这里插入图片描述
这样我们就能正确读取到图片文件了。

b. Content-Type

但是,我们上面的响应代码是有问题的,没有出问题是因为现在的浏览器很厉害,可以直接识别我们响应的文件是什么类型,但是我们是需要在响应代码中指出响应资源的类型的,这就是http协议报头中的一个字段Content-Type:
这个字段是根据文件资源的后缀来判别的,http协议中针对这些后缀也提出了一些类型,我们可以在网络上查到:
在这里插入图片描述
这里我们在响应类中添加一个unordered_map成员记录这种映射关系即可:
在这里插入图片描述
然后在响应报文中添加类型报头:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这样的响应才是比较好的。

c. 请求方法

我在上面提到过,我们在实际开发过程中大部分情况都只会用两种请求方法,也就是GET和POST发放,所以我们也只介绍这两种,其他方法也不难理解。其中:

GET方法:用来获取服务端资源,也可以传递参数,而参数的传递一般是由网页中的表单进行

下面我们来演示一下:
我们在我们的首页网页中添加一个简单的表单:
在这里插入图片描述
当我们输入数据之后,我们会跳转到我的服务端的web根目录下的x.html中,并且携带用户名和密码信息:
在这里插入图片描述
当我们输入之后,我们来看服务器端接收到的请求:
在这里插入图片描述
它的url就会携带上参数,同时也会跳转到对应页面:
在这里插入图片描述
这里网页没加载的原因是因为,url后面有了参数,在响应端处理字符串时没有正确的处理参数部分,导致出现错误,不多说。我们看到了参数确实能够被收到,那我们也就知道了,百度的搜索框它本质也是一个表单:
在这里插入图片描述
但是我们发现,在百度的表单中并没有见到请求方式,我们也将我们网页中的请求方式去掉:
在这里插入图片描述
在这里插入图片描述
可见,当请求方式不显式的填写时,默认是GET方法。
再次观察百度搜索之后的网址栏:
在这里插入图片描述
在网页对服务器传输参数时,服务器一般是要用一个可执行文件来识别这个参数的,那么对于C/C++,而言我们只需要使用fork创建子进程启动这个程序,然后将这个参数通过管道传给这个子进程,然后子进程通过管道返回结果,再由服务器返回响应正文即可。
那么POST方法又有什么作用呢?

POST:上传数据

它的作用就是上传数据,我们将我们网页中的表单改成POST方法:
在这里插入图片描述
再次看浏览器给服务器的请求:
在这里插入图片描述
我们看到使用POST方法是将参数提交到了请求正文中,也正是因为放到了请求正文中我们的网页可以正常加载了:
在这里插入图片描述
到这里认识了两个方法之后,我们发现使用GET方法是不是不太安全啊?它直接把用户名和密码直接暴露在地址栏上了,假如有人看见了,那不是我的隐私泄露了,而POST方法就比较安全了,它不会将账号密码暴露在地址栏中。
其实这样的认识是不太正确的,我只能说两种方法都不安全,我们使用POST方法提交用户名密码时,如果有人想知道你的用户名和密码,那么它可以像使用抓包那样的方式直接就能看到,我们的请求报文,那么POST也是不安全的,因为这个协议是http协议,要想安全得使用https协议,https协议会对请求报文进行加密处理,这样才能叫作安全。

d. 响应码和响应描述符

我在上面也说过,响应码和响应描述符你大概没有听过,但是你应该大概率是见过404和Not Found的,而这就是响应码和响应描述符,其中我们上面代码中的200和ok也是响应码和响应描述符,http协议也对响应码和响应描述符做出了规定,这个规定也可以在网络上查到,我们只介绍其中某些响应码以及响应码对应的响应描述符:
在这里插入图片描述
其中1开头和2开头的就不多介绍,我们也一般很少见到5开头的响应码,因为这个字段表示是服务端出了问题,使用这个响应码之后,这有可能被有心之人利用来攻击我们的服务器或者是其他不好的行为,当然我们开发人员写出的服务器又怎么会有问题呢,要有也是用户的问题(手动狗头)。
其实虽然你在可以网上搜到对应的响应码以及响应码描述符的标准,但是在实际开发中,大多数情况下,正如上面所说,开发人员不会响应给用户端5开头的响应码一样,开发人员对于响应码和响应描述符的使用是不规范的(有兴趣的可以了解背后的原因),我们也可以通过代码来掩饰,我们就直接在浏览器访问我们的服务器时,给它返回404:

在这里插入图片描述
在这里插入图片描述
我们仍然可以正常打开网页。所以我说在进行开发时,开发人员对于错误码和错误描述符的使用是不规范的。
但是我们目前得使用规范,404错误是出现在浏览器请求的资源不存在时会响应给浏览器的响应码和响应描述符,所以我们要再次改造代码:
在这里插入图片描述
现在当我们在浏览器访问一个不存在的资源时就会这样:
在这里插入图片描述
这就是响应码404,还有4开头的响应码是403,这个是当浏览器访问服务器资源时权限不足时使用的响应码。这个权限在Linux系统中不就是文件权限或者是特定的编码来识别身份嘛。
接下来我要介绍3开头的响应码,首先是307,响应描述符:Temporary Redirect,它的作用是临时重定向,用来跳转网站,我们再次通过代码体现,现在我们在浏览器访问首页时直接给响应回307,以及对应的响应码描述符:
在这里插入图片描述

我们发现没有任何反应,这是因为307需要配合一个响应报头中的字段Location使用,其中Location就是要跳转的目标网站,我们假设是B站:
在这里插入图片描述

现在我们再来试试:
在这里插入图片描述
它就会直接跳转到B站的网页,但是现在的浏览器检测到响应码307时一般会保存我们的跳转行为,当下次再次访问原网站时,仍会直接跳转目标网站,这种行为跟永久重定向301(响应描述符:Moved Permanently)的行为一致。

e. cookie和session

http协议有两个特点一个是无连接一个是无状态。
虽然http协议下层传输层使用的是面向连接的tcp协议,但是http协议的特点就是无连接,跟下层没有关系,HTTP协议的无连接特点主要体现在每次连接只处理一个请求,这跟我们的代码逻辑差不多。
而无状态是什么?无状态就是浏览器在处理完一次请求之后并不会保留其中的数据,这就会导致每一次浏览器访问服务器时,浏览器看这个服务器就像一个新面孔一样,服务器不会记录上一次请求的任何信息,也不会对客户端上一次的请求进行任何记录处理。因此,对于同一个客户端的连续请求,服务器无法判断这些请求是否来自同一个客户端,也无法对这些请求进行关联处理。
这就会导致我们在访问一些需要用户登陆的页面时可能会出现重复让用户登陆的场景,这就是无状态,服务器和浏览器并不会记录上次请求的记录:
在这里插入图片描述
有人现在就有问题了,
首先就是,为什么我访问这些网站老是要让我登陆啊,这是因为网站中的有些资源是需要判断用户身份来决定是否你能访问这个资源,比如vip的存在。
还有就是我在访问网站的时候基本上没有遇到过这种情况啊,这是因为一方面浏览器现在已经会缓存网页的一些资源数据,另一方面对于用户的登录信息也会进行处理:
在这里插入图片描述
而上图中返回的user和passwd是位于响应报头中的一个Set-Cookie字段中,而浏览器再次发送请求携带的user和passwd内容会位于请求报头中的Cookie字段中。
在edge浏览器中可以查看cookie,而对于一个登录了我们账号的网站来说,它的cookie中是会包含我们的用户名和密码的。比如文心一言:
在这里插入图片描述
此时我处于登陆状态,无论我是关闭这个网页还是关闭这个浏览器当我再次打开这个网站时,我的账号仍然处于登陆状态,而当我删除cookie时,再次刷新网页:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
我的登陆状态就没有了,这就是cookie,而cookie也分为两种,一种是内存级的cookie,一种是文件级的cookie,以及cookie的还可以设置保存时间等等。
我们也可以用代码来验证这一点:
我们直接在服务端响应回两个Set-Cookie字段,然后让浏览器再次访问:
在这里插入图片描述

在这里插入图片描述
现象正如我们所说的一样。
在这里插入图片描述
有些读者可能也看到了,这样的方案是有问题的。
首先就是我们能直接在浏览器上查看我们的cookie,假如别人看我们的电脑查看我们网站的cookie的话,我们的cookie中可是有真实信息的,这样的话我们的隐私就泄露了,或者是现在的cookie保存有可能是文件级的,那假如有人窃取我们的cookie文件呢?然后它使用这个cookie文件访问对应网站呢?
所以就有了第二种方案:
在这里插入图片描述
这种方式是服务端通过用户的登录信息,生成一个session,然后又根据session生成一个唯一的seeionid,从此之后服务端返回的Set-Cookie中不在携带用户的账户名和密码,而是sessionid了,这样保证了用户的信息的私密性。
但是我们仍无法解决别人使用你的cookie怎么办,事实上这个问题我们不能解决,只能采取一定的策略防止这种行为。方案一中,用户的账户信息是由用户来管理,而在第二种方案中,用户信息变为了sessionid,sessionid属于服务端,这样服务端就能很好的对sessionid采取各种策略,来防止别人盗用cookie的情况发生,比如给sessionid添加过期时间,添加IP(用户所在地)字段,添加其他具有识别用户的字段。这样能有效阻止这种情况的发生。
比如你现在在国内登录了一个网站,然后你的cookie被盗取了,你的IP地址突然变成了国外的,此时国外的IP主机登录网站时,服务端会根据登录端的IP地址与sessionid中的IP信息对比,发现变化较大,然后sessionid立即作废,让用户重新通过账号密码登录。这不就成功阻止了嘛。

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值