当你在浏览器的地址栏中按下 Enter 键后会发送什么?

Posted on 2018 July 23
Words 919

I make the following picture use draw.io .

The path that the browser send a HTTP request to the Web server

In these a few seconds, so many works softwares and hardwares have done that you can see the beautiful Web page.

What have they done ? There are 6 steps as followed:

Step 1 浏览器生成消息

浏览器会解析 URL 并通过调用 Socket library 中的 resolver ,即 DNS 客户端,根据解析后的 域名 发送一个 UDP 请求给 DNS 服务器 来查询相应得到 IP 地址 。下面是 Python 调用 resolver 查询 IP 地址的例子:

>>> import socket
>>> socket.gethostbyname('venusworld.cn')
'52.167.214.135'

得到了 IP 地址之后,浏览器就会通过调用 Socket 库来委托操作系统中的 协议栈 来发送 TCP/IP 请求。

Step 2 委托协议栈和网卡将信号发送出去

浏览器调用 Socket.socket 后,协议栈就会根据协议类型创建一个 套接字 并将其存在内存中。之后浏览器会调用 Socket.connect 来完成 TCP 的三次握手。首先,客户端会发送一个头部 控制位SYN:1 、初始 序号滑动窗口大小 的 TCP 包给服务器端,然后,服务器会回复 ACK 号、滑动窗口大小、SYN:1 和服务器端的初始序号给客户端,最后,客户端会发送 ACK 号给服务器端表示确认收到。在 TCP 的三次握手之后,客户端和服务器端的套接字连接状态就会变为 established。

接下来,浏览器就会调用 Socket.writeSocket.read 来委托协议栈收发数据包,客户端会发送序号和数据给服务器端,服务器端会回复 ACK 号和滑动窗口大小给客户端,它们会重复这个操作直到数据发送完成。

数据发完之后,服务器端会调用 Socket.close 发送一个 TCP 头部控制位为 FIN:1 的 TCP 包给客户端表示关闭连接,在 HTTP 1.1 中,关闭连接这一操作也可以从客户端发起,客户端会回复 ACK 号给服务器端表示确认收到并发送一个 FIN:1 给服务器端,然后服务器端会回复一个 ACK 号给客户端,这样连接就关闭了。而之前创建的套接字会在几分钟后会删除。

TCP 模块 connects, writes, reads 和 closes 期间, 它会委托 IP 模块 将数据加上 IP 头部MAC 头部 后包裹成 IP 包, NIC 会将 IP 包加上 报头SFD 起始帧分界符FCS 帧校验序列 后包裹起来形成 网络包。NIC 中的 PHY(MAU) 模块 会将原始的数字信号转换为易传输的信号格式,例如 4B/5B 格式 然后使用 RJ-45 接口(俗称水晶头)将信号发送到网线中去。

使用 Node.js 创建一个 TCP 服务器:

//tcp_server.js
const net = require('net');
const PORT = 8001;

let server = net.createServer(conn => {
	console.log('connected');

	conn.on('data', data => {
		console.log(`${data} from ${conn.remoteAddress} ${conn.remotePort}`);
		conn.write(`Repeating ${data}`);
	});
}).listen(PORT);

server.on('listen', () => {
	console.log(`listening on ${PORT}`);
});

使用 Python 创建 TCP 客户端:

# tpc_client.py
import socket
HOST = '127.0.0.1'
PORT = 8001

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

while True:
    cmd = raw_input("Please input msg:")
    s.send(cmd)
    data = s.recv(1024)
    print data

结果客户端发送的消息,服务器端都能实时收到并回复:

TCP 服务器和 TCP 客户端通信结果

Step 3 信号在集线器、交换机和路由器中传输

当电信号被传输到 双绞线 之后(“双绞”是为了抑制噪声),信号会到达 集线器(repeater hub),集线器会将信号直接转发给所有和它直连的设备。之后,信号会到达 交换机(switching hub),交换机的 MAC 模块不具有 MAC 地址,交换机会根据 MAC 地址表查新 MAC 地址,然后将信号发送到相应的端口上。然后信号继续传输到 路由器(router),路由器根据 路由表(routing table) 来查询下一个转发路由器的 IP 地址,路由器的各个端口都有 MAC 地址和 IP 地址。路由器与交换机的区别是,交换机是通过 MAC 头部中的接收方 MAC 地址来判断转发目标的,而路由器是根据 IP 头部中的 IP 地址来判断的。

路由表的主要信息如下表:

目标地址(Destination) 子网掩码(Netmask) 网关(Gateway) 接口(Interface) 跃点数(Metric)
10.10.1.0 255.255.255.0 - e2 1
10.10.1.0 255.255.255.255 - e2 1
192.168.1.0 255.255.255.0 - e3 1
192.168.1.10 255.255.255.255 - e3 1
0.0.0.0 0.0.0.0 192.0.2.1 e1 1

路由器会将网络包的 接收方 IP 地址 与路由表中的 目标地址 进行比较,注意,这里的比较只会匹配 网络号 部分,而忽略 主机号 部分。之后路由器会将网络包交给 接口列 中指定的 网络接口,并转发到 网关列 中指定的 IP 地址,匹配的结果会有多个,路由器会寻找网络号最长的一条记录,如果网络号一样长,则根据跃点数来判断。子网掩码为 0.0.0.0 的记录表示“默认路由”,对于的网关为“默认网关”,无论任何地址都能匹配到这条记录。

如果网关是一个 IP 地址,则这个 IP 地址就是要转发的目标地址;如果网关为空,则 IP 头部中的 IP 地址就是要转发的目标地址。路由器也会使用 ARP 来查询下一个转发目标的 MAC 地址。

Step 4 信号进入接入网和网络运营商

通过 互联网接入路由器 之后,就到达了 接入网 ,所谓接入网,指的是连接互联网和家庭、公司网络的通信线路。一般的接入网方式有 ADSL、FTTH、CATV、电话线、ISDN 等。

ADSL 接入网

互联网接入路由器会在网络包前面加上 MAC 头部、PPPoE 头部和 PPP 头部,然后将包发给 ADSL Modem ,ADSL Modem 将包拆分成 ATM 信元,并转换成电信号发送给 分离器 。分离器用于分离或混合 ADSL 信号和电话的语音信号。

信号从分离器中出来后,会通过室内电话线到达大楼的中间配线盘 IDF 和 总配线盘 MDF,然后信号会进入电线杆上架设的电话电缆,信号通过电缆到达电话局,信号到达用户端电话局后会经过配线盘、分离器到达 DSLAM(局端多路 Modem),此时电信号会被还原成 ATM 信元。信元从 DSLAM 出来之后,会到达 宽带接入服务器 BAS ,它会将 MAC 头部和 PPPoE 头部丢弃,取出 PPP 头部及后面的数据,然后将 ATM 信元还原成网络包转发到互联网内部。

FTTH 接入网

光纤分为 单模光纤多模光纤 ,多模光纤主要用于一栋建筑物内的连接,单模光纤则多用于距离较远的建筑物之间的连接。FTTH 主要使用单模光纤。

一种接入方式是用一根光纤直接从用户端连接到最近的电话局。用户端的 光纤收发器 将以太网内从互联网接入路由器发出的电信号转换为光信号,这里并不像 ADSL 那样需要将包拆分为 ATM 信元,然后光信号直接到达 BAS 前面的 多路光纤收发器 ,多路光纤收发器将光信号转为电信号,BAS 的端口接收后将包转发到互联网内部。

另一种接入方式是用户端不使用光纤收发器,使用 光网络单元 ONU 将电信号转为光信号,并在用户附近的电线杆上安装一个名为 分光器 的设备,分光器将多个光信号分离或混合,然后经过光缆将光信号传输到 BAS 前面的 光线路终端 OLT ,OLT 将光信号转为电信号后,BAS 将包转发到互联网内部。

在 ADSL 和 FTTH 接入网中,都需要输入用户名和密码登录后才能上网,而 BAS 使用 PPPoE(以太网的点对点协议) 的方式来实现这个功能。而 PPPoE 是由传统电话或 ISDN 拨号上网使用的 PPP 协议发展而来的。

PPP 协议是这样工作的:首先,用户向运营商的接入点拨打电话,电话接通后输入用户名和密码进行登录操作。用户名和密码通过 RADIUS(远程认证拨号用户服务) 协议从 RAS(远程访问服务器) 发送到 认证服务器 ,认证服务器校验这些信息是否正确。当确认无误后,认证服务器会返回 IP 地址等配置信息,并将这些信息下发给用户。用户的计算机根据这些信息配置 IP 地址等参数,完成 TCP/IP 手法网络包的准备工作,然后就可以发送 TCP/IP 包了。

PPP 无法直接用于 ADSL 和 FTTH 中,PPP 中没有定义以太网中的报头和 FCS 等元素,也没有定义信号的格式,因此无法直接将 PPP 消息转换为信号来发送,要传输 PPP 消息,必须有另一个包含报头、FCS、信号格式等元素的“容器”,然后将 PPP 消息装入这个容器里才行。而 PPPoE 就是将 PPP 消息装入以太网包中进行传输的方式,这样 ADSL 和 FTTH 就可以像拨号上网一样通信了

PPPoE 会降低网络效率,PPPoA 也有 ADSL Modem 和路由器无法分离的限制,这两个问题都是由 PPP 引起的。因此,有一些运营商不使用 PPP,而是使用 DHCP(动态主机配置协议) 方式来向用户下发 IP 地址等配置信息,这种方式不使用心愿,而是将以太网包调制成 ADSL 信号发送给 DSLAM。

网络运营商的内部

网络包通过接入网后,会到达运营商的 POP(Point of Presense,接入点) 路由器,这里是互联网的入口。从多个 POP 传来的网络包会集中到达 NOC(Network Operation Center,网络运行中心) ,可以简单地认为 NOC 是扩大规模后的 POP。如果客户端的运营商和服务器端的运营商不同,则运营商之间使用 IX(互联网交换中心) 来交换网络包,IX 的核心是具有大量高速以太网端口的二层交换机,可以简单理解为高性能的交换机。而运营商之间的路由信息交换使用 BGP(边界网关协议) 机制。

Step 5 信号到达服务器端的局域网

网络包到达服务器 POP 端后,会继续通过服务器前面的 防火墙缓存服务器负载均衡器 等。

防火墙

防火墙只允许特定服务器中的特定应用程序的包通过,然后屏蔽其他的包。防火墙可以分为 包过滤应用层网关电路层网关 等几种方式。包过滤方式的防火墙可以根据接收方 IP 地址、发送方 IP 地址、接收方端口号、发送方端口号、控制位等信息来判断是否允许某个包通过。防火墙不检查包的内容,检查头部信息,因此即使包中含有特定数据,防火墙也无法发现。

负载均衡器

当服务器的访问量上升时,可以通过增加带宽来解决,但是高速线路会传输大量的网络包,这会导致服务器的性能跟不上,可以使用多台服务器来分担负载,这种架构称为 分布式架构。为此,必须有一种机制将客户端发送的请求分配到每台服务器上,最简单的方法是通过 DNS 服务器来分配,在 DNS 服务器中填写多个名称相同的记录,每次查询时 DNS 服务器都会按照顺序返回不同的 IP 地址。这种方式称为 轮询(round-robin),但这种方法有许多缺点,例如如果其中一台服务器宕机了,DNS 服务器还是会返回这台宕机服务器的 IP 地址。还可以使用一种叫作 负载均衡器 的设备,将负载均衡器的 IP 地址代替 Web 服务器的实际地址注册到 DNS 服务器上,客户端请求时,由负载均衡器来决定将请求转发到哪台服务器上。

缓存服务器

除了使用多台功能相同的 Web 服务器分担负载之外,还可以将整个系统按功能分成不同的服务器,如 Web 服务器、数据库服务器。缓存服务器 就是一种按功能来分担负载的方法。

Web 服务器需要执行内部操作,而缓存服务器只需要将保存在磁盘上的数据读取出来发送给客户端就可以了,因此可以比 Web 服务器更快地返回数据。如果缓存服务器不存在缓存数据,缓存服务器就会在 HTTP 头部增加一个 Via 字段,表示这个消息经过缓存服务器转发,然后将消息转发给 Web 服务器。如果缓存服务器中存在缓存数据,缓存服务器就会添加一个 If-Modified-Since 头部字段并将请求转发给 Web 服务器,询问 Web 服务器用户请求的数据时候已经发生变化。然后 Web 服务器会根据 If-Modified-Since 的值与服务器商的页面数据的最后更新时间进行比较,如果数据没有变化,就会返回 304 Not Modified 给缓存服务器,然后缓存服务器就会知道 Web 服务器上的数据和本地缓存中的是一样的,于是就会将缓存数据返回给客户端。

除了在 Web 服务器一端部署一个代理,然后利用缓存功能来改善服务器的性能,还有一种在客户端一侧的缓存服务器,这种称为 正向代理(forward proxy)。正向代理还可以用来实现防火墙,因为代理在转发的过程中可以查看请求的内容,从而根据内容判断是否允许访问,在使用正向代理时,一般需要在浏览器的设置窗口中的“代理服务器”一栏中填写正向代理的 IP 地址(可用来实现科学上网)。

缓存服务器判断转发目标的方法还有一种,那就是查看请求消息的头部。因为 IP 头部中包含接收方 IP 地址,只要知道了这个地址,就知道用户要访问哪台服务器了(HTTP 1.1 增加了一个用于表示访问目标Web 服务器的 Host 字段,因此也可以通过 Host 字段来判断转发目标),这种方法称为 *透明代理*。这种方法也可以转发一般的请求消息,不需要像正向代理那样设置浏览器参数,也不需要像反向代理那样在缓存服务器上设置转发目标,可以将请求转发给任意 Web 服务器。为了让请求消息到达透明代理,而不需要设置浏览器设置,也不需要通过 DNS 服务器解析引导,可以在接入网的入口设置反向代理,这样用户不会察觉到代理的存在,也不会注意到 HTTP 消息是如何被转发的,因此更倾向于将透明代理说成缓存。

内容分发服务

有一些厂商和网络运营商签约,将可以自己控制的缓存服务器放在客户端的运营商处,然后租借给 Web 服务器运营者,这种服务称为 内容分发服务,提供这种服务的厂商称为 CDSP ,它们会和主要的网络运营商签约,并部署多台缓存服务器,同时也会和 Web 服务器运营者签约,使得 CDSP 的缓存服务器配合 Web 服务器工作,缓存服务器可以缓存多个网站的数据,因此 CDSP 的缓存服务器可以提供多个 Web 服务器的运营者共享。

CDN 的工作机制是在 DNS 服务器返回 Web 服务器 IP 地址时,对返回的内容进行一些加工,使其返回距离客户端最近的缓存服务器的 IP 地址。首先,作为准备,需要事先从缓存服务器部署地点的路由器收集路由信息到 Web 服务器的 DNS 服务器商,当客户端 DNS 服务器发送 DNS 查询时,找到 Web 服务器端的 DNS 服务器,Web 服务器端的 DNS 服务器根据事先收集到的路由信息查询各个缓存服务器到客户端 DNS 服务器的路由信息,然后通过比较找出哪台缓存服务器距离客户端 DNS 服务器最近,再将该缓存服务器的 IP 地址返回给客户端 DNS 服务器,这样客户端访问的就是距离客户端最近的缓存服务器了。

还有一种让客户端访问最近的缓存服务器的方法,HTTP 规格中定义了许多头部,其中有一个 Location 的字段,当 Web 服务器数据转移到其他服务器时可以使用这个字段,可以将缓存服务器的地址放到 Location 字段中返回,这种操作称为 重定向

为了改善缓存服务器的缓存命中率,还可以让 Web 服务器在原始数据发送更新时,立即通知缓存服务器,使得缓存服务器上的数据一直保持最新状态。

Step 6 消息到达 Web 服务器并返回响应

网络包到达 Web 服务器之后,Web 服务器就会接收这个包并处理。

服务器端的套接字和端口

区别客户端和服务器端之一在于如何调用 Socket 库上,服务器调用 Socket 库如下:

  1. 创建套接字(和客户端一样)
  2. (等待连接阶段)
    1. 将套接字设置为等待连接状态
    2. 接收连接
  3. 收发数据
  4. 断开管道并删除套接字

下面是使用 Python 中的 socket 库创建一个 TCP 服务器的例子:

import socket

HOST = '127.0.0.1'
PORT = 8001

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 1. 创建套接字(和客户端一样),`s` 为套接字的描述符
s.bind((HOST, PORT)) # 调用 bind 将端口号写入套接字中
s.listen(5)   # 2.1 将套接字设置为等待连接状态

print 'Server start at: %s:%s' %(HOST, PORT)
print 'wait for connection...'

while True:
    conn, addr = s.accept()     # 2.2 接收连接
    print 'Connected by ', addr

    while True:
        data = conn.recv(1024)  # 3. 接收自客户端的数据
        print data

        conn.send("server received you message.")  # 3. 发送数据给客户端
        break
    conn.close()    # 4. 断开管道并删除套接字

当客户端连接包到达时,协议栈会为这个等待连接的套接字复制一个新的副本套接字,然后让客户端连接这个新的副本套接字,否则如果直接让客户端连接到等待连接的套接字上,那么就没有套接字在等待连接了,这时如果有其他客户端发起连接就会遇到问题。

创建套接字时的端口号也是一个关键点,端口号是用来描述套接字的。新创建的套接字副本必须和原来的等待连接的套接字具有相同的端口号,但如果一个端口号对应多个套接字,就无法通过端口号来定位到某个套接字了。为此,要确定某个套接字时,不仅使用服务器端套接字对应的端口号,还同时使用客户端的端口号再加上 IP 地址,总共使用下面 4 种信息来判断:

  • 客户端 IP 地址
  • 客户端端口号
  • 服务器端 IP 地址
  • 服务器端端口号

结果:

服务器程序与客户端的的通信结果

网络包到达服务器端后。首先,网卡的 MAC 模块会将网络包从信号还原为数字信息,校验 FCS 并存入缓冲区,网卡驱动会根据 MAC 头部判断协议类型,并将包交给相应的协议栈。

协议栈中的 IP 模块会如下检查 IP 头部:

  1. 判断是不是发给自己的;
  2. 判断网络是否经过分片;
  3. 将包转交给 TCP 模块或 UDP 模块。

TCP 模块会检查包的接收方端口号,如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包(ICMP 消息),如果存在等待连接的套接字且收到的是发起连接的包,则为这个套接字复制一个新的副本,并将发送方 IP 地址、端口号、序号初始值、窗口大小等必要的参数写入这个套接字。当收到的是数据包时,TCP 模块会根据包的发送方 IP 地址、发送方端口号、接受方 IP 地址、接收方端口号找到相应的套接字,然后将数据块拼合起来并保存在接受缓冲区中,最后向客户端返回 ACK。最后 Web 服务器程序会调用 Socket 库的 read 来获取收到的数据,这时数据会被移交给应用程序,应用程序对收到的数据进行处理并根据请求的内容生成响应消息,再通过 Socket 库的 write 返回给客户端。

Web 服务器发送的效应消息会被分成多个包发送给客户端,然后客户端需要接受数据。首先网卡将信号还原成数字信号,协议栈将拆分的网络包组装起来并取出响应消息,然后将消息移交给浏览器。这个过程和服务器的接收操作相同。

原则上,浏览器可以根据响应消息头部的 Content-Type 字段进行判断数据类型,一般是 Content-Type: text/html 这样的字符串。其中 / 左边的部分称为“主类型”,表示数据的大分类;右边的“子类型”表示具体的数据类型,下表示其中主要的一些类型:

主类型 含义 子类型实例
text 表示文本数据 text/html
text 表示文本数据 text/plain
image 表示图像数据 image/jpeg
image 表示图像数据 image/gif
audio 表示音频数据 audio/mpeg
video 表示视频数据 video/quicktime
application Excel、Word 等应用程序的数据都属于这一类型 application/pdf
multipart 消息体中包含多个部分的数据 multipart/mixed

此外,当数据类型为文本时,还需要判断编码方式,这是需要用 charset 附加表示文本编码方式的信息,如 Content-Type: text/html; charset=utf-8。这里的 utf-8 表示编码方式为 Unicode,中文常用的编码包括 gb2312、gbk、gb18030、big5 等。

除了通过 Content-Type 判断数据类型,还需要检查 Content-Encoding 头部字段,其表示数据压缩的转换方式,如 Content-Encoding: gzip,其使用的表示数据类型的方法是在 MIME(Multipurpose Internet Mail Extensions,多用途因特网邮件扩展) 规格中定义的。

最终,浏览器显示网页内容,访问完成,结束这几十毫秒或几百毫秒的时间。


Thanks for the amazing book 《 网络是怎样连接的》.