现在Internet使用的主流协议族是TCP/IP协议族,它是一个分层、多协议的通信体系。
TCP/IP协议族包含众多协议,我们只详细讨论IP协议和TCP协议,因为它们对编写网络应用程序有最直接的影响。如果想系统学习网络协议,RFC(Request For Comments,评论请求)是首选资料。
TCP/IP协议族是一个四层协议系统,自底而上分别是数据链路层、网络层、传输层、应用层。每层完成不同功能,且通过若干协议来实现,上层协议使用下层协议提供的服务:
数据链路层实现了网卡接口的网络驱动程序,以处理数据在物理媒介上的传输,不同的物理网络具有不同的电气特性,网络驱动程序隐藏了这些细节,为上层协议提供一个统一的接口。
数据链路层常用的两个协议是ARP协议(Address Resolve Protocol,地址解析协议)和RARP协议(Reverse Address Resolve Protocol,逆地址解析协议),它们实现了IP地址和机器物理地址(通常是MAC地址,以太网、令牌环、802.11无线网络都使用MAC地址)之间的相互转换。
网络层使用IP地址寻址一台机器,而数据链路层使用物理地址寻址一台机器,因此网络层必须先将目标机器的IP地址转换成其物理地址,才能使用数据链路层提供的服务,这就是ARP协议的用途。RARP协议仅用于网络上的某些无盘工作站,因为缺乏存储设备,无盘工作站无法记住自己的IP地址,但它们可以利用网卡上的物理地址来向网络管理者(如服务器或网络管理软件)查询自身IP地址。运行RARP服务的网络管理者通常存有该网络上所有机器的物理地址到IP地址的映射。
网络层实现数据包的选路和转发。。WAN(Wide Area Network,广域网)通常使用众多分级的路由器来连接分散的主机或LAN(Local Area Network,局域网),因此,通信的两台主机一般不是直接相连的,而是通过多个中间节点(路由器)连接的。网络层的任务就是选择这些中间节点,以确定两台主机之间的通信路径。同时,网络层对上层隐藏了网络拓扑连接的细节,使得在传输层和网络应用程序看来,通信的双方是直接连接的。
网络层最核心的是IP协议(Internet Protocol,因特网协议),IP协议根据数据包的目的IP地址来决定如何投递它。如果数据包不能直接发送给目标主机,那么IP协议就为它寻找一个合适的下一跳(next hop)路由器,并将数据包交付给该路由器来转发,多次重复这一过程,直到数据包到达目的主机,或由于发送失败而被丢弃。IP协议通过逐跳(hop by hop)方式确定通信路径。
网络层另一个重要的协议是ICMP协议(Internet Control Message Protocol,因特网控制报文协议),它是IP协议的补充,用于检测网络连接。ICMP协议使用的报文格式如下图:
其中8位类型字段用于区分ICMP报文类型,ICMP报文分为两大类:
1.差错报文,主要用来回应网络错误,如目标不可到达(类型为3)、重定向(类型为5)。
2.查询报文,用来查询网络信息。
有的ICMP报文类型还用8位代码字段来进一步细分,如重定向报文用值为0的8位代码字段表示对网络重定向,用代码值为1的8位代码字段表示对主机重定向。
ICMP报文使用16位校验和对整个报文(头部+内容)进行循环冗余校验(Cyclic Redundancy Check,CRC),以检验报文在传输过程中是否损坏。此处作者说ICMP校验和是通过CRC完成的,但实际上计算过程与IP首部的过程相同,并非CRC。
不同的ICMP报文类型有不同的正文内容,ICMP报文格式参考RFC 792。
ICMP协议并非严格意义上的网络层协议,因为它使用处于同一层的IP协议提供的服务(一般来说,上层协议使用下层协议)。
传输层为两台主机上的应用程序提供端到端(end to end)的通信,传输层只关心通信的起始端和目的端,不在乎数据包的中转过程:
上图中垂直的实线箭头表示TCP/IP协议族各层之间的实体通信,而水平的虚线箭头表示逻辑通信线路。可见数据链路层(驱动程序)封装了物理网络的电气细节;网络层封装了网络连接的细节;传输层则为应用程序封装了一条端到端的逻辑通信链路,它负责数据的收发、超时重连等。
传输层协议主要有3个:
1.TCP协议(Transmission Control Protocol,传输控制协议):为应用层提供可靠的、面向连接的、基于流的服务。TCP协议使用超时重传、数据确认等方式确保数据包被正确地发送到目的端,因此TCP服务是可靠的。使用TCP协议通信的双方必须先建立TCP连接,并在内核中为该连接维持一些必要的数据结构,如连接的状态、读写缓冲区、定时器等。当通信结束时,双方必须关闭连接以释放这些内核数据。TCP服务是基于流的,因此数据没有边界,它源源不断从通信的一端流入另一端。发送端可以逐个字节地向数据流中写入数据,接收端也可以逐个字节地将它们读出。
2.UDP协议(User Datagram Protocol,用户数据报协议):为应用层提供不可靠、无连接、基于数据报的服务。不可靠意味着UDP协议无法保证数据从发送端正确地传送到目的端,如果数据在中途丢失,或目的端通过数据校验发现数据错误而将其丢弃,则UDP协议只是简单地通知应用程序发送失败(作者此处的表述应该指的是已连接UDP套接字),因此使用UDP协议的应用程序通常要自己处理数据确认、超时重传等逻辑。UDP协议是无连接的,即通信双方不保持一个长久的联系,因此应用每次发送数据都要指定接收端地址。每个UDP数据报都有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出,否则数据将被截断,只能读取部分数据,剩余数据将丢失。
3.SCTP协议(Stream Control Transmission Protocol,流控制传输协议):是一种较新的传输层协议,它起初是为了在因特网上传输电话信号而设计的。
应用层负责处理应用程序的逻辑。数据链路层、网络层、传输层负责处理网络通信细节,这部分必须稳定高效,因此它们都在内核空间中实现,而应用层则在用户空间实现,因为它负责众多逻辑,如文件传输、名称查询、网络管理等,如果应用层也实现在内核中,会使内核变得非常大。也有少数服务器程序是实现在内核中的,这样代码就无须在用户空间和内核空间中来回切换(主要是数据的复制),极大地提升了工作效率,但这种代码实现起来复杂,不够灵活,且不便于移植,我们只讨论用户空间的网络编程。
应用层例子:
1.ping程序:它不是协议,它利用ICMP报文检测网络连接,调试网络环境。
2.telnet协议:它是一种远程登录协议,使我们能在本地完成远程任务。
3.OSPF(Open Shortest Path First,开放最短路径优先)协议:它是一种动态路由更新协议,用于路由器之间的通信,以告知对方各自的路由信息。
4.DNS(Domain Name Service,域名服务)协议:本协议提供机器域名到IP地址的转换。
应用层协议或程序可能跳过传输层直接使用网络层提供的服务,如ping程序和OSPF协议。应用层协议或程序可以既使用TCP又使用UDP,如DNS协议。我们可通过/etc/services文件查看所有知名的应用层协议,以及它们都使用哪些传输层服务。
通过封装(encapsulation),上层协议能使用下层协议提供的服务,应用程序的数据在发送到物理网络前,将沿着协议栈从上往下依次传递,每层协议都将在上层数据的基础上加上自己的头部信息(有时还包括尾部信息),以实现该层的功能,这个过程就称为封装:
经过TCP封装后的数据称为TCP报文段(TCP message segment),或简称TCP段。TCP协议为通信双方维持一个连接,且在内核中存储相关数据,这部分数据中的TCP头部信息和TCP内核缓冲区中的数据一起构成了TCP报文段:
当发送端应用程序使用send或write函数向一个TCP连接写入数据时,内核中的TCP模块首先把这些数据复制到与该连接对应的TCP内核发送缓冲区中,然后TCP模块调用IP模块提供的服务,传递的参数包括TCP首部信息和TCP发送缓冲区中的数据,即TCP报文段。
经过UDP封装后的数据称为UDP数据报(UDP datagram),UDP对应用程序数据的封装与TCP类似,但UDP无须为应用层数据保存副本,因为它提供的服务是不可靠的。当一个UDP数据报被成功发送后,UDP内核缓冲区中的该数据报就被丢弃了,如果应用检测到该数据报未被接收端正确接收,并打算重发此数据报,则应用需要重新从用户空间将该数据报拷贝到UDP内核发送缓冲区中。
经过IP封装后的数据称为IP数据报(IP datagram)。IP数据报也包括头部信息和数据部分,其中数据部分是一个TCP报文段、UDP数据报、ICMP报文。
经过数据链路层封装的数据称为帧(frame),不同链路层,帧的类型也不同,如以太网上传输的是以太网帧(ethernet frame),而令牌环网上传输的是令牌环帧(tokenring frame)。以太网帧的封装格式如下图:
以太网帧使用6字节的物理地址表示通信的双方。4字节的CRC字段对帧的其他部分提供循环冗余校验。
帧的最大传输单元(Max Transmit Unit,MTU)是帧最多能携带的上层协议数据(如IP数据报),主要受到链路层类型的限制,以太网帧的MTU是1500字节,因此过长的IP数据报可能需要被分片传输。
帧才是最终在物理网络上传送的字节序列,至此封装过程完成。
当帧到达目的主机时,将沿着协议栈自底向上依次传递,各层协议依次处理帧中本层负责的头部数据,以获取所需信息,并最终将处理后的帧交给目标应用程序,这个过程称为分用(demultiplexing),分用是依靠头部信息中的类型字段实现的,RFC 1700定义了类型字段值对应的协议,以下是以太网帧的分用过程:
由于IP、ARP、RARP协议都使用帧传输数据,所有帧的头部需要提供某个字段来区分它们。以以太网帧为例,它使用2字节的类型字段来标识上层协议,如果主机接收到的以太网帧的类型字段的值为0x800,则帧的数据部分为IP数据报,于是以太网驱动程序就将帧交付给IP模块;若类型字段值为0x806,则帧的数据部分为RARP请求或应答报文,以太网驱动程序就将帧交付给RARP模块。
同样,因为ICMP、TCP、UDP都使用IP协议,所以IP数据报的头部采用16位的协议字段来区分它们。
TCP报文段和UDP数据报通过其头部中的16位端口号字段来区分上层应用程序,如DNS协议对应的端口号是53,HTTP协议(Hyper-Text Transfer Protol,超文本传输协议)对应的端口号是80。所有知名应用层协议使用的端口号都可在/etc/services文件中找到。
帧通过上述分用步骤后,最终将封装前的原始数据送至目标服务(如ARP服务、RARP服务、ICMP服务、应用程序),这样,在顶层目标服务看来,封装和分用似乎没有发生过。
我们准备以下测试网络:
包括两台主机,以及一个连接到因特网的路由器,后文若没有特别声明,所有测试硬件指的都是该网络,我们将使用机器名来标识测试机器。
以上测试网络主要用于分析ARP协议、IP协议、ICMP协议、TCP协议、DNS协议,我们通过抓取该网络上的以太网帧,查看其中的以太网帧头部、IP数据报头部、TCP报文段头部以获取网络通信的细节。
对于路由器,我们仅列出了其LAN IP地址,而忽略了ISP(Internet Service Provoder,因特网服务提供商)给它分配的WAN IP地址,因为全书的讨论不涉及它。
ARP协议能实现任意网络层地址到任意物理地址的转换,但本书仅讨论从IP地址到以太网地址(MAC地址)的转换。其工作原理是:主机向自己所在的网络广播一个ARP请求,该请求包括目标机器的IP地址,此网络上的其他机器都将收到这个请求,但只有被请求的机器会回应一个ARP应答,其中包含自己的物理地址。
以下是以太网ARP请求和应答报文的格式:
上图中各字段介绍:
1.硬件类型:物理地址的类型,值为1时表示MAC地址。
2.协议类型:网络层地址的类型,值为0x800时表示IP地址。
3.硬件地址长度、协议地址长度:顾名思义,单位是字节,对MAC地址来说,其长度为6,对IPv4地址来说,其长度为4。
4.操作:有4种操作类型,分别为ARP请求(值为1)、ARP应答(值为2)、RARP请求(值为3)、RARP应答(值为4)。
5.最后4个字段指定通信双方的以太网地址和IP地址。发送端填充除目的端以太网地址外的其他3个字段,以构建ARP请求并发送之。接收端发现该请求的目的端IP地址是自己,就把自己的以太网地址填进去,然后交换两个目的端地址和两个发送端地址,以构建ARP应答并返回之(此时操作字段需要改为2)。
由上图,ARP请求/应答报文的长度为28字节,如果再加上共18字节的以太网帧头部和尾部(见图1-6),则一个携带ARP请求/应答报文的以太网帧长度为46字节。但有些实现要求以太网帧的数据部分长度至少为46字节,此时ARP请求/应答报文将增加一些填充字段,以满足长度要求,此时,一个携带ARP请求/应答报文的以太网帧长度为64字节。
以太网帧数据部分长度至少为46字节的限制与碰撞检测相关,碰撞检测要求帧在传播介质上的传播时间足够长,以便在数据发送期间可以检测到碰撞。
通常ARP维护一个高速缓存,其中包含经常访问(如网关地址)或最近访问的机器的IP地址到物理地址的映射,这样就避免了重复的ARP请求,提高了发送包的速度。
Linux下可用arp命令来查看和修改ARP高速缓存,如ernest-laptop在某时刻的ARP缓存内容如下(arp -a命令输出):
其中第一项描述的是另一台机器Kongming20(其IP地址和MAC地址都与图1-8中描述的一致),第二项描述的是路由器。下面两条命令分别删除和添加一个ARP缓存项:
我们从ernest-laptop上执行telnet命令登录Kongming20的echo服务,并用tcpdump抓取这个过程中两台机器之间交换的以太网帧,操作过程如下(-i选项指定要抓取的接口,-e选项显示以太网帧头部信息,-n选项禁用主机名解析和端口号解析从而显示数字形式的IP地址和端口号,-t选项禁用每条输出前的时间戳显示,dst表示目的地址过滤,src表示源地址过滤):
在执行telnet命令前,应先清除ARP缓存中Kongming20对应的项,否则ARP通信不会执行。当执行talnet命令,在两台通信主机前建立TCP连接后(telnet输出"Connected to 192.168.1.109"后),输入Ctrl+]以调出telnet程序的命令提示符,然后再telnet命令提示符后输入quit,以退出telnet客户端。ARP通信会在TCP连接建立前完成。tcpdump抓取到的数据包中,只有最靠前的两个和ARP通信有关,现将其列出(数据包前面的编号是作者加的):
tcpdump抓取的数据包是以太网帧。
第一个数据包中,ARP通信的源端物理地址是ernest-laptop的物理地址,目的端的物理地址是以太网广播地址,用来表示整个LAN,该LAN上所有机器都会收到并处理这样的帧。0x806是以太网帧头部的类型字段的值,它表示分用的目标是ARP模块。该以太网帧长度为42字节(实际是46字节,tcpdump未统计以太网帧尾部4字节的CRC字段,未使用填充字节来满足最小帧长度),其中数据部分长度为28字节。"Request"串表示这是ARP请求,"who-has 192.168.1.109 tell 192.168.1.108"串表示是ernest-laptop要查询Kongming20的IP地址。
第二个数据包中,ARP通信的源端物理地址是Kongming20的物理地址,目的端的物理地址是ernest-laptop的物理地址。"Reply"串表示这是ARP应答,"192.168.1.109 is-at 08:00:27:53:10:67"串表示目标机器Kongming20在报告ernest-laptop的物理地址。该以太网帧的长度为60字节(实际64字节,使用了填充字节来满足最小帧长度)。
下图是以上ARP通信的过程:
关于上图:
1.我们将两次传输的以太网帧按图1-6描述的以太网封装格式绘制在上图下半部分。
2.ARP请求和应答时从以太网驱动程序发出的,而非上图这样直接从ARP模块发送到以太网上,因此我们将它用虚线表示,这是为了体现携带ARP数据的以太网帧和其他以太网帧(如携带IP数据报的以太网帧)的区别。
3.路由器也将接收到以太网帧1,因为该帧是一个广播帧,但路由器并不会回应其中的ARP请求。
我们通常使用机器的域名来访问这台机器,而非IP地址,域名查询服务可将机器的域名转换成IP地址。域名查询服务有很多实现方式,如NIS(Network Information Service,网络信息服务)、DNS和本地静态文件等。
DNS是一套分布式的域名服务系统,每个DNS服务器上都存放着大量的机器名和IP地址的映射,且是动态更新的。众多网络客户端都使用DNS协议来向DNS服务器查询目标主机的IP地址。DNS查询和应答报文格式如下:
16为标识字段用于标记一对DNS查询和应答,以此区分一个DNS应答是哪个DNS查询的回应。
16位标志字段用于协商具体的通信方式以及反馈通信的状态,其结构如下:
上图中各字段含义如下:
1.QR:区分查询和应答报文,0表示这是一个查询报文,1表示这是一个应答报文。
2.opcode:定义查询和应答的类型,0表示标准查询,1表示反向查询(由IP地址获得主机域名),2表示请求DNS服务器的状态信息。
3.AA:授权应答标志,仅由应答报文使用,1表示域名服务器是授权服务器。授权服务器是负责管理特定域名空间(用于组织和标识域名的层次结构,类似于一个大的层次化命名空间)中域名解析信息的DNS服务器,授权服务器的响应被视为权威性的答案,这意味着其响应被认为是最准确和可信的。
4.TC:截断标志,仅当DNS报文使用UDP服务时使用,由于UDP数据报有长度限制,所以过长的DNS报文将被截断。1表示DNS报文超过512字节,并被截断。此处的512字节限制是出于性能原因,但现代DNS使用EDNS(Extension Mechanisms for DNS)以支持更大的数据包大小、更多的选项和扩展功能。
5.RD:递归查询标志,1表示执行递归查询,即如果目标DNS服务器无法解析某个主机名,则目标DNS服务器将向其他DNS服务器继续查询,通常会先向根DNS服务器查询,通常根DNS服务器不知道完整的域名解析路径,于是根据要查询的域名返回一个相对应的顶级域名的DNS服务器(如对于example.com域名,会返回.com顶级域的DNS服务器地址),于是目标DNS服务器再向该顶级域名DNS服务器查询,这样一级一级向下,直到找到目标域名的授权域名服务器的IP地址,于是目标DNS服务器再向目标域名的授权域名服务器发出DNS查询,从而获取到目标域名的IP地址,然后再返回给查询者。2表示迭代查询,即迭代查询过程中,目标DNS服务器的查询过程,DNS客户也可执行迭代查询过程中,目标DNS服务器的查询过程。两种查询方式见下图:
6.RA:允许递归标志,仅由应答报文使用,1表示DNS服务器支持递归查询。
7.zero:这3位未被使用,必须置0。
8.rcode:4位返回码,表示应答的状态,常用值有0表示无错误,3表示域名不存在。
接下来的4个字段分别指出DNS报文的最后4个字段的资源记录数目。对查询报文而言,它一般包含1个查询问题,而应答资源记录数、授权资源记录数、额外资源记录数均为0。对于应答报文而言,应答资源记录数至少为1,而授权资源记录数和额外资源记录数可为0。
DNS查询问题的格式:
上图中,查询名字段以一定格式封装了要查询的主机域名,16位查询类型字段表示查询操作的类型,常见的有以下几种:
1.A查询,值为1,表示获取目标主机的IP地址。
2.CNAME查询,值为5,表示获得目标主机的别名。此处作者说CNAME查询是为了获得目标主机的别名,实际上CNAME查询是为了将一个域名(别名)映射到另一个域名(规范名称,即FQDN,Fully Qualified Domain Name,完全限定域名),CNAME代表Canonical Name,即规范名称。
3.PTR查询,值为12,表示反向查询(从IP地址查域名)。
上图中,16位查询类字段值通常为1,表示获取因特网地址(IP地址)。
应答字段、授权字段、额外信息字段都使用资源记录(Resource Record,RR)格式:
上图中,32位域名字段是该记录中与资源对应的域名,其格式和查询问题中的查询名字段相同。16位类型和16位类字段的含义也分别与DNS查询问题中的16位查询类型和16位查询类字段相同。
32位生存时间字段表示该查询记录结果可被本地客户端程序缓存多长时间,单位是秒。
16位资源数据长度字段和资源数据字段的内容取决于16位类型字段,对类型A而言,资源数据字段是32位的IPv4地址,而16位资源数据长度字段的值则为4(单位为字节)。
DNS协议的更多细节可参考RFC文档,DNS协议存在诸多RFC文档,每个RFC文档介绍其一个方面,如RFC 1035介绍域名的实现和规范,RFC 1886描述DNS协议对IPv6的扩展支持。
我们要访问DNS服务,就必须先知道DNS服务器的IP地址。Linux使用/etc/resolv.conf文件来存放DNS服务器的IP地址,在机器ernest-laptop上,该文件内容如下:
上图中的两个IP地址分别是首选DNS服务器地址和备选DNS服务器地址(系统会按顺序访问这些地址,因此第一行的是首选地址)。文件中的注释告诉我们,这两个DNS服务器地址是由网络管理程序写入的。
Linux中的host程序也会访问DNS服务器,下面是向首选DNS服务器(219.239.26.42)查询www.baidu.com的IP地址:
host命令的输出告诉我们,机器名www.baidu.com是www.a.shifen.com.的别名,且www.a.shifen.com.对应两个IP地址。host命令使用DNS协议同DNS服务器通信,其-t选项告诉DNS协议使用哪种查询类型,此处使用的是A查询,即通过机器域名获取其IP地址(但实际上返回的资源记录中还包括CNAME记录)。
在ernest-laptop上运行host命令查询www.baidu.com的IP地址,并使用tcpdump抓取这一过程中LAN上传输的以太网帧,操作步骤如下(tcpdump的-s选项指定捕获数据包的最大长度,超出部分被丢弃;"port domain"串用来过滤数据包,表示只抓取使用domain服务的数据包):
tcpdump的输出如下:
这两个数据包开始的"IP"指出,他们后面的内容描述的是IP数据报。tcpdump以"IP地址.端口号"的形式来描述通信的某一端,用">"表示数据传输的方向,">"前面是源端,后面是目的端。可见,第一个数据包是测试机器ernest-laptop(IP地址为192.168.1.108)向其首选DNS服务器(IP地址为219.239.26.42)发送的DNS查询报文(目标端口53是DNS服务使用的端口),第二个数据包是服务器反馈的DNS应答报文。
第一个数据包中,数值57428是DNS查询报文的标识值,因此该值也应出现在DNS应答报文中。"+"表示启用递归查询标志。"A?"表示使用A查询。"www.baidu.com"则是DNS查询问题中的查询名。括号中的数值31是DNS查询报文的字节长度。
第二个数据包中,"3/4/4"表示该报文中包含3个应答资源记录、4个授权资源记录、4个额外信息记录。"CNAME www.a.shifen.com., A 119.75.218.77, A119.75.217.56"表示3个应答资源记录的内容,其中CNAME表示紧随其后的是机器的FQDN,A表示紧随其后的记录是IP地址。该应答报文的长度为226字节。
我们抓包时没有开启tcpdump的-x选项(以十六进制格式显示捕获的数据包内容)或-X选项(以十六进制和ASCII文本混合格式显示数据包内容,它在-x选项的基础上还提供了可读的文本表示,使得数据包内容更易于理解和分析),如果开启了,会更清楚报文的具体字节内容。
数据链路层、网络层、传输层协议是在内核中实现的,因此操作系统需要实现一组系统调用,使得应用程序能够访问这些协议提供的服务,实现这组系统调用的API(Application Programming Interface,应用程序编程接口)主要有两套:socket和XTI,XTI现在基本不再使用,我们仅讨论socket。
由socke定义的这组API提供以下功能:
1.将应用程序数据从用户缓冲区中复制到TCP/UDP内核发送缓冲区,以交付内核来发送数据(如send函数);或从内核TCP/UDP接收缓冲区中复制数据到用户缓冲区,以读取数据。
2.应用程序可通过它们来修改内核中各层协议的某些头部信息或其他数据结构,从而精细地控制底层通信行为,比如可通过setsockopt函数来设置IP数据报在网络上的存活时间。
socket是一套通用网络编程接口,它不但可以访问内核中TCP/IP协议栈,还可访问其他网络协议栈(如X.25协议栈(一种早期的计算机网络协议)、UNIX本地域协议栈等)。