应用层Http协议
HTTP协议
虽然我们说, 应⽤层协议是我们程序猿⾃⼰定的. 但实际上, 已经有⼤佬们定义了⼀些现成的, ⼜⾮常好⽤的应⽤层协议, 供我们直接参考使⽤. HTTP(超⽂本传输协议)就是其中之⼀。
在互联⽹世界中,HTTP(HyperText Transfer Protocol,超⽂本传输协议)是⼀个⾄关重要的协议。
它定义了客⼾端(如浏览器)与服务器之间如何通信,以交换或传输超⽂本(如HTML⽂档)。
HTTP协议是客⼾端与服务器之间通信的基础。客⼾端通过HTTP协议向服务器发送请求,服务器收到 请求后处理并返回响应。HTTP协议是⼀个⽆连接、⽆状态的协议,即每次请求都需要建⽴新的连接,且服务器不会保存客⼾端的状态信息。
认识URL
平时我们俗称的 "⽹址" 其实就是说的 URL。


这个是我们的http报文,我们发现是key: value的形式每行都是,http是如何进行序列化和反序列化操作呢??
这个我们可以看成一个大字符串,每行加个/r/n即可。
没有用任何第三方库,自己完成的。

http的应答。
第一行叫做请求行。
到空白行处都是属于报头,下面是我们的有效载荷。
格式问题:

看一下这两个图,然后我们来思考一下这两个问题,首先协议就是约定就是我们的一种结构体。
空行的作用就是我读到空行的时候就代表,上面报头信息全部读完,下面就是请求正文,区分报头和有效载荷的。
人类上网行为无非就两种:
1.我想得到信息
2.我想上传信息
我们上网的过程就是APP客户端和服务端进行进程间通信,IO问题,IO就两种。

我们协议后面就是我们的域名我们通过DNS把我们的域名可以解析为公网IP,然后我们在没有拿到我们指定域名处的资源的时候,这些资源都是在哪里呢??
答案是存在在服务器上面的。
资源在Linux下都是以文件的形式保存的。
Linux文件很多,我怎么知道你要访问哪一个呢??
文件就必须要有路径了。
所以我们域名后面的内容就是指定路径下的文件了。
urI是统一文件标识符,urL是urI的一种特殊情况。
域名后面的第一个/是根目录的意思吗??
不一定是根目录,叫做web根目录,可以是linux中的任意一个目录,代码部分再展开说。
谁来帮我把文件内容发回给客户端呢??
就是我们所写的软件,我们在url中为什么没有看到端口号呢??
因为http,https,ftp等等的端口号都是固定的。所以被省略了,浏览器自己加端口号。

我们发现我们随便搜索一点东西,发现后面存在参数,上面我们也标识了编码,这个编码的作用就是不让我们的url中出现特殊字符。
为什么协议里面为什么要带版本呢??
因为我们要知道你要访问的是哪个版本。
因为推了新版本我不一定想使用,可能还想使用老版本,此时就需要带版本,就是看一些新特性是否给你提供。
我们下载微信的时候,百度怎么知道我要下载的平台呢??
windows什么的版本。

就是其中的User-Agent表示的。
我们TCP面向字节流存在粘报问题的,你怎么保证你一定读到了一个完整的请求报头??
通过空白行,读到空白行就停下。
如果有正文,你怎么保证完整读取上来呢??
存在一个Content-length表示正文长度。
对应我们上个博客中的长度字段。

第一个表示首页,第二个表示指定目录下的文件。
什么是资源呢??
资源就是文件。
HTTP的状态码

HTTP请求方法
1. GET⽅法(重点)
2. POST⽅法(重点)
3. PUT⽅法(不常⽤)
4. HEAD⽅法
5. DELETE⽅法(不常⽤)
6. OPTIONS⽅法
HTTP常⻅Header
Content-Type: 数据类型(text/html等)
Content-Length: Body的⻓度
Host: 客⼾端告知服务器, 所请求的资源是在哪个主机的哪个端⼝上;
User-Agent: 声明⽤⼾的操作系统和浏览器版本信息;
Referer: 当前⻚⾯是从哪个⻚⾯跳转过来的;
Location: 搭配3xx状态码使⽤, 告诉客⼾端接下来要去哪⾥访问;
Cookie: ⽤于在客⼾端存储少量信息. 通常⽤于实现会话(session)的功能

不仅是首页的静态信息,还有我们存在图片的时候,它还需要请求我们图片视频等资源。

存在一个Content-Type类型对照表。
我们发现文件都存在后缀,所以通过后缀区分文件类型的。

Referer主要的作用如图所示。

这是真正的代码书写思路。存在一个代理服务器。
长连接理解。
长连接就是:TCP 连接不断开,一直用,发完一次数据还能继续发。
短连接(以前的 HTTP/1.0 默认)
-
建立连接
-
发请求 → 收响应
-
直接断开连接
-
再要发 → 重新建立连接
这里是多次Tcp的连接需要多个fd和进程。
长连接(现在 HTTP/1.1 默认)
-
建立连接
-
发请求 → 收响应
-
连接不断开
-
继续发请求 → 继续收响应
-
一段时间不用,才断开
这里是一次Tcp连接,一个进程即可。
长连接效率更高,不用重复断开连接。
GET既可以获取信息,又可以上传信息,有?有参数就代表上传信息。

没有就是获取信息。
POST传参是通过正文传参的,我们一会儿后面写代码就能看出。

因为我们的GET方法传递的参数都跟在了url的后面,不是太私密,我们的POST方法参数是在正文当中的,相较于GET方法私密一点。但是这两个方法都不安全。
那怎么办呢?例如,和你在一个局域网中的人,理论上也是可以拿到你的信息的啊,只不过被自动丢弃了,如果不丢弃,岂不是能拿到你的私密的信息吗??
此时怎么办呢??
就需要一个TLS/SSL了,这两个就是加密和解密。
就是把我们传到网络中的内容进行加密,到达对端服务器再进行解密,即使中间被截获了,对方也是拿到加密后的东西。
先对无连接和无状态分析说明一下,无连接是什么呢??
就是没有连接,但是HTTP底层是TCP啊,TCP是有连接的,为什么是它是 无连接的呢??
答案就是你TCP有连接和我是没有关系的,无状态就是历史请求和用户行为我们不做任何记录,但是我们也知道,我们第一次打开抖音的时候,你需要登录账号,但是下次打开就自动被记录下来已经登录了,那为什么你还要说它是无状态的呢??

比如我们一个网站存在10000个视频,2000个需要vip才能观看,但是怎么知道你怎么它是否有vip呢??
此时就要保证你是登录状态然后查看你的缴费记录什么的看看你是否有vip,此时如果是无状态的,你看完第一个VIP视频,看完了,如果是真正无状态的,此时你想看第二个视频就必须还要登录账号然后继续供他检查,此时你看一个视频需要检查一次,对用户来说是不是太麻烦了啊。
HTTP为了保证用户进行一定程度的状态保持功能,服务器+浏览器+HTTP(Cookie+Session)层来帮助我们用户获得更好的体验感。
携带当前保存的cookie信息。

这虽然不是我们现在浏览器用的,但是为了方便我们讲解,这样子理解,就是你第一次请求登录账号密码时,你的账号密码会保存在cookie报头中,你下次请求的时候,自动携带这个报头,自动登录。

此时就很多问题,比如如果一个人拿到了你的cookie,别人拿到你的cookie登录,所以你的账号就被盗了,问题很大。
我们可以交给服务器来保护我们的cookie。

每个用户都有一个session_id把我们的cookie信息都存放到我们的服务器端,我们的客户端只存放我们的session_id,但是此时不怀好意的人还能拿到你的session_id,继续访问同一个网站,还是能盗你的号,此时无法很好的保护我们的信息,可以缓解,比如我们的异地登陆就会让你重新登录。
比如微信频繁加人,给你号封了,都是为了保证用户的信息。
会话 Cookie(Session Cookie) :在浏览器关闭时失效。
持久 Cookie(Persistent Cookie) :带有明确的过期⽇期或持续时间,可以跨多个浏
览器会话存在。
如果 cookie 是⼀个持久性的 cookie ,那么它其实就是浏览器相关的,特定⽬录下的⼀个
⽂件。但直接查看这些⽂件可能会看到乱码或⽆法读取的内容,因为 cookie ⽂件通常以⼆进
制或 sqlite 格式存储。⼀般我们查看,直接在浏览器对应的选项中直接查看即可。

这上面存在expires的参数,这个参数的作用就是过期时间,经过多长时间过期。
如果设置了 expires 属性,则 Cookie 将在指定的⽇期/时间后过期。
如果没有设置 expires 属性,则 Cookie 默认为会话 Cookie,即当浏览器关闭时过期。
使⽤ secure 标志可以确保 Cookie 仅在 HTTPS 连接上发送,从⽽提⾼安全性。
使⽤ HttpOnly 标志可以防⽌客⼾端脚本(如 JavaScript)访问 Cookie,从⽽防⽌ XSS 攻
击。
通过合理设置 Set-Cookie 的格式和属性,可以确保 Cookie 的安全性、有效性和可访问性,从
⽽满⾜ Web 应⽤程序的需求。

这是POST请求的样子存在正文也就是请求体。

我们的GET请求是不存在请求体的。
代码书写的理解
下面我们就写一个代码来帮助我们更好的理解http吧。

先看一下这个代码。
我们只实现了服务端,客户端不需要我们手动实现,因为客户端就是我们的浏览器。
首先头文件和我们定义的分隔符先看一下。
首先第一个类是用来处理我们客户端发过来的HTTP请求的,所以我们只需要实现反序列化即可,因为是处理客户端发过来的网络中的东西的,所以我们得拿到我们想要处理的内容,所以就是反序列化,序列化是我们响应客户端发送回客户端要用的,我们处理请求不需要实现。
首先就是我们的ReadOneLine函数,这个函数的作用就是你传过来一个字符串就是拿到客户端发送的序列化的请求结果之后,然后先拿到我们的一行,我们规定行之间是通过\n\r来作为分隔符的,所以先找到第一个分隔符我们不久拿到第一行了吗,然后把拿到的结果放到line中,然后把原串我们拿到的内容擦除一下,方便我们下次拿下一行,此时我们就拿到了第一行的内容了,这就是这个函数的作用。
ParseReqLine这个函数的作用就是因为我们第一行比较特殊。

这样子的形式,我们需要分别拿到这三个内容,它是很重要的,我们就通过这个如图方法拿到了我们这三个内容然后分别放在这三个变量之中。
BuildKV这个函数的作用就是把我们下面几行的内容变成我们的KV形式,我们发现它们都是通过:+空格的形式分割的,所以我们以这个区分,以KV的形式分别拿到了我们每行的信息了。
Deserialize来看这个函数了,很重要,就是反序列化的,首先就是我们通过reqline获取这个ReadOneLine的返回值,此时我们把这个拿到客户端发过来的原串进入到这个函数,此时拿到了第一行的内容,然后调用这个ParseReqLine这个方法,分别把三个重要的内容放在了我们的定义的三个变量当中了就是我们的私有成员当中,然后通过while循环我们每次拿到每行的内容,然后把它插入到我们定义的哈希表中了。此时我们就可以使用我们的K来查去V了,如果查到空白行就直接给它\n\r的分隔符就代表空白行了,否则就是非法请求了,此时我们就要拼接我们的陆经了,首先把这个webroot拼接上是必不可少的,然后我们这个uri就放的是我们
这个的内容,就是我们要访问的文件,此时就拼接好了一个路径就是/webroot/submit这样的形式了,然后如果这个uri是/我们默认就给它跳转到我们的首页,这个submit都是我们实现的前端页面,这里我们不讲前端,只看后端,拼接好之后就看它是什么请求方式,如果是GET请求,我们就从路径中拿到我们的参数,因为我们的GET请求时在路径中带参数的是以?分割的和前面的内容,所以我们拿到这个参数之后直接放在请求体中,我们的GET请求虽然没有请求体,但是我们也可以用这个变量先存放着我们的参数内容,然后重新取一下我们的路径,如果是POST请求就没这么麻烦了,直接reqstr中存放的就是我们的正文内容,此时就是我们的账号密码的信息也在这里面,直接让它等于这个即可。
下面这几个函数很简单,看一下即可,就是获取响应的变量信息的。
Suffix这个函数是为了解决什么问题呢?
我们都知道我们的文件分为很多类有.html,.jpg等等,每个文件后缀对应的类型都是不同的,导致对不同文件的处理也是不同的,我们浏览器自己设置了一套浏览器可以看懂的类型表,这个函数的作用就是拿到我们的文件后缀,它和下面的一个函数是配合着使用的。
我们看HttpResponse类,这个类开始是设置了几个状态码来返回不同的提醒的,可以让我们知道我们访问出现了什么问题,这个类的作用是对我们客户端发起的请求做出响应的。
首先就是序列化的函数了,就是把我们的标准化的内容序列化为字符串的形式发送回我们的客户端,首先就是这个_httpversion就是我们的固定值HTTP/1.1,加一个空格,然后再加状态码,再加状态码对应的返回值的描述,就是放在我们的_desc中,此时就拼接好了一行,然后判断我们的这个叫做响应体,我们的GET也是存在响应体的,但是没有请求体,响应体就是要发的全是浏览器最终要展示、要使用的数据:网页源码 html,样式文件 css等等,我们这里记录长度就是为了读的不完全导致的粘报问题的,拼接完之后,然后再把我们哈希表中存放的键值对信息拼接到我们的字符串当中,然后再把空白行和响应体拼接进去即可完成了序列化操作了。
ReadContent这个函数的作用就是以二进制的形式打开文件的,因为为了不破坏文件内容 **,保证图片、HTML、CSS、JS 原封不动发给浏览器!
文本模式打开 vs 二进制模式打开 的区别
文本模式(Text Mode)
-
会自动修改内容
-
把
\n自动转成\r\n(Windows) -
遇到特殊字符会截断、篡改、出错
二进制模式(Binary Mode)
-
原封不动读取
-
不修改任何字节
-
不添加、不删除、不转换
-
文件是什么样,读出来就什么样
只要记住我们以二进制的形式打开文件就是为了把文件的内容一字不差的发送给我们的浏览器的。里面的一些方法大家可以试着了解一下。
这个SetCode函数就是设置我们的状态码的,很简单。
SetHeader函数的作用就是把我们读到的键值对存放到我们的哈希表中,方便我们序列化。
SetCookie这个的作用我们上面也讲过,不用重点看,就是一个保护机制。
下面看这个Http的类,这个类的作用就是帮助我们处理请求的。
Suffix2Desc首先就是这个函数了,这个函数的作用就是通过后缀帮助我们拿到浏览器能看懂的类型的。
Register这个函数的作用就是为了帮助我们把一个网址路径,绑定到一个处理函数上,浏览器访问这个路径 → 自动调用你绑定的函数 。这就叫 路由注册!
此时你就可以传入你想要操作的路径,然后把它绑定到我们对应的执行函数上面,这个函数也是很好理解的。_handlers[key] = handler;这个操作就是如果key值存在的话就修改v值,不存在就新建。
HandlerRequst最后就是这个函数了,这个函数的作用就是拿到我们客户端发送过来的网络字符串,然后把这个字符串反序列化一下,然后定义一个响应对象,拿到我们要处理的路径然后在我们的哈希表中查找,看看是不是我们要进行的登录什么的操作,如果使得话就调用响应的函数,如果不是的话,就调用ReadContent这个方法以二进制的形式读取我们的网页中的资源,操作就是我们所说的,拿到后缀转换为浏览器能看懂的类型。
否则的话就跳转到我们的404页面,说明报错了。
-
SetCode(301)告诉浏览器:这个页面 moved 了,你要跳转。 -
SetHeader("Location", "/404.html")告诉浏览器:你跳转到这个新地址 →/404.html
HTTP/1.1 301 Moved Permanently
Location: /404.html
此时你给浏览器发送了这个报文,浏览器就知道了我要跳转,并且跳转到404的页面。
最后我们把处理完的这个响应结果序列化一下发送回我们的浏览器也就是客户端即可。




这几个类中的内容我们已经经常讲了,这里就不再过多赘述了。

我们直接看Main函数直接梳理一下调用的过程吧。
首先就是Main函数实现了三个函数就是供我们跳转使用的,首先就是Login函数,这个函数就是设置了302,表示我告诉浏览器我要跳转,位置就是/,就是我们的首页。
我们设置了Cookie,首先我们客户端输入账号密码的时候,首先就是我们服务器拿到账号密码之后,解析之后会放在请求体中保存着,并不发送回浏览器,而是把我们创建的Cookie发送回给浏览器,使得浏览器下次可以直接拿着Cookie发送给我们的服务器来让它自己登录我们的账号。
首先Main函数就先调用了我们http的Register方法。

把我们对应的路径和函数都放在哈希表中,使得你通过路径就能调用到函数。
放完之后,我们创建TcpServer对象。

调用了这个构造,然后调用BuildListenSocketMethod这个函数我们之前都有讲的,这里不说了,此时我们就在等待客户端的连接了,[&http]这个的作用就是捕捉这个外部的http变量,使得我们可以使用这个变量。
初始化完之后,开始调用Run函数,再调用Accept函数,此时就连接上我们的服务器了,我们服务器接受连接了。
此时调用了HandlerRequest进入了Recv函数,此时我们认为我们http接受到的就是完整报文,然后进入
此时就接收到了我们客户端发送的请求,然后我们就通过_cb处理这个请求了。

回到了这里。

调用这个,然后反序列化。

此时我们就拿到了客户端发送过来的请求,然后进行一系列的请求,我们调用ReadContent这个函数把我们想要读取的网页的信息写入到我们的响应体,此时响应体就是存放了我们读取路径中的对应的前端文件中的代码全部放到我们的响应体中了,然后返回的时候浏览器拿到了这些代码就能显示页面了,就是这个过程,然后通过前端的一些click属性就可以让你通过点击某些东西来跳转页面了,实际上就是通过点击,此时你访问服务器,服务器找到你点击的这个路径,然后把前端代码回显给浏览器,此时你救能拿到页面了。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)