例如我们是如何访问到哔哩哔哩的数据的
当你的设备开始连接到互联网时,会被分配一个IP地址。
而哔哩哔哩的服务器也接入到互联网,它也将被分配一个IP地址。
我们常说你访问某个网站,其实是在访问这个网站的服务器。
如果说你想访问index.html,则此时你的电脑被称为客户端,而哔哩哔哩服务器为服务端。
顾名思义,客户端是面向用户的应用程序。
而服务器端是在远程计算机上运行的应用程序。
客户端可以在需要时通过互联网与服务端进行通信,你的电脑发信息给哔哩哔哩服务器,你要获取index.html的内容,请把它传给我。消息将会被转化成电子信号,通过电缆发送给哔哩哔哩服务器,在服务器端再将电子信号转化为计算机可以使用的文本数据。(这是通过TCP/IP协议来做到的)
协议是一组规则,用于指定计算机如何通过网络来相互通信。
TCP/IP协议族共分为4层
应用层含义解释:负责浏览器和网络服务器相互通信的http协议,负责文件传输的FTP协议,负责电子邮件客户端检索邮件的IMAP协议。
在我们想要访问index.html的例子中:
step1、我们使用应用层的http协议,请求获取html文本,这时候需要发送一个请求消息,消息会在发送前被分解为许多片段,我们称之为数据包。
step2、通过应用层进入tcp层(传输控制层)后,每个数据包都会被分配一个端口号,端口号用来确定目标计算机的哪一个应用程序要接受并使用该数据包。
TCP是一种面向连接的可靠字节流服务协议。
TCP必须经过三次握手建立连接之后才能交换数据,每个收到的数据包都会像发送方发送ack确认,已保证发送成功。
进入IP层(网络层)后,每个数据包将会赋予目标计算机的IP地址。IP是不可靠的无连接协议,它并不关心数据包是否到达目的地,也不关心连接和端口号。它的工作是发送数据包并将其路由到目标计算机,其中每个数据包都是独立的互不依赖的,所以有可能会乱序到达目标地址,或者在传输途中丢失。
那如何保证数据包达到和顺序正确呢?这些都交给了TCP,这也就体现了分层的作用,当数据包过大时,在ip层会进行分包,由于每个数据包在物理链路层走的物理链路不一样,传输速度也不一样,导致数据包没有按顺序到达目的地,但TCP会根据数据包上携带的序列号来排序重组,并且发送方在一个特定时间内没有接受到接收方的ack确认时,则发送方会重新传送该数据包。
注意:不要把IP等同于IP地址,IP是网络层协议,而IP地址是一串数字。
IP地址有两种标准:
1、IPv4:
IPv4采用的是32位地址,即4字节,因此地址空间只有2的32次方,约40亿个地址。
所以说可以在互联网上使用的地址是有限的。
一些地址是为特殊用途所保留的,如专用网络、多播地址。
随着互联网爆炸式的发展,地址不断被分配使用,IPv4地址枯竭的问题也随之产生。
于是IPv6应运而生。
2、IPv6
IPv6采用128位的地址,因此新增的地址空间支持2的128次方,约3.4x10的38次方个地址。
有了IP地址和端口号之后,链路层将数据包的文本信息转译成电子信号。
⬇️
然后通过电缆传输。在电缆的另一端的路由器检查每个数据包中的目标地址,并确定将其发送到何处,最终数据包到达服务器,然后数据包从TCP/IP协议族的底部开始向上运行。
当数据包向上通过协议族时,客户端添加的所有路由数据,例如IP地址和端口号都将从数据包中剥离出来,当数据到达栈顶时(应用层),数据包已经恢复成最初始的形式。
通过端口号可以将数据传递给当前服务器,监听该端口的应用程序,应用程序根据当前请求数据作出反应。
比如现在我们想获取index.html,服务器则会将路径index.html的数据通过刚才的方式返回给你的电脑,也就是客户端,这样你就看见了b站的首页内容。到这里我们就完成了客户端和服务器在互联网中的一次数据交互。
总的来说,互联网是一个全球性的、基于TCP/IP的分布式计算机网络,它允许设备之间的连接和数据交换。它依赖于众多的协议、路由器、DNS、ISP和其他关键组成部分,以实现设备之间的通信。这些原理和技术的共同作用构成了互联网的基础,使其成为全球通信和信息传递的支柱
因为IP地址不是固定的,所以想要能访问到固定的网站,就有了域名,bilibili.com就是一个域名。
这里使用到了domain names service,简称DNS,DNS是一个分布式数据库,上面记录了域名和其IP地址的对应关系。
step1、输入网址时,浏览器首先连接DNS服务器,DNS返还该域名的IP地址。
step2、浏览器再连接访问该IP的服务器
有了DNS之后,就算IP地址有了变化,再重新绑定一下域名和新IP地址即可。
这就涉及到浏览器的工作原理
浏览器结构图:
大致分为用户界面、浏览器引擎、渲染引擎。
浏览器引擎:在用户界面和渲染引擎之间有个浏览器引擎,用于在用户界面和渲染引擎之间传递数据。
渲染引擎:负责渲染用户请求的页面内容,渲染器下面还有很多小的功能模块,负责请求网络请求的的网络模块,用于解析和执行js的js解释器,还有数据存储持久层,帮助浏览器存储各种数据,比如cookie等。
我们往往把渲染引擎成为浏览器的内核。
现在浏览器结构通常是多进程的,根据进程功能不同来拆卸浏览器。
浏览器进程:其中浏览器进程负责控制chrome浏览器除标签页外的用户页面,包括地址栏,书签,后退和前进按钮。以及负责与浏览器的其他进程协调工作。
网络进程:负责发起接受网络请求。
GPU进程:负责整个浏览器界面的渲染。
插件进程:负责控制网站使用的所有插件,例如flash。这里的插件并不是指的chrome市场里安装的扩展。
渲染器进程:用来控制tab标签内的所有内容,浏览器在默认情况下会为每个标签页都创建一个进程。
step1、浏览器进程的UI线程会捕捉你的输入内容,如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着开始连接服务器获取数据。
如果你的输入不是网址而是一串关键词,于是就会使用默认配置的搜索引擎来查询。
1、当网络线程获取到数据后,会通过SafaBrowsing(谷歌内部的一个站点安全系统)来检查站点是否事恶意站点。(如果是则会展示一个警告页面,告诉你这个站点有安全问题。比如通过查看该站点的IP是否在谷歌的黑名单之内)
2、当返回数据准备完毕并且安全校验通过时,网络线程会通知UI线程说我就要准备好了。然后UI线程就会创建一个渲染器进程来渲染页面,浏览器进程通过将IPC管道将数据传递给渲染器进程。
正式进入渲染流程,渲染进程接受到的数据也就是html,渲染器进程的核心任务就是html、css、js、image等资源渲染成用户可以交互的web页面。
渲染器进程的主线程将html进行解析,构造dom数据结构,dom也就是文档对象模型,是浏览器对页面在其内部的表现形式,是web开发程序员可以通过js与之交互的数据结构和API。
html首先经过Tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记进行dom树构造,在dom树构造过程中会创建document对象。
然后以document为根结点的dom树不断进行修改,向其中添加各种元素。
html代码中往往会引入一些额外的资源,比如图片、css、js脚本等。图片和css这些资源需要通过网络下载或者从缓存中直接加载。这些资源不会阻塞html的解析,因为她们不会影响dom的生成,但是在html解析过程中遇到script标签就会停止解析,转而去加载解析并执行js,因为浏览器并不知道js执行是否会改变当前页面html结构,如果js代码里调用了document.write方法来修改html,那么之前的html解析就没有任何意义了。
这也就是为什么我们一直说要把script标签放在合适的位置,或者使用async或者defer属性来异步加载执行js。
在html解析完成后,我们就会获得一个DOM Tree,但我们还不知道DOM树上的每个节点应该长什么样子,主线程需要解析css并确定每个DOM节点的计算样式。
即使你没有提供自定义的css样式,浏览器也会有自己默认的样式表。比如h2的字体大小要比h3的大。
在知道dom结构和每个节点的样式后,我们就需要知道每个节点需要放在页面上的哪个位置。也就是节点的坐标以及该节点需要占用多大的区域,这个阶段被成语layout布局,主线程通过遍历dom和计算好的样式来生成layout tree,layout tree上的每个节点都记录了x、y坐标和边框尺寸。
注意dom tree和layout tree并不是一一对应的。
dom tree设置了display:none的节点不会出现在layout tree上,而在layout tree上的before伪类中添加了content值的元素,content里的内容会出现在layout tree上,不会出现在dom树里。这是因为dom是通过html解析获得,并不关心样式,而layout tree是根据dom和计算好的样式来生成的。
layout tree是在最后展示在屏幕上的节点是对应的。
此刻我们已经知道了元素的大小、形状和位置,我们还需要知道以什么样的顺序绘制(paint)这个节点。
例如z-index这个属性会影响节点绘制的层级关系,如果我们按照dom的层级结构来绘制页面,则会导致错误的渲染。
为了保证在屏幕上展示正确的层级, 主线程遍历layout tree创建一个绘制记录表(paint record),该表记录了绘制的顺序,这个阶段被称为绘制。
栅格化(rastering):把这些信息转化成像素点显示在屏幕上
chrome早期只栅格化用户可视区的内容,当用户滚动页面时,再栅格化更多的内容来填充缺失的部分。 这种方式会有很强的延时感。
现在的chrome使用了一种更为复杂的栅格化流程,叫做合成(composting),合成时一种将页面的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程中单独进行合成页面的技术,即把页面所有的元素按照某种规则进行分图层,并把图层都栅格化好了。然后只需要把可视区的内容组合成一帧展示给用户即可。
主线程遍历layout tree生成layer(图层) tree。
当layer tree生成完毕和绘制顺序确定后,
主线程将这些信息传递给合成器线程,
合成器线程将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为许多图块儿(tiles),然后将每个图块儿发送给栅格化线程。
栅格线程栅格化每个图块,并将他们存储在GPU内存中。
当图块栅格化完成后,合成器线程将收集称为“draw quads”的图块信息,这些信息记录了图块在内存里的位置和在页面的哪个位置绘制图块的信息,根据这些信息合成器线程生成了一个合成器帧。
这个合成器帧(Frame)通过IPC传送给浏览器进程。
接着浏览器进程将合成器帧传送到GPU
然后GPU渲染展示到屏幕上,此时页面就有了画面。
当你滚动页面时,则会生成一个新的合成帧,又重复刚刚的内容。
以上过程简化图:
重排:当我们改变一个元素的尺寸位置属性时会重新进行样式计算,布局,绘制以及后面的所有流程。
重绘:当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制。
重排和重绘都会占用主线程,js也是运行在主线程的,这样就会出现抢占执行时间的问题,如果你写了一个不断导致重排/重绘的动画,浏览器则需要在每一帧都运行样式计算布局和绘制的操作。
当页面以每秒60帧的刷新率时才不会让用户感到页面卡顿,如果你在运行动画时还有大量的js任务需要执行,因为布局、绘制和js执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,还有剩余时间,js就会拿到主线程的使用权,如果js执行时间过长就会导致在下一帧开始时js没有及时归还主线程,导致下一帧动画没有按时渲染。就会出现页面动画的卡顿。
优化的办法:
1、通过requestAnimationFrame( )这个API,这个方法会在每一帧被调用,通过API的回调,我们可以把js任务分成一些更小的任务块(分到每一帧)【黄色小块代表js】
在每一帧时间用完前暂停js执行,归还主线程。
react最新的渲染引擎react fiber就是用到了API来做了很多优化。
2、栅格化整个流程不占用主线程,只在合成器线程和栅格线程中运行,css中有个动画属性叫transform,通过这个属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格化线程。
我们常常会使用位置变化、宽高变化(旋转、3d)来实现动画效果,这些都是可以使用transform来代替的。