我们程序员写的一个个解决我们实际问题,满足我们日常需求的网络程序,都是在应用层。
前面写的套接字接口都是传输层经过对 UDP 和 TCP 数据发送能力的包装,以文件的形式呈现给我们,让我们可以进行应用层编程。换而言之,前面写的所有套接字代码全都属于应用层开发。
一、协议
协议本质就是一种 “约定”。socket api 的接口在读写数据时,都是按 “字符串” 的方式来发送接收的。
1、序列化与反序列化的概念
如果我们要传输一些 “结构化的数据” 怎么办呢?
通过前面的学习,知道了 TCP 是面向字节流的方式进行通信的。
如何保证刚度读到一个完整的数据呢?
举例:我们使用 QQ 发送消息时,别人接收到的不只有消息,还包含了用户昵称、头像信息、消息内容、发送时间等,这就叫做结构化的数据。这些结构化的数据可以打包成一个报文(变成一个整体),这个过程就叫作序列化,而把这个整体报文解开的过程就叫做反序列化。
结构化数据要先序列化,再发送到网络中,收到序列字节流后,要先反序列化再使用。而这里序列化和反序列化的过程用的就是业务协议。
2、自定义协议实现网络版计算器
例如,我们需要实现一个服务器版的加法器。我们需要客户端把要计算的两个加数发过去,然后由服务器进行计算,最后再把结果返回给客户端。
(1)约定方案
A. 约定方案一
- 客户端发送一个形如 "1+1" 的字符串。
- 这个字符串中有两个操作数,都是整形。
- 两个数字之间会有一个字符是运算符,运算符只能是 +。
- 数字和运算符之间没有空格。
- ... ...
B. 约定方案二
- 定义结构体来表示我们需要交互的信息。
- 发送数据时将这个结构体按照一个规则转换成字符串,接收到数据的时候再按照相同的规则把字符串转化回结构体,这个过程叫做 “序列化” 和 “反序列化”。
// proto.h 定义通信的结构体 typedef struct Request { int a; int b; char op; } Request; typedef struct Response { int result; int code; } Response; // client.c 客户端核心代码 Request request; Response response; scanf("%d,%d", &request.a, &request.b); write(fd, request, sizeof(Request)); read(fd, response, sizeof(Response)); // server.c 服务端核心代码 Request request; read(client_fd, &request, sizeof(request)); Response response; response.sum = request.a + request.b; write(client_fd, &response, sizeof(response));
无论是采用方案一,还是方案二,亦或是其他的方案,只要保证一端发送时构造的数据在另一端能够正确的进行解析,就是可以的。这种约定就是应用层协议。
(2)准备工作
- const std::string &:输入型参数
- std::string *:输出型参数
- std::string &:输入输出型参数
(3)服务端
自定义协议里要包含两各类,一个是请求,一个是响应。服务端会收到请求,客户端收到响应。
请求就是左操作符、右操作符和符号。
响应包含了退出码和结果,如果正常结束退出码为 0,如果有错误,可以自定义不同的退出码表示不同的错误。
为什么要有计算结果的状态码?
因为在计算的过程中可能会出现异常,比如除 0 或模 0,输入的操作码 op 不属于我们规定的符号。状态码为 0 表示计算结果正确,状态码为其它数字表示不同出错含义。
A. 服务端业务处理流程
服务端处理数据流程:
客户端发过来的数据已经序列化成了一个序列字节流数据(报文),所以服务端要先把报文反序列化,构成一个结构化请求对象 Request,然后就可以进行计算处理,形成一个 Response 对象,再序列化后发送给客户端。
可以看到计算处理这一步其实跟接收发送消息、序列化与反序列化没有什么关系,所以可以把计算处理任务在服务端启动时再传递进去。
计算处理函数:typedef std::function<bool(const Request& req, Response& resp)> func_t;
这里的 req 是输入型参数(已经反序列化好的对象),resp 是输出型参数,为了获取计算结果。
B. TCP 的发送与接收缓冲区
前面使用的 write 和 read 接口并不是直接往网络里发送数据或者从网络里读取数据。write 其实是把数据拷贝到传输层的缓冲区,由 TCP 协议决定什么时候把缓冲区的数据发送到网络中,所以 TCP 协议也叫作传输控制协议。
发送数据的本质是将数据从发送缓冲区拷贝到接收缓冲区。
所以,客户端 / 服务端发送数据不会影响接受数据。所以,TCP 是全双工的。
这就会导致一个问题,数据可能堆积在缓冲区来不及度,一次会读取多个报文挨在一起。那该如何保证读取完整报文呢?
看下面解释。
C. 保证读取完整报文
因为 TCP 是面向字节流的,所以要明确报文与报文的分界。
为什么要这样做呢?
举例:现在要把两个数字合并成字符串进行发送:1、12。如果不处理的话就是 "112",如果这样的话,反序列化时就不知道该怎么组合了。而如果我们选择在分割的地方加一个符号,比如 ,,那么在序列化后:"1,12",自热就很容易拆分了。
保证报文读取完整性的方法:
- 定长: 规定长度,每次就读取这么多。
- 特殊字符: 就是上面的方法。
- 自描述方式: 比如在报文前面带上四个字节的字段,标识报文长度。
D. 自定义协议 —— 序列化与反序列化
【请求】
我们想要的序列化结果 "x_ op_ y_"
这里的反序列化将传进去的字符串把 "\r\n" 去掉了。
【响应】
我们想要的序列化结果:"_code _result"
E. 计算流程
计算结果会形成一个 resp 响应,里面包含了退出码,后续可以自己设置退出码数值含义。
计算逻辑:
F. 在有效载荷前添加长度报头(协议报头)
使用特殊字符来对内容进行区分:
"x_ op_ y_" -> "length\r\nx_ _op_ y_\r\n" "code_ result_" -> "length\r\necode_ result_\r\n"
能够保证 length 里面不会出现 '\r' 或 '\n'吗?
能,因为 length 是一个整数,其内部不会出现任何的特殊符号。
G. 发送响应 send
服务端收到请求到把响应发送出去的整个流程:
那么这里的第一步是怎么读取请求的呢?
这个请求必须是恰好一个完整的请求。
H. 读取一个完整的报文 recv
收到的请求还需要去掉报头:
服务端的业务逻辑也就完成了:
(4)客户端
整体流程跟服务端差不多,就是序列化请求,添加报头,发送,接收响应,去掉报头,反序列化,获取结果。
(5)守护进程(精灵进程)
目前学到的所有服务器都是在前台运行的。
什么是前台?
和终端关联的进程就叫作前台。
判断一个进程是否为前台进程,取决于该进程能否正常获取输入,能否正常将输入的内容进行处理,那么对应的进程就是前台进程。90% 以上的情况下,bash 就是前台进程。
只要在终端下能够输入内容,能让我们输入内容的进程就叫作前台进程。
任何 XShell 登陆都只允许一个前台进程和多个后台进程。
什么是前台进程组?
任何时刻都只能有一个前台进程组,当我们登录 Windows时,就必须要给我们提供一个图形化界面,在 Linux 下就需要(前台进程组(可以只有一个进程))给我们加载 bash(一个任务),这就是为什么我们在登录时要有 Shell。
如果我们把后台进程提到前台,那么我们的 Shell 就无法运行了,是因为只能有一个前台进程组,bash 就会自己把自己投递到后台了,所以命令行解释器就用不了了。
所以,在命令行中启动一个进程,在会话中启动一个进程组来完成某种任务,所有会话内的进程 fork() 创建子进程,一般而言依旧属于当前会话。
tips:如果电脑使用的时间长了,那么当前会话占的资源就会比较多,所以就可能会卡,那么我们可以选择退出,注销一下账号,注销就是把这个会话之前申请的资源全部释放,然后再重新登陆,这就是为什么卡的时候可以选择注销(和重启类似,但有些任务不一定通过注销能解决) 。
进程除了有自己的 pid、ppid 以外,还有以一个组 ID。
- 它的 PPID 是1(附件特征)
- COMMAND:称为进程启动的命令
- TIME:进程启动时长的问题
- UID:是谁启动的(ls-n / ls-l 就可以看见用户的UID和我们看见的用户名是对应的,就像之前文件名和inode的映射一样)
- STAT:状态
- TPGID:当前进程组和终端相关的信息(-1 就是说这个进程和中单没有任何关系,具体数字就是和终端有关)
- TTY:代表是哪一个终端
- SID:当前进程的会话 id
在命令行中,同时用管道启动多个进程,多个进程是兄弟关系,父进程是 bash,所以它们之间可以用匿名管道来进行通信。
同时被创建的多个进程可以成为一个进程组的概念,一般第一个进程被称为组长。
仔细观察上图,可以发现还有一个 SID(会话 ID)。
最后一次登陆的用户需要有多个进程(组)来给这个它提供服务(bash),用户可以自己启动很多进程或进程组。将给用户提供服务的进程或者用户自己启动的所有进程或服务,整天都是要属于一个叫作会话的机制中的。
那么我们的网络服务器就不能属于这个会话内,否则就会受这个会话,用户登录和注销的影响(不一定会退出),所以在有网络服务的时候就应该脱离这个会话,让它在计算机里面形成一个新的会话(也就是自成进程组,自成新会话),自成一个会话这样的进程就被称为守护进程 / 精灵进程。
如何将自己变成自成会话呢?
调用 setsid():将调用进程设置成独立的会话。
注意:setsid 要被成功调用就必须保证当前进程不是进程组的组长。
如何保证我不是进程组的组长呢?
fork()
如何在 Linux 中正确的写一个让进程守护进程化的代码呢?
通过自己写一个函数,让我们的进程调用这个函数,自动变成守护进程。
守护进程不能直接向显示器打印消息,一旦打印就会被暂停、终止。
在 Linux 设备中,存在一个 /dev/null 的文件,它有一个特点:向其写入的所有内容都会被自动丢弃,想从该文件中读取内容,它不会阻塞且什么都不会让我们读到,如同 Linux 下的一个文件黑洞,可以让我们进行任意操作而不影响系统运行。
将我们的服务守护进程化,让它变成一个网络服务:
相当于服务部署到了 Linux 当中,哪怕是自己的 XShell 关了也可以 ./client 继续用,就只能用信号杀了(一般守护进程的命名是 d 结尾)
那就只能用 kill -9 杀了。
守护进程的父进程是 1 号进程,叫作被领养了,也就是说,守护进程本质是孤儿进程的一种。他和孤儿进程的区别:孤儿进程可能依旧属于某一个会话,而守护进程自成会话。
(6)代码
(7)结果显示
3、使用 Json 进行序列化和反序列化
序列化与反序列化 C++ 都给我们提供了 Json 的库,可以直接使用:
Json(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于 Web 应用程序中的数据传输。它是一种基于文本的格式,易于读写和解析。Json 格式的数据可以被多种编程语言支持,包括 JavaScript、Python、Java、C#、C++ 等。Json 数据由键值对组成,使用大括号表示对象,使用方括号表示数组。
先安装 Json 库:
sudo yum install jsoncpp-devel
头文件:#include <jsoncpp/json/json.h>
注意:使用 jsoncpp 库记得在编译时加上 -ljsoncpp。
⚪ demo 代码
A. Json::StyledWriter
B. Json::FastWriter(显示结果更加精简)
二、HTTP 协议
在前面我们已经实现了网络版的计算器中,其中对数据的处理计算就是我们自己手写的应用层协议。应用层是程序员基于 socket 接口之上编写的具体逻辑,做的很多工作都是和文本处理相关的(协议分析与处理),HTTP 协议具有大量的文本分析和协议处理。
在编写网络通信代码时,我们可以自己进行协议的定制,但实际有很多优秀的工程师早就写出了许多非常成熟且好用的应用层协议来供我们直接参考使用,其中最典型的就是 HTTP(超文本传输协议),它是一个简单的请求-响应协议,通常运行在 TCP 之上。
1、认识URL
URL 就是我们平时俗称的 “网址”。在全球范围内,只要找到 url 就能访问该资源。
协议名称://server ip[:80]/a/b/c/d/e.html
要访问一个服务器,ip 地址和端口号是必须要有的,有 ip 地址就可以找到这台唯一的机器,能够访问到端口号就可以找到给我们提供服务对应端口的进程,可是上图在 url 中并没有体现出来,是因为一般在请求时,端口号是被省略的(在请求网络服务时,对应的端口号都是众所周知的(客户端知道))。
使用浏览器访问 URL(统一资源定位符):通过域名(server ip)找到唯一一台网络主机,而域名后面就是该机器提供服务的进程,接着是客户想访问的资源路径,通过资源路径找到想要的文件名,可能是图片或者文本,把资源(客户想访问的资源路径 + 客户要的文件名)返回给浏览器。
HTTP 的本质就是通过 HTTP 协议从服务端拿下文件资源,而因为文件资源的种类特别多,HTTP 都能搞定,所以叫做超文本传输协议。
(1)urlencode 和 urldecode
像 / ? : 等这样的字符已经被 url 当做特殊意义理解了,所以这些字符不能随意出现。如果用户想在 url 中包含 url 本身用来作为特殊字符的字符,那么在 url 形式时,浏览器会自动给我们进行编码 encode。
转义的规则:取出字符的 ASCII 码,将其转成 16 进制,然后再在前面加上百分号即可。比如下图,"+" 被转成了 "%2B",这个过程就叫做 encode,decode 就是把特殊符号转回去。
urlencode 工具:UrlEncode编码/UrlDecode解码 - 站长工具
实际当服务器拿到对应的 URL 后,也需要对编码后的参数进行解码,此时服务器才能拿到你想要传递的参数,解码实际就是编码的逆过程。
2、HTTP协议格式
HTTP 是基于请求和响应的应用层服务,底层采用 TCP,作为客户端可以向服务器发起 request,服务器收到这个 request 之后,会对这个 request 做数据分析,得出你想要访问什么资源,然后服务器再构建 response,完成这一次 HTTP 的请求,返回响应。
由于 HTTP 是基于请求和响应的应用层访问,所以必须要知道 HTTP 对应的请求格式和响应格式。
CS 模式:
如何保证请求和响应被应用层完整读取?
HTTP 所有请求字段都是按行为单位的字符串,比如说对于 HTTP 请求,我们使用 while 循环按行读取,直到遇到空行为止,这样就可以保证把请求行和请求报头读完。而报头的 key: val 结构就有一个属性是 Content-Length: XXX,它表示的是正文的长度,由此,正文的也能完整读取了。
如果现在我们想获得 name 的 key 值,如何把数据从字符串中反序列化呢?
- 对于报头部分,其实请求 / 响应报头布置包含 name: val 信息,后边还有字符串分隔符:name: val\r\n,序列化直接发送就行,想要反序列化就可以按照 \r\n 来按行提取,所以 HTTP 报头是用特殊字符进行信息分离。
- 对于正文部分,不需要做处理,如果需要的话,可以设计自定义序列化与反序列化方案。
(1)HTTP 请求
- 首行:[方法] + [url] + [版本]
- Header:请求的属性,冒号分割的键值对,每组属性之间使用 \n 分隔,遇到空行表示 Header 部分结束。
- Body:空行后面的内容都是 Body,Body 允许为空字符串。如果 Body 存在,则在 Header 中会有一个 Content-Length 属性来标识 Body 的长度。
(2)HTTP 响应
- 首行:[版本号] + [状态码] + [状态码解释]
- Header:请求的属性,冒号分割的键值对,每组属性之间使用 \n 分隔,遇到空行表示 Header 部分结束。
- Body:空行后面的内容都是 Body,Body 允许为空字符串。如果 Body 存在,则在 Header 中会有一个 Content-Length 属性来标识 Body 的长度,如果服务器返回了一个 html 页面,那么 html 页面内容就是在 body 中。
3、HTTP 的请求方法
其中最常用的就是 GET 方法和 POST 方法。
我们平时上网的行为无非就分为两种:
- 从服务器端获取资源数据(GET)
- 把客户端的数据提交到服务器(POST、GET)
表单负责手机用户数据并把用户数据推送给服务器。表单中的数据会被转成 http request 的一部分,表单被提交需要指明提交方法。
比方说我们在百度里搜索东西,要把数据提交到对应的输入框里:
其实本质是前端通过 form 表单进行提交的,浏览器会自动将 form 表单里的内容转成 GET/POST 方法请求。
(1)GET 方法
GET 方法通过 URL 传递参数,回显到浏览器的域名当中。
(HTML 默认大小写不敏感)
这一块整体就是个 form 表单,可以通过 GET 方法提交。
可以看到 GET 方法可以把要提交的参数拼接到到 url 的后边:
(2)POST 方法
POST 方法通过请求正文提交参数。
(3)总结
因为 POST 方法是通过正文传参的,所以一般不会回显,用户看不到,私密性(不等于安全性,加解密才具有安全性)更好,而 GET 方法会回显输入的私密信息,不够私密。但无论是 GET 还是 POST 方法都不安全(HTTP 请求都是可以被抓到的,想要安全必须加密,使用 HTTPS 协议)。一般情况下,传递大字段或者较为私密的数据的时候使用 POST 方法,其他的使用 GET 方法。
4、HTTP 的状态码
一般情况下,HTTP 的状态码都要匹配上状态码的描述。
前面我们定义的状态行中的 200 就是状态码:
常见的状态码,比如 200(OK),404(Not Found),403(Forbidden),302(Redirect,重定向),504(Bad Gateway)
(1) 3xx —— Redirection(重定向状态码)
重定向就是通过各种方法将各种网络请求重新定个方向转到其它位置(跳转网站),此时这个服务器相当于提供了一个引路的服务。
当我们发送请求给服务端,服务端返回一个新的 url,状态码是 3,浏览器自动用这个新的 url 继续发送请求给新的地址,所以重定向是由客户端完成的。而重定向又分为临时重定向和永久重定向,其中状态码 301(Moved Permanently)表示的就是永久重定向,而状态码 302(Found)和 307(Temporary Redirect)表示的是临时重定向。
临时重定向和永久重定向本质是影响客户端的标签,决定客户端是否需要更新目标地址。
- 如果某个网站是永久重定向,那么第一次访问该网站时由浏览器帮你进行重定向,但后续再访问该网站时就不需要浏览器再进行重定向了,此时访问的就是重定向后的网站。
- 如果某个网站是临时重定向,那么每次访问该网站时如果需要进行重定向,都需要浏览器来帮我们完成重定向跳转到目标网站。
A. 临时重定向演示
当我们访问浏览器的时候自动会跳转到我们指定的网站:
5、HTTP 常见的 Header 信息
- Content-Type:数据类型(text / html 等)。
- Content-Length:Body 的长度。
- Host:客户端告知服务器, 所请求的资源是在哪个主机的哪个端口上。
- User-Agent:声明用户的操作系统和浏览器版本信息。
- referer:当前页面是从哪个页面跳转过来的。
- location:搭配 3xx 状态码使用,告诉客户端接下来要去哪里访问。
- Cookie:用于在客户端存储少量信息,通常用于实现会话(session)的功能。
(1) Content-Length
6、HTTP 会话保持(Cookie & Session)
HTTP 的特征:
- 简单快捷
- 无连接
- 无状态
HTTP 实际上是一种无状态协议,每次请求并不会记录它历史上请求过什么。HTTP 的每次请求/响应之间是没有任何关系的,但在使用浏览器时发现并不是这样的,比如在登录一次 CSDN 后,就算把 CSDN 网站关闭甚至重启电脑,当再次打开 CSDN 网站时,CSDN 并没有要求我们再次输入账号和密码,这实际上是通过 Cookie 技术实现的,点击浏览器当中锁的标志就可以看到对应网站的各种 Cookie 数据。
这些 cookie 数据实际都是对应的服务器方写的,如果你将对应的某些 cookie 删除,那么此时可能就需要你重新进行登录认证了,因为你删除的可能正好就是你登录时所设置的 cookie 信息。
结论:会话保持不是 HTTP 协议天然具备的特点,而是浏览器为了满足用户的使用需求,做了相应的工作。
如何做到的呢?
用户在第一次输入账号和密码时,浏览器会进行保存(Cookie),近期再次访问同一个网站(发送 http 请求),浏览器会自动将用户信息添加到报头中推送给服务器。这样只要用户首次输入密码,一段时间内将不用再做登录操作了。
这种把用户名和密码保存起来的技术叫做 Cookie 技术,而 Cookie 又分为 Cookie 内存和 Cookie 文件。
(1)内存级别与文件级别
Cookie 就是在浏览器当中的一个小文件,文件里记录的就是用户的私有信息。Cookie 文件可以分为两种,一种是内存级别的 Cookie 文件,另一种是文件级别的 Cookie 文件。
- 将浏览器关掉后再打开,访问之前登录过的网站,如果需要你重新输入账号和密码,说明你之前登录时浏览器当中保存的 Cookie 信息是内存级别的。
- 将浏览器关掉甚至将电脑重启再打开,访问之前登录过的网站,如果不需要你重新输入账户和密码,说明你之前登录时浏览器当中保存的 Cookie 信息是文件级别的(真实的文件,保存在磁盘,进程退出也不影响)。
(2)Cookie 安全问题
本地的 Cookie 如果被不法分子拿到了,那么此时这个非法用户就可以用我们的 Cookie 信息,以我们的身份去访问我们曾经访问过的网站,将这种现象称为 Cookie 被盗取了。
为了保证安全,我们可以把信息保存在服务端,在服务端形成一个文件:Session 文件,而因为有很多 Session 文件,所以给每个文件一个名字:Session ID。并将其返回给浏览器,浏览器存到 Cookie 的其实是 Session id。接下来我们把 Session ID 放到请求中,然后发送到服务端,在服务端获取登录信息(鉴权)。目前只保证了用户信息的泄漏,接下来只能靠服务端的安全策略保障安全,例如账号被异地登录了,服务端察觉后只要让 session id 失效即可,这样异地登录就会让用户重新验证账号密码或手机或人脸信息(尽可能确保是本人),一定程度上保障了信息的安全。
(3)写入 Cookie 信息
就是向发送给浏览器的响应中写入报头中。
7、HTTP 长连接
HTTP 请求是基于 TCP 协议的,而 TCP 是需要进行连接的。对于一个完整的网页来说,可能包含多种元素资源,那就需要发起多次 Connect。为了减少连接次数,需要客户端和服务器均支持长链接,建立一条连接,传输完后不断开连接,一直传递资源,不用频繁创建连接。如果是短连接请求了一份资源后就会自动关闭连接。
客户端和服务端怎么知道是否是长连接呢?
在报头信息中会有 Connection 字段。
Connection: keep-alive //支持长连接 Connection: close //短连接
8、简单的 HTTP 服务器
(1)代码验证请求格式
这里实现的就是一个简单的 TCP 服务器,而处理的任务就是把接收到的 HTTP 请求进行打印即可,服务器会把收到的数据全部放入请求缓冲区,然后直接打印出来。客户端并不用我们自己实现,有一个现成的客户端就是浏览器。
说明以下收到的请求:
对于请求行 GET / HTTP/1.1(Get 表示请求方法)
- / 表示 url:url 当中的 / 不能称之为我们云服务器上根目录,这个 / 表示的是 web 根目录,这个 web 根目录可以是我们机器上的任何一个目录,是可以自己指定的,不一定就是 Linux 的根目录。
- HTTP/1.1 表示协议版本。
注意 :此处我们使用 9090 端口号启动了 HTTP 服务器。虽然 HTTP 服务器一般使用 80 端口,但这只是一个通用的习惯,并不是说 HTTP 服务器就不能使用其他的端口号。使用 Chrome 测试我们的服务器时,可以看到服务器打出的请求中还有一个 GET /favicon.ico HTTP/1.1 这样的请求。
为什么请求要包含版本?
因为客户端的会存在更新的情况,但是有的客户端并没有更新,所以服务端要根据版本来提供不同的服务。
而请求报头进过验证也是 name: val 的格式,里面都是属性字段。
(2)代码验证响应格式
A. telnet 命令
telnet 是一种用于远程访问和管理计算机网络设备、服务器和服务的协议和命令行工具。它可以用于连接到运行 Telnet 服务器软件的任何计算机,并在远程计算机上执行命令和操作。
通常我们会使用该命令传参测试我们自己的服务器与其他的服务器是不是能正常访问。
telnet [ip地址] [端口] telnet 127.0.0.1 8080
当使用 Telnet 命令连接到远程 IP 地址和端口时,如果连接成功,则会返回响应:
- Trying 127.0.0.1…:表示正在尝试连接指定的 IP 地址
- Connected to 127.0.0.1.:表示连接已经建立
- Escape character is ‘^]’.:是提示信息,表示可以使用 Ctrl + ] 来退出 Telnet 命令。
(3)解析状态行信息
目的是把请求状态行的信息解析出来:
(4)web 根目录
上图的 url 中的 / 是 web 根目录,这个根目录可以自己设置,比如果我们就设置在当前路径下:
以后我们想要访问的资源就从 wwwroot 目录下开始,未来的所有资源放在这个目录里,可以通过 url 请求,例如:./wwwroot/a/b/c
如果直接是 ./wwwroot 呢?
此时就可以获得主页资源。
(5)获取服务器资源
读取资源其实就是读取文件。
此时,如果客户端只请求了一个 /,直接返回默认首页:
加几个资源文件来获取试试看:
三、HTTPS 协议
1、HTTPS 是什么
TLS/SSL:可选的,一般负责加密和解密。
HTTPS 也是⼀个应用层协议,是在 HTTP 协议的基础上引入了⼀个加密层。发送和接收必须用同一种方式(HTTP 或者 HTTPS)区分就用端口号。由于经过加密层,所以在 网络中是密文发送,在应用层是明文的,保证了数据在网络中的安全。
HTTP 协议内容都是按照⽂本的方式明文传输的,这就导致了在传输过程中出现⼀些被篡改的情况。
2、加密和解密
(1)什么是加密和解密
加密 就是把明文(要传输的信息)进行⼀系列变换,生成密文。
解密 就是把密文再进行⼀系列变换,还原成明文。
在这个加密和解密的过程中,往往需要⼀个或者多个中间的数据来辅助进行这个过程,这样的数据称为 密钥 。
安全 :破解的成本远远大于破解的收益。
举例:有 a 和 key,现在要对 a 加密,那么就让它们异或得到密文:b = a^key,当我想要把密文解密时,再异或一次 key 即可:a ^ key ^ key = a。这里我们把 a 叫做原始数据,b 叫做密文,key 叫做密钥。
加密解密到如今已经发展成⼀个独立的学科:密码学,而密码学的奠基人也正是计算机科学的祖师爷之⼀,艾伦·⻨席森·图灵,计算机领域中的最高荣誉就是以他名字命名的 “图灵奖” 。
(2)为什么要加密和解密
在下载时可能会出现的情况:明明要下载的是 A 软件,但实际下载下来的却是 B 软件。为什么会出现这种情况呢?
由于我们通过网络传输的任何的数据包都会经过运营商的网络络设备(路由器,交换机等),那么运营商的网络设备就可以解析出你传输的数据内容并进行篡改。点击 “下载按钮” 其实就是在给服务器发送了⼀个 HTTP 请求,获取到的 HTTP 响应其实就包含了该 APP 的下载链接。运营商劫持之后就发现这个请求是要下载 A 软件,那么就自动的把交给用户的响应给篡改成 B 软件的下载地址了。
因为 HTTP 的内容是明文传输的,明文数据会经过路由器、Wifi 热点、通信服务运营商、代理服务器等多个物理节点,如果信息在传输过程中被劫持,传输的内容就完全暴露了。劫持者还可以篡改传输的信息且不被双方察觉,这就是中间人攻击 ,所以才需要对信息进⾏加密。
不止运营商可以劫持,其他的黑客也可以用类似的手段进行劫持,以此来窃取用户的隐私信息或者篡改内容。如果黑客在用户登陆支付宝时获取到用户的账户余额,甚至获取到用户的支付密码等。
在互联网上,明文传输是比较危险的事情。
HTTPS 就是在 HTTP 的基础上进行了加密,进⼀步的来保证用户的信息安全。
(3)常见的加密方式
A. 对称加密
采用单钥密码系统的加密方法,同⼀个密钥可以同时用作信息的加密和解密,这种加密⽅法称为对称加密,也称为单密钥加密。
- 特征:加密和解密所用的密钥是相同的。
- 常见对称加密算法(了解):DES、3DES、AES、TDEA、Blowfish、RC2 等。
- 特点:算法公开、计算量小、加密速度快、加密效率高。
按位异或就是一个简单的对称加密。假设明⽂ a = 1234,密钥 key = 8888,则加密 a ^ key 得到的密⽂ b 为 9834,然后针对密文 9834 再次进行运算 b ^ key,得到的就是原来的明文 1234。(对于字符串的对称加密也是同理,每⼀个字符都可以表示成⼀个数字)
当然,按位异或只是最简单的对称加密,不过 HTTPS 中并不是使用按位异或。
B. 非对称加密
需要两个密钥 来进行加密和解密,这两个密钥是公开密钥 (public key,简称公钥)和私有密钥(private key,简称私钥)。
- 特征:公钥和私钥是配对的。最大的缺点就是运算速度非常慢,比对称加密要慢很多。
- 常见非对称加密算法(了解):RSA,DSA,ECDSA。
- 特点:算法强度复杂、安全性依赖于算法与密钥但是由于其算法复杂,而使得加密解密速度没有对称加密解密的速度快。
通过公钥对明文加密变成密文,通过私钥对密文解密变成明文。也可以反着用,通过私钥对明文加密变成密文,通过公钥对密文解密变成明文。
非对称加密的数学原理比较复杂,涉及到⼀些数论相关的知识。举例:A 要给 B ⼀些重要的文件,但是 B 可能不在,于是 A 和 B 提前做出约定:B 说:“我桌子上有个盒子,然后我给你⼀把锁,你把文件放盒子里用锁锁上,然后我回头拿着钥匙来开锁取⽂件。”
在上面这个场景中,这把锁就相当于公钥,钥匙就是私钥。公钥给谁都行(不怕泄露),但是私钥只有 B 自己持有,持有私钥的人才能解密。
如何理解加密的安全性?
不存在不可被破解的加密,我们可以从算力成本角度来分析:比如我们加密的成本是 100 块,而解密的花费却要 100 亿,这种我们就可以称为是安全的。
(4)数据摘要 && 数据指纹
现在有一篇很大的文章,我们可以通过哈希函数把这个文章处理成一个固定长度字符串,现在就选修改了一个标点符号,字符串也会变化。把这个固定长度的字符串就叫做 hash 摘要,而这个过程就叫做数据摘要。
数字指纹 (数据摘要),其基本原理是利用单向散列函数(Hash 函数)对信息进行运算,生成⼀串固定长度的数字摘要。任意的文本经过 Hash 形成的摘要都是不一样的。数字指纹并不是⼀种加密机制,但可以⽤来判断数据有没有被篡改。
- 摘要常见算法:MD5、SHA1、SHA256、SHA512 等。
- 算法把无限的映射成有限,因此可能会有碰撞(两个不同的信息,算出的摘要相同,但是概率非常低)。
- 摘要特征:和加密算法的区别是:摘要严格意义不是加密,因为没有解密,只不过从摘要很难反推原信息,通常用来进行前后数据对比,观察数据是否被修改过,也可以用于实现网盘的秒传功能、公司数据库密码存储等。
A. 网盘秒传功能
比方说我们很多人都想保存同一个电影到网盘中,如果网盘把每个人的请求全部保存起来, 那么会浪费很多空间。其实只用保存一份形成 hash 摘要,当另一个用户也要保存同一部电影时,要先形成摘要,然后在网盘中的一大堆摘要中进行对比,如果有这个摘要,那么直接建立映射关系即可,不需要再保存。
数据密码存储同理,把密码形成摘要,因为涉及到密码的东西都要进行加密。
每次用户登录时都将转换成哈希摘要与数据库的哈希摘要进行对比,所以数据库泄露也不怕。因为摘要是把无限变有限,所以可能存在碰撞,但是概率极低,就像指纹一样。
(5)数字签名
可能摘要的信息也不想让别人看到,把对数据摘要 再加密,就得到 数字签名。
3、HTTPS 的工作过程
既然要保证数据安全, 就需要进行加密。网络传输中不再直接传输明文了,而是加密之后的密文。加密的方式有很多,但是整体可以分成两大类:对称加密和非对称加密。
网络通信的过程中,需要解决以下两个问题:
- 数据被监听
- 数据被篡改
(1)方案一 —— 只使用对称加密
如果通信双方都各自持有同⼀个密钥 X,且没有别人知道,这两方的通信安全当然是可以被保证的(除非密钥被破解)。
引入对称加密之后,即使数据被截获,由于黑客不知道密钥是什么,也就无法进行解密,自然就不知道请求的真实内容了。如果通信双方只使用一个密钥进行加密通信,那么完全可以实现加密通信,除非密钥被破解。但实际没这么简单,服务器同一时刻其实是给很多客户端提供服务的,每个客户端用的秘钥肯定是不同的(如果是相同那密钥就太容易扩散了,黑客就也能拿到了),所以服务器就需要维护每个客户端和每个密钥之间的关联关系,这也是个很麻烦的事情。
比较理想的做法就是能在客户端和服务器建立连接时,双方协商确定这次的密钥是什么。
但是如果直接把密钥明文传输,那么黑客也就能获得密钥了,此时后续的加密操作就形同虚设了。因此密钥的传输也必须加密传输,但是要想对密钥进行对称加密,就仍然需要先协商确定一个个 “密钥的密钥”,这就成了 “先有鸡还是先有蛋” 的问题了,那此时密钥的传输再用对称加密就行不通了。
所以在进行正常加密数据通信之前,首先要解决的是密钥如何被对方安全的收到。
(2)方案二 —— 只使用非对称加密
非对称加密既可以使用公钥加密,也可以使用私钥加密。使用公钥加密必须使用私钥解密,使用私钥加密必须使用公钥解密。
这样就算中间人在通信过程中获取了公钥,但是没有私钥也无法进行解密,由此保证了从客户端发送给服务端数据的安全。
但是客户端给服务器发送的消息是不安全的,因为使用公钥加密的密文发给客户端,客户端没有私钥,解不了密,那能否在响应时把私钥传过去?
不行,因为私钥一但暴露到公网中就可能被劫持,黑客拿到私钥原地破解密文。
鉴于非对称加密的机制,如果服务器先把公钥以明文方式传输给浏览器,之后浏览器向服务器传数据前都先用这个公钥加密好再传,从客户端到服务器信道似乎是安全的(有安全问题),因为只有服务器有相应的私钥能解开公钥加密的数据。
服务器到浏览器的这条路该如何保障安全呢?
如果服务器用它的私钥加密数据传给浏览器,那么浏览器用公钥可以解密它,而这个公钥是⼀开始通过明文传输给浏览器的,若这个公钥被中间人劫持到了,那他也能用该公钥解密服务器传来的信息了。
(3)方案三 —— 双方都使用非对称加密
- 服务端拥有公钥 S 与对应的私钥 S',客户端拥有公钥 C 与对应的私钥 C'。
- 客户和服务端交换公钥。
- 客户端给服务端发信息:先用 S 对数据加密再发送,只能由服务器解密,因为只有服务器有私钥 S'。
- 服务端给客户端发信息:先用 C 对数据加密再发送,只能由客户端解密,因为只有客户端有私钥 C'。
这样做看似也行,但实则速度慢、效率低,其次是这样做也会有安全问题。
(4)方案四 —— 非对称加密 + 对称加密
A. 解决效率问题
使用非对称加密让双方知道对称密钥,后续再使用对称加密的方式进行通信。
只有首次是使用非对称加密,后续所有的通信都采用对称加密。对称加密的速度快,大大提高了通信速度。
- 服务端具有非对称公钥 S 和私钥 S'。
- 客户端发起 HTTPS 请求,获取服务端公钥 S。
- 客户端在本地生成对称密钥 C,通过公钥 S 加密,发送给服务器。
- 由于中间的网络设备没有私钥,即使截获了数据也无法还原出内部的原文,也就无法获取到对称密钥。
- 服务器通过私钥 S' 解密还原出客户端发送的对称密钥 C,并且使用这个对称密钥加密给客户端返回的响应数据。
- 后续客户端和服务器的通信都只用对称加密即可,由于该密钥只有客户端和服务器两个主机知道,其他主机/设备不知道密钥,所以即使截获数据也没有意义。
由于对称加密的效率比非对称加密高很多,因此只是在开始阶段协商密钥的时候使用非对称加密,后续的传输仍然使用对称加密,但依旧有安全问题。
(5)中间人攻击方式
Man-in-the-MiddleAttack,简称 “MITM 攻击”。
在方案二、三、四中,客户端获取到公钥 S 之后,对客户端形成的对称秘钥 X 用服务端给客户端的公钥 S 进行加密,中间人即使窃取到了数据,但此时中间⼈确实无法解出客户端形成的密钥 X,因为只有服务器有私钥 S'。但是中间人的攻击如果在最开始握手协商的时候就进行了,那就不⼀定了,假设 hacker 已经成功成为中间人。
- 服务器具有非对称加密算法的公钥 S、私钥 S'。
- 中间人具有非对称加密算法的公钥 M、私钥 M'。
- 客户端向服务器发起请求,服务器明文传送公钥 S 给客户端。
- 中间⼈劫持数据报⽂,提取公钥 S 并保存好,然后将被劫持报文中的公钥 S 替换成为自己的公钥 M,并将伪造报文发给客户端。
- 客户端收到报文,提取公钥 M(自己不知道公钥被更换过了),自己形成对称秘钥 X,用公钥 M 加密 X,形成报文发送给服务器。
- 中间人劫持后,直接用自己的私钥 M' 进行解密,得到通信秘钥 X,再用曾经保存的服务端公钥 S 加密后将报⽂推送给服务器。
- 服务器拿到报文,用自己的私钥 S' 解密,得到通信秘钥 X。
- 双方开始采用 X 进行对称加密进行通信,但这⼀切都在中间人的掌握中,劫持数据、进行窃听甚至修改都是可以的。
上面的攻击方案同样适用于方案二、三。
只要已经交换了密钥,中间人就来迟了,但中间人如果在最开始的时候就可以进行篡改替换。
中间人攻击能够成功,其本质是什么呢?
本质是中间人能够对数据做篡改且客户端无法确定收到的公钥是合法的,也无法确定其含有公钥的数据报文就是目标服务器发送过来的。
这样中间人也得到了 C,利用 C 先解密再加密后发送给服务端,那么即使修改了数据客户端和服务端也不知道中间人的存在。
该场景的本质问题是服务器在返回公钥的时候,被中间人截取并替换了公钥,并且客户端没有能力辨别公钥是否合法。 所以需要客户端具有判别公钥是否合法的能力。
(6)数字证书
为了解决上面的问题,Client 需要对服务器的合法性进行认证。
A. CA 认证
服务端在使用 HTTPS 前,需要向 CA 机构(权威机构)申领⼀份数字证书(CA 证书),数字证书里含有证书申请者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明服务端公钥的权威性, 是服务端公钥的身份证明。
这个证书可以理解成是⼀个结构化的字符串,只有证书是合法的时候才会进行非对称加密, 里面包含了以下信息:
- 证书发布机构
- 证书有效期
- 公钥
- 证书所有者
- 签名
- ......
注意:申请证书时需要在特定平台生成查,会同时生成⼀对密钥对,即公钥和私钥。这对密钥对就是用来在网络通信中进行明文加密以及数字签名的。
其中公钥会随着 CSR 文件,⼀起发给 CA 进行权威认证,私钥服务端自己保留,用来后续进行通信(其实主要就是用来交换对称密钥)。
在线生成 CSR 和私钥:CSR在线生成工具 (myssl.com)
形成 CSR 后,后续就是向 CA 进行申请认证,不过⼀般认证过程很繁琐,网络各种提供证书申请的服务商,⼀般真的需要,直接找平台解决就行。
B. 数据签名
签名的形成是基于非对称加密算法的,数据签名的本质是防止被篡改。
注意 :目前暂时和 https 没有关系,不要和 https 中的公钥私钥搞混了。
a. 签名过程
假设现在我们有了数据(比如明文信息),我们把这个数据进行摘要形成数据摘要(数据指纹),然后把数据指纹用签名者(比如 CA 机构)的私钥进行加密形成了签名,然后再把明文信息和签名放在一起形成了数字签名的数据(比如证书)。
b. 验证过程
首先把数据签名的数据分成数据和签名,然后先对数据进行相同的摘要方式形成数据摘要,然后把用公钥加密过的签名用私钥解密,得到数据摘要,两者比对即可。散列值不一样说明有人篡改了签名或者数据,散列值一样就说明没有被篡改过。
c. CA 证书的申请和验证
当服务端申请 CA 证书时,CA 机构会对该服务端进行审核,并专门为该网站形成数字签名,过程如下:
生成证书:
- CA 机构拥有非对称加密的私钥 A 和公钥 A'。
- CA 机构对服务端申请的证书明文数据进行 hash 摘要(公开的),形成数据摘要。
- CA 机构用 CA 私钥 A' 加密数据摘要,得到数字签名 S。
- CA 机构把明文数据和签名结合起来形成证书。
因为我们使用的是 CA 形成的数据签名,所以只有 CA 能形成可信任的证书,此时服务器会把证书响应给客户端,证书里面包含了公钥。
服务端申请的证书明文和数字签名 S 共同组成了数字证书,这样⼀份数字证书就可以颁发给服务端了。
验证证书合法性(公钥的合法性):
- 先看有没有过期。
- 把数据签名和明文信息分开。
- 对明文信息进行 hash 摘要(公开的),形成数据摘要。然后用 CA 的公钥把数据签名解密,得到了数据摘要,这里的公钥是哪来的呢?(CA 会在所有的浏览器中内置自己的公钥)
- 把两个数据摘要进行对比,相等就说明内容没有被篡改,不相等说明被篡改了。
有没有可能原文和签名全部都被替换了呢?原文中的公钥确实能被修改,那么签名呢?
改不了,因为私钥只有 CA 有,用自己的私钥的话浏览器不认识。
中间人能不能直接把整个证书替换掉?
首先因为浏览器里面内置了 CA 的私钥,那么我们替换的证书必须是一个真正的证书,因为假证书没有办法解密。而证书里面的域名信息,域名是唯一的,不可能一样。所以做不到整体替换。
(7)方案五 —— 非对称加密 + 对称加密 + 证书认证
在客户端和服务器刚建立连接时,服务器给客户端返回⼀个证书,证书包含了之前服务端的公钥,也包含了网站的身份信息,由此可以验证公钥的合法性。
所以非对称加密 + 对称加密保证了通信的安全,数字证书保证了通信之前交换密钥的安全。
A. 客户端进行认证
当客户端获取到这个证书之后,会对证书进行校验(防止证书是伪造的)。
- 判定证书的有效期是否过期。
- 判定证书的发布机构是否受信任(操作系统中已内置的受信任的证书发布机构)。
- 验证证书是否被篡改:从系统中拿到该证书发布机构的公钥,对签名解密得到⼀个 hash 值(数据摘要),设为 hash1。然后计算整个证书的 hash 值设为 hash2,对比 hash1 和 hash2 是否相等。如果相等,则说明证书是没有被篡改过的。
B. 查看浏览器的受信任证书发布机构
Chrome 浏览器:
(8)常见问题
中间人有没有可能篡改该证书?
- 中间⼈篡改了证书的明文。
- 由于他没有 CA 机构的私钥,所以⽆法 hash 之后⽤私钥加密形成签名,那么也就没法办法对篡改后的证书形成匹配的签名。
- 如果强行篡改,客户端收到该证书后会发现明⽂和签名解密后的值不一致,则说明证书已被篡改,证书不可信,从而终止向服务器传输信息,防止信息泄露给中间人。
中间人整个掉包证书?
- 因为中间⼈没有 CA 私钥,所以无法制作假的证书。
- 所以中间⼈只能向 CA 申请真证书,然后用自己申请的证书进行掉包。
- 这个确实能做到证书的整体掉包,但是证书明文中包含了域名等服务端认证信息,如果整体掉包,客户端依旧能够识别出来。
- 记住:中间人没有 CA 私钥,所以对任何证书都无法进行合法修改,包括自己的。
为什么摘要内容在网络传输时一定要加密形成签名?
常见的摘要算法有:MD5 和 SHA 系列。
以 MD5 为例,我们不需要研究具体的计算签名的过程,只需要了解 MD5 的特点:
- 定长:无论多长的字符串,计算出来的 MD5 值都是固定长度(16 字节版本或者 32 字节版本)。
- 分散:源字符串只要改变⼀点点,最终得到的 MD5 值都会差别很大。
- 不可逆:通过源字符串⽣成 MD5 很容易,但是通过 MD5 还原成原串理论上是不可能的。
正因为 MD5 有这样的特性,我们可以认为如果两个字符串的 MD5 值相同,则认为这两个字符串相同。
理解判定证书篡改的过程(好比如判定这个⾝份证是不是伪造的身份证):
- 假设我们的证书只是⼀个简单的字符串 hello,对这个字符串计算 hash 值(比如 md5),结果为 BC4B2A76B9719D91
- 如果 hello 中有任意的字符被篡改了,比如变成了 hella, 那么计算的 md5 值就会变化很大,BDBD6F9CF51F2FD8
- 然后我们可以把这个字符串 hello 和 哈希值 BC4B2A76B9719D91 从服务器返回给客⼾端, 此时客户端如何验证 hello 是否是被篡改过?那么就只要计算 hello 的哈希值,看看是不是 BC4B2A76B9719D91 即可。
但还有个问题,如果黑客把 hello 篡改了,同时也把哈希值重新计算了一下,那么客户端就分辨不出来了。
被传输的哈希值不能传输明文,需要传输密文。所以,对证书明文("hello")hash 形成散列摘要,然后 CA 使用自己的私钥加密形成签名,将 hello 和加密的签名合起来形成 CA 证书颁发给服务端,当客户端请求时就发送给客户端,中间人截获了,因为没有 CA 私钥,就无法更改或者整体掉包,就能安全的证明证书的合法性。
最后,客户端通过操作系统⾥已经存的了的证书发布机构的公钥进行解密,还原出原始的哈希值,再进行校验。
为什么签名不是选择直接加密,而是要先 hash 形成摘要?
缩小签名密文的长度,加快数字签名的验证签名的运算速度。
如何成为中间人?(了解)
- ARP 欺骗:在局域网中,hacker 经过收到 ARP Request 广播包,能够偷听到其它节点的(IP, MAC)地址。例如:黑客收到两个主机 A、B 的地址,告诉 B(受害者),自己是 A,使得 B 在发送给 A 的数据包都被黑客截取。
- ICMP 攻击:由于 ICMP 协议中有重定向的报文类型,我们就可以伪造⼀个 ICMP 信息,然后发送给局域网中的客户端并伪装自己是⼀个更好的路由通路,从而导致目标所有的上网流量都会发送到我们指定的接口上,达到和 ARP 欺骗同样的效果。
- 假 Wifi && 假网站等。
(9)完整流程
左侧都是客户端做的事情,右侧都是服务器做的事情:
4、总结
HTTPS 整个工作过程中涉及的密钥有三组:
- 第⼀组(非对称加密):⽤于校验证书是否被篡改. 服务器持有私钥(私钥在形成CSR⽂件与申请证书时获得),客户端持有公钥(操作系统包含了可信任的 CA 认证机构有哪些,同时持有对应的公钥)。服务器在客户端请求时,返回携带签名的证书。客户端通过这个公钥进行证书验证,保证证书的合法性,进⼀步保证证书中携带的服务端公钥权威性。
- 第⼆组(非对称加密):用于协商⽣成对称加密的密钥。客户端用收到的 CA 证书中的公钥(是可被信任的)给随机生成的对称加密的密钥加密,传输给服务器,服务器通过私钥解密获取到对称加密密钥。
- 第三组(对称加密):客户端和服务器后续传输的数据都通过这个对称密钥加密解密。
其实⼀切的关键都是围绕这个对称加密的密钥,其他的机制都是辅助这个密钥工作的。
第⼆组非对称加密的密钥是为了让客户端把这个对称密钥传给服务器,第⼀组非对称加密的密钥是为了让客户端拿到第⼆组非对称加密的公钥。