网络是怎样连接的-CDN|服务器整个流程|客户端收到返回信息

227 阅读21分钟

CDN

当缓存服务器放在服务器端时,可以减轻Web服务器的负载,但无法减少互联网中的流量。

将缓存服务器放在客户端更有效。互联网中会存在一些拥塞点,通过这些地方会比较花时间。如果在客户端部署缓存服务器,就可以不受或者少受这些拥塞点的影响,让网络流量更稳定,特别是当访问内容中含有大图片或视频时效果更明显。

客户端的缓存服务器是归客户端网络运营管理者所有的,Web服务器的运营者无法控制它。比如,某网站的运营者觉得最近网站上增加了很多大容量的内容,因此想要增加缓存服务器的容量。如果缓存放在服务器端,那么网站运营者可以自己通过增加磁盘空间等方式来进行扩容,但对于放在客户端的缓存就无能为力了。进一步说,客户端有没有缓存服务器还不一定呢。

Web服务器运营者和网络运营商签约,将可以自己控制的缓存服务器放在客户端的运营商处。 1624285090661.jpg

这样一来,我们可以把缓存服务器部署在距离用户很近的地方,同时Web服务器运营者还可以控制这些服务器,但这种方式也有问题。

对于在互联网上公开的服务器来说,任何地方的人都可以来访问它,因此如果真的要实现这个方式,必须在所有的运营商POP中都部署缓存服务器才行,这个数量太大了,非常不现实。

要解决这个问题也有一些办法。首先,我们可以筛选出一些主要的运营商,这样可以减少缓存服务器的数量。尽管这样做可能会导致有些用户访问到缓存服务器还是要经过很长的距离,但总比直接访问Web服务器的路径要短多了,因此还是可以产生一定的效果。

那就是即便减少了数量,作为一个Web服务器运营者,如果自己和这些运营商签约并部署缓存服务器,无论是费用还是精力都是吃不消的。

为了解决这个问题,一些专门从事相关服务的厂商出现了,他们来部署缓存服务器,并租借给Web服务器运营者。

这种服务称为内容分发服务,也叫CDS,现在更多是叫CDN,提供这种服务的厂商称为CDSP,内容分发服务运营商。他们会与主要的供应商签约,并部署很多台缓存服务器。

另一方面,CDSP会与Web服务器运营者签约,使得CDSP的缓存服务器配合Web服务器工作。

缓存服务器可以缓存多个网站的数据,因此CDSP的缓存服务器就可以提供给多个Web服务器的运营者共享。这样一来,每个网站运营者的平均成本就降低了,从而减少了网站运营者的负担。而且,和运营商之间的签约工作也由CDSP统一负责,网站运营者也节省了精力。

如何让客户端访问最近的缓存服务器

在使用内容分发服务时,互联网中有很多缓存服务器,如何才能从这些服务器中找到离客户端最近的一个,并让客户端去访问那台服务器呢。

1624285090660.jpg

第一个方法是像负载均衡一样用DNS服务器来分配访问。也就是说,我们可以在DNS服务器返回Web服务器IP地址时,对返回的内容进行一些加工,使其能够返回距离客户端最近的缓存服务器的IP地址。

互联网中有很多台DNS服务器,它们通过相互接力来处理DNS查询,这个过程从客户端发送查询消息开始,也就是说客户端会用要访问的Web服务器域名生成查询消息,并发送给自己局域网中的DNS服务器。

如果本地没有DNS服务器,会将请求发给运营商的DNS服务器。

客户端DNS服务器会通过域名的层次结构找到负责管理该域名的DNS服务器,也就是Web服务器端的那个DNS服务器,并将查询消息发送给它。

Web服务器端的DNS服务器收到查询消息后,会查询并返回域名相对应的IP地址。

在这台DNS中,有一张管理员维护的域名和IP地址的对应表,只要按照域名查表,就可以找到相应的IP地址。接下来,响应消息回到客户端的DNS服务器,然后再返回给客户端。

1624285090659.jpg

如果一个域名对应多个IP地址,则按照轮询方式按顺序返回所有的IP地址。

如果按照DNS服务器的一般工作方式来看,它只能以轮询方式按顺序返回IP地址,完全不考虑客户端与缓存服务器的远近,因此可能会返回离客户端较远的缓存服务器IP地址。

如果要让用户访问最近的缓存服务器,则不应采用轮询方式,而是应该判断客户端与缓存服务器的距离,并返回距离客户端最近的缓存服务器IP地址。这里的关键点不言自明,那就是到底该怎样判断客户端与缓存服务器之间的距离呢。

首先,作为准备,需要事先从缓存服务器部署地点的路由器收集路由信息。

1624285090658.jpg

一共有4台缓存服务器,在这4台服务器的部署地点又分别有4台路由器,则我们需要分别获取这4台路由器的路由表,并将4张路由表集中到DNS服务器上。

接下来,DNS服务器根据路由表查询从本机到DNS查询消息的发送方,也就是客户端DNS服务器的路由信息。

实际上,客户端DNS服务器不一定和客户端在同一位置,因此可能无法得出准确的距离,但依然可以达到相当的精度。

还有另一个让客户端访问最近的缓存服务器的方法。HTTP规格中定义了很多头部字段,其中有一个叫作Location的字段。当Web服务器数据转移到其他服务器时可以使用这个字段,它的意思是“您要访问的数据在另一台服务器上,请访问那台服务器吧。”

1624285090656.jpg

这种将客户端访问引导到另一台Web服务器的操作称为重定向,通过这种方法也可以将访问目标分配到最近的缓存服务器。

首先需要将重定向服务器注册到Web服务器端的DNS服务器上。这样一来,客户端会将HTTP请求消息发送到重定向服务器上。

1624285090655.jpg

这种方法的缺点在于增加了HTTP消息的交互次数,相应的开销也比较大,但它也有优点。对DNS服务器进行扩展的方法是估算客户端DNS服务器到缓存服务器之间的距离,因此精度较差;相对而言,重定向的方法是根据客户端发送来的HTTP消息的发送方IP地址来估算距离的,因此精度较高。

重定向服务器不仅可以返回带有Location字段的HTTP消息,也可以返回一个通过网络包往返时间估算到缓存服务器的距离的脚本,通过在客户端运行脚本来找到最优的缓存服务器。这个脚本可以向不同的缓存服务器发送测试包并计算往返时间,然后将请求发送到往返时间最短的一台缓存服务器,这样就可以判断出对于客户端最优的缓存服务器,并让客户端去访问该服务器。

还有一个因素会影响缓存服务器的效率,那就是缓存内容的更新方法。

这种方法对于第一次访问缓存服务器,缓存是是无效的,而且后面的每次访问都需要向原始服务器查询数据有没有发生变化,如果遇到网络拥塞,就会使响应时间恶化。

要改善这一点,有一种方法是让Web服务器在原始数据发生更新时,立即通知缓存服务器,使得缓存服务器上的数据一直保持最新状态,这样就不需要每次确认原始数据是否有变化了,而且从第一次访问就可以发挥缓存的效果。

服务端创界套接字等待和链接

在连接过程中,客户端发起连接操作,而服务器则是等待连接操作,因此在Socket库的用法上还是有一些区别的。

服务器的程序可以同时和多台客户端计算机进行通信,这也是一点区别。

服务器需要同时和多个客户端通信,但一个程序来处理多个客户端的请求是很难的,因为服务器必须把握每一个客户端的操作状态。

因此一般的做法是,每有一个客户端连接进来,就启动一个新的服务器程序,确保服务器程序和客户端是一对一的状态。

首先,我们将程序分成两个模块,即等待连接模块和负责与客户端通信的模块。

1624285090654.jpg

当服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块。这个模块会创建套接字,然后进入等待连接的暂停状态。接下来,当客户端连发起连接时,这个模块会恢复运行并接受连接,然后启动客户端通信模块,并移交完成连接的套接字。接下来,客户端通信模块就会使用已连接的套接字与客户端进行通信,通信结束后,这个模块就退出了。

这种方法在每次客户端发起连接时都需要启动新的程序,这个过程比较耗时,响应时间也会相应增加。因此,还有一种方法是事先启动几个客户端通信模块,当客户端发起连接时,从空闲的模块中挑选一个出来将套接字移交给它来处理。

服务端socket工作流程

  • 创建套接字(创建套接字阶段)
  • 将套接字设置为等待连接状态(等待连接阶段)
  • 接受连接(接受连接阶段)
  • 收发数据(收发阶段)
  • 断开管道并删除套接字(断开阶段)

首先,协议栈调用socket创建套接字,这一步和客户端是相同的。

1624285090652.jpg

接下来调用bind将端口号写入套接字中。在客户端发起连接的操作中,需要指定服务器端的端口号,这个端口号也就是在这一步设置的。具体的编号是根据服务器程序的种类,按照规则来确定的,例如Web服务器使用80号端口。

设置好端口号之后,协议栈会调用listen向套接字写入等待连接状态这一控制信息。这样一来,套接字就会开始等待来自客户端的连接网络包。

然后,协议栈会调用accept来接受连接。由于等待连接的模块在服务器程序启动时就已经在运行了,所以在刚启动时,应该还没有客户端的连接包到达。可是,包都没来就调用accept接受连接,可能大家会感到有点奇怪,不过没关系,因为如果包没有到达,就会转为等待包到达的状态,并在包到达的时候继续执行接受连接操作。

接下来,协议栈会给等待连接的套接字复制一个副本,然后将连接对象等控制信息写入新的套接字中。

当accept结束之后,等待连接的过程也就结束了,这时等待连接模块会启动客户端通信模块,然后将连接好的新套接字转交给客户端通信模块,由这个模块来负责执行与客户端之间的通信操作。之后的数据收发操作和刚才说的一样,与客户端的工作过程是相同的。

在复制出一个新的套接字之后,原来那个处于等待连接状态的套接字会怎么样呢?其实它还会以等待连接的状态继续存在,当再次调用accept,客户端连接包到达时,它又可以再次执行接受连接操作。接受新的连接之后,和刚才一样,协议栈会为这个等待连接的套接字复制一个新的副本,然后让客户端连接到这个新的副本套接字上。

1624285090651.jpg

1624285090649.jpg

创建新套接字时端口号也是一个关键点,端口号是用来识别套接字的,因此我们以前说不同的套接字应该对应不同的端口号,但如果这样做,这里就会出现问题。因为在接受连接的时候,新创建的套接字副本就必须和原来的等待连接的套接字具有不同的端口号才行。这样一来,比如客户端本来想要连接80端口上的套接字,结果从另一个端口号返回了包,这样一来客户端就无法判断这个包到底是要连接的那个对象返回的,还是其他程序返回的。因此,新创建的套接字副本必须和原来的等待连接的套接字具有相同的端口号。

当连接模块将socket副本交给通信模块后,所有的副本端口号都会是80端口。端口号是用来识别套接字的,如果一个端口号对应多个套接字,就无法通过端口号来定位到某一个套接字了。当客户端的包到达时,如果协议栈只看TCP头部中的接收方端口号,是无法判断这个包到底应该交给哪个套接字的。

所以总共使用下面4种信息来进行判断。

  • 客户端IP地址
  • 客户端端口号
  • 服务器IP地址
  • 服务器端口号

那么要指代某个套接字时用这4种信息就好了,为什么还要使用描述符呢?这个问题很好,不过我们无法用上面4种信息来代替描述符。原因是,在套接字刚刚创建好,还没有建立连接的状态下,这4种信息是不全的。此外,为了指代一个套接字,使用一种信息(描述符)比使用4种信息要简单。出于上面两个原因,应用程序和协议栈之间是使用描述符来指代套接字的。

服务器接收

服务器在收到电信号或者是光信号过后,将他们转为数字信号,1010那种。

1624285090648.jpg

接下来需要根据包末尾的帧校验序列(FCS)来校验错误,即根据校验公式计算刚刚接收到的数字信息,然后与包末尾的FCS值进行比较。

1624285090646.jpg

如果两者不一致,则可能是因为噪声等影响导致信号失真,数据产生了错误,这时接收的包是无效的,因此需要丢弃。包丢弃后tcp会检测并重传。

当FCS一致,即确认数据没有错误时,接下来需要检查MAC头部中的接收方MAC地址,看看这个包是不是发给自己的。如果包的接收者不是自己,那么就需要丢弃这个包。

上面这些操作都是由网卡的MAC模块来完成的。

网卡的MAC模块将网络包从信号还原为数字信息,校验FCS并存入缓冲区。

接下来的接收操作需要CPU来参与,因此网卡需要通过中断将网络包到达的事件通知给CPU。

接下来,CPU就会暂停当前的工作,并切换到网卡的任务。然后,网卡驱动会开始运行,从网卡缓冲区中将接收到的包读取出来,根据MAC头部的以太类型字段判断协议的种类,并调用负责处理该协议的软件。这里,以太类型的值应该是表示IP协议,因此会调用TCP/IP协议栈,并将包转交给它。

当网络包转交到协议栈时,IP模块会首先开始工作,检查IP头部。IP模块首先会检查IP头部的格式是否符合规范,然后检查接收方IP地址,看包是不是发给自己的。当服务器启用类似路由器的包转发功能时,对于不是发给自己的包,会像路由器一样根据路由表对包进行转发。

确认包是发给自己的之后,接下来需要检查包有没有被分片。检查IP头部的内容就可以知道是否分片,如果是分片的包,则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包;如果没有分片,则直接保留接收时的样子,不需要进行重组。

接下来需要检查IP头部的协议号字段,并将包转交给相应的模块。例如,如果协议号为06(十六进制),则将包转交给TCP模块;如果是11(十六进制),则转交给UDP模块。

当TCP头部中的控制位SYN为1时,表示这是一个发起连接的包,TCP模块会执行接受连接的操作,不过在此之前,需要先检查包的接收方端口号,并确认在该端口上有没有与接收方端口号相同且正在处于等待连接状态的套接字。如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包。

如果存在等待连接的套接字,则为这个套接字复制一个新的副本,并将发送方IP地址、端口号、序号初始值、窗口大小等必要的参数写入这个套接字中,同时分配用于发送缓冲区和接收缓冲区的内存空间。

然后生成代表接收确认的ACK号,用于从服务器向客户端发送数据的序号初始值,表示接收缓冲区剩余容量的窗口大小,并用这些信息生成TCP头部,委托IP模块发送给客户端。

这个包到达客户端之后,客户端会返回表示接收确认的ACK号,当这个ACK号返回服务器后,连接操作就完成了。

如果接收的包为数据包,TCP模块会检查收到的包对应哪一个套接字。在服务器端,可能有多个已连接的套接字对应同一个端口号。

这时我们需要根据IP头部中的发送方IP地址和接收方IP地址,以及TCP头部中的接收方端口号和发送方端口号共4种信息,找到上述4种信息全部匹配的套接字。

找到4种信息全部匹配的套接字之后,TCP模块会对比该套接字中保存的数据收发状态和收到的包的TCP头部中的信息是否匹配,以确定数据收发操作是否正常。

就是根据套接字中保存的上一个序号和数据长度计算下一个序号,并检查与收到的包的TCP头部中的序号是否一致。如果两者一致,就说明包正常到达了服务器,没有丢失。这时,TCP模块会从包中提出数据,并存放到接收缓冲区中,与上次收到的数据块连接起来。这样一来,数据就被还原成分包之前的状态了。

当收到的数据进入接收缓冲区后,TCP模块就会生成确认应答的TCP头部,并根据接收包的序号和数据长度计算出ACK号,然后委托IP模块发送给客户端。

收到的数据块进入接收缓冲区,意味着数据包接收的操作告一段落了。接下来,应用程序会调用Socket库的read来获取收到的数据,这时数据会被转交给应用程序。如果应用程序不来获取数据,则数据会被一直保存在缓冲区中。

应用程序会在数据到达之前调用read等待数据到达,在这种情况下,TCP模块在完成接收操作的同时,就会执行将数据转交给应用程序的操作。

然后,控制流程会转移到服务器程序,对收到的数据进行处理,也就是检查HTTP请求消息的内容,并根据请求的内容向浏览器返回相应的数据。

在http1.0的时候,是服务器先执行断开操作的。服务器程序会调用Socket库的close,TCP模块会生成一个控制位FIN为1的TCP头部,并委托IP模块发送给客户端。

当客户端收到这个包之后,会返回一个ACK号。接下来客户端调用close,生成一个FIN为1的TCP头部发给服务器,服务器再返回ACK号,这时断开操作就完成了。

HTTP1.1中,是客户端先发起断开操作,这种情况下只要将客户端和服务器的操作颠倒一下就可以了。

当断开操作完成后,套接字会在经过一段时间后被删除。

1624285090640.jpg

服务器程序会根据收到的请求消息中的内容进行相应的处理,并生成响应消息,再通过write返回给客户端。

当服务器完成对请求消息的各种处理之后,就可以返回响应消息了。

Web服务器调用Socket库的write,将响应消息交给协议栈。这时,需要告诉协议栈这个响应消息应该发给谁,但我们并不需要直接告知客户端的IP地址等信息,而是只需要给出表示通信使用的套接字的描述符就可以了。套接字中保存了所有的通信状态,其中也包括通信对象的信息,因此只要有描述符就万事大吉了。

应用处理数据包

接下来,我们来看一看浏览器是如何显示内容的。

1624285090638.jpg

要显示内容,首先需要判断响应消息中的数据属于哪种类型。Web可以处理的数据包括文字、图像、声音、视频等多种类型,每种数据的显示方法都不同,因此必须先要知道返回了什么类型的数据,否则无法正确显示。

原则上可以根据响应消息开头的Content-Type头部字段的值来进行判断。这个值一般是下面这样的字符串。Content-Type: text/html,其中“/”左边的部分称为“主类型”,表示数据的大分类;右边的“子类型”表示具体的数据类型。

1624285090635.jpg

当数据类型为文本时,还需要判断编码方式,这时需要用charset附加表示文本编码方式的信息,内容如下。Content-Type: text/html; charset=utf-8,这里的utf-8表示编码方式为Unicode。

还需要检查Content-Encoding头部字段。如果消息中存放的内容是通过压缩或编码技术对原始数据进行转换得到的,那么Content-Encoding的值就表示具体的转换方式,通过这个字段的值,我们可以知道如何将消息中经过转换的数据还原成原始数据。

然后就可以将数据展示在浏览器上了。