Skip to content

Web 服务器

与客户端建立连接

我们将服务器程序分成 等待连接模块负责与客户端通信的模块

  1. 服务器程序启动,并读取配置文件完成初始化
  2. 运行等待连接模块 ==【创建套接字阶段】==
    • 创建套接字
  3. 等待连接模块进入等待连接的暂停状态 ==【等待连接阶段】==
  4. 当客户端发起连接时,等待连接模块恢复运行,并接受连接 ==【接受连接阶段】==
    • 协议栈给等待连接的套接字复制一个副本,然后将控制信息写入到新的套接字中
    • 让客户端连接到新的副本套接字上
  5. 等待连接模块启动客户端通信模块,并移交完成连接的套接字 ![[Excalidraw/计算机/四大件/计算机网络 Draw.md#^group=ebuoNrYm|500]]
  6. 客户端通信模块与客户端进行通信 ==【数据收发阶段】==
  7. 通信结束,模块退出 ==【断开阶段】==

[!hint] 如果不创建新副本,而是直接让客户端连接到等待连接的套接字上,那就没有套接字在等待连接了,这时如果有其他客户端发起连接就会出现问题。在复制出一个新的套接字之后,原来的那个处于等待连接状态的套接字还会以等待连接的状态继续存在,这样,当客户端连接包再次到达时,它又可以再次执行接受连接操作

[!hint] 一个端口号会有多个套接字 端口号是用来识别套接字的,一般来说,不同的套接字会有不同的端口号。但如果这样做,会出现问题【比如客户端本来想要连接 80端口 的套接字,结果从另一个端口号返回了包,客户端就无法判断这个包是否是要连接的那个对象返回的】

==因此,新创建的套接字副本和原来等待连接的套接字具有相同的端口号==

但我们还需要 四种信息 来区分同一端口号下的不同套接字:

  • 客户端 IP 地址
  • 客户端端口号
  • 服务器 IP 地址
  • 服务器端口号 ![[Excalidraw/计算机/四大件/计算机网络 Draw.md#^group=drzLD8sz|600]]

[!faq] 既然可以通过这四种信息确定某个套接字,那为什么应用程序和协议栈之间是使用描述符来指代套接字的 ?

  • 在套接字刚刚创建好还未建立连接的状态下,这四种信息是不全的
  • 使用一种信息【描述符】比使用四种信息更简单

[!hint] 我们可以事先启动几个客户端通信模块,这样当客户端发起连接需要启动新的程序时,可以马上从空闲的模块中挑选出一个来处理套接字,减少耗时

接收操作

网卡将接收到的信号转换为数字信息

  1. 到达服务器的网络包是电信号/光信号,网卡会接收到信号,然后将其还原为数字信息 ![[Excalidraw/计算机/四大件/计算机网络 Draw.md#^group=BEOQVbiF|600]]
  2. 接下来,需要根据包末尾的帧校验序列【FCS】来校验错误
    • 如果出错【可能因为噪声等影响导致信号失真,数据产生了错误】,接收的包需要丢弃
    • 当 FCS 一致时,需要检查 MAC 头部中的接收方 MAC 地址,看看这个包是不是发给自己的
  3. 将还原后的数字信息保存在网卡内部的缓冲区
  4. 网卡通过中断将网络包到达的事件通知给 CPU
  5. CPU 就会暂停当前的工作,并切换到网卡的任务
  6. 网卡驱动开始运行,从网卡缓冲区中将接收到的包读取出来,根据 MAC 头部的以太类型字段判断协议的种类,并调用负责处理该协议的软件
    • 如果以太类型的值是 IP 协议,会将包转交给 TCP/IP 协议栈【大多数情况下,网卡驱动并不会直接调用协议栈,而是先切换回操作系统,然后再由操作系统去调用协议栈,由协议栈继续执行接收操作

[!hint] 1-3 操作都是由网卡的 MAC 模块来完成的

网卡的 IP 模块接收信息

  1. 当网络包转交到协议栈后,IP 模块首先会检查 IP 头部的格式和接收方 IP 地址
    • 当服务器启用类似路由器的包转发功能时,对于不是发给自己的包,会像路由器一样根据路由表对包进行转发
    • 服务器也可以启用类似防火墙的包过滤功能,在包转发的过程中还会对包进行检查,并丢弃不符合规则的包
  2. 接下来需要检查包有没有被分片
    • 如果是分片的包,则将包暂时存放在内存中,等所有分片全部到达之后将分片组装起来还原成原始包
    • 如果没有分片,则直接保留接收时的样子
  3. 接下来需要检查 IP 头部的协议号字段,并将包转交给相应的模块
    • 如果协议号为 06,则将包转交给 TCP 模块
    • 如果是 11,则转交给 UDP 模块

网卡的 TCP 模块处理包

处理连接包

当 TCP 头部中的控制位 SYN 为 1,表示这是一个发起连接的包

  1. 检查包的接收方端口号
    • 如果指定端口号没有等待连接的套接字,则向客户端返回错误通知的包
    • 如果存在等待连接的套接字,则为这个套接字复制一个新的副本,并将必要参数【发送方 IP 地址、端口号、序号初始值、窗口大小……】写入这个套接字中
  2. 分配用于发送缓冲区和接收缓冲区的内存空间
  3. 生成代表接收确认的 ACK号,生成用于从服务器向客户端发送数据的序号初始值,生成表示接收缓冲区剩余容量的窗口大小,并用这些信息生成 TCP 头部,委托 IP 模块发送给客户端【这个包只有 TCP 头部,没有数据
  4. 这个包到达客户端之后,客户端会返回表示接收确认的 ACK 号,当这个 ACK 号返回服务器后,连接操作就完成了
  5. 当将新套接字的描述符转交给服务器程序之后,服务器程序就会恢复运行

处理数据包

假设包中的数据为 HTTP 请求消息:

  1. 首先,TCP 模块会检查收到的包对应哪一个套接字
  2. 找到 4 种信息全部匹配的套接字之后,TCP 模块会对比该套接字中保存的数据收发状态和收到的包的 TCP 头部中的信息是否匹配,以确定数据收发操作是否正常
    • 如果正常,就说明包正常到达了服务器,没有丢失。这时,TCP 模块会从包中提出数据,并存放到接收缓冲区中,与上次收到的数据块连接起来
  3. 当收到的数据进入接收缓冲区后,TCP 模块就会生成确认应答的 TCP 头部,并根据接收包的序号和数据长度计算出 ACK 号,然后委托 IP 模块发送给客户端
  4. 接下来,数据会被转交给应用程序
    • 一般来说,应用程序会在数据到达之前就等待
    • 如果应用程序不来获取数据,则数据会被一直保存在缓冲区中
  5. 然后,控制流程会转移到服务器程序,对收到的数据进行处理,也就是检查 HTTP 请求消息的内容,并根据请求的内容向浏览器返回相应的数据

网卡的 TCP 模块断开操作

当数据收发完成后,会开始执行断开操作

在 TCP 协议的规则中,断开操作可以由客户端或服务器任何一方发起,具体的顺序是由应用层协议决定的。

Web 中,这一顺序随 HTTP 协议 版本不同而不同,在 HTTP1.0 中,是服务器先发起断开操作。

这时,服务器程序会调用 Socket 库的 close,TCP 模块会生成一个控 制位 FIN 为 1 的 TCP 头部,并委托 IP 模块发送给客户端。当客户端收到这个包之后,会返回一个 ACK 号。接下来客户端调用 close,生成一个 FIN 为 1 的 TCP 头部发给服务器,服务器再返回 ACK 号,这时断开操作 就完成了。HTTP1.1 中,是客户端先发起断开操作,这种情况下只要将客 户端和服务器的操作颠倒一下就可以了。 无论哪种情况,当断开操作完成后,套接字会在经过一段时间后被删除。

收到响应

![[Excalidraw/计算机/四大件/计算机网络 Draw.md#^group=Abd79e9Y|600]]

状态码

![[Excalidraw/计算机/四大件/计算机网络 Draw.md#^group=XPJza8ki|600]]

  • 200 OK:请求已正常处理,返回所需内容
  • 201 Created:请求已成功处理,并创建了新的资源
  • 204 No Content:请求已成功处理,但没有返回内容
  • 301 Moved Permanently:请求的资源已永久移动到新位置。
  • 302 Found:请求的资源已临时移动到新位置
  • 304 Not Modified:客户端缓存的资源仍然有效,无需返回内容
  • 400 Bad Request:客户端请求有语法错误,服务器不能理解
  • 401 Unauthorized:未授权访问资源。
  • 403 Forbidden:禁止访问资源,客户端没有权限
  • 404 Not Found:请求的资源不存在
  • 405 Method Not Allowed:请求方式有误
  • 429 Too Many Requests:表示客户端发送的请求过多,在给定的时间内超出了服务器的限制
  • 500 Internal Server Error:服务器内部错误,无法完成请求
  • 503 Service Unavailable:服务器当前无法处理请求【过载/正在维护/正在初始化】