目录

LINUX网络基础-五-HTTP协议

LINUX网络基础 [五] - HTTP协议


https://i-blog.csdnimg.cn/direct/32f004335b654324b2f385a37212cb84.gif

HTTP协议

预备知识

HTTP(Hyper Text Transfer Protocol)协议又叫做超文本传输协议,是一个简单的请求-响应协议,HTTP通常运行在TCP之上。 在编写网络通信代码时,我们可以自己进行协议的定制,但实际有很多优秀的工程师早就已经写出了许多非常成熟的应用层协议,其中最典型的就是HTTP协议。 日常生活中,为什么不用ip地址和端口呢,而是直接使用域名呢?

因为域名是很容易被记住 ,ip地址体验特别差 ,给你个ip地址你都不知道是什么连接

https://i-blog.csdnimg.cn/direct/8fc30cfdfaa64d198fc9fe5df9130cdb.png其实我们平时用的域名,首先会被解析成IP地址的 ,在进行访问时,网络通信真正用到的就是IP地址

https://i-blog.csdnimg.cn/direct/33b0f4c2a8934a65a5d3f9ddab0eed18.png

我们在浏览器进行访问的时候,大部分默认使用的协议是http协议 。他们的端口号一般是要固定下来

其中的网络服务,这个端口号都要指明出来, 不能改,一旦改了,客户端就找不到你了

认识 URL

URL(Uniform Resource Lacator)叫做统一资源定位符,也就是我们通常所说的网址,是因特网的万维网服务程序上用于指定信息位置的表示方法。 https://i-blog.csdnimg.cn/direct/69420375e35f4862a81ccb5356fac840.png 图片 音频…统称为资源,所有网络上的资源,都可以用唯一的一个"字符串"标识,并且可以获取到,我们就称之为:统一资源 可是,我找到了这台主机和http服务,但是我想访问的是什么呢???比如我们平时在搜图片的时候,这个图片其实是被存储在该主机上的,而我们知道Linux一切皆文件 ,所以每个资源在自己的单机上都有自己的所属路径。因此我们还需要有一个带层次的文件路径。来标识我们具体想访问该主机上哪个地方的资源(一般来说可能是从web根目录开始 也有可能是相对路径)!/是文件路径分隔符 我们在上网的时候无非就两种网络行为。1.把别人的东西拿下来比如说下载图片 2.把自己的东西传上去 比如说登录注册 https://i-blog.csdnimg.cn/direct/53fcc1c2e7b84ba98850a7b81057d913.png 当我们找到了这个资源的路径,那么这个路径可能有很多很多格式各种的资源,那么我们继续需要锁定其中的一个,就需要有查询字符串,一般在查询标示符?的后面,表明该路径下的唯一资源,而要锁定唯一资源,就需要传入一些指定的参数,不同参数之间用 &去分隔,让url支持多参数的提交! 而#后面的是片段标示符,也就是说可能我们想访问的是该资源的某一个位置,可以用这个去进行标识 ! 我们在网络编程中,还知道要通过端口号来标识这台主机上的唯一一个服务,而该进程自带着网络协议http的解析方法,可是我们普通人使用的时候是没有端口号这个概念的! 那么浏览器怎么知道你这个端口号呢?? 因为浏览器会默认使用http协议,所以他必须知道绑定443号端口,所以默认会在请求里给我们添加端口号!

认识 urlencode 和 urldecode

urldecode (编码)

  • urldecode 函数用于将字符串中的特殊字符转化为 URL 编码格式 。在 URL 中,某些字符(如空格、 &、=、? 等)具有特殊意义,因此不能直接出现在 URL 中urldecode 通过将这些字符转换为百分号编码(也叫做 URL 编码)来避免这些问题。

urldecode(解码)

  • urlencode 函数用于将 URL 编码(百分号编码)的字符串恢复为原始的字符串 形式。它将 URL 中的% 后跟的十六进制数还原回原来的字符

https://i-blog.csdnimg.cn/direct/1888730d46f04aa6a336b2d2cd40a683.png 少量的情况,提交或者获取的数据本身包含和url中特殊的字符冲突的字符,要求BS双方进行编码(encode)和解码的过程(decode),这样可以保证特殊字符不会在url中出现。浏览器会自动执行 为什么要这样呢? 因为它本身就包含这种特殊符号字符的使用,用户本身的特殊符号不会造成影响 。这个编码和解码并不是为了加密什么的,而是为了保证用户提交的数据和url不发生冲突

urldecode 转义的规则如下:

将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY 格式

urldecode就是urlencode的逆过程:

https://i-blog.csdnimg.cn/direct/90e8d0201e14432ba95e1f19ed133ae6.png

HTTP协议格式

介绍 应用层常见的协议有HTTP和HTTPS,传输层常见的协议有TCP,网络层常见的协议是IP,数据链路层对应就是MAC帧了。其中下三层是由操作系统或者驱动帮我们完成的,它们主要负责的是通信细节。如果应用层不考虑下三层,在应用层自己的心目当中,它就可以认为自己是在和对方的应用层在直接进行数据交互。 https://i-blog.csdnimg.cn/direct/b03e2a4a50ef49449889423fc059d84b.png 下三层负责的是通信细节,而应用层负责的是如何使用传输过来的数据,两台主机在进行通信的时候,应用层的数据能够成功交给对端应用层,因为网络协议栈的下三层已经负责完成了这样的通信细节,而如何使用传输过来的数据就需要我们去定制协议,这里最典型的就是HTTP协议。 HTTP是基于请求和响应的应用层服务,作为客户端,你可以向服务器发起request,服务器收到这个request后,会对这个request做数据分析,得出你想要访问什么资源,然后服务器再构建response,完成这一次HTTP的请求。这种基于request&response这样的工作方式,我们称之为cs或bs模式,其中c表示client,s表示server,b表示browser。 由于HTTP是基于请求和响应的应用层访问,因此我们必须要知道HTTP对应的请求格式和响应格式,这就是学习HTTP的重点。

HTTP请求协议格式

Request请求分析图

https://i-blog.csdnimg.cn/direct/31aa2c7048034ec6b5ada6464f4899af.png HTTP请求由以下四部分组成:

  • 请求行:[请求方法]+[url]+[http版本]
  • 请求报头: 请求的属性,这些属性都是以key: value的形式按行陈列的。
  • 空行: 遇到空行表示请求报头结束。
  • 请求正文: 请求正文允许为空字符串,如果请求正文存在,则在请求报头中会有一个Content-Length属性来标识请求正文的长度。 其中,前面三部分是一般是HTTP协议自带的,是由HTTP协议自行设置的,而请求正文一般是用户的相关信息或数据,如果用户在请求时没有信息要上传给服务器,此时请求正文就为空字符串。 你怎么确保正文部分能够一字不落地读完呢?? 报头属性里面有 标注正文部分的长度的属性,所以他只要根据换行规则去读取整个报文,然后一直读到空行就说明报头读完了,然后再根据 Content- Length(length字段) 直接向后读相应长度的字节 就可以把正文部分也给读完了! https://i-blog.csdnimg.cn/direct/eff86b55bc2142c1910b5e38f618ae54.png

HTTP响应协议格式

Response响应示意图

https://i-blog.csdnimg.cn/direct/4d15bbb22f6d4738b766dcb788357d5a.png HTTP响应由以下四部分组成: 状态行:[http版本]+[状态码]+[状态码描述] 响应报头:响应的属性,这些属性都是以key: value的形式按行陈列的。 空行:遇到空行表示响应报头结束。 响应正文:响应正文允许为空字符串,如果响应正文存在,则响应报头中会有一个Content- Length属性来标识响应正文的长度。比如服务器返回了一个html页面,那么这个html页面的内容就是在响应正文当中的。 使用 Telnet 连接远程主机,来模拟客户端进行访问

telnet <主机名> <端口号>

https://i-blog.csdnimg.cn/direct/0fb8d25a0e3a458c8894954066ad9f7d.png

退出 Telnet 会话

Ctrl+] telnet> quit

响应结果解析: https://i-blog.csdnimg.cn/direct/31ee2f9953434e0eb31033658afa9ca2.png

fiddle 抓包软件fiddle https://i-blog.csdnimg.cn/direct/61cd5b43b04349af959bbc880527706b.png fiddle为什么能抓包呢 原理:以前是浏览器直接发到网络。fiddle启动之后,浏览器发的请求不会直接发到网络中了,而是先交给fiddle,它进行包装下再发给服务器,服务器再把请求响应返回fiddle,它再给我们的浏览器 https://i-blog.csdnimg.cn/direct/7259276018314a499b48110ce70f3542.png postman 可以通过它发送具体请求 https://i-blog.csdnimg.cn/direct/06728d79ab574462a2cdf508624f4fba.png 在Linux系统中,recv 函数用于接收通过套接字传输的数据。它是一个阻塞函数,通常用于从网络连接中接收数据。recv 函数是POSIX标准的一部分,可以用来从连接的套接字(如TCP套接字)中接收数据。 ssize_t recv(int sockfd, void *buf, size_t len, int flags); 参数说明 //sockfd:要接收数据的套接字描述符。它通常是由 socket()、accept() 或 connect() 等函数创建的套接字。 //buf:接收数据的缓冲区。 //len:缓冲区的大小,指定最多接收多少字节。 //flags:接收数据的选项,可以是以下之一(或它们的组合): //MSG_OOB:接收带外数据。 //MSG_PEEK:从队列中查看数据,但不移除它们。 //MSG_WAITALL:等待接收到指定大小的所有数据。 返回值 //如果成功,recv 返回接收到的字节数(即读取的数据量)。如果接收到的数据量小于指定的 len,可能是因为连接中没有更多的数据。 //返回值为0,表示对方关闭了连接(TCP连接的正常关闭)。 //返回值为-1,表示出错,可以通过 errno 获取错误码。 https://i-blog.csdnimg.cn/direct/6be79a154d36476f8c57a5033dd15920.png 测试效果 https://i-blog.csdnimg.cn/direct/19965c7c8d5c4ba49c0394430acc9675.png https://i-blog.csdnimg.cn/direct/edf7c7af879d4b039f75d61c1f30dced.png 我们的网页难道每一次都要通过静态编码写到服务器里面吗? 我们的网页应该单独的是一个独立的文件。需要访问哪个就通过http访问就行了 https://i-blog.csdnimg.cn/direct/3397c96bd6a94df3958af7f3a0411306.png 即http响应的正文部分,要放在网页(index.html)里面。让内容不要静态编码,而是动态的去让服务器响应给浏览器 https://i-blog.csdnimg.cn/direct/88c0abd730e8490cb8e6348cdcb125a1.png 对index文件修改之后保存,就能动态刷新内容了,不需要重启服务器 我们的文件可能不止一个,包括图片 样式文件 js文件 各种各样的文件在Linux存在的情况下一定会存在个目录结构,这个目录结构肯定整体能被web访问 一个Http协议一定有自己的web根目录,这个目录也可以由自己去指定。但是这样无论我们怎么请求只能返回一个同一个文件,等未来你可能需要不同的文件,该怎么办呢 https://i-blog.csdnimg.cn/direct/b3ab09aeb1bf4afb9b51b4b7278aaae7.png 很明显将来你不可能把所有东西都放在这个文件下,这样是不现实的。所以我们现在要实现由客户端告诉我,它想要什么文件我就给它哪个文件,那该怎么实现呢? 实现的思路 就是:我们要提取发来的请求。我们不难发现,Request请求行中的第一行URL就包括了用户想请求哪个目录 https://i-blog.csdnimg.cn/direct/173734c875714de5b01c88b5e3ddd806.png 代码实现思路: https://i-blog.csdnimg.cn/direct/df8f5b9dfa8f49758d021d98b582e6fe.png 把指明的路径拼上前缀,此时就能够动态的在服务器上找到在wwwroot目录下指定的文件了。当未来加入图片等内容的时候,就可以在wwwroot目录下,用树状结构组织好去或许对应的内容了 我们可以定义个config配置文件,把web根目录放在config里面。当服务器启动的时候就读取配置文件里面所需的内容了 https://i-blog.csdnimg.cn/direct/ba3834e5bb44454ab65b02de415297d0.png 如何拿到URL 把发来的请求进行反序列化,拿到请求行和请求报头(正文内容还没添加) https://i-blog.csdnimg.cn/direct/d6bac26531b549ad9e44cc1fc87c15cd.png https://i-blog.csdnimg.cn/direct/1491e5c161014f6ab2224e3a7e6179b0.png 在请求行中拿到URL 我们可以发现HTTP协议处理本质上就是文本的处理,跟我们之前讲的自定义协议是一样的 我们可以用一个东西:stringstream 进行分割字符串 用法如下 std::stringstream 在 C++ 中可以用于分割字符串,其基本原理是通过将字符串加载到流中,然后利用流操作符 ( ») 按照空格(或其他分隔符)逐步提取数据。这个过程利用了流的特性,流会根据输入格式将字符串转换为不同的数据类型。 分割字符串的原理

std::stringstream 分割字符串的过程可以简单理解为:

  1. 加载字符串 :将需要分割的字符串传递给 std::stringstream 对象,这样就可以使用流操作符读取字符串中的各个部分。
  2. 使用流提取符提取数据 :流操作符 (>>) 会逐个提取数据,并且在遇到空格或其他分隔符时停止提取。
  3. 重复提取直到流为空 :你可以反复使用流操作符来从流中提取多个数据,直到流的内容全部被提取完。

#include using namespace std; int main() { stringstream ss(“abc def ghi”); string s1, s2, s3; ss » s1 » s2 » s3; cout « s1 « endl; cout « s2 « endl; cout « s3 « endl; return 0; } https://i-blog.csdnimg.cn/direct/b01dfa20f87f4782b1879c799986b5e3.png 提取URL https://i-blog.csdnimg.cn/direct/bf480394e0c944499d9c3cc09d951256.png https://i-blog.csdnimg.cn/direct/ce0f5e74d29a4632b896aab9c7a7fed3.png 如果在访问的时候不指定目录,比如说我们要访问( ,那就会默认去访问www.baidu.com/)那就是默认会带一个/,代表着根目录,只会把网站的首页给你。也就是index.html。而不会把根目录下的所有内容给你。 https://i-blog.csdnimg.cn/direct/d6022a3b5ea44210915e6d6b7ccf3df0.png 那么我们也要实现这个思路,当你默认访问的时候设置把首页给你,当你指定访问的时候,就去访问对应的目录 const std::string homepage = “index.html”; //解析之后做判断 file_path = wwwroot; // ./wwwroot if(url == “/” || url == “/index.html”) { file_path += “/”; file_path += homepage; // ./wwwroot/index.html } else file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html https://i-blog.csdnimg.cn/direct/1223665a724c4fd2a9f825031ce6c935.png 可以看到,当我输入的url是:/a/b/c.html,服务器自动把路径给我填充为 ./wwwroot/a/b/c/d.html https://i-blog.csdnimg.cn/direct/e94dba25146d45eebc9ee82002a992f3.png 当我访问的是默认的话,服务器也会自动填充为首页目录 https://i-blog.csdnimg.cn/direct/3275f4f6edfb42f8b156c061a1f2b9b0.png 下一步就是我们新建别的目录 https://i-blog.csdnimg.cn/direct/2e584a14fb5e4358b4250fdaabd05273.png 测试结果: https://i-blog.csdnimg.cn/direct/f8223202690a4b9d9653502995bc842d.png 在下一步:可是对我们来说,我们也不这样访问呀,如果我们访问京东的时候,一般都是上一页,下一页,从一个链接跳转到另一个链接的去访问。这个时候我们就需要把URL给维护起来了 此时就需要这个代码了 让" “里面的内容具有超链接的作用。同样的,也能跳转到自己的地址,到第三张网页… 重点是理解HTTP的请求 ,无非就是通过套接字,读到了HTTP的请求,然后按照格式给解析出来,提取URL,给他响应再把响应返回去。HTTP底层就是TCP

HTTP的方法

同学们您们的请求全部都是get方法那么什么叫做GET方法呢? https://i-blog.csdnimg.cn/direct/4ddba16b398c45ab9a3f09bfa6406907.png 但是一般来说,最常用的就是 GETPOST GET方法一般用于获取某种资源信息,而POST方法一般用于将数据上传给服务器。但实际我们上传数据时也有可能使用GET方法,比如百度提交数据时实际使用的就是GET方法。 我们在日常使用某些网站的时候,是如何把数据提交给服务器的呢?日常又是怎么提交的呢? 其实数据都是通过表单提供的 https://i-blog.csdnimg.cn/direct/491c34bc0889490481cfbe5f46db6b86.png https://i-blog.csdnimg.cn/direct/dbaa846ce611487bada7b563e589d965.png GET 方法的提交方式 https://i-blog.csdnimg.cn/direct/ee63c297d6024c9cb0c6cc6e1612733d.png POST 方法的提交方式 https://i-blog.csdnimg.cn/direct/3e9dab618a26436794d1c390c35f38fd.png GET方法和POST方法都可以带参:

  • GET方法是通过url传参的。
  • POST方法是通过正文传参的。 从GET方法和POST方法的传参形式可以看出,POST方法能传递更多的参数,因为url的长度是有限制的,POST方法通过正文传参就可以携带更多的数据。 此外,使用POST方法传参更加私密,因为POST方法不会将你的参数回显到url当中,此时也就不会被别人轻易看到。不能说POST方法比GET方法更安全,因为POST方法和GET方法实际都不安全,要做到安全只能通过加密来完成。 其实提交参数的本质意义就是,fork子进程让他通过进程替换去执行这个程序,然后我们再通过进程间通信的方法将参数传递给他,然后让他完成相应的功能!!

HTTP的状态码

https://i-blog.csdnimg.cn/direct/1585760a6e814691b32a16a5135a7794.png 最常见的状态码, 比如 200(OK), 404(Not Found), 403(Forbidden), 302(Redirect, 重定向), 504(Bad Gateway) 404表示资源不存在 ,比如客户端请求的资源,服务器打开失败所以产生错误,是客户端的错误(所以一般来说我们除了要写一个网站首页的文件,还需要写一个如果请求失败的一个返回404页面的网页) 模拟下404界面的实现 https://i-blog.csdnimg.cn/direct/a620c9ea0f824527a2fe73ba41664be3.png 当收到的text为空时,说明没有找到路径,那就直接去找err.html提示404 Not Found https://i-blog.csdnimg.cn/direct/06e7d794575e45c1bdd1ee25288198b8.png 403是禁止访问 ,一般就是因为你没有授权然后去访问了服务端不让你访问的信息 504服务器错误 ,一般来说就是比如连接不上,或者是服务的线程创建失败 3XX 一般是由于我要访问的服务器可能处于某种原因无法为我提供服务,所以他会传一个地址告诉客户端你想要的资源应该去这里查找(通过参数Location),这就是重定向

Redirection详解 重定向 分为永久重定向和临时重定向 讲个故事:

比如说你的学校东门有一家火锅店非常有名,但是他需要长时间的装修,为了继续生意,他将生意开到了学校的西门,然后在东门这里贴了一张纸条“正在装修,请到西门用餐 ”,这个时候你和你的同学到东门时看到了这个纸条,于是就到西门去吃了了,过了一礼拜你们还打算去的时候,你知道东门那边只是暂时关闭,所以你还是会先去东门看看,如果还没开的话才会去西门,这个其实就是 临时重定向 ,因为我知道这个店只是暂时开到西门,随时可能会回来!

而后来东门开了的时候,为了能够让之前去西门吃饭的同学知道东门开启了,他会在之前西门的地方贴上“东门已经装修完成,以后请都前往东门用餐 ” 这个时候你们就知道这个店会一直在东门 ,这其实就是 永久重定向 !! 如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时你访问的直接就是重定向后的网站。而如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站。 所以有的网站时间比较老,做更新的时候需要把域名和网址换了,可是很多老用户并不知道,所以就会给老网站部署永久性定向服务,让用户直接跳转到新网站! //构建HTTP响应 string status_line = “http/1.1 307 Temporary Redirect\n”; //状态行 string response_header = “Location: \n”; //响应报头 string blank = “\n”; //空行 string response = status_line + response_header + blank; //响应报文 //响应HTTP请求 send(sock, response.c_str(), response.size(), 0); 当我们用telnet命令登录我们的服务器时,向服务器发起HTTP请求时,此时服务器给我们的响应就是状态码307,响应报头当中是Location字段对应的就是CSDN首页的网址 https://i-blog.csdnimg.cn/direct/c978056ce7f94a1290517aa804823db3.png telnet命令实际上只是一来一回 ,如果我们用浏览器访问我们的服务器,当浏览器收到这个HTTP响应后,还会对这个HTTP响应进行分析,当浏览器识别到状态码是307后就会提取出Location后面的网址,然后继续自动对该网站继续发起请求,此时就完成了页面跳转这样的功能。 此时当浏览器访问我们的服务器时,就会立马跳转到CSDN的首页。

https://i-blog.csdnimg.cn/direct/bfb06db19a814ed0a9f0503b8133dfc6.pngHTTP常见Header 以下是常见的Header

  • Content-Type: 数据类型(text/html等)
  • Content-Length: Body的长度
  • Host: 客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上;
  • User-Agent: 声明用户的操作系统和浏览器版本信息;
  • referer: 当前页面是从哪个页面跳转过来的;
  • location: 搭配3xx状态码使用, 告诉客户端接下来要去哪里访问;
  • Cookie: 用于在客户端存储少量信息. 通常用于实现会话(session)的功能;
  • connection:是否支持长连接(是基于服务费和客户端的版本去协商的)

Content-Type 读html的时候他是文本文件,那么就可以按照字符串读,但是如果我们是图片资源(png),那么就得按照二进制文件的方式来读!!因此在读取html之前需要确认一下文件格式,而不同的格式对应的content- type可以到网上搜索对照表。所以读网页之前必须确定一下后缀,然后不同的后缀就根据content-type对照表去找。 常见的Content-Type https://i-blog.csdnimg.cn/direct/03fb6b2a172c44e8ac10f97875a2c570.png Host Host字段表明了客户端要访问的服务的IP和端口,比如当浏览器访问我们的服务器时,浏览器发来的HTTP请求当中的Host字段填的就是我们的IP和端口。但客户端不就是要访问服务器吗?为什么客户端还要告诉服务器它要访问的服务对应的IP和端口? 因为有些服务器实际提供的是一种代理服务,也就是代替客户端向其他服务器发起请求,然后将请求得到的结果再返回给客户端。在这种情况下客户端就必须告诉代理服务器它要访问的服务对应的IP和端口,此时Host提供的信息就有效了。 connection 短连接其实就是一次请求响应一个资源,然后就关闭连接 长连接其实就是一次请求连接上之后可以一直服务直到服务结束再关闭连接 为什么要有长连接呢?? 因为一个巨大的网页上的元素是很多的,而每一个元素其实就是一个资源,所以我们在发出http请求申请到网页资源的时候,同时也需要把网页上附带的资源(比如图片、音频) 都申请了。 比如服务器要获取100个图片的网页,要发起101次HTTP的请求,对我们的服务器来说要一次性创建几十个线程,而HTTP底层是基于TCP的,TCP是面向连接的,是短连接!所以如果用短连接的话显然效率是不够高的!! https://i-blog.csdnimg.cn/direct/c6006d283b974f4093eca255ac343f86.png 长连接和短连接并没有绝对的优劣,只不过应用场景不一样!! 但是要注意的是,具体采用长连接还是短连接,是要基于双方的HTTP版本协商的,其中HTTP1.0其实就是短连接 而HTTP1.1其实就是长连接 要支持长连接的话必须要求双方的版本都是1.1 这样Connection就会呈现keep-alive表示支持长连接 Referer Referer代表的是你当前是从哪一个页面跳转过来的。Referer记录上一个页面的好处一方面是方便回退,另一方面可以知道我们当前页面与上一个页面之间的相关性。 Cookie HTTP实际上是一种无状态协议,HTTP的每次请求/响应之间是没有任何关系的,但你在使用浏览器的时候发现并不是这样的。 比如当你登录一次CSDN后,就算你把CSDN网站关了甚至是重启电脑,当你再次打开CSDN网站时,CSDN并没有要求你再次输入账号和密码,这实际上是通过cookie技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种cookie数据。 https://i-blog.csdnimg.cn/direct/ff6f7315c5c04c00958f88365210f273.png 这些cookie数据实际都是对应的服务器方写的,如果你将对应的某些cookie删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时所设置的cookie信息。 Cookie是什么 因为HTTP是一种无状态协议,如果没有cookie的存在,那么每当我们要进行页面请求时都需要重新输入账号和密码进行认证,这样太麻烦了。 首先我们要知道,HTTP默认是无状态的,而他之所以能够知道你你处在登录状态,是因为你之前登录的时候,在浏览器里形成了一个cookie文件,这个文件里存储着你在这个网站的认证信息,而当你打开这个网站时,浏览器向对应服务端发送请求的时候会将你的认证信息(就是用户名和密码)放在cookie参数里面带过去直接认证(认证其实就是拿着你的用户名和密码去他后端的数据库做搜索,所以你想使用的前提是必须得注册,才能在他的数据库里留存数据),认证通过之后会直接将你从原先的登录页面重定向到目标页面,这样你就不需要再次登录了!!当然这个保存一般是有时间限制的!! https://i-blog.csdnimg.cn/direct/339e410e5da7483f8970d854a92bd3c1.png https://i-blog.csdnimg.cn/direct/62e0aa06ce324ce9940ef3271287bcb6.png 从第一次登录认证之后,浏览器再向该网站发起的HTTP请求当中就会自动包含一个cookie字段,其中携带的就是我第一次的认证信息,此后对端服务器需要对你进行认证时就会直接提取出HTTP请求当中的cookie字段,而不会重新让你输入账号和密码了。也就是在第一次认证登录后,后续所有的认证都变成了自动认证,这就叫做cookie技术。也叫做HTTP会话保持功能 内存级别&文件级别 cookie就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。cookie文件可以分为两种,一种是内存级别的cookie文件,另一种是文件级别的cookie文件。

  • 将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的cookie信息是内存级别的。
  • 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的cookie信息是文件级别的。

cookie被盗 如果你浏览器当中保存的cookie信息被非法用户盗取了,那么此时这个非法用户就可以用你的cookie信息,以你的身份去访问你曾经访问过的网站,我们将这种现象称为cookie被盗取了。 比如你不小心点了某个链接,这个链接可能就是一个下载程序,当你点击之后它就会通过某种方式把程序下载到你本地,并且自动执行该程序,该程序会扫描你的浏览器当中的cookie目录,把所有的cookie信息通过网络的方式传送给恶意方,当恶意方拿到你的cookie信息后就可以拷贝到它的浏览器对应的cookie目录当中,然后以你的身份访问你曾经访问过的网站。 所以我们会面临两个问题(1)cookie被盗取 (2)个人信息泄露 我们要知道我们小白用户的防范能力基本为0,他们的电脑在黑客的眼里其实就相当于裸奔,比如你不小心点开了一个病毒,你或许可以通过杀毒软件去杀毒,但是也很有可能在你清理这个病毒之前你的信息就已经被窃取了!! 所以显然不能让客户端来维护这个安全问题,必须由更专业的服务端来维护! ! SessionID 所以引入了session技术,我们输入用户名密码的时候,他会将这个用户名密码存在服务端,然后生成一个session id(数字指纹)返回给客户端存在cookie文件里,然后服务端会将session id管理起来(redis),这样认证的时候就会用cookie文件里面存储的session id去服务端做对比,只要通过了就可以直接登录了!! https://i-blog.csdnimg.cn/direct/9c5930d75a2247aaa2e44ec3c66b1e7e.png 当我们第一次登录某个网站输入账号和密码后,服务器认证成功后还会服务端生成一个对应的SessionID,这个SessionID与用户信息是不相关的。系统会将所有登录用户的SessionID值统一维护起来。 此时当认证通过后服务端在对浏览器进行HTTP响应时,就会将这个生成的SessionID值响应给浏览器。浏览器收到响应后会自动提取出SessionID的值,将其保存在浏览器的cookie文件当中。后续访问该服务器时,对应的HTTP请求当中就会自动携带上这个SessionID。而服务器识别到HTTP请求当中包含了SessionID,就会提取出这个SessionID,然后再到对应的集合当中进行对比,对比成功就说明这个用户是曾经登录过的,此时也就自动就认证成功了,然后就会正常处理你发来的请求,这就是我们当前主流的工作方式。使用SessionID本质上是防止了个人信息泄露了。因为你的个人信息已经是在服务器上维护了,而不是在客户端上维护了 安全是相对的 引入SessionID之后,浏览器当中的cookie文件保存的是SessionID,此时这个cookie文件同样可能被盗取。此时用户的账号和密码虽然不会泄漏了,但用户对应的SessionID是会泄漏的,非法用户仍然可以盗取我的SessionID去访问我曾经访问过的服务器,相当于还是存在刚才的问题。

  • 之前的工作方式就相当于把账号和密码信息在浏览器当中再保存一份,每次请求时都自动将账号和密码的信息携带上,但是账号和密码一直在网当中发送太不安全了。
  • 因此现在的工作方式是,服务器只有在第一次认证的时候需要在网络中传输账号和密码,此后在网络上发送的都是SessionID。 这种方法虽然没有真正解决安全问题,但这种方法是相对安全的。但是跟之前最大的区别就是session id是由服务端统一管理的 ,也就意味着他具备回收、停用、甄别 异常等能力(比如说他发现你的ip地址突然发生了很大的变动 ,察觉异常就会暂时给你停止,等你发现问题了再次申诉的时候,他就会把你之前的session id清理掉然后重新分配 ) 这其实就是达到了控制客户端的目的!! 不过在安全领域有一个准则:如果破解某个信息的成本已经远远大于破解之后获得的收益(说明做这个事是赔本的),那么就可以说这个信息是安全的。

HTTP实现代码

HttpServer.hpp

#pragma once #include #include #include #include #include #include #include #include #include #include “Socket.hpp” #include “Log.hpp” const std::string wwwroot=”./wwwroot"; // web 根目录 const std::string sep = “\r\n”; const std::string homepage = “index.html”; static const int defaultport = 8082; class HttpServer; class ThreadData { public: ThreadData(int fd, HttpServer *s) : sockfd(fd), svr(s) { } public: int sockfd; HttpServer *svr; }; class HttpRequest { public: void Deserialize(std::string req) { while(true) { std::size_t pos = req.find(sep); if(pos == std::string::npos) break; std::string temp = req.substr(0, pos); if(temp.empty()) break; req_header.push_back(temp); req.erase(0, pos+sep.size()); } text = req; } // .png:image/png void Parse() { std::stringstream ss(req_header[0]); ss » method » url » http_version; file_path = wwwroot; // ./wwwroot if(url == “/” || url == “/index.html”) { file_path += “/”; file_path += homepage; // ./wwwroot/index.html } else file_path += url; // /a/b/c/d.html->./wwwroot/a/b/c/d.html auto pos = file_path.rfind("."); if(pos == std::string::npos) suffix = “.html”; else suffix = file_path.substr(pos); } void DebugPrint() { for(auto &line : req_header) { std::cout « “——————————–” « std::endl; std::cout « line « “\n\n”; } std::cout « “method: " « method « std::endl; std::cout « “url: " « url « std::endl; std::cout « “http_version: " « http_version « std::endl; std::cout « “file_path: " « file_path « std::endl; std::cout « text « std::endl; } public: std::vector req_header; std::string text; // 解析之后的结果 std::string method; std::string url; std::string http_version; std::string file_path; // ./wwwroot/a/b/c.html 2.png std::string suffix; }; class HttpServer { public: HttpServer(uint16_t port = defaultport) : _port(port) { content_type.insert({".html”, “text/html”}); content_type.insert({".png”, “image/png”}); content_type.insert({".jpg”, “image/jpeg”}); } bool Start() { _listensock.Socket(); _listensock.Bind(_port); _listensock.Listen(); for (;;) { std::string clientip; uint16_t clientport; int sockfd = _listensock.Accept(&clientip, &clientport); if (sockfd < 0) continue; lg(Info, “get a new connect, sockfd: %d”, sockfd); pthread_t tid; ThreadData td = new ThreadData(sockfd, this); pthread_create(&tid, nullptr, ThreadRun, td); } } static std::string ReadHtmlContent(const std::string &htmlpath) { // 坑 std::ifstream in(htmlpath, std::ios::binary); if(!in.is_open()) return “”; in.seekg(0, std::ios_base::end); auto len = in.tellg(); in.seekg(0, std::ios_base::beg); std::string content; content.resize(len); in.read((char)content.c_str(), content.size()); //std::string content; //std::string line; //while(std::getline(in, line)) //{ // content += line; //} in.close(); return content; } std::string SuffixToDesc(const std::string &suffix) { auto iter = content_type.find(suffix); if(iter == content_type.end()) return content_type[".html”]; else return content_type[suffix]; } void HandlerHttp(int sockfd) { char buffer[10240]; ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0); // bug if (n > 0) { buffer[n] = 0; std::cout « buffer « std::endl; // 假设我们读取到的就是一个完整的,独立的http 请求 HttpRequest req; req.Deserialize(buffer); req.Parse(); req.DebugPrint(); //std::string path = wwwroot; //path += url; // wwwroot/a/a/b/index.html // 返回响应的过程 std::string text; bool ok = true; text = ReadHtmlContent(req.file_path); // 失败? if(text.empty()) { ok = false; std::string err_html = wwwroot; err_html += “/”; err_html += “err.html”; text = ReadHtmlContent(err_html); } std::string response_line; if(ok) response_line = “HTTP/1.0 200 OK\r\n”; else response_line = “HTTP/1.0 404 Not Found\r\n”; //response_line = “HTTP/1.0 302 Found\r\n”; std::string response_header = “Content-Length: “; response_header += std::to_string(text.size()); // Content-Length: 11 response_header += “\r\n”; response_header += “Content-Type: “; response_header += SuffixToDesc(req.suffix); response_header += “\r\n”; response_header += “Set-Cookie: name=haha&&passwd=12345”; response_header += “\r\n”; //response_header += “Location: \r\n”; std::string blank_line = “\r\n”; // \n std::string response = response_line; response += response_header; response += blank_line; response += text; send(sockfd, response.c_str(), response.size(), 0); } close(sockfd); } static void *ThreadRun(void *args) { pthread_detach(pthread_self()); ThreadData *td = static_cast(args); td->svr->HandlerHttp(td->sockfd); delete td; return nullptr; } ~HttpServer() { } private: Sock _listensock; uint16_t _port; std::unordered_map content_type; };

HttpServer.cpp

#include “HttpServer.hpp” #include #include #include #include “Log.hpp” using namespace std; int main(int argc, char *argv[]) { if(argc != 2) { exit(1); } uint16_t port = std::stoi(argv[1]); // HttpServer *svr = new HttpServer(); // std::unique svr(new HttpServer()); std::unique_ptr svr(new HttpServer(port)); svr->Start(); return 0; }

Socket.hpp

#pragma once #include #include #include #include #include #include #include #include #include #include “Log.hpp” enum { SocketErr = 2, BindErr, ListenErr, }; // TODO const int backlog = 10; class Sock { public: Sock() { } ~Sock() { } public: void Socket() { sockfd_ = socket(AF_INET, SOCK_STREAM, 0); if (sockfd_ < 0) { lg(Fatal, “socker error, %s: %d”, strerror(errno), errno); exit(SocketErr); } int opt = 1; setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); } void Bind(uint16_t port) { struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(port); local.sin_addr.s_addr = INADDR_ANY; if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0) { lg(Fatal, “bind error, %s: %d”, strerror(errno), errno); exit(BindErr); } } void Listen() { if (listen(sockfd_, backlog) < 0) { lg(Fatal, “listen error, %s: %d”, strerror(errno), errno); exit(ListenErr); } } int Accept(std::string *clientip, uint16_t clientport) { struct sockaddr_in peer; socklen_t len = sizeof(peer); int newfd = accept(sockfd_, (struct sockaddr)&peer, &len); if(newfd < 0) { lg(Warning, “accept error, %s: %d”, strerror(errno), errno); return -1; } char ipstr[64]; inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr)); *clientip = ipstr; clientport = ntohs(peer.sin_port); return newfd; } bool Connect(const std::string &ip, const uint16_t &port) { struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(port); inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr)); int n = connect(sockfd_, (struct sockaddr)&peer, sizeof(peer)); if(n == -1) { std::cerr « “connect to " « ip « “:” « port « " error” « std::endl; return false; } return true; } void Close() { close(sockfd_); } int Fd() { return sockfd_; } private: int sockfd_; };

log.hpp

#pragma once #include #include #include #include #include #include #include #include #define SIZE 1024 #define Info 0 #define Debug 1 #define Warning 2 #define Error 3 #define Fatal 4 #define Screen 1 #define Onefile 2 #define Classfile 3 #define LogFile “log.txt” class Log { public: Log() { printMethod = Screen; path = “./log/”; } void Enable(int method) { printMethod = method; } std::string levelToString(int level) { switch (level) { case Info: return “Info”; case Debug: return “Debug”; case Warning: return “Warning”; case Error: return “Error”; case Fatal: return “Fatal”; default: return “None”; } } // void logmessage(int level, const char *format, …) // { // time_t t = time(nullptr); // struct tm *ctime = localtime(&t); // char leftbuffer[SIZE]; // snprintf(leftbuffer, sizeof(leftbuffer), “[%s][%d-%d-%d %d:%d:%d]”, levelToString(level).c_str(), // ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, // ctime->tm_hour, ctime->tm_min, ctime->tm_sec); // // va_list s; // // va_start(s, format); // char rightbuffer[SIZE]; // vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); // // va_end(s); // // 格式:默认部分+自定义部分 // char logtxt[SIZE * 2]; // snprintf(logtxt, sizeof(logtxt), “%s %s\n”, leftbuffer, rightbuffer); // // printf("%s”, logtxt); // 暂时打印 // printLog(level, logtxt); // } void printLog(int level, const std::string &logtxt) { switch (printMethod) { case Screen: std::cout « logtxt « std::endl; break; case Onefile: printOneFile(LogFile, logtxt); break; case Classfile: printClassFile(level, logtxt); break; default: break; } } void printOneFile(const std::string &logname, const std::string &logtxt) { std::string _logname = path + logname; int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // “log.txt” if (fd < 0) return; write(fd, logtxt.c_str(), logtxt.size()); close(fd); } void printClassFile(int level, const std::string &logtxt) { std::string filename = LogFile; filename += “.”; filename += levelToString(level); // “log.txt.Debug/Warning/Fatal” printOneFile(filename, logtxt); } ~Log() { } void operator()(int level, const char *format, …) { time_t t = time(nullptr); struct tm ctime = localtime(&t); char leftbuffer[SIZE]; snprintf(leftbuffer, sizeof(leftbuffer), “[%s][%d-%d-%d %d:%d:%d]”, levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec); va_list s; va_start(s, format); char rightbuffer[SIZE]; vsnprintf(rightbuffer, sizeof(rightbuffer), format, s); va_end(s); // 格式:默认部分+自定义部分 char logtxt[SIZE * 2]; snprintf(logtxt, sizeof(logtxt), “%s %s”, leftbuffer, rightbuffer); // printf("%s”, logtxt); // 暂时打印 printLog(level, logtxt); } private: int printMethod; std::string path; }; Log lg; // int sum(int n, …) // { // va_list s; // char // va_start(s, n); // int sum = 0; // while(n) // { // sum += va_arg(s, int); // printf(“hello %d, hello %s, hello %c, hello %d,”, 1, “hello”, ‘c’, 123); // n–; // } // va_end(s); //s = NULL // return sum; // }

Makefile

HttpServer:HttpServer.cc g++ -o $@ $^ -std=c++11 -lpthread .PHONY:clean clean: rm -f HttpServer

Web根目录

https://i-blog.csdnimg.cn/direct/0a0b16c8affa4e669904bdfdd09aa1a4.png

HTTPS协议了解

早期很多公司刚起步的时候,使用的应用层协议都是HTTP,而HTTP无论是用GET方法还是POST方法传参,都是没有经过任何加密的,因此早期很多的信息都是可以通过抓包工具抓到的。 为了解决这个问题,于是出现了HTTPS协议,HTTPS实际就是在应用层中加了一层加密层(SSL&TLS),这层加密层本身也是属于应用层的,它会对用户的个人信息进行各种程度的加密。HTTPS在交付数据时先把数据交给加密层,由加密层对数据加密后再交给传输层。 当然,通信双方使用的应用层协议必须是一样的,因此对端的应用层也必须使用HTTPS,当对端的传输层收到数据后,会先将数据交给加密层,由加密层对数据进行解密后再将数据交给应用层。 https://i-blog.csdnimg.cn/direct/09e49304ae694d579f694767887d6ae3.png 此时数据只有在用户层(应用层)是没有被加密的,而在应用层往下以及网络当中都是加密的,这就叫做HTTPS。

明文 与 密文

  • 明文 (Plaintext) :指的是未经加密的原始数据或信息。它是人类可以直接读取的内容,比如普通的文本文件、电子邮件内容、文件内容等。例如,“Hello, how are you?” 这个句子就是明文。
  • 密文 (Ciphertext) :指的是通过加密算法将明文转化后的形式,目的是保护数据的隐私。密文通常是无法直接理解的,只有通过解密操作才能还原成明文。例如,使用加密算法加密后,原来的文本可能变成类似“Xy2#&9kfj23”这样的内容。

对称加密 与 非对称加密

加密的方式可以分为对称加密和非对称加密:

  • 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密。
  • 采用公钥和私钥来进行加密和解密,用其中一个密钥进行加密就必须用另一个密钥进行解密,这种加密方法称为非对称加密。

对称加密 采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密,特征:加密和解密所用的密钥是相同的。 常见对称加密算法(了解):DES、3DES、AES、TDEA、Blowfish、RC2 等 特点:算法公开、计算量⼩、加密速度快、加密效率⾼ https://i-blog.csdnimg.cn/direct/52c6a81b050d42378ace7a15946026a0.png 对称加密其实就是通过同一个 “密钥” , 把明文加密成密文, 并且也能把密文解密成明文。 一个简单的对称加密, 按位异或:

  • 假设 明文 a = 1234, 密钥 key = 8888
  • 则加密 a ^ key 得到的密文 b 为 9834。
  • 然后针对密文 9834 再次进行运算 b ^ key, 得到的就是原来的明文 1234. (对于字符串的对称加密也是同理, 每一个字符都可以表⽰成一个数字)

非对称加密 https://i-blog.csdnimg.cn/direct/a677a30aeaf14d8db69899a0cb5c5bce.png 需要两个密钥来进行加密和解密,这两个密钥是公开密钥(public key,简称公钥)和私有密钥(private key,简称私钥)。 常见非对称加密算法(了解):RSA,DSA,ECDSA 特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快 • 通过公钥对明文加密, 变成密文 • 通过私钥对密文解密, 变成明文 或 • 通过私钥对明文加密, 变成密文 • 通过公钥对密文解密, 变成明文 非对称加密的数学原理比较复杂, 涉及到一些 数论 相关的知识. 这里举一个简单的生活上的例子。 A 要给 B 一些重要的文件, 但是 B 可能不在。

于是 A 和 B 提前做出约定: B 说: 我桌子上有个盒子, 然后我给你一把锁, 你把文件放盒子里用锁锁上, 然后我回头拿着钥匙来开锁取文件。

在这个场景中, 这把锁就相当于公钥, 钥匙就是私钥。 公钥给谁都行(不怕泄露), 但是 私钥只有 B 自己持有。持有私钥的人才能解密。 所以要注意的是:公钥不怕泄露,私钥不能泄露

数据摘要 && 数据指纹

数据摘要 && 数据指纹 数据摘要(又被称为数据指纹),其基本原理是利用单向散列函数(Hash函数)对数据进行运算,生成一串固定长度的数字摘要(散列值)。 摘要的特征:数字摘要并不是一种加密机制,数据摘要具有不可逆性,即无法从摘要推导出原始数据。 摘要的应用:数据摘要用于验证数据的完整性,确保数据在传输或存储过程中没有被修改。 摘要的常见算法:有MD5、SHA1、SHA256、SHA512等。 https://i-blog.csdnimg.cn/direct/4e05d60af8164edb8cbb260a3acced30.png https://i-blog.csdnimg.cn/direct/0be8d1cdc32347a591e10b1eca6640ea.png 数据签名 数据签名是在数据摘要的基础上添加了非对称的加密操作,用于验证数据的完整性和真实性。 数据签名包含了数据摘要、公钥密码学算法和数字证书等技术。发送者使用私钥对摘要进行加密,形成签名,接收者使用发送者的公钥对签名进行解密和验证。 数据签名不仅验证数据的完整性,还验证发送者的身份,确保数据的真实性和不可否认性。 https://i-blog.csdnimg.cn/direct/7f4a60b9fa72401a9c61b5a51821e0a2.gif