Qt中的QWebSocket 和 QWebSocketServer详解:从协议说明到实际应用解析

前言

本篇围绕 QWebSocket 和 QWebSocketServer,从协议基础、通信模式、数据传输特点等方面展开,结合具体接口应用与实战案例进行说明。

在实时网络通信领域,WebSocket 技术以其独特的全双工通信能力,成为连接客户端与服务器的重要桥梁。Qt 框架中的 QWebSocket 和 QWebSocketServer 类,封装了 WebSocket 协议的复杂细节,为开发者提供了简洁高效的接口。本文将从 WebSocket 协议基础出发,全面解析 QWebSocket 与 QWebSocketServer 的通信模式、数据传输特点、应用场景、核心接口及实战案例,帮助开发者快速掌握这一技术,轻松构建实时通信应用。

一、WebSocket 协议基础:实时通信的 "高速公路"

在了解 QWebSocket 和 QWebSocketServer 之前,我们需要先认识它们所基于的 WebSocket 协议。WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,由 IETF 在 RFC 6455 中定义,它解决了传统 HTTP 协议在实时通信场景中的局限性。

1.1 为什么需要 WebSocket?

传统的 HTTP 协议是一种 "请求 - 响应" 模式的无状态协议,客户端只能主动向服务器发送请求,服务器无法主动向客户端推送数据。在实时通信场景(如在线聊天、实时数据监控)中,这种模式会导致以下问题:

  • 轮询效率低下:客户端需要不断发送 HTTP 请求询问服务器是否有新数据(如每隔 1 秒发送一次请求),即使没有新数据,也会产生大量无效请求,浪费带宽和服务器资源。
    延迟较高:轮询的间隔时间决定了数据更新的延迟,间隔太长会导致数据滞后,间隔太短则会增加服务器负担。
  • 连接开销大:每次 HTTP 请求都需要建立 TCP 连接(三次握手),并携带大量头部信息,增加了通信成本。

WebSocket 协议的出现正是为了解决这些问题,它提供了一种持久化的连接,允许客户端和服务器之间进行双向实时通信。

1.2 WebSocket 协议的核心特性

WebSocket 协议具有以下核心特性,使其成为实时通信的理想选择:

  • 全双工通信:一旦建立连接,客户端和服务器可以同时向对方发送数据,就像一条双向车道,数据可以双向流动。
  • 持久连接:连接建立后会一直保持,直到客户端或服务器主动关闭,避免了频繁建立连接的开销。
  • 低开销:WebSocket 连接建立后,数据传输的头部信息非常精简(仅 2-10 字节),远低于 HTTP 请求的头部开销。
  • 基于 TCP:WebSocket 建立在 TCP 协议之上,继承了 TCP 的可靠性(数据有序、无丢失、无重复)。
  • 与 HTTP 兼容:WebSocket 连接的建立过程(握手)使用 HTTP 协议,因此可以与现有 HTTP 服务器和网络基础设施兼容,通过 80(ws)和 443(wss)端口通信,避免被防火墙拦截。

1.3 WebSocket 握手过程:从 HTTP 到 WebSocket 的 "变身"

WebSocket 连接的建立需要经过一次特殊的 HTTP 握手过程,具体步骤如下:

  • 客户端发送握手请求:客户端向服务器发送一个 HTTP GET 请求,包含特殊的头部信息,表明想要升级到 WebSocket 协议。
cpp 复制代码
GET /chat HTTP/1.1
Host: example.com:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
  • Upgrade: websocket和Connection: Upgrade头部告诉服务器,客户端希望将连接升级到 WebSocket 协议。
    Sec-WebSocket-Key是一个 Base64 编码的随机字符串,用于服务器验证和生成响应密钥。
    Sec-WebSocket-Version指定 WebSocket 协议版本(通常为 13)。
  • 服务器响应握手:如果服务器支持 WebSocket 协议,会返回一个 HTTP 101(Switching Protocols)响应,表明连接已成功升级。
cpp 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept是服务器通过Sec-WebSocket-Key计算得到的密钥(将Sec-WebSocket-Key与固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接后,计算 SHA-1 哈希并进行 Base64 编码),客户端会验证该密钥以确认握手成功。

连接建立:握手成功后,HTTP 连接正式升级为 WebSocket 连接,双方可以开始双向数据传输。

1.4 WebSocket 数据帧:数据传输的 "包装盒"

WebSocket 协议中,数据通过 "帧"(Frame)的形式传输。帧是数据传输的基本单位,包含以下关键字段:

  • FIN:1 位,表示当前帧是否为消息的最后一帧(1 表示最后一帧,0 表示后续还有帧)。
  • Opcode:4 位,表示帧的类型,常见值包括:
  • 0x00:延续帧(用于拼接大数据消息)
  • 0x01:文本帧(UTF-8 编码的文本数据)
  • 0x02:二进制帧(二进制数据)
  • 0x08:连接关闭帧
  • 0x09:Ping 帧(心跳检测)
  • 0x0A:Pong 帧(Ping 响应)
  • Mask:1 位,表示数据是否经过掩码处理(客户端发送给服务器的帧必须掩码,服务器发送给客户端的帧不能掩码)。
  • Payload Length:7 位、7+16 位或 7+64 位,表示数据载荷的长度。
  • Masking-Key:32 位(仅当 Mask 为 1 时存在),用于对数据载荷进行解掩码。
  • Payload Data:实际传输的数据内容。

这种帧结构设计既保证了数据传输的灵活性(支持文本和二进制数据),又通过掩码机制提供了基本的安全性,同时精简的头部设计降低了传输开销。

二、QWebSocket 与 QWebSocketServer:Qt 中的 WebSocket 实现

Qt 框架通过 QWebSocket(客户端)和 QWebSocketServer(服务器)两个类,对 WebSocket 协议进行了封装,屏蔽了底层协议的复杂性,让开发者可以专注于业务逻辑实现。

2.1 QWebSocket:WebSocket 客户端的 在Qt中"代言人"

QWebSocket 类用于实现 WebSocket 客户端功能,它继承自 QAbstractSocket,提供了连接服务器、发送数据、接收数据、关闭连接等接口。QWebSocket 的核心特点包括:

  • 自动处理握手:无需手动构造握手请求,调用connectToUrl()方法即可完成与服务器的握手过程。
  • 支持文本和二进制数据:通过sendTextMessage()发送文本数据,sendBinaryMessage()发送二进制数据。
  • 信号驱动:通过信号(如connected()、disconnected()、textMessageReceived())通知连接状态变化和数据到达,符合 Qt 的信号 - 槽机制。
  • 错误处理:提供errorOccurred()信号和errorString()方法,方便处理连接和传输过程中的错误。

2.2 QWebSocketServer:WebSocket 服务器的 "指挥官"

QWebSocketServer 类用于实现 WebSocket 服务器功能,它负责监听端口、接受客户端连接、管理连接列表等。QWebSocketServer 的核心特点包括:

  • 多客户端支持:可以同时接受多个客户端的连接,每个客户端连接对应一个 QWebSocket 对象。
  • 连接管理:通过newConnection()信号通知有新客户端连接,通过close()方法关闭服务器。
  • 协议协商:支持设置子协议(Subprotocol)和扩展(Extension),与客户端进行协商。
  • 安全支持:支持 wss(WebSocket Secure)协议,通过 SSL/TLS 加密传输数据。

2.3 两者关系:客户端与服务器的 "对话桥梁"

QWebSocket 和 QWebSocketServer 的关系就像电话和总机:QWebSocket 是 "电话机",负责与对方(服务器)建立连接并通话;QWebSocketServer 是 "总机",负责监听来电(客户端连接请求),并为每个来电分配一个 "电话机"(QWebSocket 对象)进行单独通话。

具体交互流程如下:

  1. 服务器通过 QWebSocketServer 监听指定端口(如 8080)。
  2. 客户端通过 QWebSocket 调用connectToUrl(),向服务器发送连接请求。
  3. 服务器收到请求后,通过newConnection()信号通知,调用nextPendingConnection()获取新连接的 QWebSocket 对象。
  4. 客户端和服务器通过各自的 QWebSocket 对象进行双向数据传输。
  5. 通信结束后,客户端或服务器调用close()关闭连接。

三、通信模式:全双工实时交互的 "运作机制"

WebSocket(及 QWebSocket/QWebSocketServer)的通信模式基于持久连接的全双工交互,与传统 HTTP 的 "请求 - 响应" 模式有本质区别,具体体现在以下几个方面:

3.1 连接建立:"一次握手,长期对话"

WebSocket 的连接建立是 "一次性" 的,一旦通过握手建立连接,就会保持打开状态,直到被主动关闭。这种模式避免了 HTTP 每次通信都需要建立连接的开销,特别适合频繁交互的场景。

例如,在在线聊天应用中,用户打开聊天窗口时,客户端与服务器建立一次 WebSocket 连接,之后所有的消息发送和接收都通过这个连接完成,直到用户关闭聊天窗口。

3.2 数据传输:"双向自由流动"

全双工通信意味着客户端和服务器可以随时向对方发送数据,无需等待对方的响应。这种 "双向自由流动" 的特性让实时通信成为可能:

  • 客户端主动发送:如用户在聊天窗口输入消息并发送,客户端通过 QWebSocket 的sendTextMessage()将消息发送给服务器。
  • 服务器主动推送:如其他用户发送了新消息,服务器通过对应的 QWebSocket 对象将消息推送给客户端,客户端无需主动请求。

这种模式与 HTTP 形成鲜明对比:在 HTTP 中,服务器永远是被动响应者,无法主动向客户端发送数据,除非客户端先发送请求。

3.3 连接关闭:"友好告别"

WebSocket 连接的关闭是 "协商式" 的,任何一方都可以发起关闭请求,双方通过交换关闭帧完成优雅关闭:

  • 发起方发送一个 opcode 为 0x08 的关闭帧,包含关闭状态码和原因。
  • 接收方收到关闭帧后,返回一个相同的关闭帧作为确认。
  • 双方确认后,关闭 TCP 连接。

QWebSocket 提供了close()方法(可指定状态码和原因)来触发关闭流程,通过disconnected()信号通知连接已关闭。

3.4 心跳机制:"保持连接活性"

由于网络设备(如路由器、防火墙)可能会关闭长时间没有数据传输的连接,WebSocket 提供了 Ping/Pong 机制来保持连接活性:

  • Ping 帧:一方发送 Ping 帧(opcode=0x09)给另一方,用于检测连接是否有效。
  • Pong 帧:收到 Ping 帧的一方必须立即返回 Pong 帧(opcode=0x0A)作为响应。
  • QWebSocket 自动处理 Ping/Pong 机制:当收到 Ping 帧时,会自动发送 Pong 帧响应;我们也可以通过ping()方法主动发送 Ping 帧,通过pongReceived()信号确认对方在线。

四、数据传输:文本与二进制的 "双车道"

QWebSocket 和 QWebSocketServer 支持两种数据传输类型:文本数据和二进制数据,满足不同场景的需求。

4.1 文本数据传输:适合字符型信息

文本数据传输适用于传输字符串形式的信息,如 JSON、XML、纯文本等,采用 UTF-8 编码。

4.1.1 发送文本数据
  • 客户端(QWebSocket)通过sendTextMessage()方法发送文本数据:
cpp 复制代码
// 客户端发送文本消息
QWebSocket *clientSocket = new QWebSocket;
clientSocket->connectToUrl(QUrl("ws://localhost:8080/chat"));
// 连接成功后发送消息
connect(clientSocket, &QWebSocket::connected, [=]() {
    clientSocket->sendTextMessage("Hello, Server!");
});
  • 服务器(QWebSocketServer)通过客户端对应的 QWebSocket 对象发送文本数据:
cpp 复制代码
// 服务器向客户端发送文本消息
void Server::onNewConnection() {
    QWebSocket *clientSocket = webSocketServer->nextPendingConnection();
    // 存储客户端连接(实际应用中需管理连接列表)
    clientSockets.append(clientSocket);
    // 向客户端发送欢迎消息
    clientSocket->sendTextMessage("Welcome to Chat Server!");
}
4.1.2 接收文本数据
  • 客户端和服务器通过textMessageReceived()信号接收文本数据:
cpp 复制代码
// 客户端接收文本消息
connect(clientSocket, &QWebSocket::textMessageReceived, [=](const QString &message) {
    qDebug() << "Client received: " << message;
});

// 服务器接收客户端文本消息
connect(clientSocket, &QWebSocket::textMessageReceived, [=](const QString &message) {
    qDebug() << "Server received from client: " << message;
    // 转发消息给其他客户端(群聊功能)
    foreach (QWebSocket *socket, clientSockets) {
        if (socket != clientSocket) {
            socket->sendTextMessage(message);
        }
    }
});

4.2 二进制数据传输:适合非字符型信息

二进制数据传输适用于传输图片、音频、视频、文件等非字符型数据,直接传输字节流,无需编码转换。

4.2.1 发送二进制数据
  • 客户端和服务器通过sendBinaryMessage()方法发送二进制数据:
cpp 复制代码
// 客户端发送图片(二进制数据)
void Client::sendImage(const QString &imagePath) {
    QFile file(imagePath);
    if (file.open(QIODevice::ReadOnly)) {
        QByteArray imageData = file.readAll();
        clientSocket->sendBinaryMessage(imageData);
        file.close();
    }
}

// 服务器发送二进制数据(如文件片段)
void Server::sendFileFragment(QWebSocket *client, const QByteArray &fragment) {
    client->sendBinaryMessage(fragment);
}
4.2.2 接收二进制数据
  • 客户端和服务器通过binaryMessageReceived()信号接收二进制数据:
cpp 复制代码
// 客户端接收二进制数据(如图片)
connect(clientSocket, &QWebSocket::binaryMessageReceived, [=](const QByteArray &data) {
    qDebug() << "Client received binary data, size: " << data.size() << "bytes";
    // 保存为图片
    QFile file("received_image.png");
    if (file.open(QIODevice::WriteOnly)) {
        file.write(data);
        file.close();
    }
});

// 服务器接收二进制数据
connect(clientSocket, &QWebSocket::binaryMessageReceived, [=](const QByteArray &data) {
    qDebug() << "Server received binary data from client, size: " << data.size() << "bytes";
    // 处理二进制数据(如存储文件)
});

4.3 大数据传输:分片与重组

当传输的数据量较大(如大文件、高清图片)时,WebSocket 会自动将数据拆分为多个帧进行传输,接收方会自动重组这些帧,还原为完整数据。开发者无需手动处理分片,只需关注完整数据的发送和接收。

例如,发送一个 10MB 的文件:

cpp 复制代码
// 发送方(客户端或服务器)
QFile file("large_file.zip");
if (file.open(QIODevice::ReadOnly)) {
    QByteArray data = file.readAll(); // 读取10MB数据
    webSocket->sendBinaryMessage(data); // 自动分片传输
    file.close();
}

// 接收方
connect(webSocket, &QWebSocket::binaryMessageReceived, [=](const QByteArray &data) {
    // 直接收到完整的10MB数据(自动重组)
    qDebug() << "Received total size: " << data.size() << "bytes";
});

4.4 数据格式建议:结构化与扩展性

在实际应用中,建议对传输的数据进行结构化处理,方便解析和扩展。例如,使用 JSON 格式封装消息类型、内容、时间戳等信息:

cpp 复制代码
// 发送结构化文本消息
QJsonObject messageObj;
messageObj["type"] = "chat"; // 消息类型:聊天
messageObj["sender"] = "Alice"; // 发送者
messageObj["content"] = "Hello, everyone!"; // 内容
messageObj["timestamp"] = QDateTime::currentDateTime().toString(); // 时间戳
QString jsonMessage = QJsonDocument(messageObj).toJson(QJsonDocument::Compact);
webSocket->sendTextMessage(jsonMessage);

// 接收方解析
connect(webSocket, &QWebSocket::textMessageReceived, [=](const QString &jsonMessage) {
    QJsonObject messageObj = QJsonDocument::fromJson(jsonMessage.toUtf8()).object();
    QString type = messageObj["type"].toString();
    if (type == "chat") {
        QString sender = messageObj["sender"].toString();
        QString content = messageObj["content"].toString();
        // 处理聊天消息
    } else if (type == "image") {
        // 处理图片消息(可能包含Base64编码的图片数据)
    }
});

五、应用场景:实时交互的典型应用

WebSocket(及 QWebSocket/QWebSocketServer)的实时双向通信特性,使其在众多领域中发挥重要作用,以下是一些典型应用场景:

5.1 即时通讯应用:聊天与消息推送

即时通讯是 WebSocket 最经典的应用场景,包括一对一聊天、群聊、系统消息推送等。

  • 场景特点:需要低延迟、高频次的双向数据传输,消息实时性要求高。
  • 优势体现:相比轮询,WebSocket 能显著减少网络流量和服务器负载,同时保证消息即时送达。
  • 实例:在线客服系统、团队协作工具(如 Slack)、社交软件的实时聊天功能。

使用 QWebSocket 和 QWebSocketServer 实现即时通讯的核心流程:

  1. 客户端(如聊天窗口)连接到服务器的 WebSocket 端点。
  2. 用户发送消息时,客户端将消息(含发送者、接收者、内容等)通过 QWebSocket 发送给服务器。
  3. 服务器接收到消息后,根据接收者信息,通过对应的 QWebSocket 对象将消息推送给目标客户端。
  4. 目标客户端收到消息后,在界面上显示。

5.2 实时数据监控:动态数据展示

实时数据监控系统(如股票行情、物联网传感器数据、服务器性能监控)需要将实时变化的数据及时展示给用户。

  • 场景特点:数据更新频繁(可能每秒多次),以服务器推送为主,客户端主要负责展示。
  • 优势体现:服务器可以在数据变化时立即推送,无需客户端不断查询,降低延迟和带宽消耗。
  • 实例:股票交易软件的实时行情、工厂的设备状态监控、智能家居的环境数据展示。
  • 实现思路:
  1. 客户端连接到监控服务器的 WebSocket 接口。
  2. 服务器定期(或数据变化时)采集数据(如传感器读数、股票价格)。
  3. 服务器将格式化后的数据(如 JSON)通过 QWebSocket 推送给所有连接的客户端。
  4. 客户端接收到数据后,实时更新界面(如 charts、仪表盘)。

5.3 在线游戏:实时交互与状态同步

多人在线游戏(如实时对战游戏、协作游戏)需要实时同步玩家的位置、动作、状态等信息。

  • 场景特点:低延迟要求极高(通常需低于 100ms),数据量小但频率高,需要双向交互。
  • 优势体现:WebSocket 的低延迟和全双工特性,能满足游戏状态实时同步的需求。
  • 实例:网页版多人贪吃蛇、在线棋牌游戏、实时战略游戏。
  • 实现要点:
  1. 每个玩家客户端通过 WebSocket 连接到游戏服务器。
  2. 玩家操作(如移动、攻击)通过 QWebSocket 实时发送给服务器。
  3. 服务器处理所有玩家的操作,更新游戏状态(如位置、血量)。
  4. 服务器将更新后的游戏状态广播给所有玩家客户端。
  5. 客户端根据收到的状态更新游戏画面。

5.4 协同编辑:多用户实时协作

协同编辑工具(如在线文档、思维导图)允许多个用户同时编辑同一文件,需要实时同步每个人的修改。

  • 场景特点:多用户并发操作,修改需即时可见,冲突需处理。
  • 优势体现:用户的每一次修改(如输入文字、插入图片)都能实时同步给其他用户,提升协作效率。
  • 实例:Google Docs、腾讯文档、在线白板工具。
  • 实现逻辑:
  1. 多个用户客户端连接到文档服务器的 WebSocket 端点。
  2. 用户在文档中进行修改(如输入字符、删除内容)。
  3. 客户端将修改操作(含位置、内容、用户 ID 等)通过 QWebSocket 发送给服务器。
  4. 服务器验证操作合法性,处理冲突(如同一位置的并发修改),更新文档状态。
  5. 服务器将处理后的修改操作广播给其他用户客户端。
  6. 其他客户端应用该修改,更新本地文档显示。

5.5 实时通知:事件驱动的信息推送

实时通知系统用于向用户推送重要事件(如订单状态更新、新邮件提醒、系统告警)。

  • 场景特点:事件触发式推送,数据量小,对实时性有一定要求。
  • 优势体现:相比邮件、短信等方式,WebSocket 通知更及时且成本更低。
  • 实例:电商平台的订单发货通知、社交媒体的点赞提醒、运维系统的故障告警。
  • 实现方式:
  1. 客户端登录后,通过 WebSocket 连接到通知服务器,关联用户 ID。
  2. 当事件发生时(如订单状态变化),业务系统通知通知服务器。
  3. 通知服务器根据用户 ID 找到对应的 WebSocket 连接,推送通知消息。
  4. 客户端收到通知后,通过弹窗、声音等方式提醒用户。

六、核心接口:QWebSocket 与 QWebSocketServer 的 操作说明

掌握 QWebSocket 和 QWebSocketServer 的核心接口,是实现 WebSocket 通信的关键。本节将详细介绍这两个类的常用接口、信号和使用方法。

6.1 QWebSocket 核心接口

QWebSocket 类提供了连接管理、数据传输、状态查询等功能,以下是其核心接口:

6.1.1 连接管理
  • void connectToUrl(const QUrl &url, const QByteArray &origin = QByteArray())
    功能:连接到指定的 WebSocket 服务器 URL。
    参数:url为服务器地址(格式为ws://host:port/path或wss://host:port/path);origin可选,指定请求的源地址(用于服务器验证)。
    代码如下:
cpp 复制代码
QWebSocket *webSocket = new QWebSocket;
webSocket->connectToUrl(QUrl("ws://localhost:8080/chat"));
  • void close(CloseCode closeCode = CloseCodeNormal, const QString &reason = QString())
    功能:关闭 WebSocket 连接。
    参数:closeCode为关闭状态码(如CloseCodeNormal表示正常关闭);reason为关闭原因描述。
    示例:
cpp 复制代码
webSocket->close(QWebSocket::CloseCodeNormal, "User disconnected");
  • bool isValid() const
    功能:判断连接是否有效(已建立且未关闭)。
    返回值:true表示连接有效,false表示无效。
6.1.2 数据传输
  • void sendTextMessage(const QString &message)
    功能:发送文本消息(UTF-8 编码)。
    参数:message为要发送的文本内容。
cpp 复制代码
webSocket->sendTextMessage("Hello, WebSocket!");
  • void sendBinaryMessage(const QByteArray &data)
    功能:发送二进制数据。
    参数:data为要发送的二进制字节流。
cpp 复制代码
QByteArray imageData = ...; // 二进制图片数据
webSocket->sendBinaryMessage(imageData);
  • void ping(const QByteArray &payload = QByteArray())
    功能:发送 Ping 帧(心跳检测)。
    参数:payload为可选的附加数据(最长 125 字节)。
cpp 复制代码
// 定期发送Ping帧保持连接
QTimer *pingTimer = new QTimer(this);
connect(pingTimer, &QTimer::timeout, [=]() {
    webSocket->ping();
});
pingTimer->start(30000); // 每30秒发送一次
6.1.3 信号
  • void connected()
    触发时机:WebSocket 连接成功建立后。
    用途:连接成功后执行初始化操作(如发送登录信息)。
  • void disconnected()
    触发时机:WebSocket 连接关闭后。
    用途:处理连接关闭后的清理工作(如重连逻辑)。
  • void textMessageReceived(const QString &message)
    触发时机:收到文本消息时。
    用途:处理接收到的文本数据(如解析消息内容)。
  • void binaryMessageReceived(const QByteArray &message)
    触发时机:收到二进制消息时。
    用途:处理接收到的二进制数据(如保存文件)。
  • void errorOccurred(QAbstractSocket::SocketError error)
    触发时机:发生错误时。
    用途:错误处理(如输出错误信息、尝试重连)。
    示例:
cpp 复制代码
connect(webSocket, &QWebSocket::errorOccurred, [=](QAbstractSocket::SocketError error) {
    qDebug() << "WebSocket error:" << webSocket->errorString();
    // 连接失败后重试
    if (error == QAbstractSocket::ConnectionRefusedError) {
        QTimer::singleShot(5000, [=]() {
            webSocket->connectToUrl(QUrl("ws://localhost:8080/chat"));
        });
    }
});
  • void pongReceived(const QByteArray &payload)
    触发时机:收到 Pong 帧响应时。
    用途:确认服务器在线,更新连接状态。
6.1.4 状态与信息查询
  • QAbstractSocket::SocketState state() const
    功能:返回当前连接状态。
    可能值:UnconnectedState(未连接)、ConnectingState(连接中)、ConnectedState(已连接)等。
  • QUrl requestUrl() const
    功能:返回连接的服务器 URL。
  • QHostAddress peerAddress() const
    功能:返回服务器的 IP 地址。
  • quint16 peerPort() const
    功能:返回服务器的端口号。
  • QString errorString() const
    功能:返回最近一次错误的描述信息。

6.2 QWebSocketServer 核心接口

QWebSocketServer 类用于创建 WebSocket 服务器,管理客户端连接,以下是其核心接口:

6.2.1 服务器启动与停止
  • QWebSocketServer(const QString &serverName, SslMode secureMode, QObject *parent = nullptr)
    构造函数:创建 WebSocketServer 实例。
    参数:serverName为服务器名称(用于握手信息);secureMode指定是否使用 SSL(NonSecureMode为 ws,SecureMode为 wss)。
cpp 复制代码
// 创建非加密服务器
QWebSocketServer *server = new QWebSocketServer("Chat Server", QWebSocketServer::NonSecureMode, this);
  • bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
    功能:开始监听指定地址和端口的连接请求。
    参数:address为监听地址(QHostAddress::Any表示监听所有网络接口);port为端口号(0 表示随机端口)。
    返回值:true表示监听成功,false表示失败。
cpp 复制代码
if (server->listen(QHostAddress::Any, 8080)) {
    qDebug() << "Server listening on port 8080";
} else {
    qDebug() << "Server failed to listen:" << server->errorString();
}
  • void close()
    功能:停止监听,关闭所有客户端连接。
6.2.2 客户端连接管理

QWebSocket *nextPendingConnection()

功能:获取下一个待处理的客户端连接。

返回值:指向新连接的 QWebSocket 对象(需手动管理生命周期)。

cpp 复制代码
connect(server, &QWebSocketServer::newConnection, [=]() {
    QWebSocket *client = server->nextPendingConnection();
    qDebug() << "New client connected:" << client->peerAddress().toString();
    // 存储客户端连接
    clients.insert(client);
    // 连接客户端的信号
    connect(client, &QWebSocket::textMessageReceived, this, &Server::onClientMessage);
    connect(client, &QWebSocket::disconnected, this, &Server::onClientDisconnected);
});
  • QList<QWebSocket *> clients() const
    功能:返回当前所有已连接的客户端列表(Qt 5.10+)。
  • void removeClient(QWebSocket *client)
    功能:从服务器客户端列表中移除指定客户端(Qt 5.10+)。
6.2.3 信号
  • void newConnection()
    触发时机:有新的客户端连接请求并完成握手后。
    用途:获取新连接的客户端对象,进行后续处理。
  • void closed()
    触发时机:服务器关闭后。
    用途:处理服务器关闭后的清理工作。
  • void serverError(QWebSocketServer::Error error)
    触发时机:服务器发生错误时。
    用途:处理服务器错误(如监听失败)。
    void peerVerifyError(const QSslError &error)
    触发时机:SSL 握手时客户端证书验证失败(仅用于 wss)。
6.2.4 服务器信息查询
  • quint16 serverPort() const
    功能:返回服务器当前监听的端口号。
  • QHostAddress serverAddress() const
    功能:返回服务器监听的地址。
  • QString errorString() const
    功能:返回最近一次服务器错误的描述信息。
  • bool isListening() const
    功能:判断服务器是否正在监听连接请求。

6.3 SSL/TLS 加密(wss 协议)

对于需要安全传输的场景(如涉及用户隐私、支付信息),可以使用 wss 协议(WebSocket Secure),通过 SSL/TLS 加密数据。

6.3.1 服务器配置 SSL
cpp 复制代码
// 创建支持SSL的服务器
QWebSocketServer *secureServer = new QWebSocketServer("Secure Chat Server", QWebSocketServer::SecureMode, this);

// 加载SSL证书和私钥
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
QSslCertificate cert = QSslCertificate::fromPath("server.crt").first(); // 服务器证书
QSslKey key = QSslKey::fromPath("server.key", QSsl::Rsa); // 私钥
sslConfig.setLocalCertificate(cert);
sslConfig.setPrivateKey(key);
secureServer->setSslConfiguration(sslConfig);

// 监听443端口(wss默认端口)
if (secureServer->listen(QHostAddress::Any, 443)) {
    qDebug() << "Secure server listening on port 443";
}
//注意:生产环境中一般使用权威机构签名的证书,并启用证书验证,避免安全风险。
6.3.2 客户端连接 wss 服务器

七、案例:基于 QWebSocket 的实时聊天系统

通过一个完整的案例 ------ 实时聊天系统,展示 QWebSocket 和 QWebSocketServer 的具体应用。该系统包含服务器和客户端两部分,支持多人聊天、发送文本消息和图片。

7.1 系统设计

7.1.1 功能需求

服务器:监听客户端连接,转发客户端消息给其他用户,管理在线用户。

客户端:连接服务器,发送文本消息和图片,接收并显示其他用户的消息。

7.1.2 消息格式

采用 JSON 格式封装消息,包含以下类型:

登录消息:{"type":"login","username":"Admin"}

文本消息:{"type":"text","username":"Admin","content":"Hello","timestamp":"2025-08-05 12:00:00"}

图片消息:{"type":"image","username":"Admin","filename":"photo.png","data":"base64编码的图片数据","timestamp":"2023-10-01 12:01:00"}

用户列表更新:{"type":"userList","users":["Admin","Bob"]}

7.2 服务器实现

7.2.1 服务器类定义(Server.h)
cpp 复制代码
#ifndef SERVER_H
#define SERVER_H

#include <QObject>
#include <QWebSocketServer>
#include <QWebSocket>
#include <QSet>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>

class Server : public QObject
{
    Q_OBJECT
public:
    explicit Server(quint16 port, QObject *parent = nullptr);
    ~Server();

private slots:
    void onNewConnection();
    void onTextMessageReceived(const QString &message);
    void onBinaryMessageReceived(const QByteArray &data);
    void onClientDisconnected();

private:
    void sendUserList();
    QJsonObject createMessage(const QString &type, const QString &username, const QString &content = QString());
    QWebSocketServer *m_webSocketServer;
    QSet<QWebSocket *> m_clients; // 存储所有客户端连接
    QMap<QWebSocket *, QString> m_userNames; // 客户端与用户名的映射
};

#endif // SERVER_H
7.2.2 服务器实现(Server.cpp)
cpp 复制代码
#include "Server.h"
#include <QDebug>

Server::Server(quint16 port, QObject *parent) : QObject(parent)
{
    // 创建WebSocket服务器
    m_webSocketServer = new QWebSocketServer("Chat Server", QWebSocketServer::NonSecureMode, this);
    // 监听指定端口
    if (m_webSocketServer->listen(QHostAddress::Any, port)) {
        qDebug() << "Server started on port" << port;
        // 连接新客户端信号
        connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &Server::onNewConnection);
    } else {
        qDebug() << "Server failed to start:" << m_webSocketServer->errorString();
    }
}

Server::~Server()
{
    m_webSocketServer->close();
    qDeleteAll(m_clients.begin(), m_clients.end());
}

void Server::onNewConnection()
{
    // 获取新连接的客户端
    QWebSocket *client = m_webSocketServer->nextPendingConnection();
    if (!client) return;

    qDebug() << "New client connected:" << client->peerAddress().toString();

    // 存储客户端连接
    m_clients.insert(client);

    // 连接客户端的信号
    connect(client, &QWebSocket::textMessageReceived, this, &Server::onTextMessageReceived);
    connect(client, &QWebSocket::binaryMessageReceived, this, &Server::onBinaryMessageReceived);
    connect(client, &QWebSocket::disconnected, this, &Server::onClientDisconnected);
    connect(client, &QWebSocket::disconnected, client, &QWebSocket::deleteLater);
}

void Server::onTextMessageReceived(const QString &message)
{
    QWebSocket *senderClient = qobject_cast<QWebSocket *>(sender());
    if (!senderClient) return;

    // 解析JSON消息
    QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
    if (doc.isNull()) {
        qDebug() << "Invalid JSON message:" << message;
        return;
    }

    QJsonObject obj = doc.object();
    QString type = obj["type"].toString();

    if (type == "login") {
        // 处理登录消息
        QString username = obj["username"].toString();
        if (!username.isEmpty()) {
            m_userNames[senderClient] = username;
            qDebug() << "User logged in:" << username;
            // 广播用户列表更新
            sendUserList();
            // 发送欢迎消息
            QJsonObject welcomeMsg = createMessage("system", "Server", "Welcome, " + username + "!");
            senderClient->sendTextMessage(QJsonDocument(welcomeMsg).toJson(QJsonDocument::Compact));
        }
    } else if (type == "text") {
        // 处理文本消息,转发给其他客户端
        if (m_userNames.contains(senderClient)) {
            QString username = m_userNames[senderClient];
            QString content = obj["content"].toString();
            QJsonObject forwardMsg = createMessage("text", username, content);
            QString forwardStr = QJsonDocument(forwardMsg).toJson(QJsonDocument::Compact);

            // 转发给所有其他客户端
            foreach (QWebSocket *client, m_clients) {
                if (client != senderClient) {
                    client->sendTextMessage(forwardStr);
                }
            }
        }
    }
}

void Server::onBinaryMessageReceived(const QByteArray &data)
{
    // 本案例中二进制数据主要用于图片,通过文本消息中的Base64编码传输
    // 此处可扩展为直接传输二进制文件
    QWebSocket *senderClient = qobject_cast<QWebSocket *>(sender());
    if (senderClient) {
        qDebug() << "Received binary data from" << m_userNames.value(senderClient, "unknown") 
                 << "size:" << data.size();
    }
}

void Server::onClientDisconnected()
{
    QWebSocket *client = qobject_cast<QWebSocket *>(sender());
    if (client && m_clients.contains(client)) {
        QString username = m_userNames.value(client, "unknown");
        qDebug() << "Client disconnected:" << username;

        // 移除客户端
        m_clients.remove(client);
        m_userNames.remove(client);

        // 广播用户列表更新
        sendUserList();

        // 广播用户离开消息
        QJsonObject leaveMsg = createMessage("system", "Server", username + " has left the chat.");
        QString leaveStr = QJsonDocument(leaveMsg).toJson(QJsonDocument::Compact);
        foreach (QWebSocket *c, m_clients) {
            c->sendTextMessage(leaveStr);
        }
    }
}

void Server::sendUserList()
{
    // 构建用户列表消息
    QJsonObject userListMsg;
    userListMsg["type"] = "userList";
    QJsonArray usersArray;
    foreach (const QString &username, m_userNames.values()) {
        usersArray.append(username);
    }
    userListMsg["users"] = usersArray;

    // 发送给所有客户端
    QString userListStr = QJsonDocument(userListMsg).toJson(QJsonDocument::Compact);
    foreach (QWebSocket *client, m_clients) {
        client->sendTextMessage(userListStr);
    }
}

QJsonObject Server::createMessage(const QString &type, const QString &username, const QString &content)
{
    QJsonObject msg;
    msg["type"] = type;
    msg["username"] = username;
    msg["timestamp"] = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    if (!content.isEmpty()) {
        msg["content"] = content;
    }
    return msg;
}
7.2.3 服务器主函数(main.cpp)
cpp 复制代码
#include <QCoreApplication>
#include "Server.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // 创建服务器,监听8080端口
    Server server(8080);

    return a.exec();
}

7.3 客户端实现

客户端使用 Qt Widgets 构建界面,包含登录窗口、聊天窗口(消息显示区、输入区、发送按钮、发送图片按钮)。

7.3.1 客户端类定义(Client.h)
cpp 复制代码
#ifndef CLIENT_H
#define CLIENT_H

#include <QWidget>
#include <QWebSocket>
#include <QFileDialog>
#include <QJsonObject>
#include <QJsonDocument>

QT_BEGIN_NAMESPACE
namespace Ui { class Client; }
QT_END_NAMESPACE

class Client : public QWidget
{
    Q_OBJECT

public:
    Client(QWidget *parent = nullptr);
    ~Client();

private slots:
    void on_loginButton_clicked();
    void on_sendButton_clicked();
    void on_sendImageButton_clicked();
    void onConnected();
    void onDisconnected();
    void onTextMessageReceived(const QString &message);
    void onBinaryMessageReceived(const QByteArray &data);
    void onErrorOccurred(QAbstractSocket::SocketError error);

private:
    void connectToServer();
    void sendLoginMessage();
    void sendTextMessage(const QString &content);
    void sendImageMessage(const QString &imagePath);
    void addMessageToDisplay(const QString &username, const QString &content, const QString &timestamp, bool isImage = false, const QString &imagePath = QString());

    Ui::Client *ui;
    QWebSocket *m_webSocket;
    QString m_username;
    QString m_serverUrl;
};
#endif // CLIENT_H
7.3.2 客户端实现(Client.cpp)
cpp 复制代码
#include "Client.h"
#include "ui_Client.h"
#include <QDebug>
#include <QPixmap>
#include <QDateTime>
#include <QScrollBar>

Client::Client(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Client)
    , m_webSocket(nullptr)
{
    ui->setupUi(this);
    setWindowTitle("Chat Client");

    // 初始化UI
    ui->chatWidget->setEnabled(false);
    ui->serverUrlEdit->setText("ws://localhost:8080");

    // 创建WebSocket对象
    m_webSocket = new QWebSocket;

    // 连接信号
    connect(m_webSocket, &QWebSocket::connected, this, &Client::onConnected);
    connect(m_webSocket, &QWebSocket::disconnected, this, &Client::onDisconnected);
    connect(m_webSocket, &QWebSocket::textMessageReceived, this, &Client::onTextMessageReceived);
    connect(m_webSocket, &QWebSocket::binaryMessageReceived, this, &Client::onBinaryMessageReceived);
    connect(m_webSocket, &QWebSocket::errorOccurred, this, &Client::onErrorOccurred);
}

Client::~Client()
{
    if (m_webSocket) {
        m_webSocket->close();
        delete m_webSocket;
    }
    delete ui;
}

void Client::on_loginButton_clicked()
{
    m_username = ui->usernameEdit->text().trimmed();
    m_serverUrl = ui->serverUrlEdit->text().trimmed();

    if (m_username.isEmpty()) {
        ui->statusLabel->setText("请输入用户名");
        return;
    }

    if (m_serverUrl.isEmpty()) {
        ui->statusLabel->setText("请输入服务器地址");
        return;
    }

    // 禁用登录控件,显示连接中
    ui->loginWidget->setEnabled(false);
    ui->statusLabel->setText("连接中...");

    // 连接服务器
    connectToServer();
}

void Client::on_sendButton_clicked()
{
    QString content = ui->messageEdit->text().trimmed();
    if (content.isEmpty()) return;

    // 发送文本消息
    sendTextMessage(content);

    // 清空输入框
    ui->messageEdit->clear();
}

void Client::on_sendImageButton_clicked()
{
    // 打开文件选择对话框
    QString imagePath = QFileDialog::getOpenFileName(this, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp)");
    if (imagePath.isEmpty()) return;

    // 发送图片消息
    sendImageMessage(imagePath);
}

void Client::onConnected()
{
    ui->statusLabel->setText("已连接到服务器");
    ui->chatWidget->setEnabled(true);
    // 发送登录消息
    sendLoginMessage();
}

void Client::onDisconnected()
{
    ui->statusLabel->setText("与服务器断开连接");
    ui->chatWidget->setEnabled(false);
    ui->loginWidget->setEnabled(true);
}

void Client::onTextMessageReceived(const QString &message)
{
    QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
    if (doc.isNull()) {
        qDebug() << "Invalid JSON message:" << message;
        return;
    }

    QJsonObject obj = doc.object();
    QString type = obj["type"].toString();

    if (type == "text" || type == "system") {
        // 处理文本消息
        QString username = obj["username"].toString();
        QString content = obj["content"].toString();
        QString timestamp = obj["timestamp"].toString();
        addMessageToDisplay(username, content, timestamp);
    } else if (type == "userList") {
        // 处理用户列表更新
        QJsonArray usersArray = obj["users"].toArray();
        QString userListStr = "在线用户: ";
        for (int i = 0; i < usersArray.size(); ++i) {
            if (i > 0) userListStr += ", ";
            userListStr += usersArray[i].toString();
        }
        ui->userListLabel->setText(userListStr);
    } else if (type == "image") {
        // 处理图片消息
        QString username = obj["username"].toString();
        QString filename = obj["filename"].toString();
        QString base64Data = obj["data"].toString();
        QString timestamp = obj["timestamp"].toString();

        // 将Base64数据转换为图片并保存
        QByteArray imageData = QByteArray::fromBase64(base64Data.toUtf8());
        QString tempPath = QDir::tempPath() + "/" + filename;
        QFile file(tempPath);
        if (file.open(QIODevice::WriteOnly)) {
            file.write(imageData);
            file.close();
            addMessageToDisplay(username, filename, timestamp, true, tempPath);
        }
    }
}

void Client::onBinaryMessageReceived(const QByteArray &data)
{
    qDebug() << "Received binary data, size:" << data.size();
    // 可扩展为直接处理二进制图片
}

void Client::onErrorOccurred(QAbstractSocket::SocketError error)
{
    ui->statusLabel->setText("错误: " + m_webSocket->errorString());
    ui->loginWidget->setEnabled(true);
}

void Client::connectToServer()
{
    if (m_webSocket->state() == QAbstractSocket::ConnectedState) {
        m_webSocket->close();
    }
    m_webSocket->connectToUrl(QUrl(m_serverUrl));
}

void Client::sendLoginMessage()
{
    QJsonObject loginMsg;
    loginMsg["type"] = "login";
    loginMsg["username"] = m_username;
    m_webSocket->sendTextMessage(QJsonDocument(loginMsg).toJson(QJsonDocument::Compact));
}

void Client::sendTextMessage(const QString &content)
{
    QJsonObject textMsg;
    textMsg["type"] = "text";
    textMsg["username"] = m_username;
    textMsg["content"] = content;
    textMsg["timestamp"] = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
    m_webSocket->sendTextMessage(QJsonDocument(textMsg).toJson(QJsonDocument::Compact));

    // 在本地显示自己发送的消息
    addMessageToDisplay(m_username, content, textMsg["timestamp"].toString());
}

void Client::sendImageMessage(const QString &imagePath)
{
    QFile file(imagePath);
    if (!file.open(QIODevice::ReadOnly)) {
        ui->statusLabel->setText("无法打开图片文件");
        return;
    }

    // 读取图片数据并编码为Base64
    QByteArray imageData = file.readAll();
    file.close();
    QString base64Data = imageData.toBase64();

    // 构建图片消息
    QJsonObject imageMsg;
    imageMsg["type"] = "image";
    imageMsg["username"] = m_username;
    imageMsg["filename"] = QFileInfo(imagePath).fileName();
    imageMsg["data"] = base64Data;
    imageMsg["timestamp"] = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");

    m_webSocket->sendTextMessage(QJsonDocument(imageMsg).toJson(QJsonDocument::Compact));

    // 在本地显示自己发送的图片
    addMessageToDisplay(m_username, imageMsg["filename"].toString(), imageMsg["timestamp"].toString(), true, imagePath);
}

void Client::addMessageToDisplay(const QString &username, const QString &content, const QString &timestamp, bool isImage, const QString &imagePath)
{
    // 构建消息显示HTML
    QString html;
    html += QString("<p><strong>%1</strong> <span style='color: #666; font-size: 8pt;'>%2</span></p>")
            .arg(username).arg(timestamp);

    if (isImage) {
        // 显示图片
        QPixmap pixmap(imagePath);
        if (!pixmap.isNull()) {
            // 缩放图片适应显示区域
            pixmap = pixmap.scaled(300, 300, Qt::KeepAspectRatio, Qt::SmoothTransformation);
            // 保存缩放后的图片到临时文件(用于HTML显示)
            QString tempImgPath = QDir::tempPath() + "/chat_" + QDateTime::currentDateTime().toString("yyyyMMddhhmmss") + ".png";
            pixmap.save(tempImgPath);
            html += QString("<p><img src='file:///%1' /></p>").arg(tempImgPath);
        } else {
            html += QString("<p>[无法显示图片: %1]</p>").arg(content);
        }
    } else {
        // 显示文本
        html += QString("<p>%1</p>").arg(content);
    }

    // 添加分隔线
    html += "<hr />";

    // 追加到显示区域
    ui->messageDisplay->insertHtml(html);

    // 滚动到底部
    QScrollBar *scrollBar = ui->messageDisplay->verticalScrollBar();
    scrollBar->setValue(scrollBar->maximum());
}
7.3.3 客户端 UI 设计(client.ui)

UI 设计使用 Qt Designer 完成,主要包含:

  • 登录区域(loginWidget):包含用户名输入框(usernameEdit)、服务器地址输入框(serverUrlEdit)、登录按钮(loginButton)。
  • 聊天区域(chatWidget):包含消息显示文本框(messageDisplay,设置为只读)、用户列表标签(userListLabel)、消息输入框(messageEdit)、发送按钮(sendButton)、发送图片按钮(sendImageButton)。
  • 状态标签(statusLabel):显示连接状态和错误信息。

7.4 系统测试与运行

  • 编译服务器:将服务器代码编译为控制台应用程序,运行后显示 "Server started on port 8080"。
  • 编译客户端:将客户端代码编译为带界面的应用程序,运行后显示登录窗口。
  • 多客户端连接:启动多个客户端,输入不同用户名(如 "Alice"、"Bob")和服务器地址 "ws://localhost:8080",点击登录。
  • 发送文本消息:在客户端输入框中输入文字,点击 "发送",其他客户端应能收到并显示消息。
  • 发送图片:点击 "发送图片",选择一张图片,其他客户端应能收到并显示图片。
  • 用户列表更新:新用户登录或用户退出时,所有客户端的在线用户列表应实时更新。

八、总结

掌握了 WebSocket 协议以及 Qt 中的 QWebSocket 和 QWebSocketServer 类,并且通过QWebSocket 实现客户端连接服务器进行数据传输后,可以进一步探索以下进阶内容:

  • 负载均衡:对于高并发场景,单台 WebSocket 服务器可能无法处理大量连接,可使用负载均衡器(如 Nginx)分发连接到多台服务器,并通过共享内存或消息队列实现服务器间的消息同步。
    断线重连:实现智能重连机制,根据网络状况调整重连间隔,重连时恢复会话状态(如用户登录信息)。
  • 消息确认与重试:在可靠性要求高的场景(如金融交易),实现消息确认机制,发送方未收到确认时自动重试。
  • WebSocket 子协议:通过setSubprotocol()方法协商子协议(如 JSON-RPC、STOMP),规范客户端与服务器的交互格式。

最后,QWebSocket 和 QWebSocketServer 为 Qt 开发者提供了强大的实时通信工具,通过封装 WebSocket 协议的复杂性,让开发者能够轻松构建高效、实时的网络应用。无论是简单的聊天工具,还是复杂的实时监控系统,掌握这两个类的使用都能为项目开发带来极大便利。

相关推荐
ZPC82101 小时前
参数服务器 server and client
服务器·qt
上单带刀不带妹2 小时前
Node.js 中的 fs 模块详解:文件系统操作全掌握
开发语言·javascript·node.js·fs模块
chenglin0162 小时前
制造业ERP系统架构设计方案(基于C#生态)
开发语言·系统架构·c#
凌晨7点2 小时前
控制建模matlab练习13:线性状态反馈控制器-②系统的能控性
开发语言·matlab
要记得喝水3 小时前
汇编中常用寄存器介绍
开发语言·汇编·windows·c#·.net
shi57833 小时前
C# 常用的线程同步方式
开发语言·后端·c#
凌晨7点3 小时前
控制建模matlab练习11:伯德图
开发语言·matlab
上海云盾商务经理杨杨3 小时前
2025年高防IP隐身术:四层架构拆解源站IP“消失之谜”
网络协议·tcp/ip·网络安全·架构
freed_Day4 小时前
Java学习进阶--集合体系结构
java·开发语言·学习