当我们在浏览器中输入 https://mp.csdn.net 并且按下回车之后你们猜会发送什么?
解析 URL,DNS 查询,获得服务器 IP 地址,向目标地址发送 HTTP 请求,服务器收到,响应,返回页面,浏览器接收,渲染,bingo!
你露出满意笑容,又是一个满分回答。
停!还没完呢。请问这位同学,你发送的请求是怎么到达目标服务器的,服务器的响应又是如何回来的?
你陷入沉默,说了一些 TCP 协议、OSI 模型、路由器之类的词。
嘿嘿,那此时问题又来了:浏览器又是如何将请求发送出去的?
这篇文章,我将以网络通信常用的 TCP/IP 模型为主,解释网络通信涉及的各个阶段,不管是常规的页面访问,还是我们业务中常见的 HTTP 请求,都包含在这样的过程中,了解整体对前端开发者们很有益处。开始吧!
目录
1、 TCP/IP 参考模型是什么
2、 一次完整的网络通信过程
3、 各个阶段的深入理解
一、TCP/IP参考模型是什么
在进入具体的解释之前,我们对 TCP/IP 协议的前世今身做一个简单回顾。
所谓无规矩不成方圆,网络诞生之初,为了保证网络通信的有序进行,相关组织开始着手制定各种通信协议,例如最早的网络控制协议(NCP),到后来耳熟能详的 OSI 七层协议等,整个因特网在这些协议的制约下迅速发展。事物总是发展变化的,技术自然更新换代。到上一世纪 80 年代,美国国防部的 ARPA 网(也就是阿帕网,互联网的鼻祖)项目中,TCP、IP 协议最早被提出来得到应用,并且由于其优异性迅速成为互联网通信的主流通用协议。
这一协议最早得名是因为两个最重要最先被提出的协议 TCP 和 IP,后来,互联网通信的各类协议(HTTP、IP、DNS、TCP、ARP)整体都被纳入这一协议体系中,被统称为“TCP/IP 协议族”。
也就是说,TCP/IP 协议族最早的确只有 TCP 和 IP 两个协议,现在则是一系列与网络通信有关的各类协议的集合。对应这一协议族,同时发展出了TCP/IP 参考模型,这一模型是一个抽象出来的分层模型,TCP/IP 协议族中的所有协议被归类到这一模型的 4 个层次中,每一层相互独立,下一层为上一层提供服务,各个层次间互相协作,完成了互联网通信的主要工作。
这四个层分别是:网络访问层(又叫数据链路层或者网络接口层),网络层,传输层,应用层,大家通常将这四个层次与更为详细的OSI七层模型映射:
在 TCP/IP 参考模型中,各项通信协议各有归属,例如我们在浏览器中常用的:HTTP(超文本传输协议)、DNS(域名系统)、FTP(文件传输协议)、SMTP(简单邮件传输协议等)都是属于应用层协议,而 TCP、UDP 则属于传输层协议,IP 则属于网络层协议。更多的通信协议,大家可以搜索了解。我们这篇文章主要分析以 HTTP 为主的通信过程。
(常见的协议)
二、一次完整的网络通信过程
为了对HTTP请求的通信过程有一个更好的理解,我将从TCP/IP四个层次出发,对应各个层次的通信实体,或者媒介(例如浏览器,路由器以及网线等),看各个协议是如何在这些通信实体中发生作用,将一个请求发送到服务器,服务器的响应又是如何赶回来的。
整体来讲,一次完整的通信,很像快递邮递包裹,物品被加上包装,写上地址信息和联系方式,经过一个又一个的快递中转站,到达收货人的位置,在网络请求中,请求的数据就是需要快递的物品,IP 地址和 MAC 地址就是通信的地址,网线和路由器、交换机和集线器等就是运输道路和快递中转站,网络协议则可以看做快递员和快递政策,而和快递类似的是,网络通信也会出现丢包(包裹损坏)等情况。所以说,一个简单的 HTTP 请求,要完整地拿到请求信息,中间有若干操作系统组件、通信协议、通信实体参与,才能保证通信的顺利进行。
那么,这个快递过程,具体是怎么进行的呢?
其基本原则是,通过分层的顺序,发送端由上往下发送,接收端则由下向上接收。为了保证数据顺利发送到下一个层次,会在发送在其头部加上一些必要的信息,这些信息是为了保证数据的完整和符合需求的约束信息。发送 HTTP 请求时,经常会设置 Request Header,例如 Content-Type:text/html 是向服务器说明,我需要的是HTML页面,你返回其他东西是不行滴。
这些头部信息还可能包含协议信息,请求路径、请求方法等,但这仅仅是发生在应用层,整个通信过程,经过四个层次,会被依次加上 HTTP 请求头,TCP 头部、IP 头部以及以太网头部。数据加上这些头部信息之后,才会被发往下一层,数据经过重重包装,从你的网卡出发,穿过若干路由器,网线,交换机,到达目标服务器所在的子网,服务器要做的则是相反的过程,它会根据前面加上的头部信息,层层解析,最后到达服务器被处理(处理就是服务端程序代码的责任),之后再将响应数据返回。整体过程如下图:
数据在各个层次间依次传递,其传递的数据单位我们可以统一理解为数据包,数据包在不同的层次有不同的称呼,我们熟悉的是,其从应用层出发时,将之称为 HTTP 请求消息(message)或者报文,为其加上消息头之后,发送至传输层,加上 TCP 头部,叫它报文段(segment),到网络层,加上 IP 头部,成为 IP 数据包,到了最后的网络访问层,其已经套上层层包装,在这里在为其加上以太网首部的同时,还会加上一些尾部信息,摇身变为帧(framing),这个过程叫做封装过程。不管怎么叫,数据在各个层次间,是以数据包的形式传递,当数据量大时,还会出现分包传递的情况。
接下来我将这个过程进行扩展,看看在输入/https://mp.csdn.net/之后,TCP/IP 的各个层次间,前面提到的通信协议是如何参与其中的。
三、各阶段发生了什么
1.DNS 查询
URL 只是为了人类更友好识别网络程序而设置的互联网资源的定位标识,浏览器首先会对 URL 进行解析,获取到请求的协议(https),域名(csdn.net),路径(/),由于没有其他子域名,也就是查询csdn 的默认主页。解析完毕后,DNS 出场。
DNS,也就是域名查询系统,负责 URL 对应的 IP 地址的查询,每个操作系统会内置 Socket 协议库,协议库中有解析器(resolver)专门负责这个过程,又是一个漫长的过程(当然我们等的时间不会很长,因为5G的出现,已经加快了网络速度),这里不再具体展开,感兴趣的同学可以看看我在文尾列出的参考资源 How DNS works,其用生动有趣的动画形式展示了这一过程,十分有趣。
经过 DNS 查询,浏览器拿到了请求资源的 IP 地址。IP 地址,又叫网际协议地址,是分配给网络上设备的数字标识,也就是说,只要联网的设备,例如我们的电脑手机以及路由器等都是有 IP 地址的。其是我们的请求数据识别服务器在网络中的位置的必须标识。
2.MAC地址查询
IP 地址在网络层使用,但在实际的数据链路上传递数据时,在同一个链路中不同的计算机,其必须要使用另一个地址来识别—— MAC 地址,又叫做物理地址。
说到这里,岔开一句,在网络通信中,我们通常提到三个地址:IP 地址、MAC地址以及端口号,三者分别代表的是:
IP地址:网络中互联的主机和路由器的标识。
MAC地址:每个网卡硬件的物理地址。
端口号:识别同一个主机上不同的应用程序,也可以理解为程序地址。
所以,在一个网络通信中,我们需要用到五大识别符:源IP地址、目标IP地址、协议、源端口号和目标端口号。
言归正传,MAC地址是每一个网卡被生产的时候就写死在其中的,所以其是不变且唯一的,那么如何得到服务器的MAC地址呢,又一个协议闪亮登场——ARP。
ARP,英文全称叫做 Address Resolution Protocol,也就是地址解析协议,其作用是,根据IP地址获取通信设备的物理地址,其工作原理类似于以前我们常见的广播,它会将包发送给同一以太网的所有主机,若目标主机的MAC地址与对应的 IP 地址命中,则找到了目标,但是,若每次查询都要给所有的主机发送 ARP 请求,网络中则会出现很多 ARP 包,因此,每个主机都会有一个 ARP 缓存,里面存放着常用的 MAC 地址,主机会优先从缓存中查询是否有需要的信息,如果有,就不再发送广播。Mac 地址会在网络层加入 IP 头部发往网络访问层,其实你会发现,为了提高效率和性能,网络通信中随处可见缓存,HTTP 缓存、DNS 缓存以及 ARP 缓存等。值得了解的是,在IPv6中,有一个 NDP(邻居发现协议)替代 ARP 来完成这个工作。
3. 数据传输——套接字来帮忙
我们前面提到操作系统中有一个协议库,负责本机中网络通信的很多功能,在 DNS 查询的时候,协议栈中的解析器就参与其中,当应用层获取到服务器的 IP 地址和 MAC地址,已经拥有了数据传递的必要条件,接下来,浏览器会向操作系统的协议库发出委托指令,调用其中 Socket 库中的程序组件,建立套接字(socket),套接字的本质可以理解为一个数据通道的出入口,其执行的是一个“打开,读/写,关闭”的流程。
在进行数据传输时,客户端和服务器都会创建一个套接字,之后调用 socket 库中的 connect 组件,该组件会依据描述符(套接字的匹配令符,与服务端的套接字的接头暗号),服务器的 IP 地址和端口号,在浏览器和目标服务器之间建立一个传输通道(实际上并没有真实的通道,中间隔着N多网关、路由器和防火墙,这样说更好理解),而大名鼎鼎的 TCP 三次握手实际就是发生在这个阶段,我们会在后面具体介绍 TCP 到底做了什么。
通道建立之后,接下来就是数据的读写操作,我们的程序代码无法直接控制套接字,它依然会委托 Socket 库中的组件来完成数据的读写(write 和 read),数据传输完成后,服务器会主动执行断开操作,调用 Socket 库中的 close 组件断开连接,浏览器发现之后,也会执行断开操作,数据传输完成。
可以看到,在应用程序代码的背后,操作系统中内置的组件库和互联网协议互为网络通信的左膀右臂,共同负责整个通信过程,缺一不可。
4. 三次握手与四次挥手
TCP 和 UDP 是传输层中的两个主要协议,两者的区别是:前者是面向连接的(实际就是套接字管道)、可靠的流协议,后者则并不可靠,它采取一种“尽最大努力”的传输策略,大家可以记住,浏览器、邮件等应用程序一般使用 TCP 传递数据,而像 DNS 查询等较短的收发则使用 UDP。我们主要介绍 TCP。
整体来讲,之所以要在发送数据之前建立套接字,正是因为 TCP 是面向连接的传输协议,在传输协议面前,你发送的内容无关紧要,它们会将之看为一串具有一定长度的数据。
为了保证数据传输的可靠,TCP 在套接字建立连接时,采用“三次握手”策略传输数据。
下面是一张三次握手流程图:
其中,TCP的断开连接四次挥手和2MSL等待状态,我就不详细解说。
图中的 SYN是 Synchronize Sequence Numbers 的简写,也就是同步序列编号,拥有数据传输序列的标识。
ACK指的是acknowledgement,表示确认。
通俗来讲,是这样一个过程:
第一次握手:客户端尝试连接服务器,向服务器发送syn包(同步序列编号),syn=x,其进入SYN_SEND 状态等待服务器确认:我已准备好,随时出发!
第二次握手:服务器接收客户端 syn 包并确认(ack=x+1),同时向客户端发送一个 syn 包(syn=y),也即 syn + ack 包,此时服务器进入SYN_RECV 状态:收到请求,随时接收。
第三次握手:客户端收到服务器的 syn + ack 包,向服务器发送确认包 ack(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED 状态,完成三次握手:OK,我知道了,那我们开始发送数据。
我在知乎上看到另外一种理解:之所以需要三次握手,本质的原因是,传输信道是不可靠的,你不知道网络会在什么时候出什么样的问题,所以,为了保证传输的确可靠,三次握手是理论上的最小值。
TCP 传输数据时,并不是将所有数据一股脑全丢出去,而是会将数据存放在一个的发送缓冲区,并且会等待应用程序的下一段程序到达,之所以这样干,是因为一收到数据立马发送,很可能会造成信道的浪费与闲置,导致网络效率太低,就像明明很宽的马路,非要像过独木桥一样通过。
而 TCP 如何决定数据什么时候会发送,其拥有一套计算机制,整体来说,是按网络包的长度以及应用程序送包过来的时间,虽然你的数据很小,但你半天不发送,我也不会一直傻等着。
还有一种情况是,当发送过来的数据包很大时,会在缓冲区对包进行拆分发送,拆分的时候,TCP 模块会计算好每一块数据的开头和结尾的字节数,并将之写在 TCP 头部信息中,以待对方确认,根据这些起始序号,接收方可以判断是否有数据遗漏的情况。
每发送一个包,就需要等待 ack 包的确认,这也是一种资源浪费,所以为了利用等待 ack 的空闲时间,数据可以不用等待 ack 直接发送,但是,这会出现一种情况,一边一味地发送,但超过了接收方的处理能力,就会出现网络拥堵,得不偿失。为了解决这个问题,TCP 采取了一种滑动窗口的发送策略,它的原理是这样的:
滑动窗口实际上是数据流量控制策略,我们可以将所有的数据想象为一个序列,而窗口则是数据可处理的数据大小,接收方会将窗口的大小写入 ack 包的头部信息中,告知发送方,发送方会据此动态调整发送的速率,由于传输的持续进行,这个窗口大小会动态变化,因此达到了流量的控制,其本质的目的是,不要发的太快,让接收方处理不过来。
所以说,每当我们打开网页,就会通过:
解析 URL,DNS 查询,获得服务器 IP 地址,向目标地址发送 HTTP 请求,服务器收到,响应,返回页面,浏览器接收,渲染
服务器会根据我们的请求头信息,处理返回结果,之后,请求响应信息重新出发,再次回到浏览器。
浏览器收到请求后,经过解析、渲染过程,页面呈现在我们面前。