6 请求到达Web服务器, 响应返回浏览器



热身问答

下列说法是正确的(√)还是错误的(×)?

  1. 服务器向客户端返回的响应消息不一定和客户端向服务器发送的请求消息通过相同的路由传输。
  2. 客户端计算机也可以当作服务器来使用。
  3. 一台服务器可以同时用作Web服务器和邮件服务器。
参考
  1. √。路由器和交换机是不考虑请求包和响应包之间的关联的,而是将它们作为独立的包来对待,因此请求和响应是有可能通过不同的路由来传输的,具体走哪条路由,是由路由器的路由表交换机的地址表中的配置来决定的。
  2. √。无论任何计算机,协议栈的功能和工作方式都是相同的,因此客户端计算机也可以当作服务器来用。不过,客户端计算机和服务器相比其性能和可靠性都比较差,这一点必须要注意。
  3. √。由于可以通过端口号来区分服务器上的应用程序,所以一台服务器上可以同时运行多个服务器程序,不仅限于Web和邮件。当然,这样做会增加服务器的负载,因此必须注意服务器的性能。

1 服务器概览

服务器需要同时和多个客户端通信,但一个程序来处理多个客户端的请求是很难的,因为服务器必须把握每一个客户端的操作状态。因此一般的做法是,每有一个客户端连接进来,就启动一个新的服务器程序,确保服务器程序和客户端是一对一的状态。

具体来说,服务器程序的结构如图6.1所示。首先,我们将程序分成两个模块,即等待连接模块(图6.1(a)) 和负责与客户端通信的模块(图6.1(b))。当服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块(a)。 这个模块会创建套接字,然后进入等待连接的暂停状态。接下来,当客户端连发起连接时,这个模块会恢复运行并接受连接,然后启动客户端通信模块(b), 并移交完成连接的套接字。接下来,客户端通信模块(b)就会使用已连接的套接字与客户端进行通信,通信结束后,这个模块就退出了。

每次有新的客户端发起连接,都会启动一个新的客户端通信模块(b),因此(b)与客户端是一对一的关系。这样,( b)在工作时就不必考虑其他客户端的连接情况,只要关心自己对应的客户端就可以了。通过这样的方式,可以降低程序编写的难度。服务器操作系统具有多任务、多线程功能,可以同时运行多个程序,服务器程序的设计正是利用了这一功能。

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

多任务:操作系统提供的一种功能,可以让多个任务(程序)同时运行。实际上,一个处理器在某一个瞬间只能运行一个任务,但通过短时间内在不同的任务间切换,看起来就好像是同时运行多个任务一样。有些操作系统称之为“多进程”。

多任务和多线程的区别在于任务和线程的区别。在操作系统内部,任务是作为单独的程序来对待的,而线程则是一个程序中的一部分。

服务器程序是如何调用Socket库的?
这个区别体现在如何调用Socket库上。首先,客户端的数据收发需要
经过下面4个阶段。
(1)创建套接字(创建套接字阶段)
(2)用管道连接服务器端的套接字(连接阶段)
(3)收发数据(收发阶段)
(4)断开管道并删除套接字(断开阶段)
相对地,服务器是将阶段(2)改成了等待连接,具体如下。
(1)创建套接字(创建套接字阶段)
(2-1)将套接字设置为等待连接状态(等待连接阶段)
(2-2)接受连接(接受连接阶段)
(3)收发数据(收发阶段)
(4)断开管道并删除套接字(断开阶段)

创建套接字操作的本质是分配用于套接字的内存空间,这一点上客户端和服务器是一样的。

要确定某个套接字时,不仅使用服务器端套接字对应的端口号,还同时使用客户端的端口号再加上IP地址,总共使用下面4种信息来进行判断(图6.4)。
• 客户端IP地址
• 客户端端口号
• 服务器IP地址
• 服务器端口号

既然通过客户端IP地址、客户端端口号、服务器IP地址、服务器端口号这4种信息可以确定某个套接字,那么要指代某个套接字时用这4种信息就好了,为什么还要使用描述符呢?

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

使用描述符来指代套接字的原因如下。
(1)等待连接的套接字中没有客户端IP地址和端口号
(2)使用描述符这一种信息比较简单


2 服务器的接收操作


2.1 网卡将接收到的信号转换成数字信息
接收操作的第一步是网卡接收到信号,然后将其还原成数字信息。局域网中传输的网络包信号是由1和0组成的数字信息与用来同步的时钟信号叠加而成的,因此只要从中分离出时钟信号,然后根据时钟信号进行同步,就可以读取并还原出1和0的数字信息了。

接下来需要根据包末尾的帧校验序列(FCS)来校验错误

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

以太网的基本工作方式是将数据广播到整个网络上,只有指定的接收者才接收数据,因此网络中还有很多发给其他设备的数据在传输,如果包的接收者不是自己,那么就需要丢弃这个包

还原后的数字信息被保存在网卡内部的缓冲区

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

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

在这个过程中,服务器的CPU并不是一直在监控网络包的到达,而是在执行其他的任务,因此CPU并不知道此时网络包已经到达了。但接下来的接收操作需要CPU来参与,因此网卡需要通过中断网络包到达的事件通知给CPU

网卡驱动会根据MAC头部判断协议类型,并将包交给相应的协议栈



2.2 IP模块的接收操作

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

确认包是发给自己的之后,接下来需要检查包有没有被分片。检查IP头部的内容就可以知道是否分片

  • 如果是分片的包,则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包
  • 如果没有分片,则直接保留接收时的样子,不需要进行重组

接下来需要检查IP头部的协议号字段,并将包转交给相应的模块。例如:

  • 如果协议号为06(十六进制), 则将包转交给TCP模块
  • 如果是11(十六进制), 则转交给UDP模块

协议栈的IP模块会检查IP头部

  1. 判断是不是发给自己的;
  2. 判断网络包是否经过分片;
  3. 将包转交给TCP模块或UDP模块。


2.3 TCP 模块如何处理连接包

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

如果存在等待连接的套接字,则为这个套接字复制一个新的副本,并将发送方IP地址、端口号、序号初始值、窗口大小等必要的参数写入这个套接字中,同时分配用于发送缓冲区接收缓冲区内存空间。然后生成代表接收确认的ACK号,用于从服务器向客户端发送数据的序号初始值,表示接收缓冲区剩余容量的窗口大小,并用这些信息生成TCP头部,委托IP模块发送给客户端。

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

这时,服务器端的程序应该进入调用accept的暂停状态,当将新套接字的描述符转交给服务器程序之后,服务器程序就会恢复运行。

如果收到的是发起连接的包,则TCP模块会

  1. 确认TCP头部的控制位SYN;
  2. 检查接收方端口号;
  3. 为相应的等待连接套接字复制一个新的副本;
  4. 记录发送方IP地址和端口号等信息。


2.4 TCP 模块如何处理数据包

进入数据收发阶段之后,当数据包A到达时TCP模块是如何处理的(图6.7)???

首先,TCP模块会检查收到的包对应哪一个套接字。在服务器端,可能有多个已连接的套接字对应同一个端口号,因此仅根据接收方端口号无法找到特定的套接字。这时我们需要根据IP头部中的发送方IP地址接收方IP地址,以及TCP头部中的接收方端口号发送方端口号共4种信息,找到上述4种信息全部匹配的套接字。

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

  • 具体来说,就是根据套接字中保存的上一个序号数据长度计算下一个序号,并检查与收到的包的TCP头部中的序号是否一致。
  • 如果两者一致,就说明包正常到达了服务器,没有丢失。
  • 这时,TCP模块会从包中提出数据,并存放到接收缓冲区中,与上次收到的数据块连接起来。这样一来,数据就被还原成分包之前的状态了。
  • tip: 拼合数据块的操作在每次收到数据包时都会进行,而不是等所有数据全部接受完毕之后再统一拼合的。

当收到的数据进入接收缓冲区后,TCP模块就会生成确认应答的TCP头部,并根据接收包的序号数据长度计算出ACK号,然后委托IP模块发送给客户端A。(在返回ACK号之前,会先等待一段时间,看看能不能和后续的应答包合并。)



2.5 TCP 模块的断开操作

在TCP协议的规则中,断开操作可以由客户端或服务器任何一方发起,具体的顺序是由应用层协议决定的。Web中,这一顺序随HTTP协议版本不同而不同,在HTTP1.0中,是服务器先发起断开操作。

这时,服务器程序会调用Socket库的close,TCP模块会生成一个控制位FIN为1的TCP头部,并委托IP模块发送给客户端。当客户端收到这个包之后,会返回一个ACK号。接下来客户端调用close,生成一个FIN 为1的TCP头部发给服务器,服务器再返回ACK号,这时断开操作就完成了。

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

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



3 Web服务器程序解释请求消息并作出响应


3.1 将请求的URI转换为实际的文件名

各种服务器程序的不同点在于图中(b)客户端通信部分的第一行调用read后面的如下部分。
[ 处理请求消息内容 ];
图6.7 中只写了一行,但实际上这里应该是一组处理各种工作的程序A,或者说这里才是服务器程序的核心部分。接下来让我们来对这一部分进行探索。

Web 服务器中,图6.7的read获取的数据内容就是HTTP请求消息。服务器程序会根据收到的请求消息中的内容进行相应的处理,并生成响应消息,再通过write 返回给客户端。请求消息包括一个称为“方法”的命令,以及表示数据源URI(文件路径名),服务器程序会根据这些内容向客户端返回数据,但对于不同的方法和 URI,服务器内部的工作过程会有所不同。

最简单的一种情况如图 6.8 中的例子所示,请求方法为 GET,URI 为一个 HTML 文件名。这种情况只要从文件中读出 HTML 文档,然后将其作为响应消息返回就可以了。

不过,按照URI从磁盘上读取文件并没有这么简单。如果完全按照URI中的路径和文件名读取,那就意味着磁盘上所有的文件都可以访问,Web服务器的磁盘内容就全部暴露了,这很危险。因此,这里需要一些特殊的机制。

Web 服务器公开的目录其实并不是磁盘上的实际目录,而是如图6.9这样的虚拟目录,而URI中写的就是在这个虚拟目录结构下的路径名

因此,当读取文件时,需要先查询虚拟目录实际目录的对应关系,并将URI转换成实际的文件名后,才能读取文件并返回数据


3.2 运行CGI程序

浏览器需要在发送给Web服务器的请求消息中加入一些数据。我们在第1章曾经介绍过有两种加入数据的方法。

  • 一种是在HTML文档的表单中加上method="GET",通过HTTP的GET方法,将输入的数据作为参数添加在URI后面发送给服务器。
  • 另一种方法是在HTML文档的表单中加上method="POST",将数据放在HTTP请求消息的消息体A中发送给服务器

如果判断要访问的文件为程序文件,Web服务器会委托操作系统运行这个程序,然后从请求消息中取出数据并交给运行的程序B。如果方法为GET,则将URI后面的参数传递给程序;如果方法为POST,则将消息体中的数据传递给程序(图6.11)。



3.3 Web 服务器的访问控制

Web 服务器的基本工作方式就是根据请求消息的内容判断数据源,并从中获取数据返回给客户端

不过在执行这些操作之前,Web 服务器还可以检查事先设置的一些规则,并根据规则允许或禁止访问。这种根据规则判断是否允许访问的功能称为访问控制,一些会员制的信息服务需要限制用户权限的时候会使用这一功能,公司里也可以利用访问控制只允许某些特定部门访问。

Web 服务器的访问控制规则主要有以下3种。

  1. 客户端IP地址
  2. 客户端域名
  3. 用户名和密码

以上规则可针对作为数据源的文件和目录进行设置,当收到客户端的请求消息时,服务器会根据URI判断数据源,并检查数据源对应的访问控制规则,只有允许访问时才读取文件或运行程序。

当根据客户端域名设置规则时,需要先根据客户端IP地址查询客户端域名,这需要使用DNS服务器。

如果用户名和密码已设置好,那么情况如图6.13。通常的请求消息中不包含用户名和密码,因此无法验证用户名和密码(图6.13)。 因此,Web 服务器会向用户发送一条响应消息,告诉用户需要在请求消息中放入用户名和密码(图6.13)。 浏览器收到这条响应消息后,会弹出一个输入用户名和密码的窗口,用户输入用户名和密码后(图6.13),浏览器将这些信息放入请求消息中重新发送给服务器(图6.13)。 然后,Web服务器查看接收到的用户名和密码与事先设置好的用户名和密码是否一致,以此判断是否允许访问,如果允许访问,则返回数据(图6.13)。



3.4 返回响应消息

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

接下来,协议栈会将数据拆分成多个网络包,然后加上头部发送出去。这些包中包含接收方客户端的地址,它们将经过交换机路由器的转发,通过互联网最终到达客户端。



4 浏览器接收响应消息并显示内容

Web 服务器发送的响应消息会被分成多个包发送给客户端,然后客户端需要接收数据。

  • 首先,网卡将信号还原成数字信息,协议栈将拆分的网络包组装起来并取出响应消息
  • 然后将消息转交给浏览器。
    这个过程和服务器的接收操作相同

Web 可以处理的数据包括文字、图像、声音、视频等多种类型,每种数据的显示方法都不同,原则上可以根据响应消息开头的Content-Type头部字段的值来进行判断。这个值一般是下面这样的字符串。

Content-Type: text/html

其中“/”左边的部分称为“主类型”,表示数据的大分类;右边的“子类型”表示具体的数据类型。在上面的例子中,主类型是text,子类型是html。主类型和子类型的含义都是事先确定好的A,表6.1列出了其中主要的一些类型。上面例子中的数据类型表示遵循HTML规格的HTML文档。

此外,当数据类型为文本时,还需要判断编码方式,这时需要用charset附加表示文本编码方式的信息,内容如下。

Content-Type: text/html; charset=utf-8

这里的utf-8表示编码方式为Unicode

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

Content-Type 字段使用的表示数据类型的方法是在MIME规格中定义的,这个规格不仅用于Web,也是邮件等领域中普遍使用的一种方式。

MIME:Multipurpose Internet Mail Extensions,多用途因特网邮件扩充。原本是为在电子邮件中附加图片和附件非文本信息而制定的一种规格,后来在Web 的领域也得到了广泛使用


接下来只要根据数据类型调用用于显示内容的程序,将数据显示出来就可以了。对于HTML文档、纯文本、图片这些基本数据类型,浏览器自身具有显示这些内容的功能,因此由浏览器自身负责显示。

网页中还可以嵌入图片等数据,HTML文档和图片等数据是分别存在在不同的文件中的,HTML文档中只有表示图片引用的标签。在读取文档数据时,一旦遇到相应的标签,浏览器就会向服务器请求其中的图片文件。这个请求过程和请求HTML文档的过程是一样的,就是在HTTP请求消息的URI中写上图片文件的文件名即可。将这个请求消息发送给Web服务器之后,Web 服务器就会返回图片数据了。接下来,浏览器会将图片嵌入到标签所在的位置。JPEG和GIF格式的图片是经过压缩的,浏览器需要将其解压后委托操作系统进行显示。当然,为了避免图片和文字重叠,在显示文字的时候需要为图片留出相应的位置。

CGI(Common Gateway Interface,通用网关接口)



小测验

  1. 在包收发操作中,服务器和客户端的区别是什么?
  2. 当包到达服务器时,网卡会接收信号并通知CPU,此时使用的机制叫什么?
  3. Web服务器可以同时处理多个客户端的访问,这里利用了操作系统的什么功能?
  4. 当需要对Web服务器的访问进行限制的时候,可以根据哪些条件来判断是否允许访问?
  5. Web服务器返回的数据包括文档、图片等多种类型,客户端如何判断返回数据的不同类型?
参考
  1. 没有区别(参见【6.1.1】)
  2. 中断(参见【6.1.2】)
  3. 多任务和多线程(参见【6.2.3】)
  4. (a) 客户端IP地址;(b) 客户端域名;(c) 用户名和密码(参见【6.3.3】)
  5. 原则上根据响应消息的Content-Type头部字段的值来判断(参见【6.4.1】

posted @ 2024-05-27 00:22  guanyubo  阅读(18)  评论(0编辑  收藏  举报