# 五、应用层
# 1. DNS
# 简介
DNS(Domain Name System,域名系统)是用于将域名转为 IP 地址的协议。
# 域名
# 顶级域
国家
- cn:中国
- us:美国
- uk:英国
- ca:加拿大
通用:
- com:商业机构
- net:网络服务机构
- gov:政府
- org:非营利性组织
# 二级域
- aliyun
- taobao
- baidu
# 原理
参考:https://zhuanlan.zhihu.com/p/79350395
- 浏览器输入地址
- 然后浏览器这个进程去调操作系统某个库里的 gethostbyname 函数(例如,Linux GNU glibc标准库的 gethostbyname 函数)
- 然后这个函数通过网卡给 DNS 服务器发 UDP 请求
- 接收结果
- 然后将结果给返回给浏览器
特殊:
- (1) 我们在用 Chrome 浏览器的时候,其实会先去浏览器的 DNS 缓存里头查询,DNS 缓存中没有,再去调用 gethostbyname 函数
- (2) gethostbyname 函数在试图进行 DNS 解析之前首先检查域名是否在本地 Hosts 里,如果没找到再去 DNS 服务器上查。
UDP 在哪里使用? TCP 在哪里使用?
- gethostbyname 函数通过网卡发送请求给 DNS 服务器解析域名的时候使用 UDP。
- 区域传送的时候使用 TCP DNS 的规范规定了 2 种类型的 DNS 服务器,一个叫主 DNS 服务器,一个叫辅助 DNS 服务器(其实就是一份备份)。在一个区中主 DNS 服务器从自己本机的数据文件中读取该区的 DNS 数据信息,而辅助 DNS 服务器则从区的主 DNS 服务器中读取该区的 DNS 数据信息。当一个辅助 DNS 服务器启动时,它需要与主 DNS 服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。 这种情况下,使用 TCP 协议。
# DNS 在域名解析上是用 UDP 还是 TCP?
UDP。
# 域名解析为什么要用 UDP?
因为 UDP 快!UDP 的 DNS 协议只要一个请求、一个应答就好了。而使用基于 TCP 的 DNS 协议要三次握手、发送数据以及应答、四次挥手。但是 UDP 协议传输内容不能超过 512 字节。不过客户端向 DNS 服务器查询域名,一般返回的内容都不超过 512 字节,用 UDP 传输即可。
# 区域复制为什么要用 TCP?
因为 TCP 协议可靠性好!你要从区域的主 DNS 上复制内容,必须保证可靠性。同时 TCP 协议传输的内容大,UDP 最大只能传 512 字节,同步的数据很可能会大于 512 字节。
# 为什么 UDP 最大只能传 512 字节?
- 纠正:UDP 最大不止传 512 字节。 以太网帧在局域网中的 MTU 是 1500byte,但是在非局域网环境,如:internet 下的时候,MTU 是各个路由器进行一个配置的。所以,通常路由器默认的 MTU 为576字节。所以,为了适应网络环境,DNS 协议在返回的数据报大于 512 的时候,就转化为了 TCP 协议。
# UDP 的最大传输长度讨论
UDP 允许传输的最大长度理论上 2^16 - udp head - iphead( 65507 字节 = 65535 - 20 - 8)
但是实际上 UDP 数据报的数据区最大长度为1472(1500-20-8)字节,理由如下:
首先,我们知道,TCP/IP 通常被认为是一个四层协议系统,包括链路层、网络层、运输层和应用层。
UDP 属于运输层,下面我们由下至上一步一步来看:
以太网(Ethernet)数据帧的长度必须在 46-1500 字节之间,这是由以太网的物理特性决定的。这个 1500 字节被称为链路层的 MTU(最大传输单元)。 但这并不是指链路层的长度被限制在 1500 字节,其实这这个 MTU 指的是链路层的数据区。 并不包括链路层的首部和尾部的 18 个字节。 所以,事实上,这个 1500 字节就是网络层 IP 数据报的长度限制。 因为 IP 数据报的首部为 20 字节,所以 IP 数据报的数据区长度最大为 1480 字节。
而这个 1480 字节就是用来放 TCP 传来的 TCP 报文段或 UDP 传来的 UDP 数据报的。 又因为 UDP 数据报的首部 8 字节,所以 UDP 数据报的数据区最大长度为 1472 字节。这个 1472 字节就是我们可以使用的字节数。进行 Internet 编程时则不同,因为 Internet 上的路由器可能会将 MTU 设为不同的值。
如果我们假定 MTU 为 1500 来发送数据的,而途经的某个网络的 MTU 值小于 1500 字节,那么系统将会使用一系列的机制来调整 MTU 值,使数据报能够顺利到达目的地,这样就会做许多不必要的操作。鉴于 Internet 上的标准 MTU 值为 576 字节,建议在进行 Internet 的 UDP 编程时,最好将 UDP 的数据长度控件在 548 字节(576-8-20)以内。
# 查询方式
# DNS递归查询
# DNS迭代查询
# 2. DHCP
# 简介
DHCP(Dynamic Host Configuration Protocol,动态主机设置协议)通常被应用在大型的局域网络环境中,主要作用是集中的管理、分配 IP 地址,使网络环境中的主机动态的获得 IP 地址、Gateway 地址、DNS 服务器地址等信息,并能够提升地址的使用率。
- 即插即用联网
# 特点
- DHCP 是一个局域网协议。
- DHCP 是应用 UDP 协议的应用层协议。
# 分配
# 自动分配方式
DHCP 服务器为主机指定一个永久性的 IP 地址,一旦 DHCP 客户端第一次成功从 DHCP 服务器端租用到IP地址后,就可以永久性的使用该地址。
# 动态分配方式
DHCP 服务器给主机指定一个具有时间限制的IP地址,时间到期或主机明确表示放弃该地址时,该地址可以被其他主机使用。
# 手工分配方式
客户端的 IP 地址是由网络管理员指定的,DHCP 服务器只是将指定的 IP 地址告诉客户端主机。
# 原理
DHCP 协议采用 UDP 作为传输协议,主机发送请求消息到 DHCP 服务器的 67 号
端口,DHCP 服务器回应应答消息给主机的 68 号
端口。详细的交互过程如下图:
1、DHCP Client 以广播的方式发出 DHCP Discover
报文。
2、所有的 DHCP Server 都能够接收到 DHCP Client 发送的 DHCP Discover 报文,所有的 DHCP Server 都会给出响应,向 DHCP Client 发送一个 DHCP Offer
报文。
DHCP Offer 报文中 “Your(Client) IP Address” 字段就是 DHCP Server 能够提供给 DHCP Client 使用的IP地址,且 DHCP Server 会将自己的 IP 地址放在 “option” 字段中以便 DHCP Client 区分不同的 DHCP Server。DHCP Server 在发出此报文后会存在一个已分配IP地址的纪录。
3、DHCP Client 只能处理其中的一个 DHCP Offer 报文,一般的原则是 DHCP Client 处理最先收到
的 DHCP Offer 报文。
DHCP Client 会发出一个广播的 DHCP Request
报文,在选项字段中会加入选中的 DHCP Server 的 IP 地址和需要的 IP 地址。
4、DHCP Server 收到 DHCP Request 报文后,判断选项字段中的 IP 地址是否与自己的地址相同。
- 如果不相同,DHCP Server 不做任何处理只清除相应 IP 地址分配记录;
- 如果相同,DHCP Server 就会向 DHCP Client 响应一个
DHCP ACK
报文,并在选项字段中增加 IP 地址的使用租期信息。
5、DHCP Client 接收到 DHCP ACK 报文后,检查 DHCP Server 分配的 IP 地址是否能够使用。
- 如果可以使用,则 DHCP Client 成功获得 IP 地址并根据 IP 地址使用租期自动启动续延过程;
- 如果 DHCP Client 发现分配的 IP 地址已经被使用,则 DHCP Client 向 DHCPServer 发出
DHCP Decline
报文,通知 DHCP Server 禁用这个 IP 地址,然后 DHCP Client 开始新的地址申请过程。
6、DHCP Client 在成功获取 IP 地址后,随时可以通过发送 DHCP Release
报文释放自己的IP地址,DHCP Server 收到 DHCP Release 报文后,会回收相应的IP地址并重新分配。
# 3. HTTP
# 简介
HTTP(Hypertext Transfer Protocol,超文本传输协议)是用于从万维网服务器传输超文本到本地浏览器的传送协议。
# 特点
- 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
- 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
- 无连接:限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
- 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
- 支持 B/S 及 C/S 模式
请简述 B/S 和 C/S 的区别?
B/S 是浏览器/服务器的英文缩写,是一种通过浏览器访问服务器端数据的软件形式,其特点是用户不用在本地安装软件,只要有一个浏览器即可使用产品,而且用户不用关心软件的升级更新等问题。到由于受限于网络等因素,这种模式的系统访问速度较慢。
C/S 则是客户端/服务器的英文缩写,这类软件的使用者需要在本地电脑安装客户端程序,就像 QQ。其特点就是访问速度快,界面优雅。但一旦软件有更新,用户需要手动下载,较为不便。而且在没有安装客户端的电脑上,用户无法使用系统。
硬件环境不同
C/S 一般建立在小范围里的网络环境,局域网之间在通过专门服务器提供链接和数据交换服务。 B/S 建立在广域网之上的,不必是专门的网络硬件环境,比如:电话上网,租用设备,信息自己管理有比 C/S 更强的适应范围,一般只要有操作系统和浏览器就行。
对安全要求不同
C/S 一般建立在抓用的网络上,小范围里的网络环境,局域网之间在通过专门服务器提供链接和数据交换服务。 B/S 建立在广域网之上的,不必是专门的网络硬件环境,比如:电话上网,租用设备,信息自己管理有比 C/S 更强的适应范围,一般只要有操作系统和浏览器就行
# 请求结构
# 响应结构
# 状态码
- 1xx 消息——请求已被服务器接收,继续处理
- 2xx 成功——请求已成功被服务器接收、理解、并接受
- 3xx 重定向——需要后续操作才能完成这一请求
- 4xx 请求错误——请求含有词法错误或者无法被执行
- 5xx 服务器错误——服务器在处理某个正确请求时发生错误
常见状态码
- 200 OK:正常返回信息
- 301:当前资源已经被删除了,重定向到另外一个位置,后面还是需要重定向。
- 302:当前资源暂时不可用,先重定向到另外一个位置,后面可能不需要重定向。
- 400 Bad Request:客户端请求有语法错误,不能为服务器所理解
- 401 Unauthorized:请求未经授权,这个状态码必须和 WWW-Authenticate 报头域一起使用(没认证)
- 403 Forbidden:服务器收到请求,但是拒绝提供服务(认证了但是没权限)
- 404 Not Found:请求资源不存在,一般是 URL 输错了
- 500 Internal Server Error:服务器发生不可预期的错误
- 503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常
# 工作原理
HTTP 协议定义 Web 客户端如何从 Web 服务器请求 Web 页面,以及服务器如何把 Web 页面传送给客户端。HTTP 协议采用了请求/响应
模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:
客户端连接到 Web 服务器 一个 HTTP 客户端,通常是浏览器,与 Web 服务器的 HTTP 端口(默认为 80 )建立一个 TCP 套接字连接。例如,http://www.baidu.com (opens new window)。
发送 HTTP 请求 通过 TCP 套接字,客户端向 Web 服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据 4 部分组成。
服务器接受请求并返回 HTTP 响应 Web 服务器解析请求,定位请求资源。服务器将资源复本写到 TCP 套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据 4 部分组成。
释放连接 TCP 连接 若 connection 模式为 close,则服务器主动关闭 TCP 连接,客户端被动关闭连接,释放 TCP 连接;
若 connection 模式为 keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
客户端浏览器解析 HTML 内容 客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的 HTML 文档和文档的字符集。客户端浏览器读取响应数据 HTML,根据 HTML 的语法对其进行格式化,并在浏览器窗口中显示。
在浏览器地址栏键入URL,按下回车之后会经历以下流程:
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;
- 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
- 释放 TCP 连接;
- 浏览器将该 html 文本并显示内容;
# 请求方式
- GET
- POST
- DELETE
- PUT
- BATCH
GET 请求和 POST 请求的区别:
- HTTP 报文层面:GET 将请求信息放在 URL, POST 放在报文体中。
- 数据库层面:GET 符合幂等性和安全性(GET 是做查询操作),POST 不符合(POST 是修改)。
- 其他层面:GET 可以被缓存、被存储,而 POST 不行。
- 数据长度方面:GET 因为请求信息放在 URL,URL 具有长度限制,所以 GET 请求长度有限,而 POST 没有限制。
- GET 产生一个 TCP 数据包;POST 产生两个 TCP 数据包 对于 GET 方式的请求,浏览器会把 http header 和 data 一并发送出去,服务器响应 200(返回数据); 而对于 POST,浏览器先发送 header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据)。
- 安全性方面:GET 安全性非常低,POST 安全性较高。 但是如果没有加密,他们安全级别都是一样的,随便一个监听器都可以把所有的数据监听到。
# Cookie
# 简介
Cookie 是由服务器发送给客户端的特殊信息,以文本的形式存放在客户端。
客户端再次请求时,会把 Cookie 回发。
服务器接收到后,会解析 Cookie 生成与客户端相对应的内容。
# Cookie 的设置以及发送过程
cookie 和 session 的区别?
- cookie 数据存放在客户的浏览器上,session 数据放在服务器上;
- cookie 不是很安全,别人可以分析存放在本地的 cookie 并进行 cookie 欺骗,如果主要考虑到安全应当使用 session;
- session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用 cookie;
- 单个 cookie 在客户端的限制是 3K,就是说一个站点在客户端存放的 cookie 不能 3K;而 session 则存储与服务端,浏览器对其没有限制。所以将登陆信息等重要信息存放为 session;其他信息如果需要保留可以放在 cookie 中。
cookie、session 和 token 的区别?
session 和 oauth token并不矛盾,作为身份认证 token 安全性比 session 好,因为每个请求都有签名还能防止监听以及重放攻击,而session 就必须靠链路层来保障通讯安全了。
如果你需要实现有状态的会话,仍然可以增加 session 来在服务器端保存一些状态
App 通常用 RESTful api 跟 server 打交道。REST 是 stateless 的,也就是 App 不需要像 browser 那样用 cookie 来保存 session。因此用session token 来标示自己就够了,session/state 由 api server 的逻辑处理。 如果你的后端不是 stateless 的 rest api, 那么你可能需要在app 里保存session。可以在app里嵌入 webkit,用一个隐藏的 browser 来管理 cookie session。
Session 是一种 HTTP 存储机制,目的是为无状态的 HTTP 提供的持久机制。所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SID 的不可预测性,暂且认为是安全的。这是一种认证手段。
而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是认证和授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。
转过来说 Session 。Session 只提供一种简单的认证,即有此 SID,即认为有此 User 的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。
所以简单来说,如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
Token 就是令牌,比如你授权(登录)一个程序时,他就是个依据,判断你是否已经授权该软件;Cookie 就是写在客户端的一个 txt 文件,里面包括你登录信息之类的,这样你下次在登录某个网站,就会自动调用 cookie 自动登录用户名;session 和 cookie差不多,只是session 是写在服务器端的文件,也需要在客户端写入 cookie 文件,但是文件里是你的浏览器编号。Session的状态是存储在服务器端,客户端只有 session id;而 Token 的状态是存储在客户端。
# 简单点说
Cookie 和 Token 都是放在客户端的,Cookie 只是一种存储信息的东西,而 Token 是可以定制化的存储用户的信息,协助 OAuth2 进行认证和授权等操作。
Session 是放在服务器端的,它可以保存服务器的一些状态。
事实上我们使用 Token 来实现认证授权功能的时候一般是将 token 存放在 cookie,或者将 token 存放在 session。
# HTTP1.0 与 HTTP1.1 的区别
① 持久与非持久
HTTP 1.0 规定浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个 TCP 连接,服务器完成请求处理后立即断开 TCP 连接,服务器不跟踪每个客户也不记录过去的请求。
HTTP 1.1 则支持持久连接 Persistent Connection, 并且默认使用 persistent connection。在同一个 TCP 的连接中可以传送多个 HTTP 请求和响应。多个请求和响应可以重叠,多个请求和响应可以同时进行。
(但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容。
② HTTP 1.1 增加 host 字段
在 HTTP1.0 中认为每台服务器都绑定一个唯一的 IP 地址,因此,请求消息中的 URL 并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。
HTTP1.1 的请求消息和响应消息都应支持 Host 头域,且请求消息中如果没有 Host 头域会报告一个错误(400 Bad Request)。此外,服务器应该接受以绝对路径标记的资源请求。
# HTTP1.0 与 HTTP2.0 的区别
① 相比于 HTTP/1.X 的文本(字符串)传送, HTTP/2.0 采用二进制传送。客户端和服务器传输数据时把数据分成帧,帧组成了数据流,流具有流 ID 标识和优先级,通过优先级以及流依赖能够一定程度上解决关键请求被阻塞的问题。
② HTTP/2.0 支持多路复用。因为流 ID 的存在, 通过同一个 HTTP 请求可以实现多个 HTTP 请求传输,客户端和服务器可以通过流 ID 来标识究竟是哪个流从而定位到是哪个 HTTP 请求。
③ HTTP/2.0 头部压缩。HTTP/2.0 通过 gzip 和 compress 压缩头部然后再发送,同时通信双方会维护一张头信息表,所有字段都记录在这张表中,在每次 HTTP 传输时只需要传头字段在表中的索引即可,大大减小了重传次数和数据量。
④ HTTP/2.0 支持服务器推送。 服务器在客户端未经请求许可的情况下,可预先向客户端推送需要的内容,客户端在退出服务时可通过发送复位相关的请求来取消服务端的推送。
# 如何知道 HTTP 发送的请求结束
① 如果是短连接,没有启用 keepalive,则可以通过是否关闭了连接来判断是否传输结束,即在读取时可判断 read() != -1。
② 如果是长连接的话,先把 header 直到 \r\n\r\n
整个地收下来。
然后看 Content-length 是否有,如果有了,那就读取 Content-length 长度的字节数据,读完就结束了。
如果没有 Content-length,那么 x 类型为Transfer-Encoding: chunked 说明响应数据的长度不固定,就读到 \r\n0\r\n
表示结束。
补充:头结束符定义
包含这 "\r\n\r\n"
四个字节是指头结束
- 如果头数据里包含Content-Length: x 就读取x字节数据,知道http响应数据的长度为x
- 如果头数据里不包含Content-Length: x 类型为 Transfer-Encoding: chunked 说明响应数据的长度不固定,结束符以
"\r\n0\r\n"
这 5 个字节为结束符。
# Session
# 简介
Session 是服务器端的机制,在服务器上保存的信息。
解析客户端请求并操作 session id,按需保存状态信息。
# 实现
- 使用 Cookie 来实现
- 使用 URL 回写来实现
# 4. HTTPS
# 简介
HTTPS (Hyper Text Transfer Protocol over SecureSocket Layer,超文本传输安全协议)是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。
# SSL/TSL
SSL(Security Sockets Layer,安全套接层)是为网络通信提供安全及数据完整性的一种安全协议。
SSL 是操作系统对外的 API,SSL 3.0 后更名为 TSL。
SSL 采用身份验证和数据加密保证网络通信的安全和数据的完整性。
# HTTPS 数据传输流程
- 浏览器将支持的加密算法信息发送给服务器。
- 服务器选择一套浏览器支持的加密算法,以证书的形式回发浏览器。
- 浏览器验证证书合法性,并结合证书公钥加密信息发送给服务器。
- 服务器使用私钥解密信息,验证哈希,加密响应信息回发浏览器。
- 浏览器解密响应信息,并对消息进行验真,之后进行加密交互数据。
①证书验证阶段
- 浏览器发起 HTTPS 请求
- 服务端返回 HTTPS 证书
- 客户端验证证书是否合法,如果不合法则提示告警
②数据传输阶段
- 当证书验证合法后,在本地生成随机数
- 通过公钥加密随机数,并把加密后的随机数传输到服务端
- 服务端通过私钥对随机数进行解密
- 服务端通过客户端传入的随机数构造对称加密算法,对返回结果内容进行加密后传输
为什么数据传输是用对称加密?
首先,非对称加密的加解密效率是非常低的,而 HTTP 的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的;
另外,在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。
HTTP 和 HTTPS 的区别
- HTTPS 协议需要到 CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。
- HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。
- HTTP 和 HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)
# HTTPS 加密具体分析
总结
阶段一
无加密:不可;
阶段二
对称加密:密钥泄露,数据全泄露;
阶段三
非对称加密:客户端 -> 服务端安全解决,服务端 -> 客户端安全没有解决;
阶段四
证书验证非对称,数据传输对称:公钥和随机数可能被窃取,key 暴露,数据不安全;
阶段五
CA 辅助:加密公钥,保证 key 不泄露,安全。
参考:http://www.itcast.cn/news/20200729/1516328866.shtml
# 不加密
有关于加密,我们首先来看一下不加密的情况,一般在计算机中,不加密我们成为'裸奔'。如果数据不加密,则很容易被黑客窃取到。如下图所示:
# 对称加密
所以针对这样的情况,我们应该在数据传输的过程中进行对应的加密,那么问题来了,我们应该选择哪种加密方式呢?我们知道:常见的加密方式有对称加密和非对称加密之分,例如我们这里选择对称加密的形式。则如下图所示:
我们可以把 data 数据,配合秘钥,进行 f() 函数运算,进而得到密文:XXX,再把 XXX 传递到服务器端,从而使数据的传输进行加密,但是这样也面临一些对应的问题,我们知道,对称加密的秘钥是由后端生成的,但是该秘钥往往只有一个,因为后端不可能为每一个人设置一个秘钥,否则后端存储的秘钥就太多了。既然秘钥只有一个,那么前端想要解密就需要获取该秘钥。这也就是不安全的地方了。因为黑客也可以伪装成良民(普通的客户端),拿取到对应的秘钥,从而对获取的数据进行解密处理。所以对称加密的方式其实是不安全的。虽然进行了加密但是黑客可以很容易就进行解密。
# 非对称加密
对称加密这么不安全,那么非对称加密呢?是不是非常安全。接下来让我们一起来看一下:
从上图我们可以知道,使用非对称加密有两种加密方式:
公钥加密,私钥解密
私钥加密,公钥解密
一开始服务端生成一对公私钥(pk,sk)。我们要想进行密文通讯,需要客户端获取对应的公钥(pk)。所以客户端会发送请求,请求公钥。客户端获取到 pk 后,会把数据放到 f() 方法中进行对应的加密,所用的秘钥就是刚刚获取的 pk。加密后得到的 XXX 就是密文。所以我们可以把数据传输给服务器端。
这个时候,如果黑客截取了对应的 XXX 数据,黑客将没法获取对应的明文数据,因为他没有获取对应的私钥 (sk)。
但是非对称加密的缺点来了:如果服务端想要给客户端传递数据,也需要加密该怎么办呢?如果使用私钥加密,把加密的字段 YYY 传输给客户端,看似没有问题,但是细细想一下,我们就会知道黑客也是可以充当良民从而获取公钥(pk)的。也就是说,只要是使用私钥(sk)加密的方法,黑客都可以对其进行解密。
所以:非对称加密解决了客户端->服务器数据的安全性,但是没有解决服务器->客户端的安全性。
总结发现:单独使用对称加密,不安全,单独使用非对称加密,也不安全。那么应该怎么解决呢?
# 证书验证用非对称,数据传输用对称
通过上面的图片,我们知道,先通过请求,获取服务端的 pk,拿到之后,我们可以在客户端随机产生一个 key,然后通过 f(pk,key)=XXX 的方式,把 XXX 传输给服务端,这样服务端就可以通过私钥 (sk) 对 XXX 进行解密,从而得到 key。以后我们就可以通过 key,作为秘钥,进行对称加密了。
这样的方式是非常棒的。通过这样的方式我们会觉得数据是非常安全的。
但是真的安全吗?
其实是不对的,如果我们设想一下。黑客如果从最开始就对我们的通讯进行了监听。那么我们获取公钥的过程也会被监听到。而我们知道,非对称加密的公钥(pk),黑客也是可以获取的。所以一旦公钥泄露。key 就会泄露,key 泄露,则后面的全部对称加密都称不上是安全的。所以后面很安全,但是如何保证 pk 的传输也是安全的就至关重要了。
# CA 辅助
CA机构
是一种信用机构。主要靠信用赚钱,一般来说都是全球的大型机构。这类机构也会有自己的公钥(cpk)和私钥(csk),可以使用 csk 对于 pk 公钥进行加密,从而得到证书,我们可以把证书传递给前端,让前端利用浏览器内部自带的公钥(cpk)对证书进行解密,如果解密成功,则可以获取到 pk,随后再利用 pk 对前端生成的 key 进行加密。重复之前的步骤。
为什么需要 CA 认证机构颁发证书?
CA
使用私钥对证书进行了数字签名,防止中间人进行篡改。
HTTP 协议被认为不安全是因为传输过程容易被监听者勾线监听、伪造服务器,而 HTTPS 协议主要解决的便是网络传输的安全性问题。
首先我们假设不存在认证机构,任何人都可以制作证书,这带来的安全风险便是经典的“中间人攻击”问题。
如上图所以,过程原理如下:
- 1)本地请求被劫持(如 DNS 劫持等),所有请求均发送到中间人的服务器;
- 2)中间人服务器返回中间人自己的证书;
- 3)客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输;
- 4)中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密;
- 5)中间人以客户端的请求内容再向正规网站发起请求;
- 6)因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据;
- 7)中间人凭借与正规网站建立的对称加密算法对内容进行解密;
- 8)中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输;
- 9)客户端通过与中间人建立的对称加密算法对返回结果数据进行解密。
由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。
# 5. Socket
# 简介
Socket 是 BSD UNIX 的进程通信机制,通常也称作”套接字”,用于描述 IP 地址和端口,是一个通信链的句柄。Socket 可以理解为 TCP/IP 网络的 API,它定义了许多函数或例程,程序员可以用它们来开发 TCP/IP 网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。
# TCP
# [应用1]
服务网接收客户端发过来的信息。
/**
* 服务端
*/
@Test
public void server(){
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try{
ss = new ServerSocket(8899);
//获取连接
socket = ss.accpet();
//输入流
is = socket.getInputStream();
//ByteArrayOutputStream 可以不用考虑长度
baos = new ByteArrayOutputStream();
byte[] buffer = new byte[5];
int len;
while((len = is.read(buffer)) != -1){
baos.write(buffer,0,len);
}
//输出
System.out.println(baos.toString());
}catch(IOException e){
e.printStackTrace();
}finally{
if(baos!=null){
try{
baos.close();
}catch(IOException e){
e.printStackTrace();
}
}
if(is!=null){
try{
is.close();
}catch(IOException e){
e.printStackTrace();
}
}
if(socket!=null){
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
if(ss!=null){
try{
ss.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
/**
* 客户端
*/
@Test
public void client(){
Socket socket = null;
OutputStream os = null;
try{
InetAddress inet = InetAddress.getByName("127.0.0.1");
socket = new Socket(inet,8899);
//获取输出流
os = socket.getOutputStream();
os.write("我是客户端mm".getBytes());
}catch(IOException e){
e.printStackTrace();
}finally{
//关闭资源
if(os!=null){
try{
os.close();
}catch(IOException e){
e.printStackTrace();
}
}
if(socket!=null){
try{
socket.close();
}catch(IOException e){
e.printStackTrace();
}
}
}
}
# [应用2]
客户端发送文件给服务端,服务端保存文件到本地。
/**
* 服务端
*/
@Test
public void server(){
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
FileOutputStream fos = null;
try{
//1. 建立服务端 socket
ss = new ServerSocket(8899);
//2. 监听客户端 socket
socket = ss.accept();
//3. 获取输入流
is = socket.getInputStream();
//4. 建立输出流
fos = new FileOutputStream(new File("本地保存路径"));
//5. 输出
byte[] buffer = new byte[1024];;
int len;
while ((len=is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
}catch (IOException e){
e.printStackTrace();
}finally {
//6. 关闭资源
if (fos!=null){
try {
fos.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (is!=null){
try {
is.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (socket!=null){
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (ss!=null){
try {
ss.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
/**
* 客户端
*/
@Test
public void client(){
Socket socket = null;
OutputStream os = null;
FileInputStream fis = null;
try {
InetAddress inet = InetAddress.getByName("127.0.0.1");
//1. 建立socket,连接服务器的8899端口
socket = new Socket(inet,8899);
//2. 获取输出流
os = socket.getOutputStream();
//3. 建立输入流
fis = new FileInputStream(new File("要发送的文件路径"));
//4. 输入数据并发送出去
byte[] buffer = new byte[1024];
int len;
while((len = fis.read(buffer))!=-1){
os.write(buffer,0,len);
}
}catch (IOException e){
e.printStackTrace();
}finally {
//5. 关闭资源
if (fis!=null){
try{
fis.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (os!=null){
try{
os.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (socket!=null){
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
# [应用3]
从客户端发送文件给服务端,服务端保存到本地,并返回“发送成功”给客户端,并关闭相应的连接。
/**
* 服务端
*/
@Test
public void server(){
ServerSocket ss = null;
Socket socket = null;
InputStream is = null;
FileOutputStream fos = null;
OutputStream os = null;
try {
//1. 建立服务端 socket
ss = new ServerSocket(9090);
//2. 监听客户端 socket
socket = ss.accept();
//3. 获取输入流
is = socket.getInputStream();
//4. 建立输出流
fos = new FileOutputStream(new File("receiveFile.jpg"));
//5. 保存文件
byte[] buffer = new byte[1024];
int len;
while ((len=is.read(buffer))!=-1){
fos.write(buffer,0,len);
}
//6. 回复信息
os = socket.getOutputStream();
os.write("收到文件!".getBytes());
}catch (IOException e){
e.printStackTrace();
}finally {
if (os!=null){
try {
os.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (fos!=null){
try {
fos.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (is!=null){
try {
is.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (socket!=null){
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (ss!=null){
try {
ss.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
/**
* 客户端
*/
@Test
public void client(){
Socket socket = null;
OutputStream os = null;
FileInputStream fis = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
//1. 建立 socket
InetAddress inet = InetAddress.getByName("127.0.0.1");
socket = new Socket(inet,9090);
//2. 建立输出流
os = socket.getOutputStream();
//3. 建立输入流,输入文件
fis = new FileInputStream(new File("/Users/hedon-/Downloads/IMG_0058.JPG"));
//4. 传输文件
byte[] buffer = new byte[1024];
int len;
while ((len=fis.read(buffer))!=-1){
os.write(buffer,0,len);
}
//5. 表明文件传输完成(服务端是一个read操作,会阻塞,需要明确说明已经传输完成)
socket.shutdownOutput();
//6. 接收服务端反馈
is = socket.getInputStream();
baos = new ByteArrayOutputStream();
byte[] buffer2 = new byte[1024];
while ((len = is.read(buffer2))!=-1){
baos.write(buffer,0,len);
}
System.out.println(baos.toString());
}catch (IOException e){
e.printStackTrace();
}finally {
if (baos!=null){
try {
baos.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (is!=null){
try {
is.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (fis!=null){
try {
fis.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (os!=null){
try {
os.close();
}catch (IOException e){
e.printStackTrace();
}
}
if (socket!=null){
try {
socket.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
}
# UDP
/**
* 发送端
*/
@Test
public void sender() throws IOException {
//1. socket 对象
DatagramSocket socket = new DatagramSocket();
//2. 数据包
String str = "我是UDP方式发送的导弹~";
byte[] data = str.getBytes();
InetAddress inet = InetAddress.getByName("127.0.0.1");
DatagramPacket packet = new DatagramPacket(data,0,data.length,inet,9090);
//3. 发送数据报
socket.send(packet);
//4. 关闭资源
socket.close();
}
/**
* 接收端
*/
@Test
public void receiver() throws IOException{
//1. socket 对象,需要指定监听的端口号
DatagramSocket socket = new DatagramSocket(9090);
//2. 接收数据
byte[] buffer = new byte[100];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
socket.receive(packet);
//3. 打印数据
System.out.println(new String(packet.getData(),0,packet.getLength()));
//4. 关闭 socket
socket.close();
}