查看原文
其他

从输入 URL 到页面加载完成的过程中都发生了什么事情?(下)

前端大全 2019-11-12

(点击上方公众号,可快速关注)


作者:百度FEX/吴多益(@吴多益)

fex.baidu.com/blog/2014/05/what-happen/

如有好文章投稿,请点击 → 这里了解详情


(接上篇)


需要注意的是,在 TCP 协议中并没有 IP 地址信息,因为这是在上一层的 IP 协议中定义的,如下图所示:



IP 协议同样是在 TCP 前面的,它也有 20 字节,在这里指明了版本号(Version)为 4,源(Source) IP 为 192.168.1.106,目标(Destination) IP 为 119.75.217.56,因此 IP 协议最重要的作用就是确定 IP 地址。


因为 IP 协议中可以查看到目标 IP 地址,所以如果发现某些特定的 IP 地址,某些路由器就会。。。


但是,光靠 IP 地址是无法进行通信的,因为 IP 地址并不和某台设备绑定,比如你的笔记本的 IP 在家中是 192.168.1.1,但到公司就变成172.22.22.22 了,所以在底层通信时需要使用一个固定的地址,这就是 MAC(media access control) 地址,每个网卡出厂时的 MAC 地址都是固定且唯一的。


因此再往上就是 MAC 协议,它有 14 字节,如下所示:



当一台电脑加入网络时,需要通过 ARP 协议告诉其它网络设备它的 IP 及对应的 MAC 地址是什么,这样其它设备就能通过 IP 地址来查找对应的设备了。


最顶上的 Frame 是代表 Wireshark 的抓包序号,并不是网络协议


就这样,我们解答了第二个问题,不过其实这里面还有很多很多细节没介绍,建议大家通过下面的书籍进一步学习。


扩展学习


  • 《计算机网络:自顶向下方法与Internet特色》

  • 《计算机网络》

  • 《Web性能权威指南》


第三个问题:数据如何从本机网卡发送到服务器?


从内核到网络适配器(Network Interface Card)


前面说到调用 Socket API 后内核会对数据进行底层协议栈的封装,接下来启动 DMA 控制器,它将从内存中读取数据写入网卡。


以 Nexus 5 为例,它使用的是博通 BCM4339 芯片通信,接口采用了 SD 卡一样的 SDIO,但这个芯片的细节并没有公开资料,所以这里就不讨论了。


连接 Wi-Fi 路由


Wi-Fi 网卡需要通过 Wi-Fi 路由来与外部通信,原理是基于无线电,通过电流变化来产生无线电,这个过程也叫「调制」,而反过来无线电可以引起电磁场变化,从而产生电流变化,利用这个原理就能将无线电中的信息解读出来就叫「解调」,其中单位时间内变化的次数就称为频率,目前在 Wi-Fi 中所采用的频率分为 2.4 GHz 和 5 GHz 两种。


在同一个 Wi-Fi 路由下,因为采用的频率相同,同时使用时会发生冲突,为了解决这个问题,Wi-Fi 采用了被称为 CSMA/CA 的方法,简单来说就是在传输前先确认信道是否已被使用,没有才发送数据。


而同样基于无线电原理的 2G/3G/LTE 也会遇到类似的问题,但它并没有采用 Wi-Fi 那样的独占方案,而是通过频分(FDMA)、时分(TDMA)和码分(CDMA)来进行复用,具体细节这里就不展开了。


以小米路由为例,它使用的芯片是 BCM 4709,这个芯片由 ARM Cortex-A9 处理器及流量(Flow)硬件加速组成,使用硬件芯片可以避免经过操作系统中断、上下文切换等操作,从而提升了性能。


路由器中的操作系统可以基于 OpenWrt 或 DD-WRT 来开发的,具体细节我不太了解,所以就不展开了。


因为内网设备的 IP 都是类似 192.168.1.x 这样的内网地址,外网无法直接向这个地址发送数据,所以网络数据在经过路由时,路由会修改相关地址和端口,这个操作称为 NAT 映射。


最后家庭路由一般会通过双绞线连接到运营商网络的。


运营商网络内的路由


数据过双绞线发送到运营商网络后,还会经过很多个中间路由转发,读者可以通过 traceroute 命令或者在线可视化工具来查看这些路由的 ip 和位置。


当数据传递到这些路由器后,路由器会取出包中目的地址的前缀,通过内部的转发表查找对应的输出链路,而这个转发表是如何得到的呢?这就是路由器中最重要的选路算法了,可选的有很多,我对这方面并不太了解,看起来维基百科上的词条列得很全。


主干网间的传输


对于长线的数据传输,通常使用光纤作为介质,光纤是基于光的全反射来实现的,使用光纤需要专门的发射器通过电致发光(比如 LED)将电信号转成光,比起前面介绍的无线电和双绞线,光纤信号的抗干扰性要强得多,而且能耗也小很多。


既然是基于光来传输数据,数据传输速度也就取决于光的速度,在真空中的光速接近于 30 万千米/秒,由于光纤包层(cladding)中的折射率(refractive index)为 1.52,所以实际光速是 20 万千米/秒左右,从首都机场飞往广州白云机场的距离是 1967 千米,按照这个距离来算需要花费 10 毫秒才能抵达。这意味着如果你在北京,服务器在广州,等你发出数据到服务器返回数据至少得等 20 毫秒,实际情况预计是 2- 3 倍,因为这其中还有各个节点路由处理的耗时,比如我测试了一个广州的 IP 发现平均延迟为 60 毫秒。


这个延迟是现有科技无法解决的(除非找到超过光速的方法),只能通过 CDN 来让传输距离变短,或尽量减少串行的来回请求(比如 TCP 建立连接所需的 3 次握手)。


IDC 内网


数据通过光纤最终会来到服务器所在的 IDC 机房,进入 IDC 内网,这时可以先通过分光器将流量镜像一份出来方便进行安全检查等分析,还能用来进行。。。


这里的带宽成本很高,是按照峰值来结算的,以每月每 Gbps(注意这里指的是 bit,而不是 Byte)为单位,北京这边价格在十万人民币以上,一般网站使用 1G 到 10G 不等。


接下来光纤中的数据将进入集群(Cluster)交换机,然后再转发到机架(Rack)顶部的交换机,最后通过这个交换机的端口将数据发往机架中的服务器,可以参考下图(来自 Open Compute):



上图左边是正面,右边是侧面,可以看到顶部为交换机所留的位置。


以前这些交换机的内部实现是封闭的,相关厂商(如思科、Juniper 等)会使用特定的处理器和操作系统,外界难以进行灵活控制,甚至有时候需要手工配置,但这几年随着 OpenFlow 技术的流行,也出现了开放交换机硬件(Open Switch Hardware),比如 Intel 的网络平台,推荐感兴趣的读者建议看看它的视频,比文字描述清晰多了。


需要注意的是,一般网络书中提到的交换机都只具备二层(MAC 协议)的功能,但在 IDC 中的交换器基本上都具备三层(IP 协议)的功能,所以不需要有专门的路由了。


最后,因为 CPU 处理的是电气信号,所以光纤中的光线需要先使用相关设备通过光电效应将光信号转成电信号,然后进入服务器网卡。


服务器 CPU


前面说到数据已经到达服务器网卡了,接着网卡会将数据拷贝到内存中(DMA),然后通过中断来通知 CPU,目前服务器端的 CPU 基本上都是 Intel Xeon,不过这几年出现了一些新的架构,比如在存储领域,百度使用 ARM 架构来提升存储密度,因为 ARM 的功耗比 Xeon 低得多。而在高性能领域,Google 最近在尝试基于 POWER 架构的 CPU 来开发的服务器,最新的 POWER8 处理器可以并行执行 96 个线程,所以对高并发的应用应该很有帮助。


扩展学习


  • The Datacenter as a Computer

  • Open Computer

  • 《软件定义网络》

  • 《大话无线通信》


第四个问题:服务器接收到数据后会进行哪些处理?


为了避免重复,这里将不再介绍操作系统,而是直接进入后端服务进程,由于这方面有太多技术选型,所以我只挑几个常见的公共部分来介绍。


负载均衡


请求在进入到真正的应用服务器前,可能还会先经过负责负载均衡的机器,它的作用是将请求合理地分配到多个服务器上,同时具备具备防攻击等功能。


负载均衡具体实现有很多种,有直接基于硬件的 F5,有操作系统传输层(TCP)上的 LVS,也有在应用层(HTTP)实现的反向代理(也叫七层代理),接下来将介绍 LVS 及反向代理。


负载均衡的策略也有很多,如果后面的多个服务器性能均衡,最简单的方法就是挨个循环一遍(Round-Robin),其它策略就不一一介绍了,可以参考 LVS 中的算法。


LVS


LVS 的作用是从对外看来只有一个 IP,而实际上这个 IP 后面对应是多台机器,因此也被成为 Virtual IP。


前面提到的 NAT 也是一种 LVS 中的工作模式,除此之外还有 DR 和 TUNNEL,具体细节这里就不展开了,它们的缺点是无法跨网段,所以百度自己开发了 BVS 系统。


反向代理


方向代理是工作在 HTTP 上的,具体实现可以基于 HAProxy 或 Nginx,因为反向代理能理解 HTTP 协议,所以能做非常多的事情,比如:


  • 进行很多统一处理,比如防攻击策略、放抓取、SSL、gzip、自动性能优化等

  • 应用层的分流策略都能在这里做,比如对 /xx 路径的请求分到 a 服务器,对 /yy 路径的请求分到 b 服务器,或者按照 cookie 进行小流量测试等

  • 缓存,并在后端服务挂掉的时候显示友好的 404 页面

  • 监控后端服务是否异常

  • ⋯⋯


Nginx 的代码写得非常优秀,从中能学到很多,对高性能服务端开发感兴趣的读者一定要看看。


Web Server 中的处理


请求经过前面的负载均衡后,将进入到对应服务器上的 Web Server,比如 Apache、Tomcat、Node.JS 等。


以 Apache 为例,在接收到请求后会交给一个独立的进程来处理,我们可以通过编写 Apache 扩展来处理,但这样开发起来太麻烦了,所以一般会调用 PHP 等脚本语言来进行处理,比如在 CGI 下就是将 HTTP 中的参数放到环境变量中,然后启动 PHP 进程来执行,或者使用 FastCGI 来预先启动进程。


(等后续有空再单独介绍 Node.JS 中的处理)


进入后端语言


前面说到 Web Server 会调用后端语言进程来处理 HTTP 请求(这个说法不完全正确,有很多其它可能),那么接下来就是后端语言的处理了,目前大部分后端语言都是基于虚拟机的,如 PHP、Java、JavaScript、Python 等,但这个领域的话题非常大,难以讲清楚,对 PHP 感兴趣的读者可以阅读我之前写的 HHVM 介绍文章,其中提到了很多虚拟机的基础知识。


Web 框架(Framework)


如果你的 PHP 只是用来做简单的个人主页「Personal Home Page」,倒没必要使用 Web 框架,但如果随着代码的增加会变得越来越难以管理,所以一般网站都会会基于某个 Web 框架来开发,因此在后端语言执行时首先进入 Web 框架的代码,然后由框架再去调用应用的实现代码。


可选的 Web 框架非常多,这里就不一一介绍了。


读取数据


这部分不展开了,从简单的读写文件到数据中间层,这里面可选的方案实在太多。


扩展学习


  • 《深入理解Nginx》

  • 《Python源码剖析》

  • 《深入理解Java虚拟机》

  • 《数据库系统实现》


第五个问题:服务器返回数据后浏览器如何处理?


前面说到服务端处理完请求后,结果将通过网络发回客户端的浏览器,从本节开始将介绍浏览器接收到数据后的处理,值得一提的是这方面之前有一篇不错的文章 How Browsers Work,所以很多内容我不想再重复介绍,因此将重点放在那篇文章所忽略的部分。


从 01 到字符


HTTP 请求返回的 HTML 传递到浏览器后,如果有 gzip 会先解压,然后接下来最重要的问题是要知道它的编码是什么,比如同样一个「中」字,在 UTF-8 编码下它的内容其实是「11100100 10111000 10101101」也就是「E4 B8 AD」,而在 GBK 下则是「11010110 11010000」,也就是「D6 D0」,如何才能知道文件的编码?可以有很多判断方法:


  • 用户设置,在浏览器中可以指定页面编码

  • HTTP 协议中

  • <meta> 中的 charset 属性值

  • 对于 JS 和 CSS

  • 对于 iframe


如果在这些地方都没指明,浏览器就很难处理,在它看来就是一堆「0」和「1」,比如「中文」,它在 UTF-8 下有 6 个字节,如果按照 GBK 可以当成「涓枃」这 3 个汉字来解释,浏览器怎么知道到底是「中文」还是「涓枃」呢?


不过正常人一眼就能认出「涓枃」是错的,因为这 3 个字太不常见了,所以有人就想到通过判断常见字的方法来检测编码,典型的比如 Mozilla 的 UniversalCharsetDetection,不过这东东误判率也很高,所以还是指明编码的好。


这样后续对文本的操作就是基于「字符」(Character)的了,一个汉字就是一个字符,不用再关心它究竟是 2 个字节还是 3 个字节。


外链资源的加载


(待补充,这里有调度策略)


JavaScript 的执行


(后续再单独介绍,推荐大家看 R 大去年整理的这个帖子,里面有非常多相关资料,另外我两年前曾讲过 JavaScript 引擎中的性能优化,虽然有些内容不太正确了,但也可以看看)


从字符到图片


二维渲染中最复杂的要数文字显示了,虽然想想似乎很简单,不就是将某个文字对应的字形(glyph)找出来么?在中文和英文中这样做是没问题的,因为一个字符就对应一个字形(glyph),在字体文件中找到字形,然后画上去就可以了,但在阿拉伯语中是不行的,因为它有有连体形式。


(以后续再单独介绍,这里非常复杂)


跨平台 2D 绘制库


在不同操作系统中都提供了自己的图形绘制 API,比如 Mac OS X 下的 Quartz,Windows 下的 GDI 以及 Linux 下的 Xlib,但它们相互不兼容,所以为了方便支持跨平台绘图,在 Chrome 中使用了 Skia 库。


(以后再单独介绍,Skia 内部实现调用层级太多,直接讲代码可能不适合初学者)


GPU 合成


(以后续再单独介绍,虽然简单来说就是靠贴图,但还得介绍 OpenGL 以及 GPU 芯片,内容太长)


扩展学习


这节内容是我最熟悉,结果反而因为这样才想花更多时间写好,所以等到以后再发出来好了,大家先可以先看看以下几个站点:


  • Chromium

  • Mozilla Hacks

  • Surfin’ Safari


第六个问题:浏览器如何将页面展现出来?


前面提到浏览器已经将页面渲染成一张图片了,接下来的问题就是如何将这张图片展示在屏幕上。


Framebuffer


以 Linux 为例,在应用中控制屏幕最直接的方法是将图像的 bitmap 写入 /dev/fb0 文件中,这个文件实际上一个内存区域的映射,这段内存区域称为 Framebuffer。


需要注意的是在硬件加速下,如 OpenGL 是不经过 Framebuffer 的。


从内存到 LCD


在手机的 SoC 中通常都会有一个 LCD 控制器,当 Framebuffer 准备好后,CPU 会通过 AMBA 内部总线通知 LCD 控制器,然后这个控制器读取 Framebuffer 中的数据,进行格式转换、伽马校正等操作,最终通过 DSI、HDMI 等接口发往 LCD 显示器。


以 OMAP5432 为例,下图是它所支持的一种并行数据传输:



LCD 显示


最后简单介绍一下 LCD 的显示原理。


首先,要想让人眼能看见,就必须有光线进入,要么通过反射、要么有光源,比如 Kindle 所使用的 E-ink 屏幕本身是不发光的,所以必须在有光线的地方才能阅读,它的优点是省电,但限制太大,所以几乎所有 LCD 都会自带光源。


目前 LCD 中通常使用 LED 作为光源,LED 接上电源后,在电压的作用下,内部的正负电子结合会释放光子,从而产生光,这种物理现象叫电致发光(Electroluminescence),这在前面介绍光纤时也介绍过。


以下是 iPod Touch 2 拆开后的样子:(来自 Wikipedia):



在上图中可以看到 6 盏 LED,这就是整个屏幕的光源,这些光源将通过反射的反射输出到屏幕中。


有了光源还得有色彩,在 LED 中通常做法是使用彩色滤光片(Color filter)来将 LED 光源转成不同颜色。


另外直接使用三种颜色的 LED 也是可行的,它能避免了滤光导致的光子浪费,降低耗电,很适用于智能手表这样的小屏幕,Apple 收购的 LuxVue 公司就采用的是这种方式,感兴趣的话可以去研究它的专利


LCD 屏幕上的每个物理像素点实际上是由红、绿、蓝 3 种色彩的点组成,每个颜色点能单独控制,下面是用显微镜放大后的情况(来自Wikipedia):



从上图可以看到每 3 种颜色的滤光片都全亮的时候就是白色,都灭就是黑色,如果你仔细看还能看到有些点并不是完全黑,这是字体上的反锯齿效果。


通过这 3 种颜色亮度的不同组合就能产生出各种色彩,如果每个颜色点能产生 256 种亮度,就能生成 256 * 256 * 256 = 16777216 种色彩。


并不是所有显示器的亮度都能达到 256,在选择显示器时有个参数是 8-Bit 或 6-Bit 面板,其中 8-Bit 的面板能在物理上达到 256 种亮度,而 6-Bit 的则只有 64 种,它需要靠刷新率控制(Frame rate control)技术来达到 256 的效果。


如何控制这些颜色点的亮度?这就要靠液晶体了,液晶体的特性是当有电流通过时会发生旋转,从而将部分光线挡住,所以只要通过电压控制液晶体的转动就能控制这个颜色点的亮度,目前手机屏幕中通常使用 TFT 控制器来对其进行控制,在 TFT 中最著名的要数 IPS 面板。


这些过滤后的光线大部分会直接进入眼睛,有些光还会在其它表面上经过漫(diffuse)反射或镜面(specular)反射后再进入眼睛,加上环境光的影响,要真正算出有多少光到眼睛是一个积分问题,感兴趣的读者可以研究基于物理的渲染。


当光线进入眼睛后,接下来就是生物学的领域了,所以我们到此结束。


扩展学习


  • 《Computer Graphics, 3rd Edition : Principles and Practices》

  • 《交互式计算机图形学》


本文所忽略的内容


为了编写方便,前面的介绍中将很多底层细节实现忽略了,比如:


  • 内存相关

    • 堆,这里的分配策略有很多,比如 malloc 的实现

    • 栈,函数调用,已经有很多优秀的文章或书籍介绍了

    • 内存映射,动态库加载等

    • 队列几乎无处不在,但这些细节和原理没太大关系

  • 各种缓存

    • CPU 的缓存、操作系统的缓存、HTTP 缓存、后端缓存等等

  • 各种监控

    • 很多日志会保存下来以便后续分析


FAQ


从微博反馈来看,有些问题被经常问到,我就在这里统一回答吧,如果有其它问题请在评论中问。


Q:学那么多有什么用?根本用不着


A:计算机是人类最强大的工具,你不想了解它是如何运作的么?


Q:什么都了解一点,还不如精通一项吧?


A:非常认同,初期肯定需要先在某个领域精通,然后再去了解周边领域的知识,这样还能让你对之前那个领域有更深刻的理解。


Q:晒出来培养一堆面霸跟自己过不去?


A:本文其实写得很浅,每个部分都能再深入展开。


Q:这题要把人累死啊,说几天都说不完的


A:哈哈哈,大神你暴露了,题目只是手段,目的是将你这样的大牛挖掘出来。


大家的讨论


非常感谢各位大牛的参与讨论,这里搜集了其中的一些回答。


@WOODHEAD笨笨:请求被送往本地路由,接入商路由,旁路分析是否违法地址,连接被中断,浏览器无辜得显示网页不存在。严重的有人来查水表


caoz: 这不是我的面试题么! 还有一道题,用户反应我们网站卡,请问都有哪些可能性,以及排方法。


@caoz:写的还是不错的,但是还是有一些缺漏,比如arp欺骗? 著名的GFW的阻断策略,以及,一个URL可不是只有一个请求,多个请求的排队和寻址?此外,cdn, 智能dns解析机制等。//@ZRJ-: http://t.cn/8smHpMF 从点击到呈现 — 详解一次HTTP请求 我大三的时候写的。。 啊


@唐福林:与时俱进,现在应该问从打开app到刷新出内容,整个过程中都发生了什么,如果感觉慢,怎么定位问题,怎么解决


@寒冬winter: 回复@Ivony:这题胜在区分度高,知识点覆盖均匀,再不懂的人,也能答出几句,而高手可以根据自己擅长的领域自由发挥,从URL规范、HTTP协议、DNS、CDN、到浏览器流式解析、CSS规则构建、layout、paint、onload/domready、JS执行、JS API绑定⋯⋯


@JS小组:[哈哈] 小编想起来了,貌似刚从业那会儿,前端界最美丽的姐@sherrie_wong 面试问过小编这道题.然后我当时把知道的全说了,从浏览器解析,发请求,7层网络模型实际用的模型,TCP三次握手.经路由,交换机,DNS,到服务器.在是否需要与文件系统还是数据库打交道,再者分布式运算hadoop啥的…聊了太多.


@莴怖熵崴箔:这种就是流氓问题,我还想问从你按了键盘到屏幕上出现字符,中间都发生了什么事,提示一下:设想你是一个电子。哦,不对,电子又是什么


@寒冬winter:http://t.cn/zH20bR1 http://t.cn/zH20bR1 之前写了开头两篇,后面荒废中⋯⋯


@ils传言:不提电厂发电机转了几圈的也干掉!//@Philonis高:不提交换机和路由器工作原理的全干掉!//@南非蜘蛛:从7层协议的角度说会比较全面。这种问题只有全栈工程师才能回答。


@耸肩的阿特拉斯阁下:DNS解析URL出IP/Port,浏览器连接并向此地址发出GET请求,web服务端(nginx、apache)接收到请求后,通过CGI等接口协议调用动态语言(php等),动态语言再连接数据库查询相应数据并处理,然后反馈给浏览器,浏览器解析反馈页面,通过html、javascript、css处理后呈现到屏幕⋯⋯每个细节的话估计要800页的书


@一棹凌烟:这种面试题在系统领域的招聘里其实简单好使。还有一个类似的:从在键盘上敲下一个字符键开始,到在虚拟机里的terminal里显示出来,中间的过程是什么?


@ICT_朱亚东:记得6年前上胡伟武的芯片设计课,老胡第一节课就说,上完这门课,我希望你们能搞清楚,我翻了一页PPT,计算机内部都做了那些流水操作,当然啦,我是一点都不记得了。


@julyclyde:我们运维一般问一个TCP segment in a IP packet in an ethernet frame经过一个路由器之后发生什么变化


@西西福厮:从浏览器说起,操作系统相应键盘中断,事件队列处理,到互联网路由,到服务器网卡中断,到最后输出缓冲。。。细说能说两小时。


@Xscape:从键盘中断说起?回车前的预解析都很靠后了..//@纯白色燃烧: 从键盘到弹簧入万有引力而后直达量子力学。


@Bosn:然后从硬件再到电子⋯⋯量子…薛定谔之猫…平行宇宙⋯⋯乃至万能的哲学!!


@imPony:可深入到PN结中的电子流动层面


@巩小东-TX: 猜一下,浏览器组http报文sock发出,proxy过滤,收到处理头,未过期cache返回,http svr处理校验包,转为cgi协议给后端,后端map url,load code,与逻辑交互后生成html给svr,svr过滤cache给proxy,proxy给浏览器,拉去js完成html,浏览器渲染。


@yuange1975:我算对整个过程比较清楚,包含服务器的处理,web服务器和浏览器的处理以及安全问题,估计少有对两者的安全都研究过的。但面试时要清晰的比较完整的把大块流程列出来说明白,也有难度。估计也很难有机会时间去整理文章了。


@ShopEx王磊:我也问这个问题题好多年, 或者变通一下:从输入URL到展现, 都涉及到哪些缓存环节, 缓存的更新机制是怎样的


@一棹凌烟:这种面试题在系统领域的招聘里其实简单好使。还有一个类似的:从在键盘上敲下一个字符键开始,到在虚拟机里的terminal里显示出来,中间的过程是什么?


@智慧笨蛋: 确实可以维度不同的说,主要还是看颗粒度,光网络这段从wifi 解密,到NAT,到局间交换,ip包在以太网包映射等等就可以写一本书了


/@乔3少:放开了说所有互联网相关的知识都能体现的,比如dns、浏览器缓存,tcp连接、http响应,web服务的工作原理,浏览器的响应和渲染等等,刚刚在本子上列了下想到的安全威胁,很有意思!



觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存