概述
互联网是如何运作的?
1. 网络协议(network protocol)
简称为协议,是为进行网络中的数据交换而建立的规则、标准或约定。 网络协议通过分层明确每一层的工作职责,通过定义明确的接口来协同工作,每一层都可以使用下面各层的功能,而不必担心各层是如何实现的。
2. TCP/IP协议族
TCP/IP协议簇是Internet的基础,也是当今最流行的组网形式。TCP/IP是一组协议的代名词,包括许多别的协议,组成了TCP/IP协议簇。
其中比较重要的有SLIP协议、PPP协议、IP协议、ICMP协议、ARP协议、TCP协议、UDP协议、FTP协议、DNS协议、SMTP协议等。
TCP/IP协议并不完全符合OSI的七层参考模型。传统的开放式系统互连参考模型,是一种通信协议的7层抽象的参考模型,其中每一层执行某一特定任务。该模型的目的是使各种硬件在相同的层次上相互通信。而TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的网络来完成自己的需求。
TCP/IP协议族分为四层:
- 应用层:提供特定于应用程序的协议
(比如负责浏览器和网络服务器相互通信的HTTP协议、负责文件传输的FTP协议、负责电子邮件客户端检索邮件的IMAP协议) - TCP传输控制层:发送数据包到计算机上使用特定端口号的应用程序
- IP网络层:使用IP地址将数据包发送到特定的计算机
- 链路层:将二进制数据包与网络信号相互之间转换
(1)IP
IP是不可靠、无连接的协议,它并不关心数据包是否到达目的地,也不关心连接和端口号,IP的工作是发送数据包并将其路由到目标计算机,其中每个数据包都是独立的互不依赖的,所以有可能会乱序到达目标地址或者在传输途中丢失.
那如何保证数据包到达和顺序正确呢?
这就交给了TCP,这也体现了分层的作用。TCP的工作是确保数据包到达并保持正确的顺序。IP与TCP的唯一共同之处在于它接收数据并将自己的IP首部信息添加到TCP数据。当数据包过大时,在IP层会进行分包,由于每个数据包在物理链路层走的物理链路不一样,传输速度也不一样,导致数据包没有按顺序到达目的地,但TCP会根据数据包上携带的序列号来进行排序重组,并且发送方在一个特定时间内没有接收到接收方的ack确认时,则发送方会重新传送该数据包。【超时重传】
IP≠IP地址,IP是网络层协议,而IP地址是一串数字,IP地址有两种标准,一种称为IPv4(采用32位地址)、IPv6(采用128位地址)
两级的 IP 地址可以记为:
IP 地址 ::= { <网络号>, <主机号>}
::= 代表“定义为”
IP 地址中的网络号字段和主机号字段 :
1-127 大型网络
128-191中等网络
192-223小型网络
划分子网后 IP 地址就变成了三级结构。划分子网只是把 IP 地址的主机号 host-id 这部分进行再划分,而不改变 IP 地址原来的网络号 net-id。
IP地址 ::= {<网络号>, <子网号>, <主机号>}
子网掩码:
从一个 IP 数据报的首部并无法判断源主机或目的主机所连接的网络是否进行了子网划分。使用子网掩码(subnet mask)可以找出 IP 地址中的子网部分。
子网掩码存储在路由表。
划分子网增加了灵活性,但是却减少了能够连接在网络上的主机总数(各个相对对立的网点,主机号0和1是不能取的,需要留给网络地址和广播地址。因此每多一个子网,就必须浪费两个IP地址)。
(IP 地址) AND (子网掩码) =子网网络地址
网际层的 IP 协议及配套协议:
IP地址与硬件地址的区别
物理地址(硬件地址)放在MAC帧的首部,是数据链路层和物理层使用的地址,而IP地址放在IP数据报的首部,是网络层和以上各层使用的地址,是一种逻辑地址。
主机或路由器怎样知道应当在MAC帧的首部填入什么样的硬件地址?
通过地址解析协议 ARP:从网络层使用的IP地址,解析出数据链路层使用的硬件地址。
每一个主机都设有一个 ARP 高速缓存(ARP cache),里面有所在的局域网上的各主机和路由器的 IP 地址到硬件地址的映射表,并且这个映射表经常动态更新(新增或超时删除)
(2)传输控制协议TCP
Tcp通过协议栈将数据路由到目标计算机上的正确的应用程序。
特点:
(1)TCP 是面向连接的运输层协议。面向连接意味着两个使用TCP的应用程序在交换数据之前必须先建立连接.
(2)每一条 TCP 连接唯一地被通信两端的两个端点(即两个套接字)所确定,每一条 TCP 连接只能是点对点的(一对一)。
(3)TCP 提供可靠交付的服务。因为对于接收到的每个数据包,都会向发送方发送确认以确认发送。TCP还在其报头中包含一个校验和,以检查接收到的数据(无差错、不丢失、不重复并且按序到达)。
(4) TCP 提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。
(5)面向字节流。
流:流入进程或者从进程流出的字节序列。
TCP报文段的首部格式:
TCP连接
TCP连接建立:
第一次握手:A 的 TCP 向 B 发出连接请求报文段,其首部中的同步位 SYN = 1,并选择序号 seq = x,表明传送数据时的第一个数据字节的序号是 x。发送完毕后客户端进入SYN_SEND状态。
注意:SYN=1请求报文段不能携带数据,但是要消耗掉一个序号
第二次握手: B 的 TCP 收到连接请求报文段后,如同意,则发回确认。B 在确认报文段中应使 SYN = 1,使 ACK = 1,其确认号ack = x + 1,自己选择的序号 seq = y。发送完毕后服务器端进入SYN_RECEIVED状态。
注意:SYN=1确认报文段不能携带数据,但是要消耗掉一个序号
第三次握手: A 收到此报文段后向 B 给出确认,其 ACK = 1, 确认号 ack = y + 1。
A 的 TCP 通知上层应用进程,连接已经建立。发送完毕后客户端进入ESTABLISHED状态,服务器端接收到包后也会进入ESTABLISHED状态,TCP握手结束。
注意:ACK报文段可以携带数据,但如果不携带数据则不消耗序号。
思考:为什么在TCP连接建立过程中,发送方A为什么还要发送一次确认呢?
为了防止已经失效的连接请求报文段突然又传送到B
TCP 的连接释放 :
第一次挥手: 数据传输结束后,通信的双方都可释放连接。
现在 A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据(但仍可以接收数据),主动关闭 TCP 连接。
A 把连接释放报文段首部的 FIN = 1,其序号seq = u,等待 B 的确认。
注意:FIN报文段即使不携带数据,它也要消耗掉一个序号
第二次挥手: B 发出确认,确认号 ack = u + 1,而这个报文段自己的序号 seq = v。
TCP 服务器进程通知高层应用进程。
从 A 到 B 这个方向的连接就释放了,TCP 连接处于半关闭状态。B 若发送数据,A 仍要接收。
第三次挥手: 若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。
第四次挥手: A 收到连接释放报文段后,必须发出确认。
在确认报文段中 ACK = 1,确认号 ack = w + 1,自己的序号 seq = u + 1。
(3)HTTP
超文本传送协议HTTP 是面向事务的应用层协议,它是万维网上能够可靠地交换文件(包括文本、声音、图像等各种多媒体文件)的重要基础。
主要特点
- HTTP 是面向事务的客户服务器协议。所谓面向事务:一系列的信息交换,这一系列的信息交换是不可分割的整体,要么所有信息交换完成,要么一次交换都不进行。
- HTTP 协议本身也是无连接的,虽然它使用了面向连接的 TCP 向上提供的服务,但通信双方在交换HTTP报文之前不需要先建立HTTP连接。
- HTTP 1.0 协议是无状态的(stateless),即对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息。
HTTP 的操作过程
1:每个万维网网点都有一个服务器进程,它不断监听TCP的端口80,以便发现是否有浏览器向它发出连接建立请求。
2:监听到连接建立请求并建立TCP连接
3:浏览器向该万维网服务器发出浏览某个页面的请求
4:服务器返回所请求的页面
5:释放TCP连接
当您在Web浏览器中键入URL时,会发生什么?
简答:
DNS解析→TCP连接→发送HTTP请求→服务器处理请求并返回HTTP报文→浏览器解析渲染页面→连接结束
(1) 浏览器分析超链指向页面的 URL。
(2) 浏览器向 DNS 请求解析 www.tsinghua.edu.cn 的 IP 地址。(浏览器首先在缓存中查找,查找的顺序是浏览器缓存→系统缓存→路由器缓存,缓存中查找不到则去系统的hosts文件中查找,没有则查询DNS服务器)
(3) 域名系统 DNS 解析出清华大学服务器的 IP 地址。
(4) 浏览器与服务器建立 TCP 连接(服务器监听80端口)
(5) 浏览器发出取文件命令:
GET /chn/yxsz/index.htm。
(6) 服务器给出响应,把文件 index.htm 发给浏览器。
(7) TCP 连接释放。
(8) 浏览器显示“清华大学院系设置”文件 index.htm 中的所有文本。
HTTP 常见的状态码
- 1xx
1xx 类状态码属于提示信息,是协议处理中的一种中间状态,还需要后续的操作,实际用到的比较少。 - 2xx
2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
「200 OK」是最常见的成功状态码,表示一切正常。如果是非 HEAD 请求,服务器返回的响应头都会有 body 数据。
「204 No Content」也是常见的成功状态码,与 200 OK 基本相同,但响应头没有 body 数据。
「206 Partial Content」是应用于 HTTP 分块下载或断电续传,表示响应返回的 body 数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。 - 3xx
3xx 类状态码表示客户端请求的资源发送了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
「301 Moved Permanently」表示永久重定向,说明请求的资源已经不存在了,需改用新的 URL 再次访问。
「302 Moved Permanently」表示临时重定向,说明请求的资源还在,但暂时需要用另一个 URL 来访问。
301 和 302 都会在响应头里使用字段 Location,指明后续要跳转的 URL,浏览器会自动重定向新的 URL。
「304 Not Modified」不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,用于缓存控制。 - 4xx
4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
「400 Bad Request」表示客户端请求的报文有错误,但只是个笼统的错误。
「403 Forbidden」表示服务器禁止访问资源,并不是客户端的请求出错。
「404 Not Found」表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。 - 5xx
5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
「500 Internal Server Error」与 400 类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。
「501 Not Implemented」表示客户端请求的功能还不支持,类似“即将开业,敬请期待”的意思。
「502 Bad Gateway」通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。
「503 Service Unavailable」表示服务器当前很忙,暂时无法响应服务器,类似“网络服务正忙,请稍后重试”的意思。
GET 与 POST
Get 方法的含义是请求从服务器获取资源,这个资源可以是静态的文本、页面、图片视频等。比如,你打开我的文章,浏览器就会发送 GET 请求给服务器,服务器就会返回文章的所有文字及资源。
POST 方法则是相反操作,它向 URI 指定的资源提交数据,数据就放在报文的 body 里。
比如,你在我文章底部,敲入了留言后点击「提交」,浏览器就会执行一次 POST 请求,把你的留言文字放进了报文 body 里,然后拼接好 POST 请求头,通过 TCP 协议发送给服务器。
GET 和 POST 方法都是安全和幂等的吗?
安全和幂等的概念:在 HTTP协议里,所谓的「安全」是指请求方法不会「破坏」服务器上的资源。所谓的「幂等」,意思是多次执行相同的操作,结果都是「相同」的。
那么很明显 GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。
POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。
HTTP 特性
HTTP 最凸出的优点是「简单、灵活和易于扩展、应用广泛和跨平台」。
HTTP 协议里有优缺点一体的双刃剑,分别是「无状态、明文传输」,同时还有一大缺点「不安全」。
HTTP与HTTPS
HTTP 与 HTTPS 有哪些区别?
- HTTP 是超文本传输协议,信息是明文传输,存在安全风险的问题。
- HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP 网络层之间加入了 SSL/TLS 安全协议,使得报文能够加密传输。
- HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进SSL/TLS 的握手过程,才可进入加密报文传输。
- HTTP 的端口号是 80,HTTPS 的端口号是 443。
- HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的身份是可信的。
HTTPS 解决了 HTTP 的哪些问题?
1、HTTP 由于是明文传输,所以安全上存在以下三个风险:
-
窃听风险,比如通信链路上可以获取通信内容,用户号容易没。
-
篡改风险,比如强制入垃圾广告,视觉污染,用户眼容易瞎。
-
冒充风险,比如冒充淘宝网站,用户钱容易没。
2、 HTTPS 在 HTTP 与 TCP 层之间加入了 SSL/TLS 协议。(https的SSL加密是在传输层实现的。)
3、HTTP 与 HTTPS可以很好的解决了上述的风险:
- 信息加密:交互信息无法被窃取,但你的号会因为「自身忘记」账号而没。
- 校验机制:无法篡改通信内容,篡改了就不能正常显示,但百度「竞价排名」依然可以搜索垃圾广告。
- 身份证书:证明淘宝是真的淘宝网,但你的钱还是会因为「剁手」而没。
HTTPS 是如何解决上面的三个风险的?
-
混合加密
通过混合加密的方式可以保证信息的机密性,解决了窃听的风险。
HTTPS 采用的是对称加密和非对称加密结合的「混合加密」方式:
在通信建立前采用非对称加密的方式交换「会话秘钥」,后续就不再使用非对称加密。
在通信过程中全部使用对称加密的「会话秘钥」的方式加密明文数据。
采用「混合加密」的方式的原因:
对称加密只使用一个密钥,运算速度快,密钥必须保密,无法做到安全的密钥交换。
非对称加密使用两个密钥:公钥和私钥,公钥可以任意分发而私钥保密,解决了密钥交换问题但速度慢。
-
摘要算法
摘要算法用来实现完整性,能够为数据生成独一无二的「指纹」,用于校验数据的完整性,解决了篡改的风险。
客户端在发送明文之前会通过摘要算法算出明文的「指纹」,发送的时候把「指纹 + 明文」一同加密成密文后,发送给服务器,服务器解密后,用相同的摘要算法算出发送过来的明文,通过比较客户端携带的「指纹」和当前算出的「指纹」做比较,若「指纹」相同,说明数据是完整的。 -
数字证书
客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
这就存在些问题,如何保证公钥不被篡改和信任度?
所以这里就需要借助第三方权威机构 CA (数字证书认证机构),将服务器公钥放在数字证书(由数字证书认证机构颁发)中,只要证书是可信的,公钥就是可信的。
通过数字证书的方式保证服务器公钥的身份,解决冒充的风险。
(4).用户数据报协议 UDP
主要特点
(1)UDP 是无连接的,即发送数据之前不需要建立连接。
(2)UDP 使用尽最大努力交付,即不保证可靠交付,同时也不使用拥塞控制。
(3)UDP 是面向报文的。
-
UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。
-
应用层交给 UDP 多长的报文,UDP 就照样发送,即一次发送一个报文。
-
接收方 UDP 对 IP 层交上来的 UDP 用户数据报,在去除首部后就原封不动地交付上层的应用进程,一次交付一个完整的报文。
-
应用程序必须选择合适大小的报文。
(4)UDP没有拥塞控制。适合于一些实时应用,很多实时应用如IP电话,实时视频会议,要求源主机以恒定的速率发送数据,并允许在网络拥塞时丢失一些数据,但不允许数据有太大的时延。
(5)UDP 支持一对一、一对多、多对一和多对多的交互通信。
(6)UDP 的首部开销小,只有 8 个字节
思考:你是如何访问到bilibili站点数据的?
当你的电脑【客户端】连入互联网之后,你的电脑将会获得一个编号地址,即IP地址,现在有bilibili的服务器【服务端】也接入互联网,它也将分配一个IP地址。
你的电脑发送信息给服务器要获取index.html的内容,消息被转化为电子信号,通过电缆发送给bilibili服务器,在服务器端将电子信号转化为计算机可以使用的文本数据。
这又是如何实现的?通过TCP/IP协议族。
-
在当前例子中,我们使用HTTP协议请求获取html文本,这时需要发送一个请求消息,该消息将从您的计算机上的协议栈顶部开始,并向下工作。消息在发送前会被分解为许多片段,称为数据包。
-
通过应用层进入TCP层后,每个数据包都会被分配一个端口号,端口号用来确定目标计算机的哪一个应用程序要接受并使用该数据包。
-
进入IP层后,每个数据包将会赋予目标计算机的IP地址。
-
有了IP地址和端口号之后,链路层将数据包的文本信息转译成电子信号,然后通过电缆传输,在电缆的另一端的路由器检查每个数据包中的目标地址,并确定将其发送到何处。
-
最终数据包到达服务器,然后数据包从TCP/IP协议族的底部开始向上运行。当数据包向上通过协议族时,客户端添加的所有路由数据,例如IP地址和端口号都将从数据包中剥离出来,当数据到达栈顶时,数据包已重新恢复成最初始的形式。
-
应用程序根据当前请求数据做出反应,相关数据通过刚才的方式返回给客户端,这样你就看到了b站首页内容。
网络传输
你的电脑通过调制解调器modem,也就是我们常说的猫,猫将计算机的数字信号翻译可沿普通电话线传输的模拟信号后,在公共电话网络进行传输,公共电话网络通过连接ISP(互联网服务提供商,我们生活中常见的电信、移动、联通等都是ISP)来接入互联网,数据包经过电话网络和ISP后,它们将路由到ISP的主干网络,数据包通常会从此经过多个路由器,并经过多个主干网直到找到目的地。
互联网骨干网由许多相互连接的大型网络组成,这些大型网络被称为网络服务提供商,简称NSP。NSP是为ISP提供网络主干服务的公司,ISP可以从NSP那里批量购入带宽,为客户提供网络接入服务。
NSP网络通过网络访问点NAP相连,来交换数据包流量。每个NSP都必须连接到至少三个网络访问点。数据包流量可能会通过NAP从一个NSP的主干跳到另一个NSP的主干。
互联网是如何帮数据找到一个正确路线,把数据包送到目的地呢?
ISP NSP 所有网络提供都携带路由器,每个路由有当前子网络ip的路由表,当底层向上层发送数据时候,找不到会依次向上找,
直到到达NSP 主干网为止,连接到NSP骨干网的路由器拥有最大的路由表,通过这张表可以将数据包路由到正确的骨干网,然后他将开始向下传播,进入越来越小的网络,直到找到目的地为止。
浏览器如何通过域名知道访问哪个IP地址呢?
(5)域名系统DNS
DNS是一个分布式数据库,存储了域名和IP的对应关系。
在浏览器中输入网址时,浏览器首先连接DNS服务器,获取到该域名的IP地址后,浏览器再连接访问该IP的服务器,有了DNS后,之后服务器的IP地址有了变化,重新绑定一下域名和新IP地址就可以了。
- 根域名服务器是最重要的域名服务器。所有的根域名服务器都知道所有的顶级域名服务器的域名和 IP 地址。不管是哪一个本地域名服务器,若要对因特网上任何一个域名进行解析,只要自己无法解析,就首先求助于根域名服务器。在因特网上共有13 个不同 IP 地址的根域名服务器,它们的名字是用一个英文字母命名,从a 一直到 m(前13 个字母)。
- 顶级域名服务器(即 TLD 服务器):这些域名服务器负责管理在该顶级域名服务器注册的所有二级域名。当收到 DNS 查询请求时,就给出相应的回答(可能是最后的结果,也可能是下一步应当找的域名服务器的 IP 地址)。
- 权限域名服务器:负责一个区的域名服务器。当一个权限域名服务器还不能给出最后的查询回答时,就会告诉发出查询请求的 DNS 客户,下一步应当找哪一个权限域名服务器。
- 本地域名服务器对域名系统非常重要。当一个主机发出 DNS 查询请求时,这个查询请求报文就发送给本地域名服务器。每一个因特网服务提供者 ISP,或一个大学,甚至一个大学里的系,都可以拥有一个本地域名服务器,这种域名服务器有时也称为默认域名服务器。
域名到IP地址的解析
当某一个应用进程需要把主机名解析为IP地址时,该应用进程调用解析程序(resolver),并成为DNS的一个客户,把待解析的域名放在DNS请求报文中,以UDP用户数据报方式发给本地域名服务器,本地域名服务器在查找到该域名对应的IP地址之后,把对应的IP地址放在回答报文中返回。
域名的解析过程
(1)主机向本地域名服务器的查询一般都是采用递归查询。如果主机所询问的本地域名服务器不知道被查询域名的 IP 地址,那么本地域名服务器就以 DNS 客户的身份,向其他根域名服务器继续发出查询请求报文(替该主机继续查询),而不是让该主机自己进行下一步的查询。
(2)本地域名服务器向根域名服务器的查询通常是采用迭代查询(比较少用)。当根域名服务器收到本地域名服务器的迭代查询请求报文时,要么给出所要查询的 IP 地址,要么告诉本地域名服务器:“你下一步应当向哪一个域名服务器进行查询”。然后让本地域名服务器进行后续的查询
3.内容分发网络CDN
CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络“边缘”,使用户可以就近取得所需的内容,提高用户访问网站的响应速度。
CDN的一个简单的示意图:
工作流程:当用户访问已经加入CDN服务的网站时,首先通过DNS重定向技术确定最接近用户的最佳CDN节点,同时将用户的请求指向该节点。当用户的请求到达指定节点时,CDN的服务器(节点上的高速缓存)负责将用户请求的内容提供给用户。
具体流程为: 用户在自己的浏览器中输入要访问的网站的域名,浏览器向本地DNS请求对该域名的解析,本地DNS将请求发到网站的主DNS,主DNS根据一系列的策略确定当时最适当的CDN节点,并将解析的结果(IP地址)发给用户,用户向给定的CDN节点请求相应网站的内容。
浏览器是如何运作的?
浏览器是运行在操作系统上的一个应用程序。
每个应用程序必须至少启动一个进程来执行其功能。每个程序往往需要运行很多任务,进程就会创建一些线程来帮助它去执行这些小的任务。
进程与线程
当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里。当应用关闭时,该内存空间就会被回收。
由于每个进程分配的内存空间是独立的,如果两个进程间需要传递某些数据,则需要通过进程间通信管道IPC来传递。
很多应用程序都是多进程的结构,这样是为了避免某一个进程卡死。由于进程间相互独立,这样不会影响到整个应用程序。
进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的任务,同一进程下的线程之间是可以直接通信共享数据的。
浏览器结构
- 用户界面
- 浏览器引擎:用于在用户界面和渲染引擎之间传递数据
- 渲染引擎(浏览器内核):负责渲染用户请求的页面内容。(IE使用Trident、Firefox使用Gecko、Safari使用Webkit、Chrome/Opera/Edge使用Blink)
- 渲染引擎下还有很多小的功能模块,比如负责网络请求的网络模块,用于解析和执行js的js解释器,UI后端还有数据存储持久层(帮助浏览器存储各种数据,比如cookie等等)
多进程浏览器结构
- 浏览器进程:控制除标签页外的用户界面,包括地址,书签,后退,前进按钮等,以及负责与浏览器其他进程负责协调工作
- 缓存进程
- 网络进程:负责发起接受网络请求
- 渲染器进程:用来控制显示Tab标签内的所有内容。浏览器在默认情况下会为每个标签页都创建一个渲染器进程
- GPU进程:负责整个浏览器界面的渲染
- 插件进程:负责控制网站所有的插件
在浏览器地址栏里输入内容时,浏览器内部会发生什么事情?(以Chrome为例)
在浏览器地址栏里输入地址时,浏览器进程的UI线程会捕捉你的输入内容,如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着开始连接服务器获取数据;如果你的输入不是网址,而是一段关键词,浏览器就知道你是要搜索,于是就会使用默认配置的搜素引擎来查询。
网络线程获取到数据之后会发生什么样的事情?
当网络线程获取到数据后,会通过SafeBrowsing来检查站点是否是恶意站点,如果是,则会提示个警告页面,告诉你这个站点有安全问题,浏览器会阻止你的访问,当然也可以强行继续访问。
SafeBrowsing是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全,比如通过查看该站点的IP是否在谷歌的黑名单之内。
当返回数据准备完毕并且安全校验通过时,网络线程就会通知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标签,就会停止html解析流程,转而去加载解析并且执行JS(因为浏览器并不知道JS执行是否会改变当前页面的HTML结构,如果JS代码里用了document.write方法来修改html,那之前的HTML解析就没有任何意义了)
在html解析完成后,就会获得一个DOM Tree。主线程需要解析CSS,并确定每个DOM节点的计算样式(没有自定义样式,浏览器也有自己的默认样式)。主线程通过遍历DOM和计算好的样式来生成Layout Tree。Layout Tree上的每个节点都记录了x、y坐标和边框尺寸。
DOM Tree和Layout Tree并不是一一对应的,设置了display:none的节点不会出现在Layout Tree上,而在before伪类中添加了content值的元素,content里的内容会出现在Layout Tree上,不会出现在DOM树里,这是因为DOM是通过HTML解析获取,并不关心样式,而Layout Tree是根据DOM和计算好的样式来生成。Layout Tree是和最后展示在屏幕上的节点对应的。
为了保证在屏幕上展示正确的层级,主线程遍历Layout Tree创建一个绘制记录表(Paint Record),该表记录了绘制的顺序,这个阶段被称为绘制(paint)。知道了文档的绘制顺序后,该把这些信息转化成像素点显示在屏幕上,这种行为被称为栅格(Rastering)。
现在的Chrome使用一种复杂的栅格化流程,叫做合成,合成是一种将页面的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程(Compositor Thread)中单独进行合成页面的技术。简单来说,就是页面所有的元素按照某种规则进行分图层,并把图层都栅格化好了,然后只需要把可视区的内容组合成一帧,展示给用户即可。
主线程遍历Layout Tree,生成Layer(图层) Tree,当Layer Tree生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器线程,合成器线程将每个图层栅格化。由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分为许多图块(tiles),然后将每个图块发送给栅格化线程(Raster Thread),栅格线程栅格化每个图块,并将它们存储在GPU内存中。
当图块栅格化完成后,合成器线程将收集称为“draw quads”的图块信息,这些信息记录了图块在内存中的位置和在页面哪个位置绘制图块的信息。根据这些信息合成器线程生成了一个合成器帧(Compositor Frame),然后合成器帧通过IPC传送给浏览器进程,接着浏览器进程将合成器帧传送到GPU,然后GPU渲染展示到屏幕上。
当页面发生变化,比如滚动页面,都会生成一个新的合成器帧,新的帧再传给GPU,然后再次渲染到屏幕上。
简述:
- 浏览器进程中的网络线程请求获取到html数据后,通过IPC将数据传给渲染器进程的主线程。
- 主线程将html解析构造DOM树,然后进行样式计算。根据DOM树和生成好的样式生成Layout Tree。
- 通过遍历Layout Tree生成绘制顺序表,接着遍历了Layout Tree生成了Layer Tree。
- 主线程将Layer Tree和绘制顺序信息一起传给合成器线程。
- 合成器线程按规则进行分图层,并把图层分为更小的图块传给栅格化线程进行栅格化。
- 栅格化完成后,合成器线程会获得栅格线程传过来的“draw quads”图块信息。
- 根据这些信息,合成器线程合成了一个合成器帧,然后将该合成器帧通过IPC传回给浏览器进程,浏览器进程再传到GPU进行渲染,之后就展示到你的屏幕上了。
重排重绘
当我们改变一个元素的尺寸位置属性时会重新进行样式计算(Computed Style),布局(Layout)绘制(Paint)以及后面的所有流程,这种行为我们称为重排。
当我们改变某个元素的颜色属性时,不会重新触发布局但还是会触发样式计算和绘制,这个就是重绘。
重排和重绘都会占用主线程,JavaScript也在主线程运行,会出现抢占执行时间的问题。如果你写了一个不断导致重排重绘的动画,浏览器则需要在每一帧都运行样式,计算布局和绘制的操作。
我们知道当页面以每秒60帧的刷新率刷新时,才不会让用户感觉到页面卡顿。如果你在运行动画时还有大量的JS任务需要执行,由于布局、绘制和JS执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有剩余时间,JS就会拿到主线程的使用权,如果JS执行时间过长,就会导致在下一帧开始时JS没有及时归还主线程,导致下一帧动画没有按时渲染,就会出现页面动画的卡顿。
优化手段:
(1)requestAnimationFrame(),这个方法会在每一帧被调用,通过API的回调,然后把JS运行任务分成一些更小的任务块(分成每一帧),在每一帧时间用完前暂停JS执行,归还主线程,这样的话在下一帧开始时,主线程就可以按时执行布局和绘制。
(2)栅格化的整个流程是不占用主线程的,只在合成器线程和栅格线程中运行,即它无需和JS抢夺主线程。如果反复进行重绘和重排,可能会导致掉帧,这是因为有可能JS执行阻塞了主线程,而CSS中有个动画属性叫transform,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格线程,所以不会受到主线程中JS执行的影响,通过使用transform可以节省了很多运行时间。
常用的动画效果:位置变化、宽高变化(旋转、3D等)
Cookie
HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。
cookie 存储在客户端: cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
cookie 是不可跨域的: 每个 cookie 都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是 domain)。
属性说明
name=value键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型。 如果值为 Unicode 字符,需要为字符编码。如果值为二进制数据,则需要使用 BASE64 编码。
domain指定 cookie 所属域名,默认是当前域名path指定 cookie 在哪个路径(路由)下生效,默认是 ‘/’。如果设置为/abc,则只有/abc下的路由可以访问到该 cookie,如:/abc/read。
expires:失效时间,表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。如果不设置这个时间戳,浏览器会在页面关闭时即将删除所有cookie;不过也可以自己设置删除时间。这个值是GMT时间格式,如果客户端和服务器端时间不一致,使用expires就会存在偏差。
maxAgecookie 失效的时间。与expires作用相同,用来告诉浏览器此cookie多久过期(单位是秒),而不是一个固定的时间点。
如果为整数,则该 cookie 在 maxAge 秒后失效。
如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。
如果为 0,表示删除该 cookie 。
默认为 -1。正常情况下,max-age的优先级高于expires。
secure 该 cookie 是否仅被安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效。
httpOnly 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全。
使用 cookie 时需要考虑的问题
- 因为存储在客户端,容易被客户端篡改,使用前需要验证合法性
- 不要存储敏感数据,比如用户密码,账户余额
- 使用 httpOnly 在一定程度上提高安全性
- 尽量减少 cookie 的体积,能存储的数据量不能超过 4kb
- 设置正确的 domain 和 path,减少数据传输
- cookie 无法跨域
- 一个浏览器针对一个网站最多存 20 个Cookie,浏览器一般只允许存放 300 个Cookie
- 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token
Session
session 是另一种记录服务器和客户端会话状态的机制。
session 是基于 cookie 实现的,session 存储在服务器端,sessionId 会被存储到客户端的cookie 中。
session 认证流程:
- 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session
- 请求返回时将此 Session 的唯一标识信息 SessionID 返回给浏览器
- 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID属于哪个域名
- 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息,如果存在自动将 Cookie信息也发送给服务端,服务端会从 Cookie 中获取 SessionID,再根据 SessionID 查找对应的 Session信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。
根据以上流程可知,SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。
使用 session 时需要考虑的问题
- 将 session 存储在服务器里面,当用户同时在线量比较多时,这些 session 会占据较多的内存,需要在服务端定期的去清理过期的session
- 当网站采用集群部署的时候,会遇到多台 web 服务器之间如何做 session 共享的问题。因为 session是由单个服务器创建的,但是处理用户请求的服务器不一定是那个创建 session 的服务器,那么该服务器就无法拿到之前已经放入到session 中的登录凭证之类的信息了。
- 当多个应用要共享 session 时,除了以上问题,还会遇到跨域问题,因为不同的应用可能部署的主机不一样,需要在各个应用做好 cookie跨域的处理。
- sessionId 是存储在 cookie 中的,假如浏览器禁止 cookie 或不支持 cookie 怎么办? 一般会把sessionId 跟在 url 参数后面即重写 url,所以 session 不一定非得需要靠 cookie 实现
- 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token
Cookie 和 Session 的区别
安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
存取值的类型不同:Cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,Session 可以存任意数据类型。
有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭(默认情况下)或者 Session 超时都会失效。
存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。
Token(令牌)
1、Acess Token:访问资源接口(API)时所需要的资源凭证。
简单 token 的组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)
特点:
- 服务端无状态化、可扩展性好
- 支持移动端设备
- 安全
- 支持跨程序调用
token 的身份验证流程:
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
- 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
- 服务端收到请求,然后去验证客户端请求里面带着的 token,如果验证成功,就向客户端返回请求的数据
每一次请求都需要携带 token,需要把 token 放到 HTTP 的 Header 里
基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
token 完全由应用管理,所以它可以避开同源策略
2、refresh token
refresh token 是专用于刷新 access token 的 token。如果没有 refresh token,也可以刷新 access token,但每次刷新都要用户输入登录用户名与密码,会很麻烦。有了 refresh token,可以减少这个麻烦,客户端直接用 refresh token 去更新 access token,无需用户进行额外的操作。
- Access Token 的有效期比较短,当 Acesss Token 由于过期而失效时,使用 Refresh Token就可以获取到新的 Token,如果 Refresh Token 也失效了,用户就只能重新登录了。
- Refresh Token 及过期时间是存储在服务器的数据库中,只有在申请新的 Acesss Token时才会验证,不会对业务接口响应时间造成影响,也不需要向 Session 一样一直保持在内存中以应对大量的请求。
使用 token 时需要考虑的问题
- 如果你认为用数据库来存储 token 会导致查询时间太长,可以选择放在内存当中。比如 redis 很适合你对 token 查询的需求。
- token 完全由应用管理,所以它可以避开同源策略
- token 可以避免 CSRF 攻击(因为不需要 cookie 了)
- 移动端对 cookie 的支持不是很好,而 session 需要基于 cookie 实现,所以移动端常用的是 token
Token 和 Session 的区别
Session 是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而 Token 是令牌,访问资源接口(API)时所需要的资源凭证。Token 使服务端无状态化,不会存储会话信息。
Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击,而 Session 就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加 Session 来在服务器端保存一些状态。
所谓 Session 认证只是简单的把 User 信息存储到 Session 里,因为 SessionID 的不可预测性,暂且认为是安全的。而 Token ,如果指的是 OAuth Token 或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对 App 。其目的是让某 App 有权利访问某用户的信息。这里的 Token 是唯一的。不可以转移到其它 App上,也不可以转到其它用户上。Session 只提供一种简单的认证,即只要有此 SessionID ,即认为有此 User 的全部权利,是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方 App。
所以简单来说:如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 Token 。如果永远只是自己的网站,自己的 App,用什么就无所谓了。
JWT
- JSON Web Token(简称 JWT)是目前最流行的跨域认证解决方案。
- 是一种认证授权机制。
- JWT 是为了在网络应用环境间传递声明而执行的一种基于JSON 的开放标准(RFC 7519)。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用在用户登录上。
- 可以使用 HMAC 算法或者是 RSA 的公/私秘钥对 JWT 进行签名。因为数字签名的存在,这些传递的信息是可信的。
- 阮一峰JSON Web Token入门教程
JWT 的原理
- 用户输入用户名/密码登录,服务端认证成功后,会返回给客户端一个 JWT
- 客户端将 token 保存到本地(通常使用 localstorage,也可以使用 cookie)
- 当用户希望访问一个受保护的路由或者资源的时候,需要请求头的 Authorization 字段中使用Bearer 模式添加
JWT,其内容看起来是下面这样
服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为
因为 JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要
因为 JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制
JWT 的使用方式
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
方式一
- 当用户希望访问一个受保护的路由或者资源的时候,可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
- GET /calendar/v1/events
- Host: http://api.example.com
- Authorization: Bearer
- 用户的状态不会存储在服务端的内存中,这是一种 无状态的认证机制
- 服务端的保护路由将会检查请求头 Authorization 中的 JWT 信息,如果合法,则允许用户的行为。
- 由于 JWT 是自包含的,因此减少了需要查询数据库的需要
- JWT 的这些特性使得我们可以完全依赖其无状态的特性提供数据 API 服务,甚至是创建一个下载流服务。
- 因为 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
方式二
跨域的时候,可以把 JWT 放在 POST 请求的数据体里。
方式三
通过 URL 传输
http://www.example.com/user?token=xxx
使用 JWT 时需要考虑的问题
- 因为 JWT 并不依赖 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
- JWT 不加密的情况下,不能将秘密数据写入 JWT。
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
- JWT 最大的优势是服务器不再需要存储 Session,使得服务器认证鉴权业务可以方便扩展。但这也是 JWT最大的缺点:由于服务器不需要存储 Session 状态,因此使用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦JWT 签发了,到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- JWT 适合一次性的命令认证,颁发一个有效期极短的 JWT,即使暴露了危险也很小,由于每次操作都会生成新的 JWT,因此也没必要保存JWT,真正实现无状态。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
Token 和 JWT 的区别
相同:
- 都是访问资源的令牌
- 都可以记录用户的信息
- 都是使服务端无状态化
- 都是只有验证成功后,客户端才能访问服务端上受保护的资源
区别:
- Token:服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户信息,然后验证 Token 是否有效。
- JWT:将 Token 和 Payload 加密后存储于客户端,服务端只需要使用密钥解密进行校验(校验也是 JWT自己实现的)即可,不需要查询或者减少查询数据库,因为 JWT 自包含了用户信息和加密的数据。
常见的加密算法
哈希算法(Hash Algorithm)又称散列算法、散列函数、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。哈希算法将数据重新打乱混合,重新创建一个哈希值。
哈希算法主要用来保障数据真实性(即完整性),即发信人将原始消息和哈希值一起发送,收信人通过相同的哈希函数来校验原始数据是否真实。
哈希算法通常有以下几个特点:
正像快速:原始数据可以快速计算出哈希值
逆向困难:通过哈希值基本不可能推导出原始数据
输入敏感:原始数据只要有一点变动,得到的哈希值差别很大
冲突避免:很难找到不同的原始数据得到相同的哈希值。
注意:
- 以上不能保证数据被恶意篡改,原始数据和哈希值都可能被恶意篡改,要保证不被篡改,可以使用RSA 公钥私钥方案,再配合哈希值
- 哈希算法主要用来防止计算机传输过程中的错误,早期计算机通过前 7 位数据第 8 位奇偶校验码来保障(12.5%的浪费效率低),对于一段数据或文件,通过哈希算法生成 128bit 或者 256bit 的哈希值,如果校验有问题就要求重传。
使用加密算法时需要考虑的问题
- 绝不要以明文存储密码
- 永远使用 哈希算法 来处理密码,绝不要使用 Base64或其他编码方式来存储密码,这和以明文存储密码是一样的,使用哈希,而不要使用编码。编码以及加密,都是双向的过程,而密码是保密的,应该只被它的所有者知道,这个过程必须是单向的。哈希正是用于做这个的,从来没有解哈希这种说法, 但是编码就存在解码,加密就存在解密。
- 绝不要使用弱哈希或已被破解的哈希算法,像 MD5 或 SHA1 ,只使用强密码哈希算法。
- 绝不要以明文形式显示或发送密码,即使是对密码的所有者也应该这样。如果你需要 “忘记密码” 的功能,可以随机生成一个新的一次性的(这点很重要)密码,然后把这个密码发送给用户。
引擎是如何工作的?
引擎很复杂,但是基本原理很简单。
- 引擎(如果是浏览器,则引擎被嵌入在其中)读取(“解析”)脚本。
- 然后,引擎将脚本转化(“编译”)为机器语言。
- 然后,机器代码快速地执行。
引擎会对流程中的每个阶段都进行优化。它甚至可以在编译的脚本运行时监视它,分析流经该脚本的数据,并根据获得的信息进一步优化机器代码。
参考链接:
傻傻分不清之 Cookie、Session、Token、JWT
前端学习路线
最后
以上就是谦让自行车为你收集整理的互联网是如何运作的?互联网是如何运作的?浏览器是如何运作的?的全部内容,希望文章能够帮你解决互联网是如何运作的?互联网是如何运作的?浏览器是如何运作的?所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复