了解网络的全貌,对于经常和网络技术打交道的人来说,还是很有必要的。最近看了《网络是怎么连接的》,记录一下读书笔记。
现在就让我们开启从浏览器中输入网址到网页显示的探索之旅吧!
用户在浏览器中输入网址后,浏览器的第一步工作就是对URI进行解析。
比如通过对http://www.wxample.com/dir1/file1.html 进行拆分,解析,就能知道要访问的是 www.example.com 这个web服务器上路径是/dir1/file1.html的文件了。
确定了web服务器和文件名后,浏览器就根据这些信息来生成HTTP请求消息了。
http协议定义了客户端和服务器之间交互的消息内容和步骤。根据http协议的规定,请求消息和响应消息是这样的:
生成HTTP消息之后,浏览器还需要根据域名查询IP地址,才能委托操作系统发送消息。
我们的计算机的操作系统的socket库(socket库是用于调用网络功能的程序组件集合)中,存在DNS解析器。浏览器程序中调用解析器后,会委托给操作系统的协议栈执行发送消息的操作,然后通过网卡将消息发送给DNS服务器。然后DNS服务器会返回响应消息,其中就包含查询到的IP地址。解析器会取出IP地址,并将其写入浏览器指定的内存地址中。如下图:
那DNS服务器是怎么查询ip地址的呢?
DNS服务器中的所有信息都是按照域名以分层次的结构来保存的,最顶层为根域。比如,查询www.example.com
对应的ip地址,客户端会先访问最近的一台DNS服务器,该服务器会在保存的记录中查找与查询的域名匹配的记录,如果有,就返回给客户端。如果没有,则从根域开始向下查找。根域DNS服务器中没有www.example.com
这个域名,根据域名结构知道是属于com域的,则返回com域的DNS服务器的ip地址,以此类推,最后返回www.example.com
的ip地址。
知道了ip地址后,浏览器就可以委托操作系统的协议栈向目标ip发送消息了。浏览器将按照指定的顺序来调用Socket库中的程序组件。
<描述符> = socket(<使用IPv4>, ...);
connect(<描述符>, <服务器的IP地址和端口号>, ...);
write(<描述符>, <发送数据>, ...);
<接收数据长度> = read(<描述符>, ...);
close(<描述符>);
那协议栈具体是怎么工作的呢?我们来继续探索。
浏览器、邮件等一般应用程序收发数据时用TCP
DNS查询等收发较短的控制数据时用UDP
套接字的实体就是通信控制信息。在协议栈的内部有一块用于存放控制信息的内存空间,例如通信对象的IP地址、端口号、通信操作的进行状态等。这个内存空间就是套接字的实体。可以用netstat
命令显示套接字内容,如下图
创建套接字的过程就是:
而远方的服务器在系统启动时就创建了套接字,等待客户端连接。
因为套接字中只有初始状态的控制信息,浏览器调用socket库的connect
时需要把服务器的IP地址和端口号等控制信息告知协议栈,客户端再将我们客户端的IP地址和端口号等信息(TCP头部)告知服务器。这个交换控制信息的过程就建立了连接。这个过程也就是TCP的“三次握手”。
这个过程具体是这样的:
以下为TCP头部主要字段,字段具体的设置将会在下面章节提及。
字段名称 | 含义 |
---|---|
发送方端口号 | 发送网络包的程序的端口号 |
接收方端口号 | 网络包的接收方程序的端口号 |
序号 | 发送数据的顺序编号,发送方告诉接收方 这是所有发送数据的第几个字节 |
ACK号 | 接收数据的顺序编号,接受方告诉发送方 已经收到了所有发送数据的第几个字节 |
控制位 | 该字段的每个比特分别为:URG、ACK(接收有效,通常表示已收到数据)、PSH、RST(异常中断)、SYN(连接)、FIN(断开连接) |
… | … |
当控制流程从connect
返回到应用程序的时候,接下来就是调用socket库的write
将要发送的数据交给协议栈,协议栈收到数据后执行发送操作。
MTU:一个网络包的最大长度,以太网中一般为1500字节
MSS:数据包的最大长度,理论上为MTU除去头部长度
read
来委托协议栈获取响应消息。像上面说过的接收方,协议栈在接收到所有数据后会检查数据块是否丢失,如果没有就返回ACK号,并将数据块按顺序连接起来,最后交给应用程序。收发数据完毕后,服务器和客户端都可以先发起断开,这里以从服务器断开管道并删除套接字为例子说明。断开的过程也就是TCP的“四次挥手”。
close
程序,协议栈会生成包含断开信息的TCP头部,即将控制位中的__FIN比特设为1__,委托IP模块向客户端发送数据。同时服务器的套接字中也会记录断开操作(改变state等)。read
来读取数据,如果协议栈已经收到所有数据了就能马上读取了,否则则继续等待协议栈。close
来结束操作。这时协议栈会跟步骤1中的服务器一样,生成FIN比特为1__的包,通过IP模块发送。服务器收到之后__返回ACK号。到这里就结束了。和服务器的通信结束后,客户端等待一段时间就可以删除该套接字了。
喝杯茶继续。
接下来我们来探索IP与以太网是怎么进行包收发操作的。
我们在上面的章节中经常提及协议栈的IP模块,IP模块到底做了什么工作呢?IP模块负责给包添加两个头部:
主要字段如下
字段名称 | 含义 |
---|---|
标志 | 表示是否允许分片,以及当前包是否为分片包 |
协议号 | 表示协议的类型,如TCP:06,UDP:11 |
发送方IP地址 | 网络包发送方的IP地址 |
接收方IP地址 | 网络包接收方的IP地址 |
… | … |
其中,接收方IP地址就是TCP模块告知的,而发送方IP地址则需要通过判断发送所使用的网卡,并填写该网卡的IP地址。那如何确定使用哪一个网卡发送呢?IP模块会根据路由表来确定把包交给哪块网卡。在我们可以通过route print
显示路由表。
首先,会对套接字中的目的地ip与Network Destination进行比较,找到匹配的那一行;然后查看该行,Gateway即要转发到的下一个路由器(最近的网络转发设备)的ip地址,Interface就是使用的网卡的ip地址。这样,我们就知道该用哪个网卡发送包,即IP头部的发送方ip地址字段了。
以太网在判断网络包目的地时和TCP/IP的方式不同,需要知道MAC地址才能在以太网中将包发往目的地。因此还需要加上MAC头部。
MAC头部主要字段
字段名称 | 含义 |
---|---|
接收方MAC地址 | 网络包接收方的MAC地址 |
发送方MAC地址 | 网络包发送方的MAC地址 |
以太类型 | 使用的协议类型,如0800:IP协议、0806ARP协议等 |
刚刚说到通过查路由表可以知道要转发到的下一个路由器的ip地址和使用的网卡ip地址,我们根据这两个ip地址查询得到的MAC地址就是接收方的MAC地址和发送方的MAC地址。这里我们需要使用ARP。
具体是这样的,在以太网中,我们可以通过广播的方法把包发给同一个子网中的所有设备。ARP就是利用广播向所有设备提问:“这个IP地址xxx.xxx.xxx.xxx是谁的?请把你的MAC地址告诉我。”然后就会有设备回答:“这个IP地址是我的,我的MAC地址是XX-XX-XX-XX-XX-XX。”这样,我们就能得到对应的MAC地址了。
网卡接收到网络包之后,就会把IP模块生成的网络包转换成电或光信号,这样就在网线上传输了。这里就不详细说了。
包到达路由器后,路由器会根据路由表再一次判断要转发到的下一个路由器的ip地址,改写MAC头部中的接收方MAC地址,然后转发到下一个路由器。这个过程不断重复,最终网络包就会被送到目的地了。
在转发前,当包的长度大于输出端口的MTU(一个包能传输的最大数据长度)时,且IP头部的标志字段显示可以分片,路由器会使用分片功能拆分大网络包,并更新IP头部。如果包过大且不允许分包,路由器会丢弃这个包,并通过ICMP消息通知对方。
在到达服务器前,可能网络包还会经过防火墙,又或者是经过缓存服务器时直接从缓存服务器中读出数据,这里就不详细说了。
我们在2.1有提到,服务器在启动时就创建套接字等待连接。每次有新的客户端发起连接(第一次握手)时,服务端开始接收连接操作。协议栈会给等待连接的套接字复制一个副本,然后将控制信息写入这个新的套接字中。这个套接字与等待连接的套接字的端口号是一样的,所以还需要其他信息来做区别。
服务器发送的响应消息到达客户端后,经过网卡、协议栈,最后到达浏览器。
接下来,浏览器会根据http头部的Content-Type字段、文件扩展名等判断数据类型,然后将数据显示出来就可以了。不同类型等数据显示操作的过程不一样,这里就不探讨了。
浏览器显示网页内容成功!用户访问完成!