网络笔记--服务器响应浏览器

        这一节了解服务器如何响应浏览器发来的请求;服务器的职责是响应客户端的请求, 但仅从如何响应请求这一点是无法理解服务器整体的。

客户端与服务器的区别
        根据用途, 服务器可以分为很多种类,其硬件和操作系统与客户端是有所不同的。但是,网络相关的部分,如网卡、协议栈、Socket库等功能和客户端却是相同的。无论硬件和 OS 如何变化,TCP和IP的功能都是一样的,或者说这些功能规格都是统一的 。注:客户端计算机也可以用作服务器。服务器程序结构如图

      首先,我们将程序分成两个模块,即等待连接模块(a)和负责与客户端通信的模块(b)。当服务器程序启动并读取配置文件完成初始化操作后,就会运行等待连接模块(a)。这个模块会创建套接字,然后进入等待连接的暂停状态。接下来,当客户端连发起连接时,这个模块会恢复运行并接受连接,然后启动客户端通信模块(b),并移交完成连接的套接字。接下来,客户端通信模块(b)就会使用已连接的套接字与客户端进行通信,通信结束后,这个模块就退出了。每次有新的客户端发起连接,都会启动一个新的客户端通信模块(b),因此(b)与客户端是一对一的关系。这样,(b)在工作时就不必考虑其他客户端的连接情况, 只要关心自己对应的客户端就可以了。通过这样的方式,可以降低程序编写的难度。服务器操作系统具有多任务 、多线程 功能,可以同时运行多个程序,服务器程序的设计正是利用了这一功能。这种方法在每次客户端发起连接时都需要启动新的程序, 这个过程比较耗时, 响应时间也会相应增加。 因此, 还有一种方法是事先启动几个客户端通信模块, 当客户端发起连接时, 从空闲的模块中挑选一个出来将套接字移交给它来处理。

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

  服务器端的套接字和端口号

       了解一下服务器是如何使用套接字来完成通信的。这需要深入挖掘调用 Socket库的具体过程;
        首先,回顾一下客户端与服务器的区别。从数据收发的角度来看,区分“客户端”和“服务器”这两个固定的角色似乎不是一个好办法。现在大多数应用都是由客户端去访问服务器,但其实应用的形态不止这一种。为了能够支持各种形态的应用,最好是在数据收发层面不需要区分客户端和服务器,而是能够以左右对称的方式自由发送数据。TCP也正是在这样的背景下设计出来的。不过,这其中还是存在一个无法做到左右对称的部分,那就是连接操作。 连接这个操作是在有一方等待连接的情况下, 另一方才能发起连接, 如果双方同时发起连接是不行的, 因为在对方没有等待连接的状态下, 无法单方面进行连接。 因此, 只有这个部分必须区分发起连接和等待连接这两个不同的角色。 从数据收发的角度来看, 这就是客户端与服务器的区别, 也就是说, 发起连接的一方是客户端, 等待连接的一方是服务器.这个区别体现在如何调用 Socket 库上。 首先, 客户端的数据收发需要经过下面 4 个阶段: 1) 创建套接字(创建套接字阶段)2) 用管道连接服务器端的套接字(连接阶段)3) 收发数据(收发阶段)4) 断开管道并删除套接字(断开阶段);相对地, 服务器是将阶段(2) 改成了等待连接, 具体如下:(1) 创建套接字(创建套接字阶段)(2-1) 将套接字设置为等待连接状态(等待连接阶段)(2-2) 接受连接(接受连接阶段)(3) 收发数据(收发阶段)(4) 断开管道并删除套接字(断开阶段)用伪代码来表示这个过程, 如图:

         首先, 协议栈调用 socket 创建套接字 , 这一步和客户端是相同的 。接下来调用 bind 将端口号写入套接字中。 在客户端发起连接的操作中, 需要指定服务器端的端口号, 这个端口号也就是在这一步设置的。 具体的编号是根据服务器程序的种类, 按照规则来确定的,例如 Web 服务器使用 80 号端口 。设置好端口号之后, 协议栈会调用 listen 向套接字写入等待连接状态这一控制信息。 这样一来, 套接字就会开始等待来自客户端的连接网络包。然后, 协议栈会调用 accept 来接受连接。由于等待连接的模块在服务器程序启动时就已经在运行了, 所以在刚启动时, 应该还没有客户端的连接包到达。可是,包都没来就调用accept接受连接,可能大家会感到有点奇怪,不过没关系,因为如果包没有到达,就会转为等待包到达的状态,并在包到达的时候继续执行接受连接操作。因此,在执行accept的时候,一般来说服务器端都是处于等待包到达的状态,这时应用程序会暂停运行。在这个状态下,一旦客户端的包到达,就会返回响应包并开始接受连接操作。接下来,协议栈会给等待连接的套接字复制一个副本,然后将连接对象等控制信息写入新的套接字中,这里, 我们就创建了一个新的套接字, 并和客户端套接字连接在一起了。当 accept 结束之后, 等待连接的过程也就结束了, 这时等待连接模块会启动客户端通信模块, 然后将连接好的新套接字转交给客户端通信模块, 由这个模块来负责执行与客户端之间的通信操作。 之后的数据收发操作和刚才说的一样, 与客户端的工作过程是相同的。

       其实在这一系列操作中, 还有一部分没有说到, 那就是在复制出一个新的套接字之后, 原来那个处于等待连接状态的套接字会怎么样呢? 其实它还会以等待连接的状态继续存在, 当再次调用 accept, 客户端连接包到达时, 它又可以再次执行接受连接操作。 接受新的连接之后, 和刚才一样, 协议栈会为这个等待连接的套接字复制一个新的副本, 然后让客户端连接到这个新的副本套接字上。 像这样每次为新的连接创建新的套接字就是这一步操作的一个关键点。 如果不创建新副本, 而是直接让客户端连接到等待连接的套接字上, 那么就没有套接字在等待连接了, 这时如果有其他客户端发起连接就会遇到问题。 为了避免出现这样的情况, 协议栈采用了这种创建套接字的新副本, 并让客户端连接到这个新副本上的方法。此外, 创建新套接字时端口号也是一个关键点。 端口号是用来识别套接字的, 因此我们以前说不同的套接字应该对应不同的端口号, 但如果这样做, 这里就会出现问题。 因为在接受连接的时候, 新创建的套接字副本就必须和原来的等待连接的套接字具有不同的端口号才行。 这样一来, 比如客户端本来想要连接 80 端口上的套接字, 结果从另一个端口号返回了包, 这样一来客户端就无法判断这个包到底是要连接的那个对象返回的, 还是其他程序返回的。 因此, 新创建的套接字副本必须和原来的等待连接的套接字具有相同的端口号。但是这样一来又会引发另一个问题。 端口号是用来识别套接字的, 如果一个端口号对应多个套接字, 就无法通过端口号来定位到某一个套接字了。 当客户端的包到达时, 如果协议栈只看 TCP 头部中的接收方端口号, 是无法判断这个包到底应该交给哪个套接字的。如图 

         这个问题可以用下面的方法来解决, 即要确定某个套接字时, 不仅使用服务器端套接字对应的端口号, 还同时使用客户端的端口号再加上 IP地址, 总共使用下面 4 种信息来进行判断,服务器上可能存在多个端口号相同的套接字, 但客户端的套接字都是对应不同端口号的, 因此我们可以通过客户端的端口号来确定服务器上的某个套接字。不过,使用不同端口号的规则仅限一台客户端的内部,当有多个客户端进行连接时,它们之间的端口号是可以重复的。因此,我们还必须加上客户端的 IP 地址才能进行判断。例如,IP 地址为198.18.203.154的客户端的1025端口,就和IP地址为198.18.142.86的客户端的1025端口对应不同的套接字。除此之外,既然通过客户端 IP 地址、 客户端端口号、 服务器 IP 地址、 服务器端口号这 4 种信息可以确定某个套接字, 那么要指代某个套接字时用这 4 种信息就好了, 为什么还要使用描述符呢?原因是, 在套接字刚刚创建好, 还没有建立连接的状态下, 这 4 种信息是不全的。 此外, 为了指代一个套接字, 使用一种信息(描述符) 比使用 4 种信息要简单。出于上面两个原因, 应用程序和协议栈之间是使用描述符来指代套接字的。使用描述符来指代套接字的原因如下。(1) 等待连接的套接字中没有客户端 IP 地址和端口号(2) 使用描述符这一种信息比较简单。

 服务器的接收操作

         客户端发送到达服务器的网络包其本质是电信号或者光信号,接收信号的过程和客户端是一样的。接收操作的第一步是网卡接收到信号, 然后将其还原成数字信息 。局域网中传输的网络包信号是由 1 和 0 组成的数字信息与用来同步的时钟信号叠加而成的, 因此只要从中分离出时钟信号, 然后根据时钟信号进行同步, 就可以读取并还原出 1 和 0 的数字信息了。首先从报头部分提取出时钟信号 , 报头的信号是按一定频率变化的, 只要测定这个变化的频率就可以和时钟信号同步了。 接下来, 按照相同的周期延长时钟信号 ,并在每个时钟周期位置检测信号的变化方向,(服务器接收电信号的过程和客户端发送的过程相反, 是从模拟信息转换为数字信息。)接下来需要根据包末尾的帧校验序列(FCS) 来校验错误, 即根据校验公式 计算刚刚接收到的数字信息, 然后与包末尾的 FCS 值进行比较。 FCS 值是在发送时根据转换成电信号之前的数字信息进行计算得到的, 因此如果根据信号还原出的数字信息与发送前的信息一致, 则计算出的 FCS 也应该与包末尾的 FCS 一致。 如果两者不一致, 则可能是因为噪声等影响导致信号失真, 数据产生了错误, 这时接收的包是无效的, 因此需要丢弃 。当 FCS 一致, 即确认数据没有错误时, 接下来需要检查 MAC 头部中的接收方 MAC 地址, 看看这个包是不是发给自己的。 以太网的基本工作方式是将数据广播到整个网络上, 只有指定的接收者才接收数据, 因此网络中还有很多发给其他设备的数据在传输, 如果包的接收者不是自己, 那么就需要丢弃这个包。

         网卡的MAC模块将网络包从信号还原为数字信息,校验FCS并存入缓冲区。在这个过程中,服务器的CPU并不是一直在监控网络包的到达,而是在执行其他的任务,因此CPU并不知道此时网络包已经到达了。但接下来的接收操作需要CPU来参与,因此网卡需要通过中断将网络包到达的事件通知给CPU。接下来,CPU就会暂停当前的工作,并切换到网卡的任务。然后,网卡驱动会开始运行,从网卡缓冲区中将接收到的包读取出来,根据MAC头部的以太类型字段判断协议的种类,并调用负责处理该协议的软件。这里,以太类型的值应该是表示IP协议,因此会调用TCP/IP协议栈,并将包转交给它。网卡驱动会根据MAC头部判断协议类型,并将包交给相应的协
议栈。

IP 模块的接收操作

       当网络包转交到协议栈时,IP 模块会首先开始工作,检查IP头部。IP模块首先会检查IP头部的格式是否符合规范,然后检查接收方IP地址,看包是不是发给自己的。当服务器启用类似路由器的包转发功能时,对于不是发给自己的包,会像路由器一样根据路由表对包进行转发。确认包是发给自己的之后, 接下来需要检查包有没有被分片 。 检查IP 头部的内容就可以知道是否分片 , 如果是分片的包, 则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包; 如果没有分片,则直接保留接收时的样子,不需要进行重组。到这里 我们就完成了包的接收。 接下来需要检查 IP 头部的协议号字段,并将包转交给相应的模块。例如,如果协议号为 06(十六进制),则将包转交给TCP模块;如果是11(十六进制),则转交给UDP模块。这里我们假设这个包被交给TCP 模块处理,简单总结: 协议栈的 IP 模块会检查 IP 头部,1)判断是不是发给自己的;2)判断网络包是否经过分片;3) 将包转交给TCP 模块或UDP模块。

TCP 模块如何处理连接包

        当TCP头部中的控制位SYN为1时,表示这是一个发起连接的包,这时,TCP模块会执行接受连接的操作,在此之前,需要先检查包的接收方端口号,并确认在该端口上有没有与接收方端口号相同且正在处于等待连接状态的套接字。如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包 。向客户端返回一个表示接收方端口不存在等待连接的套接字的ICMP消息。如果存在等待连接的套接字,则为这个套接字复制一个新的副本,并将发送方IP地址、端口号、序号初始值、窗口大小等必要的参数写入这个套接字中,同时分配用于发送缓冲区和接收缓冲区的内存空间。然后生成代表接收确认的ACK号,用于从服务器向客户端发送数据的序号初始值,表示接收缓冲区剩余容量的窗口大小,并用这些信息生成 TCP头部, 委托 IP 模块发送给客户端。这个包到达客户端之后, 客户端会返回表示接收确认的 ACK 号, 当这个 ACK 号返回服务器后, 连接操作就完成了。这时,服务器端的程序应该进入调用 accept 的暂停状态,当将新套接字的描述符转交给服务器程序之后, 服务器程序就会恢复运行。如果收到的是发起连接的包, 则 TCP 模块会1) 确认 TCP 头部的控制位SYN;2)检查接收方端口号;3) 为相应的等待连接套接字复制一个新的副本;4)记录发送方 IP 地址和端口号等信息。

TCP 模块如何处理数据包

        首先, TCP 模块会检查收到的包对应哪一个套接字。 在服务器端, 可能有多个已连接的套接字对应同一个端口号, 因此仅根据接收方端口号无法找到特定的套接字。 这时我们需要根据 IP 头部中的发送方 IP 地址和接收方 IP 地址, 以及 TCP 头部中的接收方端口号和发送方端口号共 4种信息, 找到上述 4 种信息全部匹配的套接字 。找到 4 种信息全部匹配的套接字之后, TCP 模块会对比该套接字中保存的数据收发状态和收到的包的 TCP 头部中的信息是否匹配, 以确定数据收发操作是否正常。 具体来说, 就是根据套接字中保存的上一个序号和数据长度计算下一个序号, 并检查与收到的包的 TCP 头部中的序号是否一致 。 如果两者一致, 就说明包正常到达了服务器, 没有丢失。 这时,TCP 模块会从包中提出数据, 并存放到接收缓冲区中, 与上次收到的数据块连接起来。 这样一来, 数据就被还原成分包之前的状态了 。当收到的数据进入接收缓冲区后,TCP 模块就会生成确认应答的 TCP头部,并根据接收包的序号和数据长度计算出ACK号,然后委托IP模块发送给客户端 。收到的数据块进入接收缓冲区,接下来, 应用程序会调用 Socket 库的 read来获取收到的数据,这时数据会被转交给应用程序。 如果应用程序不来获取数据,则数据会被一直保存在缓冲区中,但一般来说,应用程序会在数据到达之前调用 read 等待数据到达,在这种情况下,TCP 模块在完成接收操作的同时,就会执行将数据转交给应用程序的操作。然后,控制流程会转移到服务器程序,对收到的数据进行处理,也就是检查HTTP请求消息的内容,并根据请求的内容向浏览器返回相应的数据。

TCP 模块的断开操作

        当数据收发完成后, 便开始执行断开操作。 在 TCP 协议的规则中, 断开操作可以由客户端或服务器任何一方发起, 具体的顺序是由应用层协议决定的.Web 中,这一顺序随 HTTP 协议版本不同而不同, 在 HTTP1.0 中, 是服务器先发起断开操作。这时,服务器程序会调用 Socket 库的 close, TCP 模块会生成一个控制位 FIN 为 1 的 TCP 头部,并委托 IP 模块发送给客户端。 当客户端收到这个包之后,会返回一个 ACK 号。接下来客户端调用 close,生成一个FIN 为 1 的 TCP 头部发给服务器, 服务器再返回 ACK 号, 这时断开操作就完成了. HTTP1.1 中,是客户端先发起断开操作, 这种情况下只要将客户端和服务器的操作颠倒一下就可以了。无论哪种情况, 当断开操作完成后, 套接字会在经过一段时间后被删除。

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

服务器程序的工作过程,如图:

           Web 服务器中,read 获取的数据内容就是 HTTP 请求消息。 服务器程序会根据收到的请求消息中的内容进行相应的处理, 并生成响应消息, 再通过 write 返回给客户端。 请求消息包括一个称为“方法”的命令, 以及表示数据源的 URI(文件路径名) , 服务器程序会根据这些内容向客户端返回数据, 但对于不同的方法和 URI, 服务器内部的工作过程会有所不同。 请求方法为 GET, URI 为一个 HTML 文件名。 这种情况只要从文件中读出 HTML 文档, 然后将其作为响应消息返回就可以了。 不过, 按照 URI 从磁盘上读取文件并没有这么简单。 如果完全按照 URI 中的路径和文件名读取 , 那就意味着磁盘上所有的文件都可以访问, Web 服务器的磁盘内容就全部暴露了, 这很危险。 因此, 这里需要一些特殊的机制。Web 服务器公开的目录其实并不是磁盘上的实际目录,而是虚拟目录 如图:

       URI 中写的就是在这个虚拟目录结构下的路径名。因此, 当读取文件时, 需要先查询虚拟目录与 实际目录的对应关系, 并将 URI 转换成实际的文件名后, 才能读取文件并返回数据。 但对于不同的方法和 URI, 服务器内部的工作过程会有所不同。(注意: 客户端看到的 Web 服务器目录是虚拟的, 和实际的目录结构不同。 Web 服务器内部会将实际的目录名和供外部访问的虚拟目录名进行关联。)

Web 服务器的访问控制

       Web 服务器的基本工作方式就是根据请求消息的内容判断数据源, 并从中获取数据返回给客户端, 不过在执行这些操作之前, Web 服务器还可以检查事先设置的一些规则, 并根据规则允许或禁止访问。 这种根据规则判断是否允许访问的功能称为访问控制, 一些会员制的信息服务需要限制用户权限的时候会使用这一功能, 公司里也可以利用访问控制只允许某些特定部门访问。Web 服务器的访问控制规则主要有以下 3 种:1)客户端 IP 地址;2)客户端域名;3)用户名和密码

     设定访问控制规则时, 服务器是如何工作的?

     首先是根据客户端 IP 地址设置的规则,先根据客户端 IP 地址查询客户端域名, 这需要使用 DNS 服务器。 一般我们使用 DNS 服务器都是根据域名查询 IP 地址, 其实根据 IP 地址反查域名也可以使用 DNS 服务器。具体来说, 这个过程是这样的。 收到客户端的请求消息后, Web 服务器会委托协议栈告知包的发送方 IP 地址, 然后用这个 IP 地址生成查询消息并发送给最近的 DNS 服务器。 接下来,DNS 服务器找出负责管辖该 IP 地址的 DNS 服务器, 并将查询转发给它 , 查询到相应的域名之后返回结果 , 然后Web 服务器端的 DNS 服务器再将结果转发给 Web 服务器。 这样一来, 我们就可以根据发送方 IP 地址查询到域名。 接下来, 为了保险起见, 还需要用这个域名查询一下 IP 地址, 看看结果与发送方 IP 地址是否一致。 这是因为有一种在 DNS 服务器上注册假域名的攻击方式, 因此我们需要进行双重检查, 如果两者一致则检查相应的访问控制规则, 判断是否允许访问。如图:

       可以看出, 这种方式需要和 DNS 服务器进行多次查询, 整个过程比较耗时,因此 Web 服务器的响应速度也会变慢。如果用户名和密码已设置好,如图:

         通常的请求消息中不包含用户名和密码, 因此无法验证用户名和密码。 因此, Web 服务器会向用户发送一条响应消息, 告诉用户需要在请求消息中放入用户名和密码 ②。 浏览器收到这条响应消息后, 会弹出一个输入用户名和密码的窗口, 用户输入用户名和密码后③, 浏览器将这些信息放入请求消息中重新发送给服务器④。 然后, Web 服务器查看接收到的用户名和密码与事先设置好的用户名和密码是否一致, 以此判断是否允许访问, 如果允许访问, 则返回数据⑤, 注:  当访问设置了用户名和密码保护的页面时, 需要在 HTTP 请求消息中添加包含用户名和密码的头部字段(Authorization) 。 否则, Web 服务器不会返回请求的页面内容, 而是会返回一个要求提供用户名和密码的头部字段(WWW-Authenticate) 消息。

返回响应消息

        当服务器完成对请求消息的各种处理之后, 就可以返回响应消息了。 这里的工作过程和客户端向服务器发送请求消息时的过程相同。首先, Web 服务器调用 Socket 库的 write, 将响应消息交给协议栈。 这时, 需要告诉协议栈这个响应消息应该发给谁, 但我们并不需要直接告知客户端的 IP 地址等信息, 而是只需要给出表示通信使用的套接字的描述符就可以了。 套接字中保存了所有的通信状态, 其中也包括通信对象的信息, 因此只要有描述符就万事大吉了。接下来, 协议栈会将数据拆分成多个网络包, 然后加上头部发送出去。这些包中包含接收方客户端的地址, 它们将经过交换机和路由器的转发, 通过互联网最终到达客户端。

猜你喜欢

转载自blog.csdn.net/ljt2724960661/article/details/121319443