网络之旅的第一章,我们从在浏览器中输入url开始。本章主要介绍三部分内容。首先是在Web浏览器中输入URL后,浏览器是如何解析URL并生成HTTP请求消息的。生成请求消息后,浏览器需要将请求发送给Web服务器,需要知道Web服务器的IP地址。这里讲解DNS服务器的工作原理,如何通过域名查询到IP地址。最后是委托协议栈发送请求消息,本章先讲解协议栈发送消息的基本过程,而详细内容会在第二章继续展开。
1. HTTP请求消息
1.1 URL解析
访问网络,从我们在浏览器中输入网址开始。网址是什么呢?准确来说,叫做URL(Uniform Resource Locator,同一资源定位符)。比如如下所示的URL:
http://user:password@www.glasscom.com:80/dir/file1.html
在浏览器中输入URL之后,浏览器首先会对URL进行解析。以上的URL中,user:password为用户名和密码,可以省略;www.glasscom.com表示Web服务器的域名;后面的80为端口号,端口号后的/dir/file1.htm则为需要访问的文件路径。而开头的http,表示的是浏览器使用的访问方法,访问Web服务器时需要使用HTTP协议。此外,还有ftp,mailto等不同的协议。
通过URL,浏览器就可以了解到,需要访问www.glass.com的Web服务器上路径为/dir/file1.html的文件。看起来很简单。
但很多时候,我们输入的URL后面可能是一个目录,比如
http://user:password@www.glasscom.com/dir/
更有的时候,干脆连目录都省了:
http://user:password@www.glasscom.com
这时候浏览器要如何处理呢?这种情况下,会访问根目录"/"下的默认文件。
1.2 生成HTTP请求消息
解析了URL,浏览器现在知道去哪里获取需要的资源了。接下来就会生成HTTP请求消息。
HTTP协议定义了客户端和服务器端之间交互的消息内容和步骤。首先,客户端向服务器端发送请求消息。请求消息主要包含两个部分:目标和执行的操作。
目标部分被称为URI(Uniform Resource Identifier,统一资源标识符),其内容一般是一个存放网页数据的文件名或一个CGI程序的文件名,也可以直接使用"http:"开头的URL作为URI。
执行的操作被称为方法,表示需要让Web服务器完成怎样的工作。用的较多的有GET、POST、PUT等。
- GET:: 获取URI指定的信息。如果URI指定的是文件,返回文件的内容;如果是CGI程序,返回该程序的输出数据
- POST: 从客户端向服务器发送数据,一般用于发送表单中填写的数据等。
- PUT:: 替换URI指定的服务器上的文件,如果指定的文件不存在,则创建该文件。
- DELETE: 删除URI指定的服务器上的文件
此外请求消息中还有一些表示附加信息的头字段。
生成的HTTP请求消息的格式是这样的:
<方法><空格><URI><空格><HTTP版本>
<字段名>:<字段值>
……
……
<空行>
<消息体>
第一行称为请求行,包括了方法、URI和HTTP版本。根据实际需要,方法选择上面的GET、POST等其中的一种。URI可以根据URL获取,这也没什么问题。此外由于不同版本的HTTP所支持的方法还略有不同,所以这里还要附上HTTP版本号。
第二行开始就是消息头了,包含了一些额外的详细信息。完全弄懂这些信息需要对HTTP协议有很深入的了解,这里就先不深究了。消息头一般在几行到十几行不等。
消息头结束之后,需要添加一个空行,然后是需要发送的请求体。对于GET方法,这部分是不需要填写的。请求体结束之后,整个消息也就结束了。
在接收到Web浏览器的请求消息之后,Web服务器会返回响应消息。响应消息的格式是这样的:
<HTTP版本><空格><状态码><空格><响应短语>
<字段名>:<字段值>
……
……
<空行>
<消息体>
在响应消息中,第一行会包含状态码与响应短语,用来表示请求结果是否成功。状态码是一个数字,响应短语则是一段文字,用于告知执行的结果。
状态码 | 含义 |
---|---|
1xx | 告知请求的处理进度和情况 |
2xx | 成功 |
3xx | 表示需要进一步操作 |
4xx | 客户端错误 |
5xx | 服务器错误 |
返回响应消息之后,浏览器将数据提取出来并显示在屏幕上。
如果网页的内容只有文字,那么到这里就结束了。
如果网页的内容包含图片,则是在网页中的相应位置嵌入表示图片文件的标签的控制信息。浏览器在显示文字时会搜索相应的标签,遇到图片相关的标签时就会预留出用来显示图片的空间,接着再次访问Web服务器,获取到图片文件,显示到对应的位置。
1条请求消息中只能写1个URI,所以如果需要获取多个文件,每个文件都需要单独发送一条请求。
2. 通过DNS服务器获取IP地址
2.1 IP地址
HTTP消息有了,需要把消息发送给Web服务器。但是在此之前我们还需要知道服务器域名的IP地址。
在TCP/IP协议中,用集线器把几台计算机连接起来,形成一个子网;而把子网通过路由器连接起来,就组成了一个大的网络。在网络中的设备都会被分配一个地址用于通信,这就是IP地址。类似与我们现实生活中的通信地址,通过IP地址我们就可以把消息发送到想要进行通信的设备。
IP地址是一串32比特的数字,8比特为一组分为4组,每一组用十进制表示,再通过点隔开。IP地址中包含了网络号和主机号,通过子网掩码来区分,如下所示:
10.11.12.13/255.255.255.0
子网掩码是一串与IP地址长度相等的数字,与IP地址对齐之后,子网掩码为1对应的部分为网络号,为0的部分则对应主机号。
上面的例子,网络号即为10.11.12,主机号为13。此外主机号部分全部为0,表示整个子网;全部为1,表示向子网的所有设备发送包,即广播。
10.11.12.0/255.255.255.0 此IP地址代表整个子网
10.11.12.255/255.255.255.0 此IP地址表示对整个子网广播
通过域名可以定位服务器,通过IP地址也可以定位服务器,但在实际应用的过程中,采用的是人使用域名,而路由器使用IP的方式。对于人来说,域名毕竟要比IP地址容易记忆一些;而对于路由器来说,主要的问题在于域名的长度是不固定的,而且长度可能会很长,处理这样的数据需要耗费路由器很大的计算能力。综合考虑,实际的网络通信中综合使用了域名和IP地址。而为了获取域名对应的IP地址,就需要用到DNS服务器了。通过调用Socket库中的解析器函数"gethostbyname",将需要查询的域名传入,gethostname函数就会向DNS服务器发送请求,获得该域名的IP地址了。
2.2 DNS服务器的工作原理
DNS服务器如何根据域名查询到对应的IP地址呢?
发送给DNS服务器的查询消息包含以下三部分内容。
(1)域名,服务器的名称。
(2)Class,代表互联网的IN
(3)记录类型,表示域名对应何种类型的记录。比如类型为A时,表示域名对应IP地址,为MX时,表示域名对应邮件服务器。
DNS服务器中保存有包含以上三部分信息的记录数据。DNS服务器就是这些信息来对请求做出响应的。
如上表所示,如果要查询www.lab.glasscom.com这个域名对应的IP地址,客户端会向DNS服务器发送包含以下信息的查询消息:
(1)域名:www.lab.glasscom.com
(2)Class: IN
(3)记录类型:A
DNS服务器从已有的记录中查找三项内容都符合的记录,也就找到了对应的IP地址。
看起来原理很简单,但实际网络中存在着不计其数的设备,我们并不知道要查找的域名记录位于哪一台服务器上,这该怎么办呢?
我们先来了解一下域名的层次结构。如下面的例子:
www.lab.glasscom.com
越靠右的位置,表示其层级越高。一个层级称为一个域,这里com域的下一层是glasscom域,再下一次层是lab域,最后是www这个名字。
这种具有层次结构的域名信息,在注册到DNS服务器中时,每个域都要作为一个整体,保存在同一台DNS服务器中,而不能拆开保存在多台服务器中。(一台DNS服务器中也可能保存着多个域的信息)
在实际的网络中,通过划分不同的下级域,就构成了这样具有层次结构的域名信息,每一层级的DNS服务器的IP地址,都会注册到它的上一级DNS服务器中,最顶层的是根域,而根域的IP地址在全世界只有13个,根域的地址信息是保存在所有DNS服务器中的。
这样一来,如果我们想要知道查找的域名记录位于哪一台DNS服务器上,可以直接发送请求给根域DNS服务器,从根域服务器开始一层一层地往下寻找。比如根域DNS服务器中,保存了com域DNS服务器的IP地址,这样我们可以找到com域的DNS服务器;而com域的DNS服务器中,又保存了glasscom域的DNS服务器的IP地址,我们可以继续找到glasscom域的DNS服务器……这样一层一层地找下去,我们最终会找到保存着我们想要查询的域名的那台DNS服务器,并获取到对应的IP地址。这个过程,就像一个个DNS服务器的接力。
此外,为了加快访问速度,DNS服务器还具有缓存功能。将查询过的域名保存在缓存中,下一次查询同样的域名时可以先从缓存中查找,如果找到了就直接返回,而无需再从根域服务器逐级向下查找。有时候,将信息保存到缓存中之后,原来的注册信息可能发生了变化,这时候缓存的信息可能就不正确了。所以DNS服务器中缓存的信息有一个有效期限,超过期限的缓存信息会被删除。
3. 委托协议栈发送请求消息
HTTP请求消息有了,IP地址也知道了,接下来就可以把消息发送给Web服务器了。不过这一过程Web浏览器自身无法完成,需要通过网络协议栈来完成。简单来说,可以理解为在客户端与服务器端之间建立了一条数据管道,数据从管道一端进入,然后从另一端被取出。整个过程可以分为四个过程:
(1) 创建套接字
(2) 将管道连接到服务器端的套接字
(3) 收发数据
(4) 断开管道并删除套接字
这四个过程也需要分别调用Socket库中的对应函数。下面分别进行讲解。
3.1 创建套接字(Socket)阶段
建立数据管道,关键在于管道两端的数据出入口,称为套接字(Socket)。实际的通信过程中,服务器一方先创建好一个套接字等待客户端向该套接字连接管道。服务器进入准备状态之后,客户端就可以创建套接字并连接管道了。客户端创建套接字需要调用Socket库中的socket函数。该函数会返回一个描述符。由于客户端可能创建许多不同的套接字,用于不同的通信,因此该描述符用于识别不同的套接字。
3.2 连接阶段
客户端的套接字创建好之后,需要将其与服务端的套接字连接起来。这里调用的是Socket库中的函数connect。该函数需要传入的参数为描述符、服务器IP地址和端口号。
描述符即上一阶段创建套接字时返回的描述符,用来区分客户端用哪一个套接字去连接服务器端;服务器IP地址,即通过DNS服务器查询得到的IP地址;而端口号,与IP地址搭配,用于定位服务器端的套接字。也就是说,通过服务器IP地址和端口号,我们可以确定连接到服务器端的哪一个套接字。
服务器上的端口号是根据应用种类规定好的。比如Web服务使用的就是80端口号,电子邮件则是25。根据指定的端口号,就可以确定连接到服务器端的哪个套接字了。类似地,客户端在创建套接字时,协议栈会分配一个端口号,执行连接操作时,这个端口号会被通知给服务器,这样服务器也是通过客户端的端口号来找到客户端的套接字。
连接成功后,对方的IP地址与端口号等信息会被保存在套接字中,接下来就可以开始收发数据了。
3.3 通信阶段
套接字的连接建立好之后,只需要将数据送入套接字,对方就会从套接字中获取到数据。将数据送入套接字需要调用Socket库中的write函数,其需要传入的参数为描述符和发送的数据。描述符用来定位己方的套接字。由于套接字中保存了对方的IP地址与端口号,目标明确,这样数据就会被传送到服务器端。
接受返回消息时,则需要调用read函数,并指定存放响应消息的内存地址,即缓冲区。通过read函数,返回消息被读入到缓冲区中。
3.4 断开阶段
数据收发过程结束,就可以调用close函数进入断开阶段了。根据应用种类的不同,有些是客户端先调用close函数,而有些则是服务器端先调用。最终套接字之间的管道被断开,套接字也被删除,通信过程结束。
本章简单介绍了从浏览器输入网址,到HTTP信息由协议栈发送给服务器端之间的过程。这一过程实际上离不开协议栈、网卡驱动和网卡。下一章会详细探讨协议栈与网卡部分的工作,敬请期待。