计算机网络和浏览器
浏览器原理
浏览器进程
浏览器主进程(Browser Process):负责界面显示,用户交互,子进程管理,提供存储功能
控制“Chrome”包括地址栏、书签、返回 前进按钮。
还可以处理网络浏览器中不可见的特权部分,例如 网络请求和文件访问
渲染进程(Renderer Process):将html、css、js转换为用户可以看到和交互的网页
控制标签页内显示网站的一切内容。
- 主线程(main thread)
- 合成器线程(compositor thread)
- 栅格化线程(raster thread)
- worker thread
网络进程:负责页面网络资源加载
GPU 进程(GPU Process):绘制图像和视频,或是3d的css效果
与其他进程分开处理 GPU 任务。 它分为不同的进程 因为 GPU 会处理来自多个应用的请求,并在同一 Surface 上绘制它们。
插件进程(Plugin Process):控制网站使用的所有插件,比如Flash
跨域
浏览器同源策略
- 同源:协议、域名和端口号全部一致
- 限制从一个源加载的文档或(js)脚本与另一个源的资源进行交互
- js 脚本不能访问其他域的 cookie、localStorage 和 indexDB
- js 脚本不能访问其他域的 DOM
- ajax 请求不能跨域
跨域请求方案
CORS
主要是依赖后端解决
一般地,浏览器在发起跨域请求时会携带 origin 请求头,值为当前源。服务器根据该字段判断是否同意访问服务器资源。
浏览器将CORS跨域请求分为简单请求和非简单请求。
TIP
简单请求和预检请求
只要同时满足以下两个条件,就属于简单请求
(1) 使用下列方法之一:
- head
- get
- post
(2) 请求的Heder是
- Accept
- Accept-Language
- Content-Language
- Content-Type: 只限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain
不同时满足上面的两个条件,就属于非简单请求。浏览器对这两种的处理,是不一样的。
主要流程
- 对于简单请求,浏览器直接发出 CORS 请求。
- 服务器返回CORS响应头字段,都以 Access-Control- 开头:
Access-Control-Allow-Origin
(必选)它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
浏览器发现用户请求不是简单请求,就会先发送一个“预检请求”(options请求方法),确认这样的请求服务器是否允许,然后再发送正式的请求
服务器收到"预检"请求:
a. 检查了
Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。b. HTTP回应中,除了关键的
Access-Control-Allow-Origin
字段,其他CORS相关字段如下:
Access-Control-Allow-Methods
(必选)它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
JSONP
jsonp 的原理就是利用<script>
标签没有跨域限制,通过<script>
标签src属性,发送带有 callback 参数的 GET 请求,服务端将接口返回数据拼凑到 callback 函数中,返回给浏览器,浏览器解析执行,从而前端拿到 callback 函数返回的数据。
因为这里 type 是 text/javascript
,所以返回的内容会被立刻当作 js 代码执行。
<script
src="http://localhost:1111?callback=bb"
type="text/javascript">
</script>
<script>
function bb (data) {
console.log(data)
}
</script>
后端获取 url 中的 query 参数后,返回一个调用 bb 函数的 json 数据
比如这里返回 json 格式的 bb('hello world')。
缺点:只能使用 GET 请求
代理
利用服务器和服务器之间没有同源策略可以直接交流的特性,先将数据发给同源的代理服务器,由服务器转发请求
可以直接在 vue 项目的配置文件中设置
// vue.config.js 文件中设置
module.exports={
devServer:{
proxy:{
'/api01':{
target:'http://localhost:5000',
// 重写请求
pathRewrite:{
'^/api01':''
}
},
'/api02':{
target:'http://localhost:5001',
// 重写请求
pathRewrite:{
'^/api02':''
}
}
}
}
}
浏览器渲染
浏览器导航
浏览器主线程中有多个线程,在 chrome 浏览器中,标签页以外的所有内容都由浏览器进程处理。
浏览器进程中有多个不同的线程
- UI thread: 界面线程,当您在地址中输入网址时 则表示您的输入由浏览器进程的界面线程处理。
- Network thread: 处理网络堆栈以从互联网接收数据的网络线程
- Storage thread: 控制文件访问等的存储线程
流程
- UI 线程请求网络线程导航到某个网站
- 网络线程阅读回复内容,是什么类型的文件(html,zip或其他)、是否安全
- 网络线程向 UI 线程确认(confirm)已经导航到对应网站,UI线程找到或者创建(maybe proactive)一个渲染进程
- 浏览器主进程向渲染进程发起 IPC 通信以提交导航(commit navigation),并将网络线程得到的数据传递给渲染进程
- 浏览器线程监听到渲染进程对提交导航的确认,则导航完成,页面加载阶段开始
- 地址栏、安全指示和站点设置 UI 都会更新并反映当前网站的信息
- 每个标签页的会话历史会更新,以便前进后退按钮能实现对应功能
- 会话历史会被保存到硬盘中,以帮助恢复
- 当渲染进程“完成”页面加载(load),他将向浏览器主进程发起一个 IPC 通信告知页面加载完毕
- 整个页面加载完成发生在页面所有帧(frames)的
onload
事件都已触发并执行完成 - “完成”之后,客户端js脚本仍然能加载额外的资源并渲染新的视图
- 整个页面加载完成发生在页面所有帧(frames)的
浏览器渲染进程
渲染进程获取html文件后,需要将其渲染为用户实际看到的像素。
- 主线程 解析 HTML 文件
- 构造 DOM
- 计算样式 CSSDOM
TIP
解析 HTML 时如果遇到 <script>
标签,会阻塞解析 HTML 并转去执行 js 代码。 这是因为 js 脚本可能会改变 DOM 的形态,比如通过 document.write()
方法。
- 主线程 根据 DOM 和 CSSDOM 计算布局 (Layout Tree),这是和最后显示在页面上的元素一一对应的
- 主线程 根据 layout tree 生成绘制记录表 (Paint Record) 以告诉浏览器该以什么顺序绘制元素,这个阶段被称为绘制阶段(Paint)
TIP
Layout Tree 和 DOM 以及 CSSDOM 并不是一一对应的,比如 display: none
并不会出现在 Layout Tree 中,而伪类中content
的内容会出现在布局中
- 主线程 遍历 layout tree 计算 layer tree ,将 layer tree 和 paint record 交给 合成器线程(compositor thread)
- 合成器 线程合成 / 栅格化(Compositing / Rasterizing)
- 合成器线程 将每个 layer 切成图块 tiles 分发给多个栅格化 Raster 线程,Raster 线程将栅格化完毕的 tiles 存储在 GPU memory 中
- 合成器线程 收集图块的信息称为 draw quads,据此就可以创造出一个合成器帧(compositor frame)
- 渲染进程 通过 IPC 将一个合成器帧提交给浏览器主线程,然后连带着 UI 线程或者其他渲染进程提交过来的合成器帧,交给 GPU 显示到屏幕上
Draw quads
Contains information such as the tile's location in memory and where in the page to draw the tile taking in consideration of the page compositing.
Compositor frame
A collection of draw quads that represents a frame of a page.
以下是渲染管线的示意图。
渲染优化
通过以上的渲染流程分析,可以得出一些前端性能优化思路。
渲染进程的主线程负责如此之多的任务:包括上面提到的解析 HTML 文件,计算布局,绘制和计算图层信息,除此以外,还要执行 js 脚本和与其他进程通信。
浏览器会在执行完style-layout-paint 生成一帧后执行js脚本,但 js 脚本可能会占用主线程过长时间导致后续的帧没有按时生成,导致用户感到卡顿。
我们可以将 js 脚本分成多个小块并在每一帧生成后执行,这可以通过requestAnimationFrame()
实现。或者使用 js 的 WebWorker 将 js 运行在后台,防止阻塞主线程。
重排是指浏览器重新计算元素的位置和几何信息的过程。具体来说,即需要 Style, Layout, Paint, Composite 4个过程。
当DOM的变化影响了元素的布局 (layout)时,浏览器需要重新计算元素的尺寸和位置。以下是一些可能导致重排的操作:
- 改变元素的宽度、高度、边距、填充或边框
- 添加或删除DOM元素
- 通过display: none隐藏元素或使其可见
- 激活伪类(如:hover)
- 改变窗口大小或字体大小
- 获取某些属性,如offsetWidth、offsetHeight、clientWidth、clientHeight等
重绘是指浏览器更新元素的外观的过程。也就是说只进行了 Paint 和 Composite 两个过程。
当元素的样式发生变化,但并未影响布局(如颜色、背景色、阴影等)时,浏览器会执行重绘操作。以下是一些可能导致重绘的操作:
- 改变元素的背景颜色或文本颜色,边框颜色等
- 添加或者删除元素的伪类如:hover
- 操作 canvas,svg 等图形元素
所以如果能够尽量减少重排甚至重绘,就可以在一定程度上提高页面性能。这可以通过避免频繁 DOM 操作、使用 css3 动画等方式解决。
对于动画,可以使用 compositing only animations, 即只需要 Composite 过程的动画。
目前只有两个 css 属性可以实现这一点:
- transform
- opacity
还可以通过分层来实现优化,即将多变的元素放在单独一层,以防止其经常变化导致整个 layer 的其他不变元素也被重新计算。但这不应该成为主要的手段,因为分层信息的开销同样不小。
INFO
will-change:通常大多数元素例如<div>
不会单独分为一层,但是如果它的内容经常需要更新、需要重新渲染,可以添加一个属性:will-change
。
如果这个元素的transform属性需要经常发生变化,那么可以声明will-change: transform;
,告知浏览器其需要经常更新,但是最后是否决定分层依然是浏览器的具体实现决定的。
重排和重绘(专门总结)
重排(Reflow)和重绘(Repaint)是浏览器渲染网页时的两个重要过程。
重绘(Repaint) 发生在元素的外观发生变化,但其几何属性(如位置和大小)不变时。
- 例如,改变元素的背景颜色、文字颜色等。重绘仅涉及外观的改变,不会影响页面布局,因此其开销相对较小。
- 颜色,边框,outline,文字样式等属性都只会触发重绘
重排(Reflow),也常称为回流,是指当元素的大小、位置或布局发生改变,需要浏览器重新计算元素的位置和几何信息。
- 这通常涉及到整个文档的布局,是一个开销较大的过程。例如,改变元素的宽高、边距、边框、填充等,或者添加、移除DOM元素,都会触发重排
- 改变窗口大小,伪类等也会触发重排
以下是一些减少重排和重绘的方法:
- 集中修改样式:一次性修改多个样式属性,而不是频繁地修改单个属性。
- 使用CSS动画或过渡:通过CSS的
transform
和opacity
属性进行动画处理,因为这些属性不会触发重排。 - 使用硬件加速:例如,在CSS中使用
transform
或translate
属性,可以启用GPU加速,提高渲染性能。 - 减少DOM操作:尽量减少DOM操作,因为DOM操作可能会导致重排和重绘。
浏览器缓存
三大件
主要有三种方式:Cookie, LocalStorage, SessionStorage。
cookie 属于文档对象模型中(DOM)中的根节点 document。剩下两个属于浏览器对象模型(BOM)的对象 window,由html5 web api 提供。
Cookie
- 有效期由后端返回的响应头决定
Max-Age
: 相对时间Expires
: 绝对时间
- 由服务器产生,保存在客户端浏览器,每次请求都会发送给服务端
- 大小限制为 4kb
- 安全性不好,明文传输
- cookie 同源策略以 domain 和 path 作为限制,不区分端口和协议
- web 同源策略以 协议、端口和域名作为限制
- 设置 cookie 属性 secure 则只能在 https 中传递
- 设置 cookie 属性 httponly 则无法通过程序读取
- 不要在 cookie 中存放敏感信息
LocalStorage
- 不发送给客户端
- 网站只能访问自己域下的 localstorage
- 在浏览器中长期存储键值对数据,直到手动清理
- 大小限制 5MB
典型场景:存储用户的个人偏好设置,比如主题选择,隐私策略设置
SessionStorage
- 存储期限仅限于会话期间,可以跨越页面刷新存在,浏览器标签或窗口关闭即失效
- 每个标签或窗口之间数据相互独立
- 单标签页限制,在标签页内进行同源页面访问都可以共享 sessionstorage 数据
典型场景:多步骤表单流程,点击上一步下一步都能保留填好的信息
IndexedDB
概述
将大量数据储存在客户端,这样可以减少从服务器获取数据的频率,直接从本地获取数据。
通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。 IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
基本概念
- 数据库:是一系列相关数据的容器
- 每个 url 都可以创建任意多的数据库
- 有数据库版本的概念,同一时刻只能有一个版本的数据库存在
- 要修改数据库结构(新增删除表、索引或者主键),只能通过升级数据库版本完成
- 对象仓库:每个数据库包含若干个对象仓库,类似关系数据库的表
- 数据记录:对象仓库中保存的数据是数据记录,类似关系数据库的行
- 只有主键和数据体两部分
- 主键可以是数据记录里的一个属性,也可以指定为一个递增的整数编号
- 数据体可以是任意数据类型,不限于对象
- 索引:为了加速数据的检索,可以在对象仓库里面为不同的属性简历索引
- 事务:数据的读写删改,都要通过事务完成
- 事务对象提供
error
,abort
,complete
三个事件监听结果
- 事务对象提供
特点
(1)键值对储存。 IndexedDB 内部采用**对象仓库(object store)**存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以“键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
(2)异步。 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
(3)支持事务。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
(4)同源限制。 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
(5)储存空间大。 IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
(6)支持二进制储存。 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
IndexedDB API
以下代码说明如何打开一个数据库,获取数据库对象
let db
// 打开数据库,如果指定名称数据库不存在就会新建数据库,返回一个 IDBRequest 对象
const requst = window.indexedDB.open(dbName, version)
// 提供三个事件用于处理打开数据库的结果
request.onerror = function (event) {
// ...
}
// 成功打开数据库,通过 result 属性拿到数据库对象
request.onsuccess = function(event) {
db = request.result
// ...
}
// 如果指定的版本号大于实际版本号,就会发生升级事件,新创建数据库也算
request.onupgradeneeded = function (event) {
db = event.target.result
}
以下代码说明如何新建对象仓库
let objectStore
// 在升级事件中获取数据库对象,并借此创建一个对象仓库
request.onupgradeneeded = function (event) {
db = event.target.result
if (!db.ObjectStoreNames.contains('person')) {
objectStore = db.createObjectStore('person', {keyPath: 'id'})
}
}
// 或者可以用自动生成主键
var objectStore = db.createObjectStore(
'person',
{ autoIncrement: true }
);
// 下一步可以创建一个索引,参数分别是索引名,索引所在属性和配置对象
objectStore.createIndex('name', 'name', {unique: false})
读写数据
// 新增一行数据
function add() {
// 创建一个事务以完成新增数据的操作,参数需要指定数据仓库名和操作模式(只读或者读写)
const request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' });
request.onsuccess = function (event) {
console.log('数据写入成功');
};
request.onerror = function (event) {
console.log('数据写入失败');
}
}
// 读取数据
function read() {
// 创建事务,指定表名
var transaction = db.transaction(['person']);
var objectStore = transaction.objectStore('person');
// 读取数据
var request = objectStore.get(1);
request.onerror = function(event) {
console.log('事务失败');
};
request.onsuccess = function( event) {
if (request.result) {
console.log('Name: ' + request.result.name);
console.log('Age: ' + request.result.age);
console.log('Email: ' + request.result.email);
} else {
console.log('未获得数据记录');
}
};
}
更多操作比如遍历数据、更新数据和删除数据等可以点击上面的链接查看。
计算机网络
http 和 https
get 和 post 请求有什么区别
Notes:破坏资源、幂等、安全性(url)、语义、缓存
概念
在 HTTP 协议里,所谓的「安全」是指请求方法不会「破坏」服务器上的资源。
所谓的「幂等」,意思是多次执行相同的操作,结果都是「相同」的。
如果「安全」放入概念是指信息是否会被泄漏的话,虽然 POST 用 body 传输数据,而 GET 用 URL 传输,这样数据会在浏览器地址拦容易看到,但是并不能说 GET 不如 POST 安全的。
因为 HTTP 传输的内容都是明文的,虽然在浏览器地址拦看不到 POST 提交的 body 数据,但是只要抓个包就都能看到了。
所以,要避免传输过程中数据被窃取,就要使用 HTTPS 协议,这样所有 HTTP 的数据都会被加密传输。
- get 一般用于请求资源,读取数据(比如搜索、筛选之类的),不会对服务器产生修改动作,是“幂等且安全”的;post 请求一般用于写入数据,新增内容,会对服务器资源进行修改;
- get 通过 url 附上请求参数,post 则放在请求体中,这使得 get 请求相对不安全,因为会被 url 会被记录到浏览器历史中,而 post 请求一般是写到服务器端的日志中;
- get 请求静态资源一般会被浏览器缓存,再次请求相同的内容耗时很短,但这同样导致相对不安全
- post 请求能携带更多数据,即请求体能携带更多数据,而 get 请求携带的数据只能放在 url 中,而浏览器对 url 的长度是有限制的;当然这也让 post 请求比 get更慢一些
根据 RFC 规范,get 的语义是从服务器获取指定的资源,一般是静态的文本页面图片视频等。get 的请求参数放在 url 中,只支持 ASCII ,并且浏览器会对 url 的长度进行限制。
根据 RFC 规范,post 的语义是根据负荷对指定的资源进行处理,一般是提交数据,新增内容。请求参数和数据都放在 body 中,可以是任意格式,浏览器也不会对 body 的大小进行限制。
http 常见字段
请求头
- Accept:浏览器可以处理的内容类型
- Accept-Encoding:gzip, deflate 压缩编码
- Accept-Language
- Accept-Charset
- Connection: close / Keep-Alive
- Cookie
- Host:目的主机和端口号
- Referer:请求源页面的域
- User-Agent:客户端使用的操作系统和浏览器及其版本
响应头
- Date:消息发送的日期和时间
- Server:服务器信息
- Connection:连接特性
- Content-Length:数据长度
- Cache-Control:控制 http 缓存
- private:默认,响应内容只能作为私有缓存不能在用户间共享
- public:内容被缓存并在多用户间共享
- must-revalidate:内容在特定情况下被重用,但它必须到服务器端验证是不是最新的版本
- no-cache:不缓存,使强制缓存失效
- max-age:设置缓存最大的有效时间,这个参数定义的是时间大小,而不是确定的时间点。单位是秒(实现强制缓存)
- no-store:使强制缓存和协商缓存全部失效
- content-type
- Content-Length:响应内容的长度(字节),是 http body 的边界,解决了 tcp 面向字节流的“粘包”问题
Http Keep-alive
注意,这是 Http 的长连接机制,而不是 tcp。
- http 1.0 需要设置请求头 connection: Keep-Alive
- http 1.1 默认开启长连接,需要关闭的话设置请求头为 connection: close
为了避免浪费资源维持 tcp 连接,web 服务软件会启动一个定时器,客户端在完成某个请求后长时间不再发起新的请求,就触发定时器的回调函数释放 tcp 连接。
TCP Keepalive
注意,这是 tcp 的保活机制:确认连接的另一头是否还活着。
如果 tcp 两端一直没有数据交互,达到了触发保活机制的条件,内核的 tcp 协议栈就会发送探测报文看对方是不是死了。
- 对方正常响应探测报文:重置 tcp 保活时间
- 对方主机宕机或者由于未知原因导致报文不可达,没有相应:tcp 协议栈报告该 tcp 连接已死亡
http 状态码
- 1xx:提示,表示现在是协议处理的中间阶段,还需继续
- 2xx:成功
- 200 ok
- 204 No Content,响应没有 body
- 206 Partial Content,http 断点续传机制,本报文只是一部分
- 3xx: 重定向
- 301 Redirect Permanently 永久重定向
- 302 Found 临时重定向
- 304 Not Modified 资源未修改,缓存重定向,客户端可以继续使用缓存资源
- 4xx: 客户端错误
- 400 Bad Request 客户端请求有误
- 403 Forbidden 禁止访问
- 404 Not Found 找不到这个资源
- 5xx:服务端错误
- 500 Internal Server Error 服务端错误
- 501 Not Implemented 未实现,即将开业敬请期待
- 502 Bad Gateway 通常是作为网关或者代理时返回的错误,服务器自身正常但是访问后端发生错误
- 503 Service Unavailable 服务器忙
http 缓存机制
对于一些具有重复性的 HTTP 请求,比如每次请求得到的数据都一样的,我们可以把这对「请求-响应」的数据都缓存在本地,下次就直接读取本地的数据,不必再通过网络向服务器发起请求。
避免发送 HTTP 请求的方法就是通过缓存技术,HTTP 设计者早在之前就考虑到了这点,因此 HTTP 协议的头部有不少是针对缓存的字段。
HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存
强制缓存
强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:
- Cache-Control,相对时间(优先于 expires)
- Expires,绝对时间
流程:
- 当浏览器第一次请求访问服务器资源时,服务器会在返回这个资源的同时,在 Response 头部加上
Cache-Control
,Cache-Control
中设置了过期时间大小; - 浏览器再次需要该资源时,会先通过当前时间与
Cache-Control
中设置的过期时间大小,来计算出该资源是否过期,如果没有,则使用该缓存,否则重新请求服务器; - 服务器再次收到请求后,会再次更新 Response 头部的
Cache-Control
。
协商缓存
两种实现方法
通过比较最近修改时间比较
- 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;
- 请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间
基于资源唯一标识符(优先于上面那种)
- 响应头部中 Etag:唯一标识响应资源;
- 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值(If-None-Match: Etag)
为什么 ETag 的优先级更高?这是因为 ETag 主要能解决 Last-Modified 几个比较难以解决的问题:
在没有修改文件内容情况下文件的最后修改时间可能也会改变,这会导致客户端认为这文件被改动了,从而重新请求;
可能有些文件是在秒级以内修改的,If-Modified-Since 能检查到的粒度是秒级的,使用 Etag就能够保证这种需求下客户端在 1 秒内能刷新多次;
有些服务器不能精确获取文件的最后修改时间。
注意,协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。
一图流
http 1.1 特性
正面特性:
- 简单易理解:报文格式是简单的 key-value 对
- 灵活易于扩展:url、状态码、头字段都允许开发人员自定义和扩充;工作在应用层,下层随意变化
- https 就是在 tcp 和 http 之间增加了 ssl/tls 安全传输层
- 跨平台应用广泛
负面特性:
- 无状态:减少维持连接状态的开销、但是要完成关联性操作非常麻烦(增加 cookie 或者 session)
- 明文传输:抓包可看,但是信息裸奔
- 不安全:明文传输泄露信息、不验证双方身份(假网站)、不验证报文完整性会被篡改
传输特性:
- 长连接
- 管道网络传输(pipeline),也就是流水线式的发起和回应请求
notice
这个功能不是默认开启,浏览器基本都不支持,所以这里其他的讨论都当作没有开启这个特性。
- 队头阻塞:顺序发出的请求被阻塞时(比如服务器一直不回应),后续的请求无法发送
https
HTTP 与 HTTPS 有哪些区别?
- HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
- HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
- 两者的默认端口不一样,HTTP 默认端口号是 80,HTTPS 默认端口号是 443。
- HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
混合加密
- 通信建立前通过非对称加密交换会话密钥
- 通信过程使用对称加密的会话密钥加密数据
- 非对称加密使用两个密钥,速度慢但是能解决密钥交换问题;对称加密只用一个密钥,运算速度快但是无法做到安全交换密钥
摘要算法 & 数字签名
摘要算法:目的是防止数据被篡改(而不是为了保密)
- 这里哈希算法也是公开的,但是如果内容被篡改后再用哈希算法生成哈希值必然是与 A 不同的值。
- 所以如果要不被发现,需要把 A 也替换成篡改后内容对应的哈希值
数字签名:建立在收发双方已经分别持有公私钥的前提下,目的是确保消息来源不会被冒充
- 内容会被私钥加密,只有对应的公钥能解密,也就是说这个内容一定是来自私钥持有者
- 如果内容和哈希值被整体替换,中间人没有私钥无法加密,那接收方用公钥发现解密出来是乱七八糟的内容就知道有人在中间冒充发送方
- 但是实际上接收方也就是客户端的公钥是由网站发送过来的(如果浏览器预存了所有网站的公钥开销也太大了),这一步骤如果被黑客劫持,把网站发来的公钥替换成自己的公钥,那黑客就可以用自己私钥加密的内容与客户端通信不被发现
- 解决上述问题就需要 CA 颁发证书
证书:目的是解决传递公钥过程中会被替换的问题,防止客户端拿到假的网站公钥
- 网站发送的是网站证书:网站信息+网站公钥+CA私钥签名(对应数字签名hash) ,如果黑客在中间劫持并用浏览器自带的 CA 公钥对这个网站证书解密,然后将其中的网站公钥替换为自己的公钥,但签名无法修改,因为黑客没有 CA 的私钥。此时客户端验证时就会发现自己计算得到的 hash 和 CA 公钥解密出来的 hash 不相等
xss - 跨站脚本攻击
跨站脚本攻击(也常被称为XSS)是一种漏洞/缺陷:
网页允许将HTML/脚本标签作为输入(比如在评论输入框中),并在渲染到浏览器时没有进行编码或过滤。而后端接收请求后将评论内容返回给前端,直接用 innerHTML 插入到 dom 中。
比如评论中写 <script> 标签,如果没有过滤,直接渲染到前端页面,那么其他访问该网站的人会直接访问 src 属性中的恶意链接。
http 1.1 轮询、长轮询和 WebSocket
Motivation:服务器需要主动发送信息通知前端,也就是说,在用户没有进行动作的情况下,服务器主动推送信息。
轮询:间隔两三秒客户端发送一次请求给后端
- 比如扫二维码登录,前端不知道到底扫没扫,只是固定间隔时间发送请求询问服务器有没有人扫这个码(微信扫码就这样干)
长轮询:客户端的请求将过期时间设置得很长(比如30s),服务器一旦需要推送信息就回应这次请求
- 百度云的扫码登录就这么干的
注意
以上的这两种方式,其实只是【伪】服务器推送,而造成服务器不能主动推送的根本原因在于 http 协议的自有特点。
http 是一个半双工协议,客户端和服务器不能同时发送信息。只能客户端先发送请求,服务器才能响应。
WebSocket:是一个全新的基于 TCP 的全双工协议,同一时间双方都可以主动向对方发送数据
连接过程:
- 先进行 http 三次握手
- 再进行一次通信,如果是普通的 http 请求,那后续就老样子还是 http 通信
- 如果需要建立 websocket 连接,请求头需要特殊设置
- Connection: Upgrade
- Upgrade: websocket
- Sec-WebSocket-Key: 随机生成的 base64 码
- 服务端响应头也需要特殊设置
- 101 Switch Protocol
- Upgrade: websocket + Connnection: Upgrade
- Sec-WebSocket-Accept: 根据客户端的 key 生成,简单验证身份
TIP
Websocket 连接建立后这个连接就跟 http 没有关系了,http 只是个工具人
websocket 数据帧格式:
- 消息头
- opcode: 数据帧类型,1=text(string)数据包,2=二进制数据(bytes)类型,8=关闭连接的信号
- payload 长度:传输数据的长度,单位字节。占7 / 7+16 / 7+64 bit,前 7bit 的126、127是标志位
- 消息体
INFO
TCP 本身就是全双工,但如果直接使用纯 TCP 传输数据,会有粘包的问题。所以上层协议一般都会用消息头(含有数据长度信息) + 消息体的格式包装要发送的数据
应用场景:服务器和客户端频繁交互的大部分场景,比如网页聊天室,类似飞书的网页协同办公软件,还有小程序游戏,网页游戏等
粘包、半包问题
TCP 协议中的问题,因为 TCP 是一个面向字节流的协议,难以确定消息边界。
主要解决方法有三个
- 固定长度的消息
- 约定用户消息都是固定的长度,比如都是64个字节,那么 tcp 接收64字节就认为这个内容是完整的信息
- 不够灵活,实际很少使用
- 特殊字符作为边界(HTTP)
- 两个信息之间插入一个特殊的字符串,接收方读到这个特殊字符就认为读完了一个完整的消息
- HTTP 通过设置回车符、换行符作为 HTTP 报文协议的边界,头字段 Content-Length 指出消息的长度
- 如果消息中有这个特殊字符,需要转义处理
- 自定义消息结构
我们可以自定义一个消息结构,由包头和数据组成,其中包头包是固定大小的,而且包头里有一个字段来说明紧随其后的数据有多大。
比如这个消息结构体,首先 4 个字节大小的变量来表示数据长度,真正的数据则在后面。
struct {
u_int32_t message_length;
char message_data[];
} message;
当接收方接收到包头的大小(比如 4 个字节)后,就解析包头的内容,于是就可以知道数据的长度,然后接下来就继续读取数据,直到读满数据的长度,就可以组装成一个完整到用户消息来处理了。