用户输入url指定浏览器的运作方式,常见的url开头有http(访问web服务器)、ftp(访问FTP服务器,上传下载文件时使用)、file(读取本地文件)、mailto(发送电子邮件)、news(读取新闻)。格式见书P6。
根据url解析出来我们需要的信息,最基本的有运作方式(http等,// 后面跟的是服务器名称)、服务器名称、文件路径。
(a)url的文件路径可能以/结尾,并没有指定一个文件,就像是只进入了一个文件夹。服务器端可以设置此目录下的默认文件,比如www.lab.glass.com/dir/,服务器事前设置好了此目录下默认返回的文件名称。
(b)www.lab.glass.com/,结尾是一个“/”,就是根目录。直接访问根目录下默认文件。
(c)如果没有指定路径,如www.lab.glass.com,那么就访问根目录下默认文件。
(d)若www.lab.glass.com/whatisthis,若whatisthis是根目录下文件,则返回此文件,若是目录下文件夹名称,则进入此文件夹下。
简单来讲,客户端对服务器发送一个请求消息,其中包括使用什么方法对什么资源进行处理(url指定了资源),服务端对其返回一个状态码表示执行结果。请求方法有GET、POST、HEAD、PUT、DELETE等,可以对服务器上的文件进行一系列操作。但是出于种种原因,最常用的是GET和POST方法。请求方式的具体含义见书中P12。
HTTP消息在格式上有严格规定,浏览器按照规定格式生成客户端请求信息。有个问题是浏览器怎么根据用户的行为知道请求方法(GET、POST之类)?请求方法在HTML文档中已经定义好了,用户点击什么按钮就触发按钮事先定义好的方法对资源进行操作。
请求消息和相应消息严格按照HTTP消息格式进行封装发送,格式详情见书P15。请求信息包括请求行、消息头、消息体三部分。响应信息包括状态行、消息头、消息体三部分。消息头中每行包含一个头字段以及相应信息,具体头字段名称以及含义见书P17。
响应信息包括状态行、消息头、消息体三部分。响应信息第一行的内容为状态码和响应短语。状态码和响应短语内容应当一致,都用来告知执行结果。返回响应消息之后浏览器进行解析将内容呈现出来,但是如果还有内置图片之类的资源,则浏览器根据事先在HTML文件中预留的url对服务器再次发送请求。例如一个网页中有3张图片资源,浏览器需要向Web服务器请求4次资源。Web服务器只负责对于一个请求返回一个响应。
响应信息的消息头字段会指定日期时间(Date)、数据长度(Content-Length)返回的消息体类型(Content-Type)等信息。这也是消息头字段存在的意义。
在应用程序委托操作系统发送信息时,域名不是必须要提供的,IP地址才是必须提供的信息。IP地址是32位2进制数组成,平常所见的IP地址格式以十进制表达,8位为一组分为4组,最大为255使用圆点隔开。使用子网掩码来区分IP地址中的网络号(指向子网)和主机号(指向子网下设备)。子网掩码格式同IP地址,左边都是1,右边都是0。详见P28页图,主机号部分全部为0表示整个子网(不特指某一设备),主机号部分全部为1(255)表示广播,向子网下全部设备发送包。
人使用域名的原因是因为域名更方便人类理解记忆,而机器使用IP地址的原因是因为更加高效,具体来说路由器处理IP地址只需要处理4个字节的数据就可以了,保证了路由器的工作效率。根本原因是因为计算机本身逻辑是二进制,字符串并不是计算机首选的表达方式(但是是人类首选的表达方式)。因此域名和IP共同使用,但是在数据转发过程中真正需要的是IP地址,因此需要一种机制将IP和域名关联起来,这个机制就是DNS(Domain Name System,域名服务系统)。
从宏观逻辑上来讲,查询IP的步骤很简单,就是询问最近的DNS服务器这个域名的IP是什么,DNS就会把该域名的IP返回。很容易理解,只要想对DNS服务器发起请求,我们的本机中必须有DNS客户端,本机中的DNS客户端我们将之称为DNS解析器,通过域名获取IP的操作我们称为域名解析。
DNS客户端(解析器)是一个应用了Socket库网络通信程序,集成在了操作系统中。Socket库是加州伯克利开发的C语言库,封装了很多网络通信功能。
一些网络通信应用程序在编写的使用可以直接调用DNS解析器。
<内存地址> = gethostbyname("www.example.com")
像这样就可以直接把IP地址拿到内存当中。也就完成了IP地址的查询,接下来浏览器在向Web服务器发送消息时只要把IP地址拿出来和HTTP请求信息一起交给操作系统就可以了。
解析器的内部原理其实就是程序中的栈,网络通信调用Socket库中封装好的gethostbyname函数,然后进入函数内部给DNS服务器发送查询信息,之后调用操作系统内部的协议栈将数据发送。详细见P33。顺带一提的是,DNS服务器的IP地址大多情况需要手动写入(配置DNS服务器地址)。
简单来说,DNS服务器的工作就是接收客户端发来的信息,然后根据请求的内容返回响应。DNS客户端发送的查询信息包括以下三种信息:
①域名:服务器、邮件服务器(邮件地址中@后面的那一部分)的名称。
②Class:Class的值现在都是代表互联网的IN(设计之初可能考虑其他用途)。
③记录类型:类型为A是对应IPv4的地址;类型为AAAA对应的是IPv6的地址;类型为MX时对应的是邮件服务器的域名;类型为PTR表示根据IP地址反查域名;类型为CNAME表示查询域名相关别名;类型为NS表示查询DNS服务器的IP地址;类型为SOA表示查询域名的属性信息。
DNS接收到客户端的信息之后在自己的配置文件中找到与之对应的响应数据,随后返回。特别一提的是,当记录类型为MX时,DNS会在记录中保存两种信息,分别是邮件服务器的优先级和域名。一个邮件地址可能对应多个邮件服务器(比如@qq.com,qq.com对应着好几个邮件服务器,根据优先级来判定优先返回哪一个邮件服务器的域名,随后返回该服务器的IP地址),优先级越小越优先。DNS服务器中的信息都保存在配置文件当中,一行信息被称为一条资源记录。
我们很容易想到,一个DNS服务器中难免出现客户端请求的域名信息不存在的情况,因此我们需要一个机制,在DNS服务器中没有域名记录时快速从其他DNS服务器中找到该域名对应的IP地址。域名的层次结构确保了这个机制的运行。
DNS中的域名都是使用句点来分隔。在域名中,越靠右的位置表示层级越高。一个域的信息是作为一个整体存放在DNS服务器中,不能将一个域的信息拆开存放在多个DNS服务器中。实际使用的情况是,一个DNS服务器中可以存储多个域的信息。为了简化问题,我们假设一台DNS服务器中只存放了一个域的信息。这样DNS服务器就同域绑定了起来拥有了层级结构。(最顶层的域是.,例如(www.baidu.com.)最后的句点就代表根域)
见P42、P43图,最近的DNS服务器找不到域名信息时,就开始从根域服务器开始询问,然后逐级向下进行查询,最后在一台DNS服务器中找到答案,最近的DNS服务器请求到这个信息之后返回响应信息给客户端。(域是一个泛称,比如glasscom.com的域DNS服务器中应该包括lab1.glasscom.com、lab2.glasscom.com、lab3.glasscom.com等)
在实际情况中,上级域和下级域可能使用一个DNS服务器(并不是每一级域都有单独的DNS服务器,考虑到显示情况)。此外,就算最近的DNS服务器没有域名信息也不是每次都需要从最上级的根域查找,因为DNS服务器有缓存功能(短期记忆)。接下来的查询从缓存的位置进行查询(最接近)。当查询的域名不存在时,“不存在”这一个结果也会被缓存。缓存机制并不能实时反应域名的注册信息,因此缓存都设有一个有效期,有效期过后缓存信息就被删除。在对DNS客户端进行响应的时候,DNS服务器也会告诉客户端这是来自缓存还是负责该域名的DNS服务器。
在得到IP地址后,就可以委托操作系统内部的协议栈向该目的地发送信息(二进制信息)了。当网络程序向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用Socket库中的程序组件(调用好几个函数)。在网络中两端进行通信之前首先需要连接(也就是书中说的管道),此时出现一个抽象概念为套接字,套接字代表着网络通信中的一个端点,一个套接字需要分配一个IP地址和端口号。一般来讲,服务器端先创建套接字并分配IP和端口号,然后套接字处于监听状态,当客户端想要连接的时候也会创建一个套接字,与服务器端的套接字进行连接(TCP三次握手)。至此双方套接字进入连接状态,双方就可以传输数据了。套接字的连接一般由客户端发起,但是断开双方都可以发起,其中一方断开后,另一端也会随之断开。管道断开后,套接字也被删除,通信操作就结束了。
整个过程(创建套接字阶段、连接阶段、通信阶段、断开连接)都是由操作系统中的协议栈来执行的。至于协议栈在收到委托后的细致工作会在第二章讲解。Socket库只是网络程序和协议栈之间的桥梁,网络程序通过Socket库中的函数对协议栈进行委托。
套接字的创建对于网络应用程序来说很简单(毕竟都是委托给协议栈),只需要调用Socket库中的socket函数就可以创建套接字了。(通过socket函数的参数我们能看出来套接字定义了连接通信的方式,比如使用IPv4,使用TCP连接)
<唯一表示符> = socket(<使用IPv4>, <流模式>, ...);
唯一标识符也被称为描述符,就是用来识别不同的套接字(毕竟一个计算机中同时会存在多个连接)。
对于应用程序来说,只需要调用Socket库中的connect函数就可以进行连接。
connect(<描述符>, <服务器的IP地址和端口号>, ...);
IP地址我们通过DNS机制已经查询到了,端口号是事先规定好的,比如浏览器访问Web服务器时使用80端口号。个人理解对于服务端80端口并不是实际数据传输的端口,只是套接字连接的端口,服务器的80端口在收到客户端套接字信息之后(操作系统分配给客户端套接字的端口会通过connect函数告知服务器),操作系统进行处理,分配空余端口给服务端套接字。此时服务端再将实际通信的端口号通知给客户端,至此两方都知道了彼此的IP地址和通信端口,连接才算真正建立。(后面协议栈和服务器端应该会讲)
应用程序调用Socket库中的函数委托协议栈进行传递信息的操作。
# 发送
write(<描述符>, <发送数据>, <发送数据长度>);
# 接收
<接收数据长度> = read(<描述符>, <接收缓冲区>, ...)
发送:通过write函数实现,<描述符>指定套接字,套接字中已经保存了已连接对象的相关信息。<发送数据>是根据用户的请求生成的HTTP请求信息。
接收:调用read函数实现,<描述符>还是用来指定读取哪个管道的数据,接收缓冲区是指定的一片内存空间,接收到的数据就放在其中,至此应用程序就知道了在哪片内存中存储了多长的数据,应用程序就算是接收到了信息。
双方都是通过close函数断开连接,并且删除套接字。在Web服务器发送完响应信息之后,应该主动执行断开操作,调用close函数。当浏览器调用read执行接收数据操作时,read会返回连接已经断开的结果,然后客户端再执行close函数断开连接。
HTTP协议将HTML文档和图片都作为单独的对象来处理,每获取一次数据都需要执行一次连接、请求、响应、断开连接的过程,如果一个网页的所有资源都在一个服务器上面,反复的建立连接和断开连接无疑是效率低下的方式,因此后来人们又设计出了能再一次连接中收发多个请求和响应的方法,HTTP版本1.1就支持这种方法,在这种情况下,当所有的数据都请求完成后浏览器会主动断开连接。
协议栈大概分为两层,第一层大概有两块,分别是负责用TCP协议收发数据的部分和负责用UDP协议收发数据的部分,他们会接受应用程序的委托执行收发数据的操作。浏览器和邮件等需要稳定连接的需要使用TCP协议,DNS查询等收发较短的控制数据使用UDP。
第二层是用IP协议控制网络包收发操作的部分。信息在网络中传输时会被切成一个一个的网络包,负责将网络包发送给通信对象的操作就是IP协议。IP中包括ICMP协议和ARP协议:
ICMP:ICMP协议用于告知网络包传输过程中产生的错误以及各种控制信息。
ARP:根据IP地址查询以太网的物理MAC地址。
协议栈结构见P61图。
在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,比如通信对象的IP地址、端口号、通信操作的进行状态。我们把协议栈中的信息封装成套接字。计算机通信的时候会使用到套接字的信息,比如在发送信息的时候需要查看目的IP和端口号,需要记录发送时间(以判断包是否丢失),需要记录是否收到响应等记录。协议栈需要根据套接字中的信息以进行下一步操作。
在命令行中使用netstat命令可以查看套接字内容,需要一提的是,对于正在连接的套接字来讲,源IP或者是目的IP都可能是0.0.0.0,那是因为还没有连接成功。源IP是0.0.0.0的原因是因为还没有给套接字分配网卡。
创建套接字使用的是socket函数,调用函数之后,协议栈会在内存中分配一块内存,然后存入初始状态的控制信息,至此套接字创建完成。然后协议找把这个套接字的描述符返回给应用程序,后面应用程序想使用这个套接字的时候就把该描述符传递给协议栈就可以了,协议栈会自动读取相关通信信息,然后进行通信操作。
套接字创建完成后,客户端协议栈并不知道通信对象的IP地址和端口号,但是应用程序知道,所以需要应用程序调用connect函数将IP地址和端口号等信息传递给客户端套接字。服务端的套接字从一开始就被创建好了(被动等待连接),创建好的套接字一开始也是初始状态,甚至应用程序都不知道接下来要和谁通信,因此客户端需要在连接阶段向服务器传达开始通信的请求,告知客户端的IP地址和端口号。连接就是双方交换控制信息,完善套接字内容,方便后续的通信。
在通信操作中控制信息分为两类,一类是在网络包头部的控制信息,包括TCP头部、以太网头部、IP头部。一类是在套接字中(协议栈中的内存空间)记录的信息
在应用程序调用connect函数时,connect(<描述符>, <服务器IP和端口号>, ...)相关信息会被传递给协议找中的TCP模块,然后客户端的TCP模块会和服务器的TCP模块交换控制信息,具体步骤如下(TCP三次握手):
①客户端先创建一个包含表示开始数据收发操作的控制信息的头部,也就是TCP头部。这里重点关注源端口号和目的端口号,至此客户端的套接字就定位到了服务端的套接字(确定要找谁建立管道,服务端的套接字信息通过读取TCP头部和IP头部获得),然后我们将SYN位设置为1,可以先理解为表示连接,然后设置适当的序号和窗口大小。TCP头部创建好之后会把信息传递给IP模块并且委托进行发送。
②客户端的IP模块执行网络包发送操作后,网络包就会通过网络到达服务端,服务器上的IP模块将接收到的数据给TCP模块,服务器的TCP模块根据TCP头部信息找到端口号对应的套接字,找到套接字之后,服务器套接字中会写入响应的信息,并将状态修改为正在连接。之后服务器的TCP模块会返回响应,同客户端一样,在TCP头部设置发送方和接收方的端口号以及SYN位。然后将ACK位设置为1,这表示已经收到相应的网络包,设置ACK的目的就是确认网络包已经顺利送达。然后TCP模块委托IP模块向客户端发送相应。
③然后网络包到达客户端,通过IP模块到达TCP模块,并通过TCP头部的信息确认连接服务器的操作是否成功。如果SYN位为1则表示连接成功(也可能是RST位为1,表示连接异常中断),此时会向套接字中写入连接服务器的IP地址、端口号等信息,同时还会更改状态为连接完毕。到这里,客户端的操作就已经完成了,但是还需要告诉服务端连接建立成功,于是客户端也需要将ACK位设置为1,发给服务器,当服务器收到这个包时,更改套接字的状态为连接成功。到此连接操作才算完成。
现在双方套接字就可以进入随时可以收发数据的状态了,在调用close断开之前,连接是一直存在的。至此connect已经执行完毕,控制流程被交回应用程序。
当控制流程connect回到应用程序之后,接下来就进入数据的收发阶段。数据收发操作是从应用程序调用write函数,将要发送的数据和数据长度交给协议栈,协议栈收到数据之后执行发送操作。
首先,协议栈并不关心应用程序要传送的数据是什么,write函数会指定数据的长度,在协议栈角度来看,要发送的数据就是一定长度的二进制字节序列。
其次,对于那些小段的数据,协议栈并不是一收到数据就马上发出去,而是将这样的小段数据放在协议栈内部的发送缓冲区中,等待应用程序的下一段数据。应用程序转交给协议栈的数据长度是应用程序决定的,换句话说就是应用程序想要给协议栈多少数据就能给多少数据。打个比方,有的应用程序一次只传递给协议栈2个字节的数据,协议栈不能拿到这2字节就马上发送,因为这样会使网络中充斥大量的小包,导致网络效率下降,因此数据在累计到一定程度再发送数据更加合理。
至于数据包要累积到什么程度才会发送,不同种类和版本的操作系统会有所不同,但是都是根据以下要素来判断的:①MTU(Maximum Transmission Unit)最大传输单元,表示一个网络包的最大长度,在以太网中一般是1500字节。在其他一些网络中需要增加一些额外的头部数据,因此可能小于1500字节。MTU指的是网络包的长度,就是IP头部、TCP头部和数据载荷的总长。MSS(Maximum Segment Size)最大分段大小,这是MTU减去头部的长度,就是数据的最大容纳长度。MTU和MSS的区别见P77图。当协议栈缓冲区的数据长度超过或者接近MSS时再发送出去就可以解决网络中充斥小包的问题。②时间,当应用程序发送数据的频率不高的时候,显然等待缓冲区大小接近MSS大小是不明智的,可能会因为等待时间太长而造成延迟,因此在协议栈的内部有一个计时器,当经过一段时间(以毫秒为单位)之后,哪怕是缓冲区内数据大小没有接近MSS,也会把网络包发送出去。
判断要素就是这两个,就是MSS和时间,这两个要素显然是矛盾的,因此我们需要综合考虑这两个要素达到平衡,但是平衡的方式TCP协议中并没有规范,因此不同的版本和类型的操作系统在相关操作中存在差异。
需要补充的是,如果仅靠协议栈来判断发送的时机可能会带来一些问题(比如在应用程序要求极低延迟的情况下),因此协议栈给应用程序了一些权限,应用程序在发送数据时可以指定一些选项,可以做到委托协议栈直接发送数据,而不需要等待缓冲区长度接近MSS。浏览器这种会话型的应用程序在向服务器发送数据时,一般会使用直接发送的选项。
HTTP请求信息一般不会很长,一个网络包就能装得下,但是如果其中要提交表单数据,长度就可能超过MSS长度,比如在博客中发布一篇文章就是这种情况。
这种情况下,发送缓冲区的数据就会超过MSS的长度,这时我们不需要等待后续的数据,发送缓冲区的数据会被以MSS长度为单位进行拆分,拆分出来的每块数据被放进单独的网络包中。当需要发送这些数据包时,就会在每一块数据前面加上TCP头部,并根据套接字信息标记源端口号和目的端口号,然后交给IP模块执行发送操作。见P78图。(值得一提的是,应用程序数据中只有一个HTTP头部,也就是第一个拆分的包中有HTTP头部信息,剩下的包中只包含消息体)
在数据拆分完之后,将套接字中的信息写入TCP头部字段(这里其实就是源端口号和目的端口号),但是此时数据发送操作还没有结束。TCP具备确认对方是否成功收到网络包的功能,以及当对方没收到时进行重发的功能,因此在发送网络包之后,接下来还需要进行确认操作。本小节比较难理解。
①首先,Seq是序号,它用来表明这是从第几字节开始的部分。接收方通过包总长减去头部长度得到数据的长度。然后给发送方一个响应包,通过ACK号告诉发送方多少字节前的内容已经完全收到。见P79。
②在实际的通讯中,序号并不是从1开始的,而是随机计算出一个初始值,以防止网络攻击。这个初始值是在连接的时候,第一个发起连接的网络包中,SYN位设置为1,序号的位置写的就是这个随机初始值。
③在实际的情况中,数据是双向传输的,也就是说有两份数据需要处理。结合之上我们说的Seq序号是一个随机计算出来的值,这个值的交互在TCP连接阶段进行。1)在连接阶段客户端首先给用户端发送包,SYN值设置为1,序号部分是客户端->服务端的初始值,记为Seq1。2)然后服务端返回给客户端的包,将SYN和ACK位设为1,序号部分是服务端->客户端的初始值,记为Seq2,此时的TCP头部ACK的值是Seq1+1,符合之前我们的描述。3)随后客户端返回网络包,将ACK位设为1,ACK值是Seq2+1。以上过程也就是TCP的三次握手过程。
TCP采用以上机制确认对方是否收到了数据,在得到对方确认之前,发送过的包都会保存在发送缓冲区,如果对方没有返回某些包的ACK号,那么将重新发送。通过TCP的机制是可以实现稳定连接的(比如在发生错误的时候重传数据包)。因此,网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃包。当然,如果网络出现了一些问题,比如服务器宕机等情况,无论TCP怎样重传都不管用,因此,如果TCP在尝试几次重传都没有结果之后便会强制结束通信,并向应用程序报错。
之前所说的都只是一些基本原理,实际上网络的错误检测和补偿机制非常复杂,比如在收不到ACK响应包的情况下,要等待多久ACK包,并且在这个事件过去之后再重传网络包。ACK号的等待事件叫做超时时间。超时时间并不是一个固定值,而是TCP模块持续检测ACK号的返回时间动态调整的。
具体来说,当网络堵塞时,ACK号的返回时间就会变长(网络中路由等硬件工作变长导致网速变慢)。这种情况下我们必须将超时时间设置的长一点,否则可能会发生已经重传了包之后,前面包的ACK响应才姗姗来迟。这样的重传是多余的并且给网络增添了不必要的负担。但是如果超时时间设置的过长,那么包的重传就会出现很大的延迟,也会导致网络速度变慢。
因此,很明显现实情况要求超时时间的设置需要根据网络情况动态调整,这是如何实现的呢?首先根据服务器物理距离的远近,ACK号返回的时间也会有很大的波动,而且还需要考虑拥塞带来的影响。比如在公司局域网下几毫秒就可以返回ACK号,但是在互联网环境中,拥塞时可能需要几百毫秒才能返回ACK号。TCP采用了动态调整等待时间(超时时间)的策略,等待时间是根据ACK号返回所需的时间来判断的。TCP会在发送数据的过程中持续测量ACK号的返回时间,如果ACK号返回时间变慢,就延长等待时间;相反,如果ACK号马上就能返回,则相应缩短等待时间。
如P85图所示,在一来一回的情况下,单包发送后等待ACK号的时间实在是太长了,这段时间实在是太浪费了。但是如果发送方在等待ACK号的时候持续发包,但是接收方处理不过来,就会导致接收方内存缓冲区溢出,就会有数据明明接收到了,但是并没有给应用程序的情况发生。因此为了防止这个情况的发生,接收方有必要告知发送方自己的缓冲区剩余多少空间,以防止缓冲区溢出。这个能接收的最大数据量称为窗口大小,为了防止接收方缓冲区数据溢出而被设计出来。需要一提的是,发送操作是双向进行的,也就是双方都有窗口大小这个概念。
根据前面我们可以知道,接收方的窗口大小需要告知发送方,但是什么时候告知?比较容易理解,当缓存区数据被拿走的时候应该更新窗口大小,以示意发送方可以发送更多的数据。
之前我们也说过,ACK号也需要接收方反馈给发送方,我们可以认为当收到数据之后应该马上发送ACK号给发送方。
将这两个结合起来看,我们可以把这两件事当成独立的来看,即收到包之后就返回ACK号包;缓冲区扩大之后(数据被拷贝到应用程序内存)后告知发送方窗口大小。但是问题也随之展现,就是接收方给发送方发的包太多了,而且都是小包,导致网络效率下降,因此考虑将ACK和窗口合并发送给数据发送方。具体方式如下:
接收方在发送ACK和窗口更新时,并不会什么都不管直接将包发送,而是设定了一个等待时间,如果这个时间内有其他的操作通知,,就可以把两种通知合并在一个包里。举例子,1)在等待发送ACK包时正好需要更新窗口大小,于是把ACK包和窗口更新放到一个包中。2)在等待ACK2时,ACK3也需要发送时,就只发送ACK3一个就可以了。因为ACK的意思是告知发送方,在ACK之前的数据我都已经收到了。当出现多个ACK包需要发送时,只发送最后一个就可以了,中间的可以全部省略。当需要连续发送多个窗口更新时也可以减少包的数量,因为连续发送窗口更新说明应用程序连续请求了数据,接收缓冲区的剩余空间连续增加。(其实不一定是连续增加)只要发送窗口更新的最终结果就可以了。
TCP处理乱序到达的包也是通过ACK来实现的,举例子来说明:
假设发送方发送了五个数据包,序列号分别是1, 2, 3, 4, 5(每个包包含1个单位数据)。如果数据包到达顺序是1, 3, 2, 5, 4,情况应该是这样:
数据包1:接收方收到数据包1(包含序列号1的数据),发送ACK 2(期望收到序列号为2的数据)。
数据包3:此时数据包2尚未到达,接收方不能按顺序确认包3,因此它继续发送ACK 2(期望收到序列号为2的数据)。
数据包2:现在数据包1和2都已顺序到达,接收方可以确认包2,并检测到包3也已在缓冲区内,因此发送ACK 4(期望收到序列号为4的数据)。
数据包5:由于数据包4尚未到达,接收方不能按顺序确认数据包5,因此继续发送ACK 4(期望收到序列号为4的数据)。
数据包4:现在数据包1到4都已按顺序到达,接收方确认数据包4,因为数据包5也已到达,因此发送ACK 6(期望收到序列号为6的数据)。
之前讲过,浏览器通过委托协议栈发送HTTP请求消息之后,还需要等待Web服务器返回响应信息,是通过read函数来实现的。浏览器在委托协议栈发送请求消息之后就会立即调用read函数,然后控制流程进入协议栈。协议栈尝试从接收缓冲区取出数据并且交给应用程序,但是此时请求消息刚刚发送,响应信息还没传递过来,因此接收缓冲区中并没有数据,也就是浏览器所需要的资源没有准备充分,于是协议栈会把应用程序的委托挂起(操作系统)。等待资源准备充分后再继续执行接收操作。当数据到达后,协议栈将接收到的数据复制到应用程序的内存中,然后将控制流程交回应用程序(这些已经接收到的数据对于应用层可能是不完整的,比如使用ASCII编码,收到的二进制位数并不是7的倍数,但是对于网络层来说,一个数据包就是完整的,通常是以字节为单位发送数据,具体的编码方式需要应用层来做,这些事情不归网络层负责)。将数据交给应用程序之后,协议栈还需要找到合适的时机给发送方发送窗口更新通知。
当数据传输完毕后,就进入到断开连接,删除套接字的过程了。应用程序发起断开连接过程,一般是数据发送完毕的一方发起断开过程,这个决策是应用程序做的,协议栈允许双方都可以发起断开连接。
我们以服务器一方发起断开为例。①首先服务器一方的应用程序会调用Socket库中的close函数。然后服务器的协议栈会生成包含断开信息的TCP头部,即控制位中的FIN为1。然后协议栈委托IP模块向客户端发送这个网络包。同时服务器的套接字中也会记录断开操作的相关信息。②当客户端收到FIN位为1的TCP头部时,协议栈也会将套接字标记为进入断开操作状态。然后客户端告诉服务器已经收到该通知,会向服务器返回一个ACK号。至此,协议栈等待应用程序来取数据(协议栈不会主动联系应用程序,因此只能等应用程序调用read函数时才能告知应用程序已经进入断开连接状态了)。③协议栈调用read函数后,协议栈缓冲区中如果有剩余的已接收数据就交给应用程序,并且通知应用程序来自服务器的数据已经全部接收到了。根据规则,只要收到服务器返回的所有数据,通信操作就结束了,因此客户端应用程序会调用close函数来结束数据的收发操作。客户端协议栈也会生成控制位FIN位为1的TCP包,然后委托IP模块发送给服务器。④然后服务器返回一个ACK包,表示已经收到客户端的通知,至此通信全部结束。以上过程也就是TCP断开连接的四次挥手过程,见P91图。
正常的通信结束之后,双方的套接字就不会再使用了,我们就可以删除套接字了,但是套接字不会被理解删除,而是等待一些时间才会删除,主要是防止四次挥手过程中的丢包情况,比如第四次挥手时客户端发送的ACK包丢失,服务器没有收到FIN信号,就会重新发送FIN包。如果此时客户端的套接字已经被删除了,而且正好端口被分配到新的套接字上,那么新的套接字会以为新的服务端发起了断开连接的操作,这种情况是我们不希望看到的。因此套接字会被保留一段时间再被删除,至于等待多长时间,我们必须保证网络中没有重传的FIN包时,才可以将套接字信息删除,通常重传操作会持续几分钟,如果重传了几分钟仍然无果就停止重传。所以一般会等待几分钟,确保网络中没有重传的FIN包之后再删除套接字。
至此,使用TCP协议收发应用程序数据的操作就全部结束了。这节是整理之前的内容。复习时记得看P94图。
数据收发的第一步是创建套接字。一般来讲,服务器一方的应用程序在启动时就会创建好套接字并且进入等待连接的状态。客户端则是在用户出发特定动作,需要访问服务器的时候创建套接字。在这个阶段,还没有开始传输网络包。
创建套接字后,客户端会向服务器发起连接操作。首先,客户端生成一个SYN位为1的TCP包并发送给服务器。这个TCP包的头部包含了客户端向服务器发送数据时使用的初始序号,以及客户端的窗口大小。当这个包到达服务器之后,服务器会返回一个SYN为1的TCP包,此外这个包还包含已收到包的ACK号(设置ACK号时需要将ACK控制位设置为1)。当这个包到达客户端之后,客户端会返回一个确认的ACK号的TCP包。到这里,连接操作就完成了,双方进入数据收发阶段。
然后就是数据收发阶段,此阶段根据应用程序的不同而有一些差异,以Web为例,首先客户端向服务器发送请求信息。TCP会将请求信息切分成一定大小的块,并加上TCP头部,然后发给服务器。TCP头部中包含序号,表示当前发送的是第几个字节的数据。当服务器收到数据时,会向客户端返回ACK号。在最初的阶段,服务器不断接收数据,随后将数据传递给应用程序,接收缓冲区就会被释放,此时服务器就会向客户端告知新的窗口大小。当服务端收到客户端的请求消息后,就会向客户端发送响应数据,客户端也会向服务器发送ACK包和窗口更新通知。
服务器的响应消息发送完毕之后,数据收发操作就结束了,就会执行断开操作。以HTTP1.0的Web为例(HTTP1.1可能是客户端发起断开),服务器发送全部数据后执行close函数,服务器发送一个FIN位为1的TCP包,然后客户端返回一个表示收到的ACK号包。然后客户端通知完应用程序之后再向服务器发起一个FIN位为1的TCP包,服务器端返回ACK号的包。然后在等待一段时间之后套接字删除,连接完全结束。
本章介绍了一些包的基本信息,之前了解过一些,再次不作太多总结。一般来讲TCP头部加上应用层数据部分就是称为包,IP包就是在包的基础上加上IP头部,以太网包就是在IP包的基础上加上MAC头部,头部基本就是协议的控制信息,在协议的实际运转上起到作用。
本章区别一下MAC协议(以太网协议)和IP协议。IP头部的信息是不变的,在终端节点就被写好,但是MAC头部的信息是变化的,根据目的IP地址,通过路由器中的路由表,查到下一个节点的MAC相关信息(MAC地址之类的)然后写入MAC头部。送往集线器,又集线器进行发送到路由器。然后路由器再重复此前的工作,直至包到达目的端点。(本节书中就是给出了一个大概的逻辑过程,详细的内容后续章节会介绍)
我们之前说委托IP模块进行发包,其实是透明了包的传送的细节(通过集线器、路由器发送),我们后续会有讲解,IP模块只是整个包传送的入口。我们先大致的整理一下IP模块需要进行的工作。
TCP将包(TCP头部+数据)交给IP模块,IP模块负责加入IP头部和MAC头部(两种头部信息)。MAC头部是以太网用的头部,用于实际发送包,IP头部更类似于逻辑上的终点。然后IP模块将包交给网卡(此时的数据是二进制),网卡使用光信号或者电信号对消息进行发送。值得一提的是,IP模块并不关心TCP头部和数据部分,因为对它而言只是一块二进制数字。对于IP模块来讲,任何包的处理机制都是一样的。
此小节主要说明了以下内容:
①IP模块负责生成IP头部,IP头部内容包括目的IP和发送IP。
②IP是分配到网卡上面的,因此对于多个网卡的计算机,在生成IP头部的时候需要知道使用哪一个网卡来发送,不是哪个网卡空闲就使用哪个网卡,而是根据IP模块中的路由表确定的。简单理解就是哪个网卡方便使用哪个网卡,详细来说在路由表中有规定,下一步向哪一个路由器发送包,同时就指定好了使用哪个网卡,见P105图,第三列Geteway是下一个路由器(网关)的IP地址,第四列就是网卡的IP地址(指定使用哪一个网卡)。
③如果路由表中目的IP匹配不成功,就匹配到第一行(默认网关),第三章中讲解。
④IP头部还有协议号这一个部分,意思是IP模块受哪个协议的委托生成的IP头部,TCP:06;UDP:17;ICMP:01(均为16进制)
IP模块生成IP头部和MAC头部,MAC头部包含了接收方和发送方的MAC地址(48比特),还有以太类型,我们可以认为以太网类型后面就是以太网包的内容,以太网类型表示后面内容的类型。以太类型包括:0800,IP协议;0806,ARP协议;86DD,IPv6协议。
MAC头部一共就包含三部分信息,见P107图,我们很容易想到,这三部分信息从哪里来的呢?首先发送方的MAC地址是网卡的MAC地址(如何选择网卡在上一节已经阐述过),在出厂的时候就已经被写到网卡的ROM中了。接收方的MAC地址从哪里获得呢?我们通过路由表已经得知,我们将包发往的下一个路由器,但是此时我们还不知道这个路由器的MAC地址,因此我们需要ARP查询目标路由器的MAC地址。
有很多细节我们还不宜深究,目前首先需要知道,ARP是通过广播的形式,询问同一个子网下的所有设备,提供目的IP,想要收到MAC地址。如果路由表配置有问题,那么下一个路由器的不跟客户端是同一子网下,就会发生错误。
在实际使用的过程中,我们不能每次发送包都查询一次,因此我们将查询结果放入到一块叫做ARP缓存的区域中,在发送包时首先查找缓存,如果缓存中不存在匹配的数据则再进行查询。查询结果同样放在ARP缓存中。ARP缓存的内容是IP和MAC地址的一一对应。因为在网络环境中,IP是会变动的,因此防止出现IP变换后MAC地址无效,因此ARP缓存在经过几分钟之后就会全部删除,再进行ARP查询就能再次获得地址了。
将MAC包头放到IP包头后面,整个包的生成就结束了,网卡只需要根据MAC头部的内容发送接收数据包就好了。
下面的知识涉及到网络拓扑方面。目前以太网的结构如P112图(c)所示。一个客户端将包发送到交换式集线器,然后交换式集线器将包转发给正确的接收方(根据MAC地址)。
我们之前所说生成各种各样的头部,都只是方便接收方和发送的处理的控制信息(这样理解应该没错)。真正传送的只是电或光信号。
我们首先看一下网卡的基本结构图P115图。网卡不是通电之后就马上开始工作的,而是和其他硬件一样首先进行初始化。打开计算机启动操作系统的时候,网卡驱动程序会对硬件进行初始化操作,包括硬件错误检查、初始设置等步骤。
网卡的结构分为几个部分:①缓冲区:临时保存需要收发的数据。
②ROM:存放网卡的MAC地址。
③MAC模块:初始化的时候读取ROM中的MAC地址,控制包的收发操作(根据MAC地址)。
④PHY(MAU):发送和接收信号的电路(硬件)。
⑤RJ-45接口:连接网线的插座。
在正常的初始化情况下,MAC模块读取ROM中的网卡的MAC地址,但是也可以通过命令和配置文件将MAC地址直接写入到MAC模块中。当然,硬件的初始化是通过驱动来完成的,驱动是和硬件配套的软件,现在大多数人不熟悉的原因是太多的驱动已经被集成到操作系统中了,以至于大家对驱动如此陌生。
网卡驱动在从IP模块获取包之后,会将其复制到网卡内的缓冲区中,然后向MAC模块发送发送包的命令。然后就是MAC模块的工作了。
首先MAC模块将包从缓冲区中取出,并在开头加上报头和起始帧分界符,在末尾加上用于检测错误的FCS(帧校验序列)。网卡中发送的包的结构如P117页图所示。
要理解报头和起始帧分界符存在的意义,我们就必须搞明白如何通过电信号来读取数据。
如P118图所示,数据通过电压和电流来传播的,数据被转成电压信号进行传播,但是如果连续出现0电压或者1电压,那么电压将没有变化,我们就没有办法保证数据是正确的。因此出现时钟信号来解决这个问题,当时钟信号的电压从下往上变化时读取数据信号,判断是低电压或者是高电压就可以了。但是如果两个信号分开发送就是导致当距离比较远时,两种信号没有办法同时到达,那么时钟信号也就失去了原本的意义。为了解决这个问题,将数据信号和时钟信号叠加在了一起。由于时钟信号是按照固定频率进行变化的,因此只要能找到变化的周期,我们就可以通过叠加的信号还原出来时钟信号和数据信号。
于是确定时钟信号的频率就成为了重点。时钟信号是按照固定频率进行变化的,因此一开始讲的报头和起始帧分界符的作用就在于帮助接收方找到时钟信号的频率,以方便后续的接收。起始帧分界符是两个连续的1作为信号,告知接收方报头已经结束了,后续要传输包的内容了(即MAC头部、IP头部之类)。
末尾的FCS(帧校验序列)用来检查包传输过程中因噪音导致的波形紊乱、数据错误。FCS是一串32比特的序列,是通过一个公式对包中的内容进行计算得出来的,当原始数据中的某一个比特发生变化,接收方计算出来的结果就会和FCS有差异,这样就可以判定传输过程中数据有没有错误。
也就是说,当传输过程中,接收方和发送方以FCS为准,如果接收方计算出来的FCS和接收的FCS不一致(哪怕数据本身没有任何问题,只是FCS收到噪音影响),都不会发送ACK确认收到,而是丢弃网络包,等待发送方重传。
首先介绍通信中的两个概念:全双工和半双工。全双工是指发送和接收可以同时进行。半双工是指发送和接收只能有一边进行操作。
网卡工作完之后,就可以把包发送出去了,发送有两种形式,一种是使用集线器的半双工形式,一种是交换机的全双工形式。
在半双工的工作模式下,为了避免信号碰撞,首先需要判断网线中是否存在其他设备发送的信号。如果有的话需要等待网线中的信号传输完毕。当网线是空闲状态下的时候,就可以传输信号了。首先,MAC模块将从报头开始将数字信号按每个比特转换成电信号,然后由PHY,或者叫MAU的信号收发模块发送出去(叫法不一样)。在这里,将数字信号转换为电信号的速率就是网络的传输速率,例如每秒将10Mbit的数字信息转换为电信号发送出去,则速率就是10Mbit/s.
然后,PHY(MAU)模块将MAC传达过来的信号进行转换。MAC模块生成通用信号,PHY(MAU)模块将信号转换为可在网线中传输的格式。网卡中的PHY根据网线类型和传输速率对MAC模块生成的通用信号进行不同的转换(就先理解成网卡中适配了很多类型的网线,网卡通过某种渠道,可能是电缆检测或者接口不同得知网线的类型和速率),并通过网线发送出去。需要提到的是,见P121图,MAC输出信号是00001110,但是PHY会进行重新编码,最后的信号是1111011100(按照4B/5B格式进行编码,编码方式不是重点),然后将信号发送出去。
PHY不仅负责将信号发送出去,还负责接收信号,在发送之前需要确认没有其他信号进来才可以。以太网这一层不会确认发送的数据是否被收到,因为发生错误的概率太小了,并且有TCP协议负责错误的处理。
在使用集线器的半双工模式中,如果数据发送和数据接收同时发生(这种情况很容易发生,比如一台集线器下的两台设备同时发送数据,但是集线器的特性就是拿到包之后直接广播,谁能处理就是谁的),那么就会发生信号碰撞。这种情况下,继续发送信号是没有意义的,因此发送操作会停止。网卡为了通知其他设备当前线路已经发生碰撞,还会发送一段时间的阻塞信号(阻塞信号在设计之初就保证不会受到信号碰撞的影响),然后所有的发送操作会全部停止。然后根据网卡的MAC地址计算出来一个随机的静默时间,这个时间内设备会停止包的发送。如果随机时间过后,发送包时仍然存在碰撞的情况,那么就把静默时间延长一倍,最多尝试10次,然后报告通信错误。
另一种全双工的就没有碰撞的问题了,接收和发送可以同时进行。见P112图,(b)是集线器,(c)是交换机。
网卡通过MAC模块转换通用信号,再使用PHY进行格式转换,然后发送出去,这样网卡将包转换为电信号的过程就结束了,我们继续看看接受网络包的过程。
在使用集线器的半双工模式中,一台设备发送的信号会到达连接在集线器上的所有设备,因此接收操作的第一步是将所有的信号都收进来。当然,在使用交换机的全双工模式中,不需要接收所有的信号。
在接收过程中,信号的开头是报头,通过报头的同步时钟进行同步,然后遇到起始帧分界符开始将后面的信息转为数字信息。这个操作是和发送相反的(逆过程),首先PHY(或者MAU)模块开始工作,将信号转为通用格式并且发给MAC模块,MAC模块再从头开始将信号转为数字信息,并且存放在缓冲区中。当到达信号的末尾时,还需要检查FCS。具体来说就是使用公式计算所有的比特,然后将结果与包末尾的FCS进行对比,正常情况是一致的,若发生错误不一致则丢弃包。
然后就要看一下MAC头部中接收方的MAC地址是否和网卡再初始化时分配给自己的MAC地址(一种是ROM中出厂时写入的MAC地址,一种是通过配置文件自己分配MAC地址,MAC地址可以仿冒)是否一致,以判断这个包是不是发送给自己的。如果不是发送给自己的就直接丢掉。如果是自己的包就放入缓存中(之前已经放入网卡缓冲区了,个人理解这次是放到操作系统可以读的缓冲区了,换了个地方)。到这里,MAC模块的工作已经做完了,就该通知计算机收到一个包。
通知计算机会使用中断的机制,在网卡接收数据的时候,计算机中的CPU还忙着处理别的事情,因此网卡需要通过这种机制打断一下CPU。具体来说是这样。首先,网卡向扩展总线中的中断信号线发送信号,这个信号线通过计算机中的中断控制器连接到CPU。当产生中断信号时,CPU会暂时挂起手中的任务,切换到操作系统中的中断处理程序。然后,中断处理程序会调用网卡驱动,控制网卡执行相应的接收操作。
中断是有编号的,网卡在安装的时候就在硬件中设置了终端号(很多硬件都有),在中断处理程序中将硬件的中断号和相应的驱动程序绑定。因此中断处理程序能准确的调用网卡驱动程序,而不是什么别的驱动。现在的硬件设备都遵循即插即用的规范自动设置中断号(PnP,自动配置扩展卡和周边设备)。
网卡驱动被中断处理程序调用后,后从网卡的缓冲区中取出数据,然后根据MAC头部中的以太类型字段判断协议的类型,然后交给相应的协议栈。最常见的就是使用TCP/IP协议(0800,十六进制),交给TCP/IP协议栈,还有Mac电脑中使用的AppleTalk协议(089B,十六进制),就交给AppleTalk协议栈。但是如果发现操作系统内部不存在以太类型所对应的协议栈,就会被当成错误,直接丢弃。然后协议栈对包进行解析,交给相应的通信程序。
我们假设以太类型是0800,即IP/TCP协议,接下来就是轮到IP模块进行工作了。首先检查IP头部,确认格式是否正确。如果格式没有问题就查看接收方IP地址,接收方IP地址没有问题我们就可以正式接收这个包了。如果发生问题,比如接收方的IP地址不对,那IP模块会通过ICMP协议向发送方报告错误。ICMP主要消息如P126表所示。
IP协议有一个分片的功能(区分TCP拆分),在网线和局域网中只能传输小包,因此在一些情况下是需要将包进行分切的。如果接收到的包是经过切分的小包(分片的包会在IP头部进行标记),IP模块将其暂存在内部的内存空间中,然后等待IP头部中具有相同ID的包全部到达(同一个包的所有分片都有同一个ID),然后通过IP头部的分片偏移量字段判断分片包在完整包中所处的位置。根据这些信息在所有的分片包到达之后,将其还原成原始的包,这个操作叫做分片重组。
然后包就会被交给TCP模块。TCP模块会根据IP头部的接收方和发送方IP地址,以及TCP头部的端口号信息来查找对应的套接字,再根据套接字中的信息执行相应的操作了。
TCP的可靠性是通过得知丢失包的具体信息(比如一大堆包中,具体哪一个丢了可以通过ACK得知)得以展现的,这样就不用重新发送这么一大堆包了。但是对于数据很短的情况下,用一个小包就足够包括所有信息,我们没有必要得知是哪个具体的包丢了,如果没收到就重新发送这个小包,这就是UDP协议。在UDP协议中,没有专门的确认包,只要对方给了我们想要的请求就算是确认包了,如果没有回应就重传。比如DNS查询的时候就是使用的UDP协议。
UDP没有接收确认、窗口等机制,也不需要交换控制信息,只需要加上UDP头部然后委托给IP模块就可以了。UDP头部只有发送方接收方端口号,数据长度和校验和(检查错误)。UDP协议的接收也很简单,根据IP头部中的IP地址,以及UDP头部的端口号找到相应的套接字,并将数据给相应的应用程序就可以了。
UDP的特点就是高效(毕竟不用来回那么多控制包),因此在音频和视频数据的传输中常常使用,一方面是因为传送速度快,另一方面是哪怕丢包也只是会发生卡顿,这在正常情况下都是可以接受的。这种情况下是无需重发数据的。
本章到此就结束了,因为这章重点实在太多了,我也是一边理解一边记录的,因此很多地方都很啰嗦,不够简练。希望自己懂得越来越多,逐渐把内容读薄。
每个包在传输层面都是独立的。业务都是独立的。
本章来探索一下网络包在进入互联网之前经历的传输过程。我们假设,网络包从客户端计算机发出之后,要经过集线器、交换机和路由器最终进入互联网。实际现代社会中,家用的路由器已经集成了这些功能,也就是说,现在的网络包只需要通过家用路由器就可以直接接入互联网。
网卡中的PHY模块将包转换为电信号,然后信号通过RJ-45接口进入双绞线。在硬件传输的过程中,信号的能量会损失(涉及到物理学知识,产生的电磁波带走了一部分能量)。总体而言,在传输过程中,信号会衰减,如果不采取措施,很容易产生通信错误。
------------------------------------------------书已经看完,有空会完善该笔记------------------------------------