目录
1.2 向 DNS 服务器查询 Web 服务器的 IP 地址
前言
在工作中,经常会遇到一些生产问题是和网络相关的,同时自己也对整个网络包的生命流程也不太熟许,看见网上有人推荐这本书,所以在8月中-9月初是慢慢读完了这本书,确实收获到了不少知识。现在一个多月过去了,打算通过此次文章总结并复习巩固一下。而且一直有个热门的面试题是“从输入URL到页面显示发生了什么”,个人认为可以分为两部分,即网络部分和浏览器部分。《网络是怎样连接的》的这本书完整详细地回答了前半部分。
书的整体章节如下图。一共有六个章节。个人觉得前三个章节非常详细,第4章这部分更偏向于运营商,所以后面不会花篇幅回顾,第5章第6章讲得也比较浅,所以重点主要是前3章,主要是第2章和第3章。
图1
1. 第一章-浏览器生成消息-探索浏览器内部
1.1 生成Http请求消息
1.1.1 探索之旅从输入网址开始
大概介绍了URL分为协议、域名、端口号,同时有种协议,比如http、ftp、file协议等等。
1.1.2 浏览器先要解析URL
这一部分就是告诉浏览器识别URL的各个部分,直接看图。
图2
1.1.3 省略文件名的情况
这一部分讲解了各种URL的写法。
比如:
a) http://www.lab.glasscom.com/dir/ 这个其实就是访问dir文件下的默认文件,比如前端页面一般是index.html。
b) http://www.lab.glasscom.com/ 这个只以斜杠结尾,其实也是访问"/"目录里的index.html。
c) http://www.lab.glasscom.com 这个写法和b写法一样。
d) http://www.lab.glasscom.com/whatisthis 这个就分情况,如果"/"目录下有whatisthis文件,那么就是访问whatisthis下的这个文件,如果是一个文件夹,那么就是访问whatisthis文件夹下的默认文件(index.html)。
1.1.4 Http的基本思路
1.1.5 生成Https的请求消息
1.1.6 发送请求后会收到响应
以上三章就简单介绍了下Http的方法、请求体、请求头的一些字段、响应码、响应体的格式等,这部分就不介绍了,在工作开发过程中这都是最常使用到的。
延伸
延伸一下浏览器输入url并解析url这个事情。以前读过阮一峰老师的一篇文章,讲得是当我们在浏览器的输入文字时,浏览器首先要做的事是判断用户输入的是网址还是文字,是网址就跳转到网址,是文字就为我们进行搜索。
由这一点再延伸出来的问题是,网络标准RFC 1738做了规定:只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-_.+!*'(),"[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。这意味着中文字符必须经过编码,而且这个编码规则是由浏览器自己决定的。各个操作系统和各个不同的浏览器可能对编码的实现不同,比如有的用的utf-8,有的用的Gb2312编码。所以有个js函数可以保证编码格式一样,就是encodeURI()和encodeURIComponent(),这两个函数的区别大家可以自己下去查一下。
1.2 向 DNS 服务器查询 Web 服务器的 IP 地址
1.2.1 IP地址的基本知识
实际的IP地址是一串32比特的数字,按照8比特(1字节)为一组分成4组,分别用十进制表示然后再用圆点隔开。如10.11.12.13 。但仅凭这一串数字是无法区分哪部分是网络号,哪部分是主机号的,所以还需要子网掩码。子网掩码的作用就是将某个IP地址划分成网络地址和主机地址两部分(还有划分子网),子网掩码中为1的表示网络号,为0的表示主机号。其实简单理解就是别人规定的必须用子网掩码来和ip地址进行“与运算”,最后得出的结果计算机才能知道哪部分是网络号和哪部分是主机号,常用的子网掩码就是255.255.255.0和255.255.0.0,子网掩码是“255.255.255.0”的网络:最后面一个数字可以在0~255范围内任意变化,因此可以提供256个IP地址。但是实际可用的IP地址数量是256-2,即254个,因为主机号不能全是“0”或全是“1”。因为全0的表示整个子网,全1表示向子网上所有设备发送包,即“广播”。结合下面图3图4更容易理解。
可以见这篇文章,解释什么是子网掩码:子网掩码
图3
图4
1.2.2 域名和 IP 地址并用的理由
域名和IP并用的理由:为什么要有从DNS服务器查IP这一步操作,因为人直接记IP很困难,记名称更容易。那为什么又不只用域名去确定通信对象呢,而是要通过域名转换为ip地址?这是因为处理IP地址只需要处理4个字节的数字,而处理域名则需要处理几十个到255个字节的字符,增加了路由器的负担,传送数据也会花费更长时间。
1.2.3 Socket 库提供查询 IP 地址的功能
书中原话:向 DNS 服务器发出查询,也就是向 DNS 服务器发送查询消息,并接 收服务器返回的响应消息。换句话说,对于 DNS 服务器,我们的计算 机上一定有相应的 DNS 客户端,而相当于 DNS 客户端的部分称为 DNS 解析器,或者简称解析器。通过 DNS 查询 IP 地址的操作称为域名解析,因此负责执行解析(resolution)这一操作的就叫解析器 (resolver)了。 解析器实际上是一段程序,它包含在操作系统的 Socket 库中。
这个库其实就是在操作系统中用于调用网络功能的,之后发起tcp等请求也是由这个库来调度,所以其实就记住这个库就是操作系统开发者开发用来进行网络传输的。
1.2.4 通过解析器向 DNS 服务器发出查询
这一部分不用太关心,就用伪代码简单介绍了下socket是如何调用向DNS服务器发出查询的。
书中原话:“解析器的用法非常简单。Socket 库中的程序都是标准组件,只要从应 用程序中进行调用就可以了。具体来说,在编写浏览器等应用程序的 时候,只要像图 1.11 这样写上解析器的程序名称“gethostbyname” 以及 Web 服务器的域名“www.lab.glasscom.com”就可以了,这样就完成了对解析器的调用。”
图5
1.2.5 解析器的内部原理
如下图就是解析器的工作流程,首先第一步,应用程序(此时就是浏览器)执行代码调用Socket库(序号1),然后Socket库的解析器开始运行,生成一条请求消息委托操作系统去发送给DNS服务器(序号3),然后操作系统通过UDP协议发送给DNS服务器(序号4),网卡将消息发送出去(序号5)DNS服务器响应消息(序号6),然后再将响应消息返回给Socket库,解析器将Ip地址存放到内存中(序号8),最后将控制权限返回给浏览器(序号9),浏览器就可以从内存中取出Ip地址了。这就是整个流程。
图6
书中原话,这一段可以看一下:“顺带一提,向 DNS 服务器发送消息时,我们当然也需要知道 DNS 服 务器的 IP 地址。只不过这个 IP 地址是作为 TCP/IP 的一个设置项 目事先设置好的,不需要再去查询了。不同的操作系统中 TCP/IP 的 设置方法也有差异,Windows 中的设置如图 1.13 所示,解析器会根据这里设置的 DNS 服务器 IP 地址来发送消息。”
图7
1.3 全世界 DNS 服务器的大接力
1.3.1 DNS 服务器的基本工作

1.3.2 域名的层次结构
这个其实大家都知道,就是比如www.baidu.com这个URL,顶层的域名就是com,其次是二级域名是baidu,三级域名是www,以“.”来划分各层级域名。一般一个域的信息就是作为一个整体存放在DNS服务器的,不能将一个域的信息拆开存放到多个DNS服务器中;不过一个DNS服务器可以存放多个域的信息。
1.3.3 寻找相应的 DNS 服务器并获取 IP 地址
首先一个知识就是,平时看来.com这种就是最顶层的域名了,但其实不是,url完整写法是:www.baidu.com. 最后面还有个点,这个就是顶层域名,称为根域,根域的DNS服务器保存着.com,.cn等一级域名的DNS信息,然后一级域名的DNS服务器又存放着二级域名的DNS信息,以此往下类推。
另外,需要将根域的DNS服务器信息保存在互联网中所有的DNS服务器中,分配给根域DNS服务器的IP地址在全世界只有13个,而且这些地址几乎不发生变化。一般电脑出厂设置中就会在DNS服务器程序的配置文件中保存这13个DNS服务器的IP地址。
1.3.4 通过缓存加快 DNS 服务器的响应
这就是缓存机制,DNS服务器也有缓存功能。如果要查询的域名已经在缓存中,那么就直接返回响应。并且,当要查询的域名不存在时,“不存在”这一响应结果也会被缓存,这样下次查询这个不存在的域名时也会快速响应。同时,DNS服务器中保存的信息都设置有一个失效期,当缓存中的信息超过有效期后,数据就会从缓存中删除。在对查询进行响应时,DNS服务器也会告知客户端这一响应的结果是来自缓存中还是来自负责管理该域名的DNS服务器。
1.4 委托协议栈发送消息
这一节其实就是对第二章的铺垫。
1.4.1 数据收发操作概览
收发数据的操作大致可以总结为4个:
(1)创建套接字(创建套接字阶段)
(2)将管道连接到服务器端的套接字上(连接阶段)
(3)收发数据(通信阶段)
(4)断开管道并删除套接字(断开阶段)
书中原话,这一段挺重要:“在每个阶段,Socket 库中的程序组件都会被调用来执行相关的数据收 发操作。不过,在探索其具体过程之前,我们来补充一点内容。前面 这 4 个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序 并不会自己去做连接管道、放入数据这些工作,而是委托协议栈来代劳。本章将要介绍的只是这个“委托”的操作。关于协议栈收到委托之后具体是如何连接管道和放入数据的,我们将在第 2 章介绍。此外,这些委托的操作都是通过调用 Socket 库中的程序组件来执行的,但这些数据通信用的程序组件其实仅仅充当了一个桥梁的角色,并不执行任何实质性的操作,应用程序的委托内容最终会被原原本本地传递给协议栈。因此,我们无法形象地展示这些程序组件到底完成了怎样的工作,与其勉强强调 Socket 库的存在,还不如将 Socket库和协议栈看成一个整体并讲解它们的整体行为让人更容易理解。因此,后文将会采用这样的讲法。不过,请大家不要忘记 Socket 库这一桥梁的存在,正如图 1.12 中所示的一样。”
其实我的理解就是Socket库就是一个像SDK一样的存在,应用程序调用Socket库某些封装好的方法,然后这些方法去调用协议栈,比如tcp、udp等去通信获取响应结果,然后Socket库再把获取的值返回为调用Socket库的程序。
图9
1.4.2 创建套接字阶段
创建套接字阶段就是调用Socket库中的socket程序组件就可以,假如其实就是一个 new socket()这种,就是下图第一步。创建完套接字后,套接字创建完成后,协议栈会返回一个描述符,应用程序会将收到的描述符存放在内存中。描述符是用来识别不同的套接字的,大家可以作如下理解。我们现在只关注了浏览器访问 Web 服务器的过程,但实际上计算机中会同时进行多个数据的通信操作,比如可以打开两个浏览器窗口,同时访问两台 Web 服务器。这时,有两个数据收发操作在同时进行,也就需要创建两个不同的套接字。这个例子说明,同一台计算机上可能同时存在多个套接字,在这样的情况下,我们就需要一种方法来识别出某个特定的套接字,这种方法就是描述符。我们可以将描述符理解成给某个套接字分配的编号。
图10
1.4.3 连接阶段:把管道接上去
连接阶段是调用Socket库的connect函数,它有三个参数:描述符、服务器Ip地址、端口号。就是图10的第二步,连接成功后,协议栈会将对方的IP地址和端口号等信息保存在套接字中,这样我们就可以开始收发数据了。
1.4.4 通信阶段:传递消息
发送消息是应用程序调用Socket库委托协议栈来发送消息,使用的是write函数,需要制定描述符和发送数据,然后协议栈就会将数据发送到服务器。接收消息时是调用Socket库中的read函数,调用read函数时需要指定用于存放接收到的响应消息的内存地址,这一地址称为接收缓冲区。由于接收缓冲区是一块位于应用程序内部的内存空间,因为当消息被存放到接收缓冲区时,就相当于已经转交给了应用程序。
1.4.5 断开阶段:收发数据结束
调用Socket库中的close函数执行断开操作,连接在套接字间的管道就会被断开,套接字本身也会被删除。
第四小节总结
其实这里的这些概念都很新,我看的时候也懵了,但是正如作者所说,与其把Socket库拆开来看,不如把Socket库和协议栈看作一个整体,那其实连接阶段就好记了,不就是tcp三次握手吗,传递消息,不就是tcp之间的报文传递吗(当然中间还会有网卡、光电信号转换、运营商通信等),断开阶段不就是tcp四次挥手吗。其实这样来记就好记了,只不过我们可以理解应用程序无法直接去调用协议栈,需要中间有个桥梁,那就是Socket。而套接字这个东西也挺新,不过我们可以理解成就是计算机用来记录当前进行数据通信的是哪个应用程序,是chrome浏览器呢,还是微信呢,还是其他应用程序呢。作者会在第二章对这一节的概念做一些详细讲解。