网络是怎样连接的
前言
浏览器生成消息
生成 HTTP 请求消息
从输入网址开始
URL 网址开头部分为协议,常见协议有:
- http:访问远程 Web 服务器,例如:http://user:password@www.glass.com:80/dir/file1.html,注意 URL 中可携带用户名和密码,一般都为可省略
- ftp:下载或上传文件;同样可以携带用户名和密码(可省略);
- file:读取本地文件;
- mailto:发送电子邮件;例如:mailto:tony@glass.com,冒号后的部分为邮件地址;
- news:阅读新闻组的文章,例如:news: comp.protocols.tpc-ip,冒号后的部分为新闻组名称;
浏览器先解析 URL
省略文件名的情况
- http://www.lab.glass.com/dir/,末尾有斜杠,但没有文件名,因此访问目录中的默认文件,一般名称为 index.html 或者 default.html;
- http://www.lab.glass.com/dir,末尾没有斜杠,先将 dir 作为文件名来处理,如果找不到,再将其当作目录名来处理;
HTTP 的基本思路
HTTP 是发送数据的一种报文格式规范,由报头和消息体两部分组成,报头是必须的,消息体根据情况是可选的;消息体的格式可以有很多种,以支持不同形式的数据,格式在头部的 content-type 字段中备注;
报头由必要的请求行(首行)和一些可选的字段组成。首行的信息有:
- 方法:GET,POST,PUT 等;
- URI:资源路径,例如 /dir/file.html;
- 协议版本:例如 HTTP1.1
生成 HTTP 请求消息
发送请求后会收到响应
响应的格式和请求基本一致,也是由报头+可选的消息体组成,报头由响应行+可选的头字段组成;响应行由协议版本+状态码+说明组成,例如 HTTP/1.1 200 OK;
向 DNS 服务器查询 Web 服务器的 IP 地址
IP 地址的基本知识
整个互联网实际上是由各种小的局域网组成的。每个小的局域网络通过一台路由器对外发放和接受数据,内部则由路由器为各终端设备分配内部 IP 地址进行管理;
域名和 IP 地址并用的理由
IP 地址不好记,而且同一个网站可能会有多个 IP 地址,同时由于设备的宕机和维护,IP 地址还会变动,因此使用人们更容易记住的域名来访问网站是一种更加健壮的方案;
Socket 库提供查询 IP 地址的功能
操作系统内置的 socket 库提供了查询 IP 地址的功能,这样应用程序只需直接调用该库的功能,即可将域名解析成 IP 地址;
通过解析器向 DNS 服务发出查询
由于 socket 是使用 C 语言编写的,让它被调用时,实际的过程是预先分配一段内部,用来存储后续的结果,然后解析器向 DNS 服务器发出请求,并在得到解析结果后,将其写入预先分配的内存地址中;
域名解析:resolution,解析器:resolver
解析器的内部原理
DNS 服务器的地址可以有多个来源,例如:
- 由 ISP 运营商提供;
- 由用户手工在路由器中提供;
- 由用户手工在网卡的属性中进行设置;
全世界 DNS 服务器的大接力
DNS 服务器的基本工作
DNS 服务器的工作本质即使就是维护一张域名和 IP 地址的映射记录表。当收到一个域名解析请求后,就在表中分组查询询对应类型的 IP 地址。如果找不到,就将请求转发给下级域名的服务器进行查找;
注意:这里有一个很重要的点,即 DNS 服务器的解析是分级。不同级的映射记录有可能存储在同一台服务器上面,也有可能存储在不同的服务器上面;另外解析记录还区分类型,例如 A 类型,MX 邮件类型,CNAME 别名类型等;
域名的层次结构
域名是分层次的,每一层使用点号隔开,例如 www.baidu.com.cn,其中 cn 是国家级别的域名,com 是商业类型域名,baidu 则是域名申请者的自定义名称,www 是域名申请者的内部子域名;
寻找相应的 DNS 服务器并获取 IP 地址
通过缓存加快 DNS 服务器的响应
为了加快响应速度,一般 DNS 服务器上面有会缓存机制,将最近的查询结果缓存起来,这样再收到相同的解析请求时,就可以立即返回结果,而无须再次对外发起请求了;
委托协议栈发送消息
数据收发操作概览
创建套接字阶段
套接字本质上其实是一个存在于内存中的对象,该对象有多个字段,每个字段存放着管理连接的有关信息,例如 IP 地址、连接状态等;因为网络在本质上是不可靠的,数据在传输过程中会出现丢失或中断,因此需要在本地记录当前的连接状态,这样当发生意外时,仍然能够正常工作;
当应用程序调用 socket 库创建套接字后,被调用的函数会返回一个文件描述符,用来代表该套接字对象,这样应用程序后续的操作,都可以向读写文件一样,与套接字对象进行交互;
连接阶段
‘连接’相对当于客户端和服务器之间的一种准备工作,对方交换一下必要的信息和状态,例如起始字节等;交换成功后,即表示网络是畅通的,并进入了可通信的阶段;
由于套接字返回的文件描述符是本地的,因此无法在本机之外使用。因此需要使用端口号来解决这个问题,因为一台机器上,不管是本地计算机,还是远程服务器,正常都会有多个应用程序进行互联网通信,因此需要使用端口号机制来区分不同的应用程序;
通信阶段
当连接成功后,应用程序即可调用 socket 库中的 write 函数,将数据写入套接字返回的文件描述符,并使用 read 函数远程服务器返回的响应;
断开阶段
当客户端收到服务器返回的响应后,本次连接即告结束,但这时不一定马上调用 close 函数断开连接,有两方面的原因:
- 有可能在返回的响应中,发现需要额外的请求数据,例如网页上的图片;
- 有可能应用程序没有马上读取返回的数据,如果马上释放文件描述符,又恰好被操作系统分配给了另外的应用程序,会导致原应用程序无法读取到数据,或者其他应用程序读取到本不属于自己的数据;
用电信号传输 TCP/IP 数据
创建套接字
协议栈的内部结构
套接字的实体就是通信控制信息
套接字的本质就是内存中存放着的一段类似对象的数据结构,该数据结构保存着控制整个连接的各项重要信息,例如连接的源地址、目标地址、端口号、各种连接状态等;各个 socket 函数在需要的时候,就会从该对象读取所需要的信息,来完成自身的工作。并在工作完成后,改写信息,标记最新的状态;
调用 socket 时的操作
连接服务器
连接是什么意思
所谓连接实际是在做建立通信的初始化动作,这些动作包括准备好发送方和接收方的 IP 地址、端口号、状态、内存分配、MAC 地址等事宜;
负责保存控制信息的头部
TCP 协议报文的头部数据来源于在上一步的连接阶段初始化好的那些数据(存放在 socket 对象中),主要字段如下:
- 发送方的端口号:16bit
- 接收方的端口号:16bit
- 序号:32bit,告知对方当前发送的数据包在所有数据包中的序号;
- ACK 号:32bit,告知对方已经收到哪个序号的包了,因此一般是接收方发给发送方才使用;
- 数据偏移量:4bit,告知对方主体数据在当前数据包的起始位置,因此变相也是头部的长度;
- 保留字段:6bit
- 控制位:6bit,告知对方连接指令,包括
- RST:告知对方将强制断开连接
- SYN:告知对方将建立连接;
- FIN:告知对方将断开连接;
- 窗口:16bit,告知对方当前可用窗口大小;
- 校验和:16bit,方便对方检查数据是否有错误
- 紧急指针:16bit,告知对方应紧急处理的数据位置(说实话暂时不知道有啥使用场景);
- 可选字段:长度不固定,较少使用;
连接操作的实际过程
- 客户端的 TCP 模块从 socket 对象中读取需要的值,组成 TCP 报文头部。之后由 IP 模块接手,添加 IP 报文头部,最后交给网卡驱动程序,将数据转成电信号发送出去(此阶段会告知服务端自己的 ACK 序号初始值和窗口大小)。
- 服务端的网卡驱动程序接收到电信号,转成数据后交给 IP 模块解析出 TCP 报文,之后再转给 TCP 进一步解析出数据;服务端按相同的方式发送报文给客户端(此阶段会告知客户端自己的 ACK 序号初始值和窗口大小);
- 客户端收到后,将 socket 对象中的 syn 字段值更新为 1,表示客户端到服务端的通信建立成功;并再次发送 ACK 报文给服务端;
- 服务端收到后,也将其 socket 对象中的 syn 字段值更新为1,表示服务端到客户端的通信建立成功;
收发数据
将 HTTP 请求消息交给协议栈
协议栈并不关心所要发送的内容是什么,无论内容是什么,都看作是一定长度的二进制序列即可;
由于协议栈无法预知应用程序是一次性将数据全部给过来,还是分多次给。因此,在收到数据后,它不一定马上将数据发送出去,而是先将数据放到缓冲区中,然后根据情况来处理;
- 如果收到的数据大于或接近包的最大数据容量(MSS,MTU 扣减头部后的值),则马上将数据发出去,因为多等也无意义;
- 如果收到的数据小于包的最大容量,则看一下计时器,是否超过了最长可等待时间:
- 如果没超过,就再等等;
- 如果超过了,就把包发出去,即使还没有满;
由于应用程序对自己的数据最了解,而栈议栈并不了解,因此协议栈提供了参数选项,应用程序可传递参数给协议栈,告知其准确高效的发送办法;
对较大的数据进行拆分
当所要发送的数据超过了 MSS 时,就需要对包进行拆分,为每个包加上必要的头部信息,示例如下:
使用 ACK 号确认网络包已收到
数据包发送出去后,发送方的事情并没有就到此结束,它还需要确认对方是否已经收到;如果没有收到,需要重新发送,直到对方收到为止;
为了让接收方知道当前数据包的序号,发送方会将当前包中数据在完整数据中的偏移量放在头部中,作为序号信息,这样接收方收到该包后,马上就知道这段数据,应该放置在整体数据的具体位置;另外,由于头部中还有一个字段是记录当前数据包的头部长度的,因此接收方也能够很快速的知道从数据包中的哪个具体位置开始读取数据;
根据网络包平均往返时间调整 ACK 号等待时间
发送方需要等待接收方返回 ACK 号,然后再判断是否需要重发。这个等待时间的大小很有讲究,因为太大或太小都需要付出代价;太大的,等待过久,用户体验不好;太小的话,重复发送,导致网络堵塞,最终网络速度变慢,同样用户体验不好;
由于 ACK 的返回速度跟接收方的地理位置有关,理论上双方离得越远,返回的就越慢,因此最好的办法是根据实际情况,动态设置 ACK 等待时间;
使用窗口有效管理 ACK 号
当有了 ACK 等待时间后,在 ACK 号返回前,如果傻傻的等待,啥事也不做,显然有点浪费资源,因此便产生了滑动窗口的概念。它的意思是根据窗口大小,一次发出一批多个包,然后在收到接收方的 ACK 信号后,再发送下一批;
由于接收方为接收数据所分配的缓冲区大小是有上限的,因此当到达的数据过快过多,而数据又未能被及时处理并清空缓冲区的话,就有可能导致缓冲区溢出。为了避免这个情况,接收方会在一开始将自己可用的缓冲区大小告知发送方。发送方在收到该信息后,在不超过该值的情况下,发送合适数量的数据包出去,然后等待接收方下一次告知可用缓冲区大小后,再开始新一轮的发送动作;
虽然这张图显示的好像发送方有等待的情况出现,但在现实情况中,由于接收方的处理速度往往远大于网络速度,因此这种等待的情况不常出现(除非应用程序不来及时取走数据)。接收方在收到数据并清空缓冲区后,发送方的下一个包经常还没有到达,因此接收方发出的窗口大小仍然是完整的大小;
ACK 与窗口的合并
如果接收方每次收到包就马上发送 ACK 号,每次缓冲区大小出现变化,就马上发送新的窗口大小,则接收方发送的包数量将大大增加,从而增加网络堵塞的风险。为了降低这种网络,接收方会设置一段等待时间,在该段等待时间内,如果出现多条 ACK 消息和多个窗口消息,则可以将它们合并成一条,这样就可以有效的提高传输效率;
接收 HTTP 响应消息
应用程序(如浏览器)在发出请求后,会紧接着调用 read 函数从缓冲区中读取数据,但由于数据没有那么快就回来,因此协议栈会先将该操作挂起,等待数据回来后(发触发事件),然后结束挂起,开始读取数据;
在收到数据后,发送方的协议栈所做的事情如下:
- 收到包,开始解析包,检查数据是否完整;
- 如果完整,给发送方发 ACK 信号;如果不完整,丢弃该包;
- 将包中的数据暂存到缓冲区,回到第一步,解析下一个包,直到解析完已收到的所有包;
- 将已到达的多个数据包组装还原成连续的数据,写入应用程序指定的内存地址,清空缓冲区;
- 给发送方发送新的窗口(可用缓冲区)大小;
从服务器断开并删除套接字
数据发送完毕后断开连接
断开连接的时机和操作由应用程序(接收和发送的任意一方都可以)来决定的,因为协议栈无法知道什么数据算是发送完了。一般来说,完成发送的一方会断开连接(Web 程序一般是由服务器来断开,因为它最清楚本次请求的数据是否已经发送完毕了,客户端反而不是第一时间知道);
当服务端想要发起断开连接的操作时,只需调用 socket 库中 的 close 函数,它会生成一个只有头部没有主体的 TCP 数据包,包头部中的 FIN 字段值会设置为 1,表示 FINISH 为 True,然后将该包交给 IP 模块发送出去;
客户端在收到该 FIN 包后,会更新 Socket 对象中的状态字段,以标记为断开状态,然后发一个 ACK 包给服务端,表示自己收到 FIN 包了;
之后当应用程序调用 read 函数来读取数据时,如果缓冲区中有数据,就将数据交给应用程序,直至清空缓冲区 ;如果没有数据,则给应用程序返回信号,告知应用程序数据已经全部接收完毕了,没有更多的数据了。然后应用程序会调用 socket 库的 close 函数,关闭连接。此时客户端会发送一个 FIN 包给服务端;服务端收到该 FIN 包后,会返回一个 ACK 信号;
删除套接字
当连接断开后,双方的套接字并不会立即删除,而是会保留一段时间后(一般是几分钟)再删除。之所以如此,是为了避免出现异外情况。例如客户端在收到服务端的 FIN 包后,会返回一个 ACK 信号给服务端,但有可能该 ACK 在网络中丢失了。因此过了一段时间服务端会重发一次 FIN 信号。如果客户端在发出 ACK 信号后,即将套接字删除,并将端口分配给一个新的应用程序,则有可能导致该应用程序收到服务端的 FIN 包,导致连接出错;
数据收发操作小结
IP 与以太网的包收发操作
包的基本知识
MAC 头部也是由 IP 协议栈来写入的,IP 协议栈将下一个转发设备的 MAC 地址作为 MAC 头部的字段。之后以太网协议根据这个 MAC 头部,就可以知道应该将数据包发给哪台转发设备;因此,在整个传输过程中,MAC 头部的信息是不断变化的,由当前转发设备改写它,改写后的新内容是下一个转发设备的 MAC 地址;
转发设备在收到数据包后,会先去掉 MAC 头部,然后查看里面的目的地服务器的 IP 地址,然后基于该 IP 地址,从自己维护的映射表中,找到下一个转发设备的 MAC 地址,然后为数据包添加新的 MAC 头部;
此处基于以太网来举例,所以用到了 MAC 头部。当数据包在某个非以太网的网络中进行传输时,转发设备就会按照不同的协议规定,为其添加相应的头部,例如无线网、ADSL等,以便该数据包在新的网络中,也可以进行传输。这就是分层的好处,某一层的改变,不会影响到其他层,从而在使用过程中可以非常的灵活,任意的组合,兼容各种使用场景;
包收发操作概览
当收到 TCP 数据包后,IP 模块需要为其添加两个头部,分别是 IP 头部(带 IP 地址)和 MAC 头部(带 MAC 地址);IP 模块并不关心 TCP 包的类型(控制包、数据包)和内容,一视同仁,处理方式完全一样;
生成包含接收方 IP 地址的 IP 头部
IP 头部主要字段如下:
- 版本号:4 bit,IP 协议版本号,例如 IPv4 还是 IPv6;
- 头部长度 IHL:4 bit,由于存在可选字段,头部长度是动态的,因此需要有字段来标注长度;
- 服务类型 ToS:8 bit,包传输的优先级;
- 总长度:16 bit,整条 IP 消息的总长度;
- ID 号:16 bit,用来标识包的编号,类似于序列号;如果包被分片,则所有分片的 ID 号会相同,以便识别它们属于同一个包;
- 标志 Flag:3 bit,表示当前包是否允许分片,以及是否存在分片;
- 分片偏移量:13 bit,表示当前分片从整条 IP 消息的起始位置;
- 生存时间 TTL:8 bit,为了避免出现 网络回环时,包在网络中被无限制的传递下去;每经过一个中转设备,该值就会减 1,当减为 0 后,就不转发该包,而是直接丢弃(貌似该值可用来判断包被转发了多少次?);
- 协议号:8 bit,用来表示 TCP、UDP、ICMP 等协议信息;
- 头部检验和:16 bit,用来数据完整性,据说现在已经弃用;
- 发送方 IP 地址:32 bit
- 接收方 IP 地址:32 bit
- 其他可选字段:很少使用;
IP 地址并非分配给计算机的,而是分配给网卡的,因此一台计算机有多个网卡时,就可以拥有多个 IP 地址;例如笔记本电脑既支持有线连接,也支持无线连接,因此其实它配备了两张网卡,如果两种连接都启用的话,会有两个 IP 地址;
当计算机拥有多张网卡和多个 IP 时,如果判断数据包应该交给哪张网卡进行转发呢?答案是使用路由表(route table);路由表中记载着转发规则,其中会有一条通用规则,当其他规则都无法匹配时,就使用该通用规则来转发。通用规则一般表示默认网关,它的目标地址和掩码都是 0.0.0.0;
路由表一般有下面几列:
- Destination:表示目标地址;
- Netmask:表示掩码,用来和 TCP 告知的目标 IP 地址进行掩码计算,再根据计算结果匹配对应的 Destination;
- Gateway:表示路由器的 IP 地址;
- Interface:表示负责发送数据包的网络接口(即网卡),当匹配成功时,就会在 IP 头部中填写该 Interface 的 IP 地址作为发送方地址,然后将数据包交给该网卡进行发送;发送的目的地即是 Gateway(此处的目的地猜测不是去改写 TCP 包中的目标 IP 地址,而是在 MAC 头部中填写该 Gateway 对应的 MAC 地址);
- Metric:表示线路的传输成本,值越高,表示距离 越远;
IP 模块在给数据包添加 IP 头部和 MAC 头部前,需要先到路由表中查询对应的信息,之后才有办法生成头部;
生成以太网用的 MAC 头部
MAC 头部主要有以下几个字段:
- 接收方的 MAC 地址:48 bit,不像 IP 地址有层级结构,整个地址是一个整体,没有层级规律;
- 发送方的 MAC 地址:48bit,网卡自带的 MAC 地址;
- 以太类型:表示不同的协议,例如 ARP协议、IPv6 协议、IP 协议、IEEE802.3 协议等;
通过 ARP 查询目标路由器的 MAC 地址
ARP:address resolution protocol,地址解析协议,基于 IP 地址查询相应的 MAC 地址;
ARP 有点类似广播机制,它会向同一子网中的所有设备广播一条查询消息;收到消息的设备,会检查自己的 IP 是否与查询信息匹配,如果匹配,就应答;如果不匹配,就保持沉默;(如果是连接到集线器的话,广播是可以理解的;但好奇如果网卡是连接到路由器而,路由器应该不会广播吧,而是直接返回结果?)
为了避免重复查询,IP 模块使用了 ARP 缓存。每次查询一条新记录后,就将结果保存到缓存中。这样下次查询的时候,先查找一下缓存中的记录,如果能够找到,就不需要对外广播查询了,提高效率;
由于外部的 IP 地址可能是动态变化,同一个 IP 地址,一段时间后,可能绑定到了一台新的设备上面。因此 ARP 缓存中的数据是会失效的。为了规避失效问题,简单粗暴的办法就是每隔一段时间,让删除缓存记录,这样就可以定期更新了;尽管如此,在刷新之前,有可能缓存记录就已经是错的了,这个时候就只能手动删除缓存来解决了;
添加 MAC 头部的动作理论上也可以交由网卡来完成,但这样会导致网卡(硬件)和网络类型耦合,降低了网卡的通用性,因此设计成由 IP 模块来完成添加 MAC 头部的动作会更加合理;
以太网的基本知识
最早的以太网是设计成广播式的,以太网中的各个设备通过一条主干网线连接起来,然后任意一台设备发出的数据包,会传输给所有的设备。收到的数据包的设备检查包中的 MAC 地址是否与自己的相符,如果相符,就处理它;如果不符,就丢弃它;显然,这种方式效率有点低,后来进一步进化,衍生出了交换机。交换机作为中介接收数据包,再将数据包发给匹配的设备,不再广播给所有设备了,这样提高了处理效率;
简化来说,以太网可以视作基于 MAC 地址进行相互通讯的一个局域网络;
将 IP 包转换成电或光信号发送出去
IP 模块完成添加头部的动作后,接下来就需要将数据包交给网卡驱动进行处理了。网卡驱动负责将数据写入到网卡的缓冲区,然后通知网卡内部的 MAC 模块进行发送;MAC 模块收到指令后,从缓冲区读取数据后,交给 PHY 模块转成电信号发送出去;
虽然每张网卡在出厂的时候的内置一个全球唯一的 MAC 地址,但是实际通信过程中不一定会使用它。该地址可由网卡驱动程序从网卡内置的 ROM 中读取,也可以由用户自己设置写入驱动程序,此时会忽略内置的 ROM;
给网络包再加 3 个控制数据
网卡的 MAC 模块从缓冲区读取完数据后,会为它们再次添加三个数据,分别是:
- 报头:56 bit,由 1 和 0 组成的比特序列;这些 1 和 0 序列转成电信号后,会出现特定形状的波形,接收方基于该波形判断什么时候开始读取数据(用来同步双方的时钟周期);
- 起始桢分界符 SFD:8 bit, 也是一个特定 1 和 0 组成的序列,接收方在看到该序列时,就知道接下来的部分是主体数据了;
- FCS:32 bit,校验和,方便接收方判断所接收到的数据是否完整和正确,用来排除传输过程中的信号干扰;
之所以会有波形的概念,然后在于 1 和 0 会被转化成特定的电压和电流,这样电压和电流就会出现变化,形成波;理论上,接收方在收到电信号波后,可以将其还原成 0 和 1。但这里面存在一个问题,当连续出现 1 或 0 的时候,波形没有变化,这个时候就抓瞎了,不知道该段波形中包含几个 1 或 0;
为了能够能够一段没有变化的波形中,包含几个 1 或 0,就需要引入时钟单位,每个时钟单位对应一个比特。这样就可以知道一段没有变化的波形包含几个比特的 1 或 0 了;最简单粗暴的办法,是增加一条线路,将时钟信号也发给接收方。这样接收方就可以根据时钟信号,对另外一条线路中的数据信号进行解读。
但是这又引入了一个新的问题,当有两条线路时,它们很难是一样长度的。随着距离超长,二者的长短误差就会变得越大。大到一定程度时,就会导致时钟偏移(偏左或偏右了),这样最终读取出来的数据就不准确了;
为了解决偏移问题,一个巧妙的办法是将两种信号叠加起来,然后额外告知对方时钟的变化周期,让双方的时钟周期实现同步。这就是报头的作用,报头本质上是一段让接收方获得发送方时钟周期的特殊信号;接收方在收到数据包后,解析报头,获得时钟信号。然后反算出数据信号;
向集线器发送网络包
网卡的 MAC 模块为数据添加头部、分界符和校验值之后,就可以调用 PHY(MAU)模块将数据转成电信号发送出去。日常生活中经常听到的 10M 或 100M 带宽的意思即是每秒种可以将多少数字信号转成电信号;
集线器的工作方式是半双工的,意思是同一个时刻,要么是发送状态,要么是接收状态,两种状态不能同时存在,有点像对讲机;因此,在给集线器发送信号之前,网卡需要先判断一下当前的线路状态,如果处于空闲就可以发送;如果正在发送上一组信号,或者正在接收信号,则需要等待;当多台设备同时发送信号时,就会出现信号碰撞,导致传输的信号无效。这时检测到碰撞的设备会广播碰撞信号,所有收到广播的设备都会终止发送。然后根据各自的 MAC 地址计算出各自的等待时间,之后再开始发送;
交换机的工作方式是全双工的,接收和发送可以同时发生,因此不会发生信号碰撞的问题,传输效率要高很多,也比较简单;
MAC 模块转换成的电信号是通用格式,但实际是线路有很多规格,不同规格有不同的电信号模式。因此 PHY 模块需要负责将通用电信号转换成特定类型的电信号。例如 10BASE-T 类型的电信号以变化代表 1,没有变化代表 0,如下图:
接收返回包
包的接收过程和发送过程刚好是反过来的,区别在于 MAC 模块将数据存入缓冲区后,需要发出中断信号,这样 CPU 才会宠幸一下,将控制权转移给中断处理程序,中断处理程序再通知网卡驱动程序,把缓冲区中的数据拿走处理;
网卡驱动程序取到数据后,从 MAC 头部解析出以太网类型(如 0800 表示 TCP/IP),然后调用相应的栈议栈(如 TCP/IP)对数据包进行处理。协议栈拿到数据后,对头部进行解析,判断消息应该交给哪个应用程序进行处理;
将服务器的响应包从 IP 传递给 TCP
IP 模块在收到网卡驱动程序的数据包后,会先检查一下这个包是否属于自己(通过比对接收方 IP 地址和当前网卡的 IP 地址是否一致);如果一致,就接收;如果不一致,就报错;
IP 模块报错的方法是按照 ICMP 协议给发送方发一条 ICMP 消息(类型 3,Destination Unreachable),常见的消息如下:
- 类型0:Echo reply,用来响应类型 8 的 Echo 消息;
- 类型3:Destination unreachable,告知对方包未送达目的地,中途被丢弃了;例如因为目标 IP 地址不在路由表中、目标端口号没有对应的套接字等;
- 类型4:Source quench,告知对方收到太多包了,超负荷了,要求对方降低发送速度;
- 类型5:Redirect,重定向,告知对方正确的发送地址;
- 类型8:Echo,ping 消息,用来检查一下对方是否存在。如果存在,对方会回个类型 0 的消息;如果没有回,表示不存在;
- 类型11:Time exceeded,告知对方收到当前数据包时, TTL 已经减为 0 因此包被丢弃了;
- 类型12: Parameter problem,告知对方头部存在字段错误;
有时候 IP 模块还需要做一项“分片重组”的工作。出现这种情况是因为 TCP 数据包比较大,因此需要分成多个小包;这些小包在头部有标记相同的包 ID,同时还有偏移量。因此,IP 模块可以基于这些信息实现重组;
IP 模块完成工作后,会将数据包交给 TCP 模块。TCP 模块会根据接发双方的 IP 地址和端口号,从映射表里面找到对应的套接字,然后根据套接字中的状态,执行不同的操作:
- 如果是数据包,则返回 ACK 消息,并将数据存入缓冲区,等待应用程序来取;
- 如果是控制包,则按规则执行相应的动作;
注意,由于 TCP 模块需要用到 IP 头部中的信息,因此 IP 模块在将数据包转给 TCP 模块处理时,并没有将 IP 头部去掉,不然 TCP 模块就得不到该数据了;
UDP 协议的收发操作
不需要重发的数据用 UDP 发送更高效
TCP 协议为了保证数据完整到达,建立了一套 ACK 机制,因此整个传输过程相对比较复杂(交换控制信息、交换窗口大小、互发 ACK,互发断开信号等);但有些场景并不需要确保数据完整到达,例如音频和视频数据,丢几个包也没什么大不了;又或者像 DNS 查询,数据很少,一个包就装得下了,不存在丢失其中一个包的情况;此时就可以使用更加简化的 UDP 协议来传输数据。由于不需要保证每个包都到达,一下子事情就变得简单多了;
发现发送一段数据,UDP 协议只要一个来回就行了,TCP 要好几个来回,多搞不少事情;
控制用的短数据
类似 DNS 查询之类的短数据,就很适合使用 UDP 来通信;UDP 协议有多简单呢,简单到在收到应用程序的消息后,只需要加上 UDP 头部,之后就可以将数据包转给 IP 模块处理了,其他工作统统没有;如果丢包了,只要过一段时间发现没有返回响应,再重发一次就好了;
UDP 头部总共有 8 字节(64 bit),包含以下几个字段:
- 发送方端口:16 bit
- 接收方端口:16 bit
- 数据长度:16 bit,头部之后的数据长度,注意这里头部的长度是固定的,但数据部分的长度不固定;
- 校验和:16 bit
音频和视频数据
由于音频和视频播放场景对丢包的容忍度比较高,但对播放流畅度有要求,因此也很适合使用 UDP 来传输数据;
有时防火墙会阻止 UDP 协议,因此需要更改规则对 UDP 放行,要么就只能改用 TCP 协议来发送了;
从网线到网络设备
信号在网线和集线器中传输
每个包都是独立传输的
包从网卡出来后,在网络中的传输过程是只涉及到 MAC 头部和 IP 头部,因此 TCP 头部和内容,都与传输过程无关,因为在传输过程中用不到里面的数据;
防止网线中的信号衰减很重要
信号传输的本质其实是在网线上施加正负变化的电压,但是在发送端很清晰准确的电压值,在传输过程中,随着距离加大,会出现衰减,并且也会受到干扰。因此,当信号到达接收端的时候,其波形以及电压值有可能失真。如果失真的程度很大,则会造成信号值解析错误;
双绞是为了抑制噪声
当网线周围存在电磁波时,由于信号线是金属,因此会在信号线上产生电流,叠加到原本的信号电流,导致电流受到干扰出现失真;双绞线的原理,就是让网线不是直的,而是呈螺旋型,这样网线一直在左右两个方向拐来拐去改变方向。而根据电磁波的原理,它会在不同方向的网线上产生方向相反的电流,因此这两部分电流刚好相互抵销;
其他降低噪声的措施:
- 在信号线外部包裹金属屏蔽网;
- 在信号线之间增加隔板;
集线器将信号发往所有线路
以太网最初的设计是广播机制,即信号被广播到所有连接到同一网络的设备,然后由各个设备自行判断当前数据包是否属于自己;是就接收,不是就丢弃;
网线支持全双工,这意味着发送信号和接收信号的线是分工单独运行的,因此当两个设备使用网线进行连接的时候,需要使用交叉接线,不然发送对发送,信号就碰撞了;网卡一般是直线接线(MDI),集线器默认是交叉接线(MDI-X),这样网卡可以用网线直接连接到集线器即可正常工作。但是如果是两台集线器之间,就需要做切换转换,集线器一般自带信号转换开关。开关的作用是将当前接口由默认的 MDI-X 模式转换为 MDI 模式;如果是两台计算机直连,则需要使用交叉网线;
集线器的功能非常简单,大部分是使用中断电路,将发送方的信号,原封不动的广播到所有其他设备上面(貌似非常吻合早期的原始场景);
交换机的包转发操作
交换机根据地址表进行转发
交换机比集线器智能一点,不再广播数据包,而是维护一张 MAC 地址和端口的映射表,然后收到数据包里面,从 MAC 头部解析出 MAC 地址,再从映射表中查询到相应的端口,之后将数据包转发给该端口上面的设备即可;
当然,为了得到数据包中的 MAC 地址,交换机也是需要会出不少代价的,它需要做一遍普通网卡需要做的数据包解析工作。因此,可以将每个交换机上面的端口近似看做一张网卡。唯一的不同是交换机端口会接收所有的包,不像网卡会丢弃不属于自己的包;
网卡支持开启 MIX 模式,在该模式下,网卡会接收所有的包。如果在计算机中安装一个软件对包按 MAC 地址进行转发,那么计算机就可以扮演交换机的功能,相当于”软交换机“;
交换机通过交换电路来实现转发。交换电路的原理也非常简单,它通过一个二维的开关网格,来实现输入端和输出端之间的电路连接。通过操控网格中的电子开关,就可以将任意一路输入端和任意一路输出端连接起来;而且可以并发转不同的输入端信号;
MAC 地址表的维护
MAC 地址表的维护涉及两个动作,分别如下:
- 添加:当收到一个数据包时,解析出其中的发送方 MAC 地址后,将其写入地址表,映射到相应的端口号上面;
- 删除:定期删除记录,以避免设备断开旧端口,切换到新接口;
特殊操作
- 当交换机发现某个包的接收方 MAC 地址和发送方 MAC 地址相同时,就会丢弃该包,因为无法实现转发;
- 当交换机发现某个包的接收方 MAC 地址不在地址表中时,就会在网络中广播这个包;
- 当交换机发现某个包的接收方 MAC 地址是一个广播地址时,也会广播该包;广播地址有标准格式,即 6 个 FF,或者 IP 地址中的 4 个 255;
全双工模式可以同时进行发送和接收
早期的以太网规范只规定了半双工模式,但显然这种传输方式由于要避免信号碰撞,导致传输效率低下。后来规范进行了更新,开始支持全双工模式;
为了支持全双工模式,网卡中的 MAC 模块需要由分别负责发送和接收两个独立模块组成;
自动协商:确定最优的传输速率
在以太网的连接中,当双方没有传输数据时,线路并不是空闲的,而是会持续发送脉冲信号,用来检测当前线路是否处于正常连接的状态;当连接正常时,网卡上的 LED 指示灯会显示绿色;
早期的脉冲信号很简单,仅用来探测连接正常即可。后来人们利用信号中的偶数位置,来传递有意义的信号,用于双方的工作模式和传输速率自动协商;
交换机可同时执行多个转发操作
由于交换机每次只将数据包转发给特定的端口,其他端口并不会接收到该数据包,因此其他端口也可以同时发送或接收自己的数据,即所有的端口都是可以同时工作的,不像集线器模式下,当某个端口广播数据时,其他端口只能停下来并接收数据;
路由器的包转发操作
路由器的基本知识
路由器的核心功能也是包的转发,它与交换机的区别在于,交换机是面向以太网设计的,因此基于 MAC 地址进行转发判断;而路由器是基于互联网设计的,因此它是基于 IP 地址进行转发判断;
路由器主要由两个模块组成,一个是端口模块,一个是转发模块。转发模块负责判断数据包的目的地(类似 IP 模块,基于路由表的查询),端口模块则负责具体收发操作(类似网卡);
不同的路由器拥有一种或多种端口,可以支持多种不同的网络,例如无线局域网、ADSL、FTTH(光纤)、以太网等;
路由器和交换机的一个最大区别在于,交换机只是通过交换电路,帮助收发双方的电路实现连接,它本身并不参与其中。但路由器则不同,它是以独立的中间人身份介入整个收发过程的。发送方和接收方并没有直接互联,而是对接路由器这个中间方;相当于接收方只知道路由器的存在,并没有和发送方直接打交道;为了扮演中间人的作用,路由器上的每个端口都有自己的 IP 地址,而交换机则没有(但也可以有,如果开启 DHCP 功能的话);
路由器有点像是一个透明代理
路由表中的信息
交换机维护的是 MAC 地址映射表,而路由器维护的则是 IP 地址映射表;
10.10.1.0 / 255.255.255.0 所表达的含义和 10.10.1.0 / 24,是完全相同的,都是用来表示匹配该地址的前 24 个位即可;子网掩码的功能是用来表示需要匹配目标地址多少个位;因此,会存在多种匹配级别,例如前8位、前16位、前24位,前32位等;当为前32位时,由于 IPv4 地址总共也才 32 位,因此已经是全部匹配了,此时一般对应某台具体的主机;
当匹配到某条路由记录后,就可以读取记录中的端口号,用来作为数据包的转发目标;
交换机的 MAC 地址表是由交换机自动维护的,而路由器上的路由表,则同时支持自动维护和手工维护两种模式;自动维护会涉及到路由协议,常见的路由协议有 RIP, OSPC, BGP 等;
路由器的包接收操作
路由器的包接收操作跟普通网卡基本没有区别,如果它发现数据包中的 MAC 地址跟当前端口的 MAC 地址不同,它也会像网卡一样,直接丢弃该包,而不会像交换机那样将包收下来;
查询路由表确定输出端口
当路由器在路由表中匹配到多条记录时,最长匹配长度的那条记录将胜出,做为转发目标;如果有多条记录匹配的长度相同,那么跃点数最少的那条记录胜出;如果在路由表中查询不到匹配记录,则丢弃该包,并给发送发送一条 ICMP 消息,告知目标 IP 地址有误;
由于路由表中一般设置有默认匹配路由,所以丢弃包的情况貌似不太容易出现;
找不到匹配路由时选择默认路由
默认路由的子网掩码为 0.0.0.0,它表示需要匹配的比特位数量为 0 个,因此相当于匹配所有了;计算机的网卡 IPv4 属性设置中,也有一个默认网关,其实它就是在设置默认路由;
包的有效期
路由器在转发包的时候,会更新其中数据包头部的 TTL 值,以避免回环;默认值一般为 64 或者 128;
通过分片功能拆分大网络包
由于路由器支持多种端口,因此可能存在收到的数据包尺寸大于端口能够支持的最大尺寸,此时路由器就需要对包作分片的动作,以便能够将包转发出去;
有两种情况不允许分片:
- 头部字段禁止分片;遇到情况会丢弃该包,并发送 ICMP 消息通知发送方;
- 数据包已经分过片了;
路由器的发送操作和计算机相同
路由在转发数据包时与计算机网卡发送数据包的过程基本相同。唯一的不同点是在计算机上面,网关地址即是目标发送地址,根据该地址查询 MAC 地址并添加到 MAC 头部即可。但路由器的网关地址一般为空,此时需要根据最终目标 IP 地址来查询 MAC 地址并添加到 MAC 头部(注:查询回来的结果并不是绑定目标 IP 地址的设备 MAC 地址,而其实是下一个中转设备的 MAC 地址(如交换机或路由器);
路由器与交换机的关系
IP 路由器工作在 IP 层面,它通过查询目标 IP 地址的 MAC 地址,然后将其写到 MAC 头部中;之后交换机通过该头中的 MAC 地址,将数据包转发到下一个路由器;
路由器的附加功能
通过地址转换有效利用 IP 地址
如果全世界每一个计算机都分配一个 IPv4 地址的话,按照 IPv4 地址的长度来计算,是完全不够分配的,因为计算机的数量增长得太快了。因此,人们制订了规则,将当时还未分配的三段 IPv4 地址,设置为仅限内部子网使用。这样每家公司都可以在其子网内自行分配 IP 地址,而不用担心和其他公司产生冲突。而为子网的出口设备分配一个公共 IP,这样子网内的计算机可以和子网外的计算机进行通信了;这个机制是很好的,但是在数据包从子网内到子网外时,需要增加一个动作,即对其做地址转换才行;
私有地址分别有以下三段:
- 10.0.0.0 至 10.255.255.255,约可容纳 1600 万台;
- 172.16.0.0 至 172.31.255.255,约可容纳 100 万台;
- 192.168.0.0 至 192.168.255.255,约可容纳 6 万台
地址转换的基本原理
地址转换的原理非常简单,就是将数据包 IP 头部中的私有地址和端口,改成公有地址和新端口,并在内部建立映射表,然后发送出去。建立映射表的目的是当接收方返回数据时,可以根据映射表再次转换,将数据包转发给内网的设备;
改写端口号的原因
如果不改写端口号,那么只能通过 IP 地址来区分不同的内网设备,这时就需要分每台内网分配一个唯一的公共 IP 地址,并在连接结束后收回。这种方式虽然也可行,但无法最大化的节省公共 IP 地址。因为当内网设备很多的时候,并且有同时上网的需求,就会分配一大堆的公共 IP 地址;
从互联网访问公司内网
地址转换会带来一个有趣的副作用,即如果地址转换映射表中没有记录,从互联网进入内网的包就不知道应转发给哪个设备,此时路由器就会丢弃该包。该副作用是可以保护内网设备的安全性,防止非法入侵;
路由器的包过滤功能
所谓的包过滤,指根据 MAC 头部、IP 头部、TCP 头部中的内容,与提前预设的规则进行匹配,然后根据匹配结果,决定是否丢弃包,还是转发包;多数防火墙软件即是基于包过滤来实现;
虽然包过滤的原理很简单,但是想要实现正确的配置,在实现正常访问的同时,还能够防止非常入侵,是非常困难的
通过接入网进入互联网内部
ADSL 接入网的结构和工作方式
互联网的基本结构和家庭、公司网络是相同的
互联网的基本结构和家庭或公司内部网络是相同的,主要不同点如下:
- 由于物理距离变长和线路的信号衰减,因此需要在中间增加很多路由器作为中间转发设备;
- 路由器上面的路由表由于转发记录很大,其维护机制有所不同;
连接用户与互联网的接入网
不管是互联网接入路由器,还是以太网路由器,它们的主要职责和原理都是一样的,即根据路由包负责包的转发。有多种方式可以将家庭或公司接入互联网,例如 ADSL(电话线)、FTTH(光纤)、CATV(有线电视)等,因此互联网接入路由器会基于入网规则来转发包;
ADSL:asymmetric digital subscriber line,不对称数字用户线路。利用现有电话线进行通信的技术,它的特点是上行和下行不对称;
ADSL Modem 将包拆分成信元
当使用 ADSL 来接入网络时,接入路由器通常使用 PPPoE 方式进行连接,因此路由器会按照 PPPoE 规则,给包加上PPP 头部、PPPoE 头部和 MAC 头部,然后再将包发送给 ADSL Modem;Modem 的职责是将收到的包拆成 ATML 信元,然后转成电信号发送给运营商的 Modem;
ATM:Asynchronous Transfer Mode,异步传输。ATM 信元也是一种数据包,但是它很小,头部只有 5 个字节,数据主体只有 48 字节,使用 ATM 通信协议;
ADSL 之所以使用 ATM 将包拆分成更小的单位,其初衷是尽可能提高兼容性,降低设备的开始和投入成本;
ADSL 将信元“调制”成信号
网线在传输数字信号时,一般使用方波,它的优点是简单,但缺点是容易失真,随着距离变长,出错率会上升。ADSL 为了克服容易失真的缺点,使用了正弦波(圆滑波形)合成信号(调制)来表示 0 和 1;
调制信号有很多种方法,例如振幅调制、相位调制。它们的区别在于使用不同的方法来表示 0 和 1,甚至表示更多的位,例如用 4 种振幅分别表示 00、01、10、11 等(虽然振幅越多可以表示更多种情况,但也更容易误判出错);
相位调制也可以让波从不同位置开始来表示四种情况,示例如下:
正交振幅调制通过结合振幅调制和相位调制,就可以用表示四种情况;由于振幅和相位是不同维度的特征,因此这种维度结合方案,相对单维度的细分方案,更加健壮不容易出错;
ADSL 通过使用多个波来提高速率
波是可以有多种频率的,而通过滤波器我们又可以将不同频率的波分离出来。因此,通过将多种频率的波合成在一起,我们就可以在单位时间内传递更多的信号,而单位时间可以传递的信号数量即是带宽。
ADSL 通过合成上百个不同频率的波,来实现更大的带宽。为了降低分离难度,每种波使用一定的频率范围,不同波之间的频率间隔为 4.312 KHz,并且都使用正交振幅进行调制;
另外不同频段的波,其受到的环境噪声不一样(一般来说,频段越高,衰减和噪声越大)。如果某个频段的环境噪声小,就可以分配更多的比特位;如果噪声大,就分配较少的比特位;
ADSL 之所以能够实现上下行不同的速率,其原因就在于它为上行和下行分配了不同的数量的频段。上行的频段数量少一些,下行的多一些。
噪声和衰减由环境的影响很大,因此每条线路都会存在不同的情况。在为频段分配比特位时,为了提高分配效率,ADSL 在线路通电初始化时,会先做一个测试。根据测试结果,为不同的频段分配最合理数量的比特数(该过程称为训练,一般需要消耗几秒到几十秒左右的时间);
分离器的作用
由于 ADSL 借助电话线进行信号的传播,因此电话信号和网络信号会同时在电话线上存在。分离器的作用,就是根据信号的频率,将电话信号(低频信号)分离出来,并将电话信号转给电话机。不然电话机如果收到所有的信号,就会导致电话声音包含很多噪声,无法听清;
电话机在接通和挂断信号的瞬间,会导致线路的信号出现突然的增加和减少,因此会改变噪声条件。正常情况下,当噪声条件改变时,Modem 之间就需要重新握手。显然这样很不合理,因为在接听或挂断电话时,就会导致网络中断。分离器的另外一个作用,就是避免这种情况的出现。
从用户到电话局
电信号从 Modem 出来后,就是走的日常的电话线路了。一般一幢大楼有很多住户或公司,因此每家住户或公司的电话线会先到达本栋大楼的 IDF(中间配线盘) 或 MDF (主配线盘),然后到达保安器(用来防雷击),最后汇成一股,接到室外的电线杆,延伸到电信局附近,然后走到地下,通过电缆隧道,进入电信局大楼的地下室。之所以最后一段要走地下,是为了避免电信局附近竖起大量的电线杆,一来占地太多,二来有火灾隐患;
配线盘的目的是将电信局出来的线路与各住户或公司进行一一对应;
噪声的干扰
由于电话线也是使用金属来传递电信号,因此它不可避免也会出现噪声干扰的问题;由于电话线设计之初并未考虑到会用来传递 ADSL 高频信号,因此它比双绞网线更容易受到噪声的干扰;
由于电话线传递的是多频合成的信号,其中只有和噪声频率相近的频段会受到噪声的影响,其余频段不受影响,因此最终接收方收到的可用信号变少了。由于在通电初始化时会检测可用频段,因此当出现噪声时,它不会像网线一样出现信号丢失,而是可用频段变少,传输速度下降而已;
通过 DSLAM 到达 BAS
DSLAM:DSL Access Multiplexer,数字用户线路接入复用设备;相当于多路 Modem 集成器,可以同时处理多个用户端 Modem 发过来的信号;理论上电话局也可以为每个用户配备一个 Modem,但显然这样需要大量的空间来放置 Modem;使用 DSLAM 就可以节省空间了;
家用 Modem 有一个以太网接口,用来连接用户家里的路由器,而电信局的 DSLAM 一般不使用以太网接口,因为它不跟电信局路由器直接连接,而是使用 ATM 接口,先跟 BAS 设备连接,之后 BAS 设备再跟路由器连接;
BAS 是一台包转发设备(一种特定类型的专用路由器),它的职责是将 ATM 信元还原成原始的包,然后去掉 MAC 头部和 PPPoE 头部,为余下的 PPP 头部及其主体数据添加隧道协议的专用头部(例如 L2TP 协议),发给后面的隧道专用路由器;
光纤接入网(FTTH)
光纤的基本知识
FTTH:Fiber To The Home,或许可以翻译为光纤入户;
ADSL 在电话中上复合多个不同的频率的电信号来传输数据,光纤则简单得多,它使用明暗两种光线来表示 0 和 1;
单模与多模
光本质上也是一种电磁波,因此光之间也会相互干扰;从光源射入光纤的光线有很多束,光束在被反射后,会出现相位的改变,而相位不同的光线会相互抵销;因此,只有特定入射角的光束能够在光纤中顺利向前传播,其他光束则因彼此相互干扰而抵销了;
光纤分单模和多模,其区别在于单模光纤的直径较细,因此只有最小入射角的光线,才能够保持相位一致并在光纤中传播;多模光纤由于直径较粗,存在多个满足相位一致的入射角光束,可以有多条光线在光纤中传播,因此接收设备对光敏元件的要求比较低,可以降低成本;
但它带来了另外一个问题,随着反射角度的变大,光线在光纤中反射的次数就会增多,传输距离变长,传输用时变多,因此和反射角度小的光线有到达时间差。如果时间差足够大,就会造成信号失真,因此,多模光纤的传输距离上限较小一些;
通过光纤分路来降低成本
FTTH 架构和 ADSL 差不多,只是 ADSL 的 Modem 在 FTTH 中被换成了光纤收发器,它的职责就是将数据转在光信号发送出去。为了避免上行光信号和下行光信号产生互相干扰,上行和下行会使用不同波长的光线来传输数据,并使用棱镜分离原理获取所需的信号;
除了光纤收发器外,还有一种成本更低的做法,它的原理是上行的时候使用排队机制。用户端使用 ONU 设备,电信局端使用 OLT 使用;OLT 会给接入的多个用户信号进行排队,然后指示 ONU 设备按分配到队列序号发送光信号,这样多台客户端 ONU 设备发送的信号就不会出现冲突。当服务器返回响应时,OLT 会给数据包添加用户端编号,并广播到所有 ONU 设备上。ONU 检查收到的数据包,如果编号匹配,就接受;如果不匹配,就丢弃(有点像集线器和网卡的配合);
不管 FTTH 使用直连还是分路的方式,都可以使用 PPPoE 来传输数据包;
接入网中使用的 PPP 和隧道
用户认证和配置下发
多数 ADSL 使用 PPPoE 协议来完成认证和配置下发(例如分配公共 IP 地址)的工作。传统的 PPP 拨号流程如下:
RADIUS:Remote Access Dial-In User Service,拨号用户远程登录服务;
LCP:Link Control Protocol,连接控制协议
PAP:PPP Authentication Protocol,PPP 认证协议;
IPCP:Internet Protocol Control Protocol;
在以太网上传输 PPP 消息
由于 ADSL 或 FTTH 线路是由 ISP 提供并直接连接 BAS 端口,因此理论上并不需要用户+密码的登录动作。但为了方便管理用户,多数运营商会使用 PPPoE 协议进行用户认证,这样有两个好处:
- 用户可以输入不同的用户名和密码,在不同的运营商之间进行切换;
- 运营商可以根据用户名,统计用户的流量;
在拨号上网的时代,使用专线传输,因此 PPP 协议可以使用 HDLC 协议作为容器。但到了非专线的 ADSL 和 FFTH 的时代,PPP 协议无法和以太网兼容,因此需要做一些改进,新的协议标准即为 PPPoE,Point to Point Protocol over Ethernet.
ADSL Modem 在收到路由器发过来的以太网数据包后,需要先将其转成 ATM 信元,之后再转成电信号,然后传送出去,到达电信局的 DSLAM;
FTTH 光纤收发器在收到路由器的以太网数据包后,无须转 ATM 信元,而是直接转成光信号,然后传送出去,到达电信局的多路光纤收发器;
通过隧道将网络包发送给运营商
BAS 在收到 PPPoE 数据包后,进行解析,去掉 MAC 头部和 PPPoE 头部,从中取出 PPP 消息体,进行认证;
所谓的隧道,其实只是一种抽象。它表示通过建立某种形式的连接,将数据包从一头原封不动的传输到另一头。TCP/IP 栈即是一种隧道方案,另外还可以其他封装方案;
接入网的整体工作过程
- 用户在接入互联网的路由器上面配置 ISP 运营商提供的用户名和密码;
- 路由器基于 PPPoE 协议,广播一条消息,查询 BAS 的 MAC 地址(有点像 ARP 的 MAC 寻址);
- BAS 返回消息,告知路由器自己的 MAC 地址;
- 路由器得到 BAS 的 MAC 地址,使用 CHAP 或 PAP ,将用户名和密码发送给 BAS;
- BAS 收到用户名和密码,进行校验,如果密码正确,使用 IPCP 将配置信息(公有 IP 地址、DNS 服务器 IP 地址和默认网关的 IP 地址等)发送给路由器;
- 路由器收到配置信息,更新自身的配置参数;
- 客户端开始发送 TCP/IP 数据包;
- 路由器为数据包添加 PPP 头部、PPPoE 头部、MAC 头部,由 Modem 转成电信号,发出给 DSLAM;
- DSLAM 将电信号还原成 PPPoE 数据包,转给 BAS;
- BAS 去掉数据包的 PPPoE 头部,得到 PPP 包,通过隧道发给运营商内部的路由器;
好奇客户端在完成 PPPoE 拨号连接后,后续的包是否需要携带用户名密码?还是携带会话 ID 即可?理论上首次登录时,运营商的 BAS 路由器应该会与认证服务器(存储用户信息)通信,验证用户名和密码是否正确;如果正确,理论上 BAS 应该在本地生成会话,并将会话 ID 下发给客户端即可;这样后续每次收到客户端的数据包,只需检查其中的 PPPoE 头部是否包含有效的会话 ID 即可,无需再次连接认证服务器进行验证;
不分配 IP 地址的无编号端口
假设用户家里的接入路由器和运营商的 BAS 路由器是一对一连接的,那么包只有一条传输线路,肯定会到达 BAS 路由器,理论上完全没有必要为数据包添加 PPPoE 头部,而且也不需要为用户的接入路由器分配公有 IP 地址。这种不分配 IP 地址的方式称为无编号端口;
互联网接入路由器将私有地址转换成公有地址
当使用路由器接入互联网时,BAS 会将公有地址分配给路由器。而位于路由器背后的局域网中的计算机,则只拥有路由器分配的私有地址。因此,当路由器收到内部局域网中的计算机的数据包时,需要做地址转换的动作,即将数据包中的私有 IP 地址转换成公网 IP 地址;
如果没有使用路由器,而是让计算机直连到 ADSL Modem 或者光纤收发器上面,那么计算机就会直接拥有公网 IP 地址;
除 PPPoE 以外的其他方式
除了 PPPoE 外,还有一些其他连接方式,例如 PPPoA;PPPoA 和 PPPoE 的区别在于不给 PPP 消息添加 MAC 和 PPPoE 头部,而是直接将 PPP 消息转换成 ATM 信元 直接发送出去。PPPoE 之所以要添加 MAC 头部,是因为这样可以遵守以太网协议,Modem 可以充当一条以太网直接,和路由器用网线连接。当 PPPoA 不使用 MAC 头部时,就意味着 Modem 和路由器之间不可以使用网线连接,而是需要集成在一起;
最近几年国内运营商提供的 Modem 好像都是集成路由功能的,但里面的路由功能很弱鸡,所以大部分用户又不得不再自行购买一台更高性能的路由器;
集成在一起会减少一些灵活性的损失,例如更换设备时,需要整套更换,而不能只更新其中的一部分。但好处是少了一些头部后,数据包可容纳的数据量变多了,MTU 比较大,间接提高了传输速度;
另外还可以不使用 PPP,而是使用 DHCP 的方式给用户的路由器下发配置信息,这样就无须用户填写用户名和密码并对其进行验证;由于免去了 PPP 头部,还可以间接增加 MTU;
虽然 DHCP 也使用 ADSL Modem,但这个 Modem 的作用有所不同,它无须将数据转成信元,而转成 ADSL 信号发直接送出去;
DHCP:Dynamic Host Configuratin Protocol,动态主机配置协议;路由器可以借助该协议,将网络配置信息告知主机,这样主机就可以通过路由器接入互联网;
网络运营商的内部
POP 和 NOC
互联网上面的内容,不管是内容访问方,还是内容提供方,他们都需要依赖运营商来实现对接;运营商跟使用者之间使用 POP 设备进行连接;从 ADSL、FTTH 发出的信号,最终会到达 POP 设备,然后进入互联网;
POP:Point of Presense,互联网接入点,即运营商暴露给使用者的路由器;
NOC:Network Operation Center,网络运行中心,即运营商内部对接多个 POP 设备的中心设备。
POP 设备有很多种类型,具体使用哪种类型,取决于运营商使用哪种线路和上下游进行对接;
POP 中连接用户端的路由器需要配备很多端口,以便可以和很多用户同时对接;由于 POP 到用户端的线路速率,因此相对于接骨干网的路由器,接入用户端的路由器的性能要求会低一些;而 NOC 设备由于要同时处理很多 POP 设备的数据,因此它的性能要求很高,其数据吞吐能力是普通 POP 设备的 3-4 个数量级;
室外通信线路的网络包
家庭或公司内部的网络连接,由于数据量较小,因此一般使用双绞线就足够了;但对于运营商来说,网线不够用,一般需要使用光纤来传输数据;
在室外铺设光纤的费用是很高的,只有拥有足够多客户的公司才能够承担。对于小运营商来说,更合理的方案是向大运营商租借线路的通信能力;
跨越运营商的网络包
运营商之间的连接
当用户的数据包到达 POP 路由器之后,如果目的地服务器刚好也是使用同一家运营商,那么就好办了。POP 只需查询自己的路由表,即可知道应该将数据包转发给哪台内部的 POP 路由器或者 NOC 路由器;
路由器之间会相互交换路由信息,从而自动更新自身的路由表;
如果目的地服务器属于另外一家运营商管理,那也没有问题。因为运营商之间也是用路由器相互连接的,因此彼此也拥有对方的路由信息,从而可以查询到转发目标;
运营商之间的路由信息交换
分配给运营商的 IP 地址不是单个,而是整段的。当运营商在自己的路由器中配置了该段 IP 地址后,该路由器就可以使用 BGP 协议,将该信息发给相连接的其他运营商路由器,同时对方路由器也会发过来它负责的 IP 段;
BGP:Border Gateway Protocol,边界网关协议;
有两种路由交换方案:
- 转接:A 不但发送自己的子网,还发送自己知道的互联网上所有的路由信息;这种方案的好处是被告知者 B 可以知道 A 背后还有谁,有些包可以通过 A 作为中介到达 A 背后的运营商;
- 对等:A 仅发送自己的子网;被告知者 B 只会将属于 A 子网的数据包发给 A,不属于 A 的就不会发了;
与公司网络中自动更新路由表机制的区别
运营商之间的路由器,跟普通公司内部的路由器在本质上并没有什么区别。但是由于运营商之间存在线路费用的关系,因此在制定和交换路由规则上面,需要有所考量,不能像普通路由那样使用最短路径;
假设某个运营商的线路对外是收费的,那么他就会在路由规则中设置只转发交过费用的运营商的数据包,同时拒收那些未交费的运营商的数据包;另外,由于不同运营商的收费标准不同,因此在选择路由路径时,会设置一定的优先级,以降低成本;
保证用户能够访问互联网中任意一台机器,是运营商的基本职责。虽然出于成本考量,不一定选择最短路径,但终究是可达的;
IX 的必要性
运营商之间可以使用专线实现一对一的连接,但如果运营商很多,这种一对一的专线方案就显得成本太高了,更合适的方案是引入一个中心设备,各家运营商只需连接到该中心设备,数据包通过中心设备统一转发即可
IX:Internet Exchange,互联网交换中心;
由于 IX 同时对接多家运营商,因此其数据吞吐量非常大,有些甚至高达 200Gbit 每秒;
运营如何通过 IX 互相连接
IX 本质上是一台交换机,与普通交换机的区别在于它的端口特别多,而且性能非常好(目前主流是使用 10Gbit/s 的光纤端口);
服务器端的局域网中有什么玄机
Web 服务器的部署地点
在公司里部署 Web 服务器
Web 服务器上面通常会安装很多软件,如果某个软件存在安全漏洞,就会导致服务器受到攻击,但其实大部分服务器上的软件并不对外提供服务;因此,通过引入防火墙,只允许访问特定软件的数据包通过,这样就可以避免服务器受到某个软件漏洞的影响;
目前仅靠防火墙已经不够用了,还需要配合反病毒软件、非法入侵检测软件、访问隔离机制等多套方案,才能有效提高安全性;
将 Web 服务器部署在数据中心
由于数据中心离 IX 很近,因此可以让服务器获得更快的访问速度。同时由于数据中心通常还提供各种增值服务,因此一般也更加安全;
防火墙的结构和原理
主流的包过滤方式
防火墙的作用是只允许满足条件的流量通过,实现这个目标有很多种方案,但简单和低成本的方案是使用包过滤的方式;
如何设置包过滤规则
由于数据包的头部包含一些关键的信息,因此可以基于这些头部信息制作相应的规则来实现包过滤;
上图的例子中,通过加入 TCP 包的控制位规则,实现 Web 服务器无法发起对互联网的访问,而只能响应客户端的请求;
通过端口号限定应用程序
通过在规则中增加端口号,就可以实现只允许特定应用程序被外部访问;因为端口号映射着绑定该端口号的应用程序;
通过控制位判断连接方向
TCP 头部中的控制位可以用来判断包的方向,因为在建立 TCP 连接时,发起的第一个数据包中的控制位为 SYN=1 和 ACK=0,后续的其他包的控制位都不再是这个组合。因此通过限制该组合的出现,就可以阻止建立 TCP 连接;
控制位只能适用于 TCP 连接,而其他连接协议(如 UDP)则不适用,因为 UDP 头部都没有这些控制位;这个时候要么妥协,要么需要增加其他防火墙方案,而不能只使用包过滤机制;
从公司内网访问公开区域的规则
仅在发送方的 IP 地址为公司内网的 IP 地址时,才允许通过;
想到了访问后台时,可以临时添加 IP 地址来实现访问;先在内网中设立一个专用的后台程序来暴露接口。由于该程序处于内网,因此可以访问主程序的敏感接口;然后只允许特定 IP 访问该后台程序;
通常来说,内网的计算机分配的是私有 IP 地址,当访问外网时,路由器需要对其作地址转换。但内网之间的访问,则不需要地址转换,此时可以在路由器中配置相应的规则,让内网中的计算机对公开区域的访问,仍然使用私有地址;
从外部无法访问公司内网
如果路由器没有地址转换的映射记录,当收到外网发进来的包时,由于不知道应该转发给哪台内网设备,路由器默认会丢弃该数据包;
通过防火墙
使用防火墙软件时,可考虑开启包丢弃日志,这样可以分析入侵者的攻击方法;路由器由于内置存储很小,一般不适合开启日志功能;
防火墙无法抵御的攻击
服务器程序本身的 BUG 引发的安全漏洞,是无法使用包过滤来规避的。常规的方法是增加部署检查包内容的软件或硬件;但这种方法的效力也是有限,因为某个包是否安全,是由服务器程序本身是否存在 BUG 来决定的,而这种 BUG 在早期是未知的,因此包检查软件也难以判断该数据包是否安全;
通过将请求平均分配给多台服务器来平均负载
性能不足时需要负载均衡
负载均衡可以有多种方案,最简单的方案是使用 DNS;在设置 DNS 解析时,新增多条相同域名的解析记录,每一条对应一个不同的 IP 地址;这样每次查询 DNS 时,DNS 服务器都会返回所有地址,但不同的地址顺序;
虽然 DNS 机制最简单,但是它也有一些缺点,例如:
- 当某台服务器宕机时,DNS 服务器无法知晓,仍然会给客户端返回宕机的服务器 IP 地址;除非客户端在发现第一个地址无效时,会自动访问第二个 IP 地址(多数浏览器已经实现该功能);
- 当多台服务器时,如果用户的某个会话信息存储在其中一台服务器上面,之后用户访问另外一台服务器时,会导致会话丢失;
使用负载均衡器分配访问
负载均衡的另外一种方案是使用专门的负载均衡器;DNS 解析指向该负载均衡器,当客户端访问负载均衡器时,会负载均衡器判断应该将包转发给哪台服务器;
负载均衡器会根据服务器当前的负载情况来转发包,有多种判断办法,例如:
- 定期查询服务器的 CPU 和内存使用情况;
- 根据服务器的性能参数按比例转发;
- 会话亲和性;
如果需要让负载均衡器将某个客户端请求固定转发到特定的机器上面,一般会使用 HTTP 的 cookie 字段作为判断;其原理很简单,当某个客户端首次访问时,负载均衡器为其分配一个唯一的 cookie 值,并记录映射的服务器;之后客户端的请求都需要携带该 cookie 值,这样负载均衡器就可以通过查询映射表,将请求转发给固定的服务器;
使用缓存服务器分担负载
如何使用缓存服务器
负载均衡可以有两种思路,这种思路可以单独使用,也可以组合使用:
- 使用多台功能相同的服务器:每台服务器分担的请求变少;
- 使用多台功能不同的服务器:每台服务器分担的工作内容变少
缓存服务器通过更新时间管理内容
缓存服务器需要前置在 Web 服务器之前先处理请求,如果没有负载均衡器,那么缓存服务器需要直接注册到 DNS 解析记录中;
当客户端首次访问某个资源时,缓存服务器没有命中缓存,会直接将请求转给 Web 服务器,之后缓存 Web 服务器返回的结果;当客户端下次再访问相同资源时,缓存服务器在转发请求时,会在头部增加 If-Modified-Since 字段,用来沟通该资源是否发生了变更;
最原始的代理–正向代理
缓存服务器的方案最早其实是部署在客户端的(公司),而不像现在部署在服务端。当时客户端的缓存服务器是为了实现防火墙,然后在此过程中发现还可以顺便充当缓存服务器,因为当时的网速很慢,因此使用缓存服务器可以有效提高访问速度;另外公司还可以利用该机制,控制员工可访问的网站列表,避免访问危险的网站;
后来由于出现了多种多样的代理方案,不同方案之间为了相互区分,因此才有了正向代理(forward proxy)、反向代理(reverse proxy)之类的名称;
当在浏览器中开启代理功能时,浏览器发送出去的请求会有所不同,主要区别如下:
- 没有代理
- 请求发往目标网站所在的服务器;
- HTTP 报文的 URI 只有路径,没有域名;
- 有代理
- 请求发往代理服务器;
- HTTP 报文的 URI 包含完整的域名和路径;
正向代理的改良版–反向代理
由于使用正向代理要求用户修改浏览器配置,比较麻烦而且容易出错导致浏览器无法正常工作。因此改进的办法是将缓存服务器部署到服务端,将 HTTP 报文中的 URI 和目标 Web 服务器进行关联(因为 HTTP 1.0 版本没有 Host 字段),这样就可以得到完整的网站,从而能够转发任意消息。为了跟传统的前端代理以示区别,这种方式称为反向代理(reverse proxy);
透明代理
正向代理需要配置浏览器,反向代理需要配置服务器,二者才能正常工作;还有一种方案是根据数据包中 IP 头部来判断转发目标,这样就既不需要配置服务端,也无须配置浏览器,同时还获得了各自的优点;
由于透明代理作为中间人在工作,因此它必须知道最终访问的目标 IP 地址,而不能让自己像反向代理一样成为被访问目标,否则数据包中的最终目标 IP 地址就变成它自己了(貌似可以通过域名再次查询出来?);为了实现透明代理的功能,透明代理需要放置于数据包发出方和接收方之间的传输线路上面;
内容分发服务
利用内容分发服务分担负载
如果将缓存服务器放在服务端,那么它可以减轻后端 Web 服务器的负担,提高后端这一段的访问速度,但由于所有的用户流量仍然会到达缓存服务器,因此它无法避免网络传输线路上存在的堵塞;
如果缓存服务器由用户自己部署在客户端,那么它可以很好的避免网络上的堵塞,但是 Web 服务器无法控制客户端缓存服务器中的内容;
第三种方案就是将缓存服务器放在互联网的边缘(即客户端所在的运营商机房),这样客户端在进入堵塞区域之前,能够先到达运营商机房内部的缓存服务器,既提高了客户端的访问速度,也减轻了服务端的负担,同时服务端也能够控制缓存服务器上面的内容;
单个 Web 服务器的运营者跟运营商签合同的话,费用成本很高,因此出现了专门的第三方,他们和主要的几家运营商签合同,之后再租借给单个的 Web 服务器运营者,实现三赢;这种模式称为内容分发服务;
如何找到最近的缓存服务器
寻找最近的缓存服务器有多种方案,其中一种是使用 DNS;大致原理如下:
- Web 服务器注册的 DNS 服务器先收集好各缓存服务器的路由信息;
- 当收到客户端发出的 DNS 解析请求时,基于上一步的路由信息,判断客户端到哪台缓存服务器的路径最短,并返回结果;
通过重定向服务器分配访问目标
另外一种寻找最近缓存服务器的方法是添加重定向服务器,并将其添加到 DNS 解析记录中;这样当客户端发起请求时,数据包先到达重定向服务器,之后重定向服务器基于提前收集好的路由信息,判断最近的缓存缓存器,并存储在 HTTP 响应头部中的 Location 字段。客户端在收到该响应后,会向 Location 中的缓存服务器地址,发起一个新的连接;
缓存的更新方法会影响性能
缓存机制最早是被动式的,首次收到请求后,发现缓存中没有数据,因此向 Web 服务器请求资源;后续的请求则每次询问 Web 服务器资源是否变更,若没有变更,则返回缓存中的内容;这种被动式的缓存有两个缺点,一是首次访问较慢,二是后续的每次查询仍然会给 Web 服务器带来一定的负担;
更好的办法是使用主动式的缓存,它的原理是当 Web 服务器上面的内容有变更时,就通知缓存服务器更新。这种方法可以避免后续 Web 服务器收到大量关于资源是否变更的询问;
请求到达 Web 服务器,响应返回浏览器
服务器概览
客户端与服务器的区别
客户端与服务器并没有本质上的区别,都是计算机,唯一的区别是服务器要先做好开门候客的动作(在没有客户端请求到达之前,需要先创建好套接字并进入待连接状态;而客户端只需要发起连接前,再临时创建套接字即可);因为在 TCP 协议的设计中,必须有一方处于待连接的状态,连接才有可能建立;
虽然在本质上没有不同,但所使用的硬件型号一般有所区别,服务器的使用场景有两个关键特点,一个是并发量高,需要应付很多客户端的访问;二是需要长时期的稳定运行。为了满足这两种需求,一般服务器的 CPU 核数更多,但单核性能不高;内存更大,而且带校验机制,但内存性能较低;通常不配备显卡,网卡比较多,可拓展更多的硬盘,以满足存储需求;
服务器程序的结构
服务端要应付多个客户端发起的连接请求,因此服务端会给每个请求创建一个单独的套接字;大概过程如下:
当服务端创建完第一个套接字后,先进入监听状态和等待连接的状态;之后如果有新的客户端连接请求到达,它就会复制一个套接字副本出来(使用新的文件描述符),并将客户端的控制信息填写到副本里面,而旧的套接字内容保持不变,并继续处于待连接状态,以便应付新的客户端请求;当有多个客户端同时连接服务端时,就会存在多个套接字副本,更有意思的是,它们都绑定到相同的端口号;
协议栈会维护一张套接字映射表,通过四项信息的组合,来识别接收到的数据包到底是属于哪个套接字,这四项信息分别是客户端IP+客户端端口+本机IP+本机端口;根据这四项信息,通过查找映射表,就知道当前的数据包是属于哪个套接字了;
而对于应用程序来说,是通过文件描述符来跟套接字打交道的;多个套接字副本,意味着有多个文件描述符;当某个文件描述符进入就绪状态后,它会发起一个中断,之后操作系统会通知应用程序,并将控制权转移给应用程序进行处理;
特别注意:套接字跟端口可以是多对一的关系,协议栈会维护一张映射表,根据数据包中的头部信息判断,该数据包隶属于哪个套接字负责;每个套接字中存储着不同的连接信息;
以前在学 《深入》和 C 语言时,由于 socket 初始化后,总是有一个 bind 的动作,误以为套接字和端口是一对一的关系,现在才发现其实不是;以前没有留意到 accept 动作会返回新的套接字,回头看了一下笔记,才发现当时写的是描述符,因此没有意识到 accept 返回的新描述符背后,其实是一个新的套接字;
服务器端的套接字和端口号
服务端的应用程序大致可以划分为两个模块:
- 负责建立连接的模块,即下图的连接模块;
- 负责生成响应的模块,即下图的通信模块;
调用 accept 并不会马上返回新的套接字,在客户端的请求没有到达之前,它其实是进入阻塞的状态,要一直等到客户端的请求进来后,才会返回新套接字;该新套接字其实不是从头新建的,而是复制旧套接字并进行补充客户端连接信息后而来的;
当出现多个套接字对应同一个端口号时,为避免混乱,协议栈必须维护一张映射表,以便知悉哪个客户端请求对应的是哪个套接字;
服务器的接收操作
网卡将接收到的信号转换成数字信息
网卡收到电信号后:
- 先根据规范约定的波形判断出头部的位置,
- 从头部中读取到时钟周期
- 基于该时钟周期,从原始被发送方叠加过时期周期的电信号中,分离出原始信号;
- 将原始信号转换成 0 和 1 表示的数字信号;
- 根据规范,从数字信号末尾读取校验值;
- 计算解析后的数字信号的校验值,看是否跟收到的校验值一致;
- 若不一致,丢弃该数据包;
- 若一致,检查头部中的 MAC 值是否跟当前网卡的 MAC 值一致;
- 若不一致,丢弃该数据包;
- 若一致,将数字信号放到缓存中,触发中断事件,以便 CPU 介入,并将控制权转移给网卡驱动程序;
- 网卡驱动程序读取缓存中的数据,根据 MAC 头部判断使用何种协议;然后触发中断事件,以便 CPU 介入,并将控制权转给相应的协议栈进行处理;
IP 模块的接收操作
IP 模块收到数据包后的动作:
- 检查 IP 头部中的目标 IP 地址,判断该包是否发给自己;
- 若不是,则根据情况(是否开启转发功能)转发该数据包;
- 若是,检查是否分片;若有分,则等待所有分片到达后组装它们;
- 组装好后,检查头部中的协议号字段,看使用何种协议,并转交给相应的协议模块(如 TCP )进行处理;
TCP 模块如何处理连接包
TCP 模块收到 IP 模块转交的数据后:
- 检查头部中的端口号字段,看有无套接字在监听该端口号;
- 若没有,发送一条 ICMP 消息给客户端告知错误(黑客可基于该条规则对端口号进行扫描,了解有哪些端口号处于监听状态);如果有,继续往下;
- 检头部中的控制位 SYN,如果值为 1,表示这个一个尝试建立连接的控制包;复制一份当前监听访端口的套接字,写入客户端相关信息(如客户端的 IP 地址、端口号、窗口大小、序列起始值等);然后生成包含己方连接信息的控制包,转交给 IP 模块处理发出去;
TCP 模块如何处理数据包
如果 TCP 头部中的控制位没有值,则说明当前包是一个数据包,而不是连接控制包,动作如下:
- 判断该数据包属于哪个套接字处理,判断办法基于接发双方的 IP 地址和端口号四项信息即可;
- 根据套接字中保存的信息,了解之前已收到第几个序列号的包,以及当前包的序号是否能够连上,以便后续可以将各个分包组装成完整的数据;若可连上,将数据放入缓冲区;
- 每隔一小段时间,发送 ACK 控制信息给客户端,告知某个序号以前的包已经收到了;
- 收到所有包后,触发中断事件,CPU 介入,通知应用程序来读取数据;
TCP 模块的断开操作
当收到完整的数据包之后,连接就可以断开了,断开动作可以由客户端发起,也可以由服务端发起;
- HTTP 1.0,规定由服务端发起;
- HTTP 1.1,规定由客户端发起;
Web 服务器程序解释请求消息并作出响应
将请求的 URI 转换为实现的文件名
对于静态资源的访问,请求中的 URI 通常会映射到服务器上面某个目录中的某个文件, 只需提前在服务器程序中进行配置好可;文件名可以一一对应,也可以不一样,然后通过配置好的改写规则进行映射即可;
运行 CGI 程序
对于动态资源的访问,一般 URI 会被映射给某个符合 CGI 标准的程序文件;当访问该 URI 时,就会委托操作系统运行相应的 CGI 程序文件,此时会将 HTTP 请求报文的内容作为参数传递给该 CGI 程序;
传统的 CGI 程序在运行结束后,会生成 HTML 格式的内容作为响应消息,现在则衍生出了很多种格式,例如 XML,JSON 等格式;之后将响应内容交给服务器模块发送给客户端,服务器模块不会修改内容,但有时会添加一些头部字段;
Web 服务器的访问控制
如果某些资源限制访问,当收到访问这些限制资源的请求时,需要验证用户的身份,以检查是否满足访问条件,常用的办法是使用用户名和密码,偶尔会使用客户端的 IP;
返回响应消息
返回响应消息的过程跟发出消息的过程差不多,只是反过来而已;
浏览器接收响应消息并显示内容
通过响应的数据类型判断其中的内容
浏览器在收到服务器返回的消息后,需要先根据头部的 content-type 字段判断一下消息的类型,以便能够正常显示它们;常见的消息类型有 text、image、audio、video、application、multipart(复合类型)等;例如:
Content-Type: text/html; charset=utf-8,text 表示主类型为文本,斜杠右边的 html 表示子类型,charset 表示编码方式;
另外为了提高传输速度,数据有可能被压缩了,此时可以根据头部的 Content-Encoding 字段了解使用的压缩方式,然后进行解压;
浏览器显示网页内容
有些数据类型如 HTML、图片是浏览器能够直接显示的,有些浏览器显示不了的数据类型,就会调用相应的应用程序来处理数据;
其他
TSL 连接过程
单向认证
- 客户端:发起问候,告知服务端自己所支持的协议和加密套件,以及客户端生成的随机数;
- 服务端:发起问候,告知客户端自己的 SSL 公钥以及相应的签发机构,所支持的协议和加密套件,以及服务端生成的随机数;
- 客户端:用证书机构的公钥,验证服务端的 SSL 公钥是否和所访问的域名一致;若不一致,结束;若一致,下一步;
- 客户端:生成第二个随机数(称为 premaster secret),使用服务端的公钥加密后,发给服务端;
- 服务端:用私钥解密收到的数据,得到 premaster secret,加上之前双方各自生成的两个随机数,共三个随机数,使用加密套件生成会话密钥;使用会话密钥,加密“已完成”的消息,发给客户端,告知对方自己就绪;
- 客户端:同样使用三个随机数生成会话密钥,加密“已完成”的消息,发给服务端,告知对方自己就绪;
- 双方完成握手;
双向认证
双向认证与单向认证的唯一不同点在于第4步,客户端在发送 premaster 时,还会同时附上自己的公钥证书,以及将之前双方的随机字符串使用自己的私钥进行加密并发送,这样服务端收到该公钥证书,使用 CA 可以进行验证;如果有问题,中断通信,握手失败;如果没问题,后续跟单向认证相同,即开始用自己的私钥解密得到 premaster,然后使用加密套件计算出会话密钥;
非对称密钥
- 使用 openssl 等工具,可以生成非对称密钥,即一把公钥和一把私钥,其中一把加密后的内容,可以由另外一把解密;
- 所谓的证书,是指使用权威 CA 机构的私钥,对公钥拥有者(例如域名)和公钥本身进行加密后,生成的内容;由于 CA 机构的公钥是公开的,因此任何人都可以使用 CA 机构的公钥对证书进行解密,解密后就可以验证其中的内容(公钥+公钥所有者的身份信息),例如域名是否为预期想访问的网站域名;如果是,说明该证书是有效的;
- 权威机构在受理证书申请时,需要验证申请人的身份信息,例如申请网站的域名证书时,就需要验证该域名是否真的被申请人持有;Let’s Encrypt 的方法是生成两个随机字符串,一个做为域名的访问路径,然后发起对该路径的访问,看访问结果是否为另外一个随机字符串;如果是,说明域名确实由申请者所拥有,因为申请者能够将字符串添加到访问路径和访问结果中;