众所周知计算机软件的架构目前最为常见的是 B/S 和 C/S 架构,B/S 和 C/S 架构的软件都采用了分层结构,将系统划分为不同的模块层次,每个模块层负责相应功能,这种架构便于管理和维护,同时提高了系统的可扩展性和灵活性。笔者感觉最主要的特点是在使用软件时都会产生数据,而这些数据如果存储在本地时很不安全的,并且用户产生的数据才是开发对应软件公司想用的东西,数据才是一个软件科技公司的财产。两种架构都将用户界面和数据处理分离,有助于系统的灵活性和维护性,用户界面只需要负责展示信息,而数据处理由服务器端或客户端中的逻辑处理模块完成;无论是 B/S 还是 C/S 架构,都涉及客户端和服务器之间的通信,客户端向服务器发送请求,并接收服务器的响应,这种通信机制实现了应用程序的交互和数据交换,这篇博文将探讨一些在 B/S 架构下的网络通信协议技术。


Web 浏览器和客户端

笔者将其 B/S 架构归纳为 C/S 架构中,其 C/S 架构的中的 C (client)一般是指的安装在用户电脑上的软件,那么其浏览器也是归于客户端软件的,浏览器本体也是一款软件,只不过它可能比其他客户端的软件实现了多种的应用层的网络通信协议。B/S 架构中,客户端通常是 Web 浏览器,用户通过浏览器访问应用程序,这种架构具有跨平台、易部署和易维护的优势。B/S 和 C/S 架构都可以支持多个用户同时访问系统,多用户的需求可以通过在服务器端处理实现,架构图:

当浏览器发生数据请求就会采用 HTTP 协议和应用服务器交换数据,而应用服务器和数据库服务器交互则采用传输层的 TCP 协议。


HTTP Protocol

HTTP 协议是 HyperText Transfer Protocol 的缩写 (即超文本传输协议),是由 w3c(万维网联盟)制定的一种应用层协议,用来定义浏览器与 Web 服务器之间如何通信以及通信的数据格式。因为 B/S 架构中的通信模块就是以 HTTP 这个协议作为标准协议的,所以对该协议有所了解可以更好的编写程序。 HTTP 协议的发展历史悠久,已经不断的迭代多个版本,不同时期的 HTTP 版本特性,整理表格:

时间段HTTP版本主要特点
1989-1995HTTP/0.9- 最初版本,仅支持 GET 请求。
- 无请求头或状态码,只有响应主体。
- 用于传输 HTML 文件。
1996-1999HTTP/1.0- 引入多种 HTTP 方法(GET、POST、HEAD 等)。
- 引入状态码、请求头、响应头等概念。
- 服务器在每次请求后关闭连接。
1999-2015HTTP/1.1- 引入持久连接,减少连接建立的开销。
- 引入管道化(Pipelining),实现多请求并发。
- 引入 Host 头字段,允许多个域名共享 IP 地址。
- 引入分块传输编码和压缩,提高效率。
2015-至今HTTP/2.0- 完全重新设计,旨在提高性能和效率。
- 改进多路复用,支持并行请求和响应。
- 引入头部压缩,减少数据传输大小。
- 允许服务器推送资源,提高效率。
- 强制使用加密(TLS),提高安全性。
2020-至今HTTP/3.0- 基于 QUIC 协议,进一步提高性能和安全性。
- 改进连接建立速度和稳定性,减少握手延迟。
- 实现数据包级别的错误纠正和拥塞控制。
- 采用基于 UDP 的传输协议。

HTTP 协议一次请求对应一次连接,当浏览器再次发请求给服务器时,Web 服务器并不知道这就是上次发请求的客户端,这也是 HTTP 协议的一个特点:无状态协议。这种需要时建立连接,使用结束后立即断开连接的方式使得 Web 服务器可以利用有限的连接为尽可能多的客户提供服务,并且能够处理分散的网络请求,处理来自不同地区浏览器请求,并且服务器也可以多台分散在不同地区的,正因为是一种应用层协议还可以使用多层代理架构,例如使用 Nginx 做代理网关,正是具备了这样的特点,才使得 B/S 架构能够承载企业级应用的大量访问。

如果要测试 HTTP 协议的一些特性这里推荐使用 telnet 命令行工具,可以在命令行进行 HTTP 数据包封装,Telnet 使用了 TCP/IP 协议进行通讯,Telnet 协议默认是 23 端口。它允许用户通过网络连接到远程主机,并在远程系统上执行命令,就像在本地终端上输入命令一样。完全可以使用 telent 命令行工具,来模拟整个 HTTP 协议连接建立的过程,HTTP 请求和响应的过程如下:

  1. 浏览器根据 IP 地址和端口号与服务器建立连接
  2. 向 Web 服务器发送请求数据包
  3. Web 服务器接收请求数据包后,发送相应的响应数据包
  4. 浏览器接收响应数据后关闭连接

在 Windows 中的 telent 默认会在高级功能设置中,而 MacOS 和 Linux 用户则需要手动进行安装对应的 telent 命令,下图是一次使用 telent 进行 HTTP/1.1 版本请求响应的例子日志细节。

$: telnet www.ibyte.me 80
Trying 172.67.151.15...
Connected to www.ibyte.me.
Escape character is '^]'.
GET / HTTP/1.1

HTTP/1.1 400 Bad Request
Server: cloudflare
Date: Mon, 09 Oct 2023 11:55:59 GMT
Content-Type: text/html
Content-Length: 155
Connection: close
CF-RAY: -

<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>cloudflare</center>
</body>
</html>
Connection closed by foreign host.

在这个例子使用的 HTTP/1.1 版本,这个版本最大特性是多了 Host 头字段,允许多个域名共享 IP 地址,可以让一台 Web 服务器主机占用 80 端口来服务多个子网站网站,Web 服务器可以通过 Host 头字段区分具体要返回域名的内容,Host 字段是区别于 DNS 的,DNS 只是帮助浏览器通过域名查询到 IP 建立 TCP/IP 的连接再进行 HTTP 协议通讯,请求流程如下图:

HTTP/1.0 相比前两个版本做了很多改进,Host 字段只是其中一个改进之一,另外为提高性能引入了 HTTP Keep-Alive 机制,老版本在发送 HTTP 数据包之前,每次都需要建立新的 TCP/IP 连接通信,如果读者了解过 TCP/IP 协议如何建立连接的,会明白这个过程很费时,所以 HTTP/1.1 版本引入了持久连接,以减少在每次请求时建立新连接所带来的开销,提高网络通信的效率。Keep-Alive 默认超时机制是 60 秒,也就是说如果连接在连接池中的空闲时间超过 60 秒,则该连接会关闭。

另外 Keep-Alive 超时间隔时间在各个 Web 服务器配置可以设置,Keep-Alive 机制对应的请求头中的是 Connection 字段,服务器会根据此字断来做相应的处理。

字段作用
Connection: keep-aliveTCP 持久连接
Connection: close要求服务器关闭 TCP 连接

HTTP/1.1 为提升连接性能,旧版本的 HTTP/1.0 每发送一次 HTTP 请求都会重新建立一次 TCP 的连接才能正常发送正常的请求数据包,导致的问题如果客户端请求到一个页面里面包含了 5 个图片文件,那么浏览器会继续创建 5 次的 TCP 连接来完成这些 HTTP 对象资源的请求;而另外则为上面提到的 Keep-Alive 机制,持久化连接,多个 HTTP 请求共享一个 TCP 连接。还加一个 Pipelining 机制,允许在发送数据包多个请求一起发送,类似于 Redis 命令中的 Pipelining 机制,批量处理数据包请求,不使用 Pipelining 时,每个请求都需要等待前一个请求的响应返回才能发送下一个请求,这样会导致网络的低效利用。

下面是笔者一个使用 Telnet 例子日志信息:

$: telnet www.ibyte.me 80
Trying 172.67.151.15...
Connected to www.ibyte.me.
Escape character is '^]'.
GET /path1 HTTP/1.1
Host: www.ibyte.me
Connection: keep-alive

GET /path2 HTTP/1.1
Host: www.ibyte.me
Connection: keep-alive

GET /path3 HTTP/1.1
Host: www.ibyte.me
Connection: keep-alive

在 HTTP/2.0 又引入了 Multiplexing 技术来提升性能,因为 Pipelining 存在 Head-of-Line Blocking 问题,这是什么问题? Head-of-Line Blocking 问题根本原因是假设客户端发送了 3 个请求 A 、B 和 C 到服务器,请求的顺序是 A 、B 、C 。如果服务器处理请求 A 的时间较长,那么请求 B 和 C 就必须等待 A 完成后才能开始处理,响应也必须按照 A 、B 、C 的顺序返回给客户端,即使后续的请求处理时间很短,也需要等待前面的请求完成。

而 HTTP/2.0 引入了多路复用 Multiplexing 机制,允许客户端同时发送多个 HTTP 请求并在同一个 TCP 连接上进行传输,同时服务器也可以将多个响应并行地返回给客户端,不同请求的数据可以交错传输,而不必等待前面的请求响应完成。这样可以充分利用连接,减少了等待时间,提高了效率;这个机制可以想象为霰弹枪一样,同时可以发射多个子弹,传统步枪只能单道连发,总体来看不同性能提升方案差异对照图。

HTTP/2.0 将每个请求或回应的所有数据包,称为一个数据流(stream),每个数据流都有一个独一无二的编号,数据包发送的时候,都必须标记数据流 ID,用来区分它属于哪个数据流;数据流的编号是递增的,客户端和服务器都可以创建和使用数据流,数据流的编号从 1 开始,分别由客户端和服务器独立维护,客户端发起的数据流使用奇数编号,服务器发起的数据流使用偶数编号。

此外 HTTP/2.0 为提升整个传输效率问题,针对每次需要重新传递的 HTTP Handler 请求协议头,大部分情况下请求协议头都是固定,除了极少的字断经常变动之外;HTTP/1.1 版本每次传输会增加 500-800 字节的开销,如果使用 Cookie 来存储数据会增加千字节 KB 。而在新的版本 HTTP/2.0 则使用 HPACK 压缩格式压缩请求和响应标头元数据,HPACK 核心就是采用 Static Table Definition 结构来服用存储请求头元数据信息,和 Huffman Coding 编码对数据进行压缩传输。

HTTP/2.0 协议中的 HPACK 复用机制,协议服务端可以维护一个动态缓存表,在发送新请求时协议会和之前请求做对比,之前传输的值的索引列表让我们可以通过传输索引值对重复值进行编码,索引值可用于高效查找和重建完整的标头键和值,如下图:

至于对于具体值和索引,建议查阅 IETF RFC7541 规范文档,如果不是自己去开发实现 HTTP/2.0 协议的话。

整个的 HTTP/2.0 协议的设计可以认为借鉴 TCP 协议的设计,相比 HTTP/1.1 协议采用基于 TCP 多连接的进行数据传输,而 HTTP/2.0 总体将协议分为了 3 个部分:流 、消息 、帧 部分,采用 TCP 单连接多数据流的方式进行交叉传输,流可以认为是一种针对单个 TCP 连接的做平分的方式,多个流在一个 TCP 连接中传输形成一个虚拟信道。消息则是在流中传输的请求和响应数据包,帧则是消息中的数据包实体,多个帧可以组成一个消息数据包,帧是最小的通信单位。


全双工通信

通过上面篇幅已经知道 HTTP 只能适用于从客户端主动发起,而服务器只能被动提供服务,对请求做出对应的请求数据响应;当然这里在 HTTP/2.0 允许服务器推送资源,提高效率,HTTP/2.0 会根据客户端请求主动推送对应的数据包到客户端中。在这种架构下客户端和客户端之间不是直接 P2P 方式进行通信的,而是通过服务器进行通信交互数据。典型的全双工通信例子有股票市场的 K 图,或者一些在线的 IM 即时通讯应用,目前 Web 实现这类应用的方案有通过 HTTP 协议进行请求轮训的方式,或者 WebSocket 的方式,这也是本篇技术博客要详细讲解的技术。

Ajax 轮训方式,所谓的轮训请求数据也就是异步的向服务器端定时发送 HTTP 请求来获取最新的数据,但是缺点也是明显的这种方式数据不能及时显示到客户端,轮训的频率只能由客户端来控制。因为 HTTP 协议书是基于 DNS 和 TCP 协议来传输数据,所以如果每次使用 HTTP 进行轮训请求,那么网络延迟会很大,由于发送 HTTP 请求要先查询 DNS 和建立一次 TCP 连接,建立 TCP 连接需要交换控制信息和三次握手。如果基于 Ajax 轮训方式采用的是 HTTP/1.0 版本性能更慢,因为每次轮训一次需要建立一次 TCP 连接,导致会出现 TCP 连接慢启动和 RTT 延迟。

什么是协议?那么我有一句话总结:对等层的实体在通讯过程中应该遵守的规则集合通常被称为通讯协议。这些协议定义了通信双方之间的规范和约定,以确保数据能够正确、有序和可靠地传输。通信协议涵盖了各种方面,包括数据格式、数据传输方式、错误处理、安全性等。


其他资料

最后修改:2023 年 11 月 30 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !