众所周知计算机软件的架构目前最为常见的是 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 协议的话。

另外仿照 TCP 的流量控制策略,HTTP/2.0 也设计了流量控制策略算法,在同一个 TCP 连接上的多个数据流是共享当前运输层的 TCP 连接的,基于 SETTING 和 WINDOW_UPDATE 帧进行数据窗口的调节,具体如何调节看客户端和服务器端的场景。所谓窗口调节也就是在请求头上双方协商调整滑动窗口的大小,这是一段 Java 代码的例子:

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class Http2FlowControlExample {

    public static void main(String[] args) throws IOException {
        // 创建 HTTP/2 服务器
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);

        // 创建上下文处理器
        server.createContext("/", new MyHandler());

        // 启动服务器
        server.start();
    }

    static class MyHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            // 发送流控制窗口大小为 4096 的响应头
            exchange.getResponseHeaders().add("Content-Type", "text/plain");
            exchange.getResponseHeaders().add("Window-Size", "4096");

            // 获取输出流
            OutputStream os = exchange.getResponseBody();

            // 模拟发送数据,每次发送 1024 字节
            byte[] data = new byte[1024];
            for (int i = 0; i < 10; i++) {
                // 发送数据并检查流控制窗口
                os.write(data);
                os.flush();

                // 模拟接收方处理数据
                handleData(data);

                // 发送 WINDOW_UPDATE 帧,增加流控制窗口大小
                exchange.getHttpContext().getServer().getExecutor().execute(() -> {
                    exchange.getResponseHeaders().add("Window-Update", "4096");
                });

                // 暂停 1 秒以模拟处理延迟
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 关闭输出流
            os.close();
        }

        private void handleData(byte[] data) {
            // 模拟接收方处理数据
            System.out.println("Received data: " + new String(data));
        }
    }
}

正常情况下,流量控制的处理应该由 HTTP/2 协议自动进行,而不需要手动添加 Window-Update 这样的字段,而发送 WINDOW_UPDATE 帧是由 HTTP/2 实现自动处理的。而不是通过添加响应头的方式,HTTP/2 协议会自动进行流量控制,确保在不超过对端流控制窗口大小的情况下发送数据。

针对 HTTP/1.1 主动请求资源的方式,HTTP/2.0 升级了主动推送服务,当服务器决定要推送一些资源给客户端时,如资源的 URL 、头部信息等。客户端可以选择接受或拒绝这个推送,如果客户端接受了推送,那么服务器就会在接下来发送相应的数据帧,而不需要等待客户端发起请求,在经典的 Web 服务器 Nginx 中可以使用 http2_push 指令来实现这类的推送功能:

server {
    # 启用 HTTP/2 协议版本
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/your/certificate.crt;
    ssl_certificate_key /path/to/your/private_key.key;

    # 启用 HTTP/2 服务器推送
    http2_push_preload on;

    location / {
        # 设置推送的资源
        http2_push /styles.css;
        http2_push /script.js;

        # 处理正常的请求
        try_files $uri $uri/ =404;
    }
}

整个的 HTTP/2.0 协议的设计可以认为借鉴 TCP 协议的设计,相比 HTTP/1.1 协议采用基于 TCP 多连接的进行数据传输,而 HTTP/2.0 总体将协议分为了 3 个部分:流 、消息 、帧 部分,采用 TCP 单连接多数据流的方式进行交叉传输,流可以认为是一种针对单个 TCP 连接的做平分的方式,多个流在一个 TCP 连接中传输形成一个虚拟信道。消息则是在流中传输的请求和响应数据包,帧则是消息中的数据包实体,多个帧可以组成一个消息数据包,帧是最小的通信单位,为了提升性能又添加了服务器端推送事件,而针对数据共享 TCP 连接又添加了流量控制窗口,针对反复传输的 HTTP 请求头 Handler 做了首部压缩,客户端和服务器端各自维护来一张动态缓存表 “首部表” 来跟踪已经被存储的请求头字断,不再通过每次请求和响应发送。


Ajax 异步 HTTP

通过上面篇幅已经知道 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 延迟。

在使用基于 HTTP/1.x 版本进行 Ajax 轮询的时候会出现一个问题,每次轮询都需要建立一次 TCP 连接,了解 TCP 协议的都明白每次建立一次 TCP 协议连接代价很大,都需要进行三次握手。为了解决这个问题开发者又提出了长轮询的概念 (Long Polling),普通轮询在服务器接收到请求后,开始处理如果有新的数据可用,服务器立即将数据返回给客户端,没有也会立即断开连接;而长轮询的做法,如果服务器暂时没有新的数据可用,服务器和客户端会保持连接打开,等待直到有新的数据为止,或者加一些策略在一定时间后关闭连接做超时控制,下面是一段 Java 实现的代码:

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

@WebServlet("/polling")
public class PollingServlet extends HttpServlet {

    private static BlockingQueue<String> dataQueue = new LinkedBlockingQueue<>();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            // 设置响应类型为纯文本
            response.setContentType("text/plain;charset=UTF-8");

            // 设置响应缓冲区不缓存数据
            response.setBufferSize(0);

            // 设置响应字符编码
            response.setCharacterEncoding("UTF-8");

            // 从队列中取出数据,如果队列为空,则等待一定时间
            String data = dataQueue.poll(10, TimeUnit.SECONDS);

            // 没有新数据,返回适当的响应
            response.getWriter().write(data != null ? data : "No new data");
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 处理异常,例如返回错误信息
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
        }
    }

    
    // 此方法可以在其他地方先调用,例如其他的 Servlet 方法
    private static void startDataProducer() {
        // 启动数据产生者线程
        Thread producerThread = new Thread(() -> {
            try {
                int count = 1;
                while (true) {
                    String data = "Data-" + count++;
                    dataQueue.put(data);

                    // 模拟数据每两秒产生一次
                    Thread.sleep(2000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        producerThread.start();
    }
}

对于客户端只需要在收到消息之后继续发送一个 XHR 请求到服务器,JavaScript 代码:

function longPolling() {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'https://example.com/longpoll', true);

  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4) {
      if (xhr.status == 200) {
        var responseData = JSON.parse(xhr.responseText);
        // 处理从服务器返回的数据
        longPolling(); // 再次发起长轮询请求
      } else {
        // 处理错误,重新发起长轮询请求
        longPolling();
      }
    }
  };

  xhr.send();
}

longPolling(); // 初始调用

对于采用轮询的方式采用那个版本的 HTTP 协议进行数据交换需要考虑。另外一个缺点也是很明显的,如何对轮询的请求间隔时间做选择?因为是采用的定时轮询的方式导致客户端和服务器端存在一定同步延迟,如果碰巧服务器消息在客户端轮询请求之前产生,那么客户端和服务器消息同步延迟时间是最短的,只存在请求网络的延迟。当如果客户端请求刚刚返回,服务器就产生了消息,那么此时两端同步的延迟就是最大,长轮询的缺点也是很明显的如果服务器产生的新数据频率很快,对于客户端来说长轮询能及时获取到最新数据,但是意义不大不如设置一定间隔时间去获取这些数据,采用常规轮询的方式进行。


SSE 事件推送

HTTP 和采用 HTTP 轮询的方式都是为了解决,服务器和客户端之间的数据交换,轮询又是基于 HTTP 协议基础之上一种技术应用的实现,目的是让及时获取到服务器产生的数据变更,客户端能获取得到。这两种方式的都是采用客户端主动发送 HTTP 协议来获取数据,遇到的问题也很明显存在消息同步延迟,不能由服务器在产生新的数据同时就将消息同步到客户端中。针对这个轮询延迟交付问题,多浏览器厂商设计一款新的 API 来帮助解决服务器推送消息的问题,采用 Event Source 和 Event Stream 进行服务器推送通知,一个 Go 的实现自定义事件类型和数据推送功能:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type CustomEvent struct {
    Type      string `json:"type"`
    Message   string `json:"message"`
    Timestamp string `json:"timestamp"`
}

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        // 设置响应头,指定内容类型为 text/event-stream
        w.Header().Set("Content-Type", "text/event-stream")
        w.Header().Set("Cache-Control", "no-cache")
        w.Header().Set("Connection", "keep-alive")

        // 设置初始数据
        initialData := CustomEvent{
            Type:      "message",
            Message:   "Initial data",
            Timestamp: time.Now().Format(time.RFC3339),
        }
        sendEvent(w, initialData)

        // 模拟定时向客户端推送不同类型的事件
        ticker := time.NewTicker(5 * time.Second)
        defer ticker.Stop()

        for {
            select {
            case <-ticker.C:
                // 向客户端发送 "message" 事件
                messageEvent := CustomEvent{
                    Type:      "message",
                    Message:   "Server time",
                    Timestamp: time.Now().Format(time.RFC3339),
                }
                sendEvent(w, messageEvent)

                // 向客户端发送 "alert" 事件
                alertEvent := CustomEvent{
                    Type:      "alert",
                    Message:   "Important message",
                    Timestamp: time.Now().Format(time.RFC3339),
                }
                sendEvent(w, alertEvent)

            case <-r.Context().Done():
                // 客户端断开连接时关闭循环
                fmt.Println("Client disconnected")
                return
            }
        }
    })

    // 启动服务器
    fmt.Println("Server listening on :8080")
    http.ListenAndServe(":8080", nil)
}

func sendEvent(w http.ResponseWriter, event CustomEvent) {
    // 序列化事件为 JSON 格式
    jsonData, err := json.Marshal(event)
    if err != nil {
        fmt.Println("Error encoding JSON:", err)
        return
    }

    // 向客户端发送 JSON 格式的事件
    fmt.Fprintf(w, "event: %s\n", event.Type)
    fmt.Fprintf(w, "data: %s\n\n", string(jsonData))
    w.(http.Flusher).Flush() // 强制将数据推送到客户端
}

Server-Sent Events (SSE) 代码实现起来比传统 Ajax 轮询要简单,只需要知道服务器的 SSE 协议地址,就可以发送 SSE 连接,服务器可以发送简单的文本消息到客户端,客户端通过事件监听器捕获这些消息,设置对应的事件处理逻辑即可。 SSE 是基于一个长连接的,相比轮询来推送消息,消息延迟是相对比较低的,但是只需要服务器推送消息到客户端,如果客户端要发送新消息则新建一个 HTTP 连接到服务器进行处理。在正常的 Event Source 使用中,服务器不需要重新建立连接来发送自定义事件,如果服务器和客户端遇到一些特殊情况断开连接了,服务器和客户端双方要考虑维护连接断开之后恢复,此外消息是文本格式同样可以使用 gzip 进行压缩。


WebSocket 全双工通信

与传统的 Web 通信方式不同,WebSocket 允许客户端和服务器之间进行双向通信,双方都可以独立地发送消息。WebSocket 在客户端和服务器之间建立了一个持久连接,这个连接保持打开状态,允许低延迟通信。这样设计的好处允许客户端和服务器之间能交叉发送数据,客户端和服务器双方都可以随时发送消息到给对方。WebSocket 从一个 HTTP 握手开始,然后 HTTP 协议升级到 WebSocket 协议,然后再通过 WebSocket 连接发送的数据被组织成帧,帧可以携带文本或二进制数据,帧的机制支持高效的通信并支持不同的数据类型。

因为和上述的通信方式一样,没有固定的消息格式,通信采用二进制数据流的方式,如果需要交互数据,可以在应用和服务器端设计子协议,子协议可以定义应用层的通信规范,规定在 WebSocket 连接上双方如何交换数据、消息和命令。这有助于确保在 WebSocket 连接上进行的通信是按照特定协议的规范进行的。子协议存在的目的统一客户端和服务器端都知道消息首部,各方面采用子协议就能正常编解码消息体,如果双方在握手时都同意使用特定的子协议,那么连接将在该子协议下进行通信,否则连接将使用默认的 WebSocket 协议。


网络延迟是必然存在的,数据包要从一台电脑到另外一台电脑必须经过 5 层网络模型,在这些协议栈上封包发包都需要时间,链路上传输取决于传输介质,传播延迟 + 传输延迟 + 处理延迟 + 排队延迟 = 整体网络延迟。这些延迟属于 ISO 网络层面的延迟,没有算上应用程序处理的延迟,和使用场景问题。何时使用 XHR ?如果你请求有突发性和短暂性这时应该使用 XHR 进行请求;如果服务器和客户端需要一些消息通知场景不涉及到大文件传输,可以考虑使用 SSE 进行;如果应用和服务器需要实时同步建议使用时久化连接的通信,WebSocket 可以提供此种方案,只需要首次建立连接握手,传输而使用二进制流方式进行高效的传输,并且可以使用子协议对通信协议进行扩展。


其他资料

便宜 VPS vultr
最后修改:2023 年 12 月 31 日
如果觉得我的文章对你有用,请随意赞赏 🌹 谢谢 !