Qt C++ WebSocket本地服务开发:从入门到生产环境部署 1. 项目概述为什么要在QtC中搞WebSocket本地服务如果你正在用Qt开发一个需要实时数据交互的桌面应用比如一个股票行情监控工具、一个局域网内的即时通讯软件或者一个需要从嵌入式设备接收实时数据的控制台那么传统的HTTP轮询Polling或者长轮询Long Polling可能会让你头疼。频繁的请求-响应不仅浪费带宽还会带来显著的延迟。这时候WebSocket就该登场了。WebSocket协议提供了一种在单个TCP连接上进行全双工通信的通道。简单来说就像在客户端和服务器之间拉了一根“电话线”双方可以随时主动说话而不需要每次都先“拨号”建立HTTP连接。这对于需要服务器主动推送数据Server Push的场景是再合适不过了。Qt作为一个成熟的跨平台C框架其Qt WebSockets模块为我们提供了开箱即用的WebSocket支持。它封装了底层的协议细节让我们能用熟悉的Qt信号槽机制来处理连接、收发消息等事件开发体验非常顺畅。今天我就结合自己多次在项目中集成qtwebsocket的经验详细拆解如何用它来构建一个稳定、高效的本地WebSocket服务。无论是用于进程间通信还是作为一个小型后端服务供其他客户端如Web前端连接这套方案都经得起考验。2. 核心思路与架构设计在动手写代码之前理清架构思路至关重要。一个基于Qt WebSockets的本地服务其核心组件和交互流程并不复杂但设计上的考量决定了后续的扩展性和维护成本。2.1 服务端核心类QWebSocketServerQWebSocketServer是整个服务端的心脏。它的角色类似于QTcpServer负责监听特定的网络端口接受来自客户端的WebSocket连接请求。一旦握手成功它会为每个新连接创建一个QWebSocket对象这个对象代表了与那个特定客户端的通信通道。选择QWebSocketServer时你需要决定两件事服务器名称这只是一个标识符会出现在WebSocket握手阶段的HTTP头中通常填写你的应用名即可。安全模式这是关键选择。QWebSocketServer::NonSecureMode使用普通的ws://协议。这是我们构建本地服务localhost时最常用的模式。因为通信完全发生在同一台机器内部不经过外部网络使用非加密模式可以省去TLS/SSL证书的配置开销性能也稍好。QWebSocketServer::SecureMode使用加密的wss://协议。如果你的服务需要被局域网内其他机器访问或者未来有部署到公网的可能强烈建议从一开始就使用安全模式。虽然本地开发时配置证书稍显麻烦但这能确保通信的机密性和完整性避免中间人攻击。注意即使是在本地如果你的应用会处理敏感信息如用户凭证、配置密码使用SecureMode也是一个好习惯。Qt支持使用自签名证书进行本地加密通信。2.2 客户端连接对象QWebSocket每个成功的连接都会对应一个QWebSocket对象。这个对象是双向通信的端点接收数据通过textMessageReceived和binaryMessageReceived信号。发送数据调用sendTextMessage或sendBinaryMessage方法。管理连接可以主动close连接也会在连接断开时触发disconnected信号。服务端需要维护一个所有已连接QWebSocket对象的列表例如用QListQWebSocket*以便进行广播向所有客户端发送消息或定向发送。2.3 典型工作流程初始化与监听创建QWebSocketServer实例调用listen方法绑定到特定IP和端口例如QHostAddress::LocalHost和8080。接受连接连接QWebSocketServer::newConnection信号到一个槽函数。在该槽函数中调用nextPendingConnection获取新连接的QWebSocket指针并将其存入连接列表。处理消息将新QWebSocket对象的textMessageReceived等信号连接到对应的处理槽函数。业务逻辑处理在槽函数中解析收到的消息通常是JSON或自定义协议执行业务逻辑然后通过该QWebSocket对象或遍历连接列表发送响应。清理资源连接QWebSocket的disconnected信号在槽函数中将其从连接列表中移除并调用deleteLater进行销毁防止内存泄漏。这个流程清晰地将网络层连接管理和业务层消息处理解耦是Qt事件驱动编程模型的典型体现。3. 环境准备与项目配置在开始编码前确保你的开发环境已经就绪。这里没有太多玄学但细节决定成败。3.1 Qt版本与模块引入首先确认你的Qt版本。Qt WebSockets模块在Qt 5.3及以上版本中作为附加模块提供在Qt 6中已是核心模块的一部分。对于Qt 5你需要手动在安装时勾选该模块或者通过包管理器安装如Linux下的qtwebsockets5-dev。在你的项目配置文件.pro文件中必须添加对应的模块引用# 对于 Qt 5 QT core gui websockets # 对于 Qt 6websockets通常已包含在core或network中但显式声明更安全 QT core gui websockets如果是使用CMake构建则在CMakeLists.txt中find_package(Qt6 COMPONENTS Core Gui WebSockets REQUIRED) target_link_libraries(your_target PRIVATE Qt6::Core Qt6::Gui Qt6::WebSockets)一个常见的坑是忘记添加websockets模块导致编译时找不到QWebSocketServer等类定义。编译前务必检查。3.2 构建一个最小化的服务端骨架让我们从一个最简单的、能跑通的服务端代码开始。这个例子只实现回显Echo功能即客户端发什么服务端就原样发回去。但它包含了所有核心环节。// websocket_server.h #ifndef WEBSOCKETSERVER_H #define WEBSOCKETSERVER_H #include QObject #include QWebSocketServer #include QWebSocket #include QList class WebSocketServer : public QObject { Q_OBJECT public: explicit WebSocketServer(quint16 port, QObject *parent nullptr); ~WebSocketServer(); private slots: void onNewConnection(); void onTextMessageReceived(const QString message); void onSocketDisconnected(); private: QWebSocketServer *m_pWebSocketServer; QListQWebSocket * m_clients; // 保存所有客户端连接 }; #endif // WEBSOCKETSERVER_H// websocket_server.cpp #include websocket_server.h #include QtCore/QDebug WebSocketServer::WebSocketServer(quint16 port, QObject *parent) : QObject(parent) , m_pWebSocketServer(new QWebSocketServer(QStringLiteral(Echo Server), QWebSocketServer::NonSecureMode, this)) { // 尝试监听指定端口 if (m_pWebSocketServer-listen(QHostAddress::LocalHost, port)) { qDebug() WebSocket server listening on port port; // 连接新客户端信号 connect(m_pWebSocketServer, QWebSocketServer::newConnection, this, WebSocketServer::onNewConnection); } else { qCritical() Failed to start server: m_pWebSocketServer-errorString(); } } WebSocketServer::~WebSocketServer() { // 关闭服务器会自动断开所有客户端 m_pWebSocketServer-close(); // 清理客户端列表 qDeleteAll(m_clients.begin(), m_clients.end()); } void WebSocketServer::onNewConnection() { // 获取新连接的socket对象 QWebSocket *pSocket m_pWebSocketServer-nextPendingConnection(); if (!pSocket) { return; } qDebug() Client connected from: pSocket-peerAddress().toString(); // 将新客户端加入列表 m_clients.append(pSocket); // 连接信号接收文本消息、断开连接 connect(pSocket, QWebSocket::textMessageReceived, this, WebSocketServer::onTextMessageReceived); connect(pSocket, QWebSocket::disconnected, this, WebSocketServer::onSocketDisconnected); } void WebSocketServer::onTextMessageReceived(const QString message) { // 获取发送消息的socket对象通过QObject::sender() QWebSocket *pClient qobject_castQWebSocket *(sender()); if (!pClient) { return; } qDebug() Message from client: message; // 回显消息给发送者 pClient-sendTextMessage(QStringLiteral(Echo: %1).arg(message)); } void WebSocketServer::onSocketDisconnected() { QWebSocket *pClient qobject_castQWebSocket *(sender()); if (!pClient) { return; } qDebug() Client disconnected: pClient-peerAddress().toString(); // 从列表中移除 m_clients.removeAll(pClient); // 删除对象防止内存泄漏 pClient-deleteLater(); }这个骨架代码虽然简单但已经是一个功能完整的WebSocket服务器。你可以在main函数中实例化它指定一个端口如12345然后运行。接下来我们需要用客户端来测试它。3.3 快速测试使用简单的Web客户端由于WebSocket是标准协议我们不需要专门写Qt客户端来测试。利用浏览器内置的JavaScript API就能快速验证。创建一个简单的test.html文件!DOCTYPE html html head titleWebSocket Test Client/title /head body input typetext idmessageInput placeholder输入消息 button onclicksendMessage()发送/button br div idoutput stylemargin-top: 20px; border:1px solid #ccc; min-height:100px;/div script const socket new WebSocket(ws://localhost:12345); socket.onopen function(event) { log(连接已建立); }; socket.onmessage function(event) { log(收到服务端回复: event.data); }; socket.onerror function(error) { log(连接错误: error); }; socket.onclose function(event) { log(连接关闭); }; function sendMessage() { const input document.getElementById(messageInput); const message input.value; if (message) { socket.send(message); log(已发送: message); input.value ; } } function log(text) { const output document.getElementById(output); output.innerHTML p text /p; } /script /body /html用浏览器打开这个HTML文件点击发送如果看到“Echo: xxx”的回复恭喜你第一个WebSocket服务已经跑通了这个测试方法在开发调试阶段极其高效。4. 核心功能实现与进阶技巧基础的回显服务只是个开始。在实际项目中我们需要处理更复杂的场景比如协议设计、广播、心跳保活等。4.1 设计一个简单的应用层协议直接发送纯文本不利于结构化数据的交换。通常我们会定义一种简单的应用层协议最流行的就是使用JSON。假设我们开发一个简单的“指令-响应”式服务协议格式可以这样定义// 客户端发送的指令 { cmd: get_status, // 指令类型 id: 123, // 请求ID用于匹配响应 params: {} // 可选参数 } // 服务端响应 { cmd: resp, id: 123, // 对应请求ID code: 0, // 错误码0表示成功 data: { // 响应数据 status: running, uptime: 3600 } }在服务端的消息处理函数中我们需要解析JSON并路由到不同的处理函数void WebSocketServer::onTextMessageReceived(const QString message) { QWebSocket *pClient qobject_castQWebSocket *(sender()); if (!pClient) { return; } QJsonParseError parseError; QJsonDocument doc QJsonDocument::fromJson(message.toUtf8(), parseError); if (parseError.error ! QJsonParseError::NoError) { // 协议解析错误 sendError(pClient, -1, Invalid JSON format); return; } if (!doc.isObject()) { sendError(pClient, -1, Message must be a JSON object); return; } QJsonObject obj doc.object(); QString cmd obj.value(cmd).toString(); int reqId obj.value(id).toInt(); // 根据指令类型路由 if (cmd get_status) { handleGetStatus(pClient, reqId); } else if (cmd set_config) { handleSetConfig(pClient, reqId, obj.value(params).toObject()); } else { sendError(pClient, reqId, QString(Unknown command: %1).arg(cmd)); } } void WebSocketServer::handleGetStatus(QWebSocket *client, int reqId) { QJsonObject data; data[status] running; data[uptime] QDateTime::currentSecsSinceEpoch() - m_startTime; QJsonObject resp; resp[cmd] resp; resp[id] reqId; resp[code] 0; resp[data] data; sendJson(client, resp); } void WebSocketServer::sendJson(QWebSocket *client, const QJsonObject obj) { QJsonDocument doc(obj); client-sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact))); } void WebSocketServer::sendError(QWebSocket *client, int reqId, const QString msg) { QJsonObject resp; resp[cmd] resp; resp[id] reqId; resp[code] -1; resp[error] msg; sendJson(client, resp); }这样服务端的业务逻辑就变得清晰且易于扩展。每新增一个指令只需添加一个handleXXX函数并在路由中添加分支即可。4.2 实现消息广播与连接管理向所有连接的客户端广播消息是一个常见需求例如在聊天室或实时数据监控中。有了我们维护的m_clients列表这很简单void WebSocketServer::broadcastTextMessage(const QString message) { for (QWebSocket *client : qAsConst(m_clients)) { // 检查连接状态避免向已断开的连接发送 if (client-state() QAbstractSocket::ConnectedState) { client-sendTextMessage(message); } } } void WebSocketServer::broadcastJson(const QJsonObject obj) { QJsonDocument doc(obj); QString message QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); broadcastTextMessage(message); }但这里有个重要的细节QWebSocket::sendTextMessage是异步的它会将数据放入底层的写缓冲区。如果客户端网络极差或已断开但未及时触发disconnected信号持续快速调用sendTextMessage可能导致缓冲区积压最终内存耗尽。一个稳健的做法是检查socket-bytesToWrite()如果积压过多可以采取丢弃旧消息或断开连接的策略。4.3 心跳机制与连接健康度维护在网络不稳定的环境或者客户端可能意外崩溃如进程被强制结束的情况下TCP连接可能不会立即被操作系统检测为断开。这会导致服务端一直维护着一个“僵尸连接”。实现一个简单的心跳Ping-Pong机制可以有效解决这个问题。WebSocket协议本身定义了Ping/Pong帧用于保活。QWebSocket类提供了ping方法。我们可以启动一个定时器定期向所有客户端发送Ping并期待Pong回复。如果一个客户端在超时时间内没有回复Pong则认为其已断开主动清理。// 在WebSocketServer类中添加 private slots: void onPingTimeout(); private: QTimer *m_pingTimer; QMapQWebSocket*, QDateTime m_lastPongTime; // 记录每个客户端最后一次Pong时间 // 在构造函数中初始化定时器 m_pingTimer new QTimer(this); connect(m_pingTimer, QTimer::timeout, this, WebSocketServer::onPingTimeout); m_pingTimer-start(30000); // 每30秒发送一次Ping // 在新连接建立时初始化记录并连接pong信号 void WebSocketServer::onNewConnection() { QWebSocket *pSocket m_pWebSocketServer-nextPendingConnection(); // ... 其他连接代码 ... m_lastPongTime[pSocket] QDateTime::currentDateTime(); // 初始时间 connect(pSocket, QWebSocket::pong, this, [this, pSocket](quint64 elapsedTime, const QByteArray payload) { // 收到Pong更新最后活跃时间 m_lastPongTime[pSocket] QDateTime::currentDateTime(); qDebug() Received pong from client, latency: elapsedTime ms; }); } // 定时器超时槽函数 void WebSocketServer::onPingTimeout() { QDateTime now QDateTime::currentDateTime(); for (auto it m_lastPongTime.begin(); it ! m_lastPongTime.end(); ) { QWebSocket *client it.key(); if (it.value().secsTo(now) 60) { // 超过60秒没收到Pong qWarning() Client heartbeat timeout, disconnecting: client-peerAddress().toString(); client-close(QWebSocketProtocol::CloseCodeAbnormalDisconnection, Heartbeat timeout); // 注意实际断开和清理会在 onSocketDisconnected 中处理 it m_lastPongTime.erase(it); // 从心跳Map中移除 } else { // 发送Ping client-ping(); it; } } } // 在连接断开时记得从心跳Map中移除 void WebSocketServer::onSocketDisconnected() { QWebSocket *pClient qobject_castQWebSocket *(sender()); // ... 其他清理代码 ... m_lastPongTime.remove(pClient); // 关键 }这个心跳机制能确保连接池的清洁避免资源泄漏。超时时间如60秒可以根据你的网络环境和应用容忍度进行调整。5. 性能优化与生产环境考量当你的本地服务需要处理大量连接或高频消息时一些优化措施就变得必要。5.1 使用二进制消息传递对于传输大量数据如图片、文件片段、序列化的结构化数据二进制帧binaryMessageReceived/sendBinaryMessage比文本帧效率更高因为它避免了文本编码/解码的开销。你可以使用QByteArray直接发送原始数据或者使用QDataStream与QByteArray配合来序列化复杂的C结构体。// 发送二进制数据示例 QByteArray buffer; QDataStream stream(buffer, QIODevice::WriteOnly); stream.setVersion(QDataStream::Qt_DefaultCompiledVersion); stream someInt someString someVector; pClient-sendBinaryMessage(buffer); // 接收端 void onBinaryMessageReceived(const QByteArray message) { QDataStream stream(message); int val1; QString val2; QVectordouble val3; stream val1 val2 val3; // ... 处理数据 }5.2 异步处理与线程模型默认情况下所有网络事件新连接、收数据和你的业务处理代码都在同一个线程通常是主线程中执行。如果某个消息的处理非常耗时比如复杂的计算或阻塞的IO它会阻塞整个事件循环导致其他客户端请求得不到及时响应服务器看起来就像“卡住”了。解决方案是将耗时的业务逻辑移到单独的线程或线程池中处理。Qt的QtConcurrent或QThreadPool非常适合这种场景。// 在消息处理函数中将耗时任务提交到线程池 void WebSocketServer::onTextMessageReceived(const QString message) { QWebSocket *pClient qobject_castQWebSocket *(sender()); QJsonDocument doc QJsonDocument::fromJson(message.toUtf8()); QJsonObject obj doc.object(); // 使用QtConcurrent在后台线程处理 QFutureQJsonObject future QtConcurrent::run([obj]() { // 这里是耗时的业务处理例如查询数据库、复杂计算 QThread::sleep(2); // 模拟耗时操作 QJsonObject result; result[processed] true; result[original_cmd] obj[cmd].toString(); return result; }); // 使用QFutureWatcher来监听任务完成 QFutureWatcherQJsonObject *watcher new QFutureWatcherQJsonObject(this); connect(watcher, QFutureWatcherQJsonObject::finished, this, [this, pClient, watcher]() { QJsonObject result watcher-result(); // 注意这个lambda是在主线程对象所属线程执行的可以安全操作GUI或发送消息 sendJson(pClient, result); watcher-deleteLater(); }); watcher-setFuture(future); }这样网络IO线程永远不会被阻塞服务器的响应能力得到极大提升。记住在子线程中不要直接操作QWebSocket对象它不是线程安全的必须通过信号槽或在线程完成后回到主线程再发送数据。5.3 资源管理与防内存泄漏WebSocket服务器是长时间运行的服务内存管理必须严谨。及时删除断开的QWebSocket对象务必在disconnected信号的槽函数中调用deleteLater()。清理连接列表在析构函数中除了关闭服务器还要清空m_clients列表。使用qDeleteAll可以方便地删除所有指针指向的对象。避免循环引用如果在一个连接对象中持有了服务端对象的引用比如通过lambda捕获this要确保断开连接后这些引用能被正确释放否则会导致服务端对象无法被销毁。6. 常见问题排查与调试实录在实际开发中你肯定会遇到各种问题。下面是我踩过的一些坑和解决方法。6.1 连接失败QWebSocketServer无法监听端口症状listen()返回falseerrorString()提示“Address already in use”或“Permission denied”。排查端口占用使用命令行工具检查端口是否被其他进程占用。Windows:netstat -ano | findstr :你的端口号Linux/macOS:lsof -i :你的端口号或netstat -tulpn | grep :你的端口号权限不足在Linux/macOS上监听1024以下的端口如80、443需要root权限。本地开发建议使用1024以上的端口如8080、8888。防火墙/安全软件拦截某些本地防火墙或安全软件可能会阻止应用程序监听端口。尝试临时关闭它们或添加规则放行。解决换一个端口或者终止占用端口的进程。6.2 客户端能连接但收不到消息或服务端收不到客户端消息症状连接建立成功但textMessageReceived信号始终不触发。排查协议不匹配确保客户端发送的是文本消息sendTextMessage而非二进制消息。服务端需要连接textMessageReceived信号来接收。反之亦然。事件循环未运行如果你在控制台程序中没有启动事件循环QCoreApplication::exec()那么Qt的信号槽机制不会工作。确保你的程序进入了事件循环。对象生命周期问题检查处理消息的槽函数所属的对象是否还存活。如果对象被提前销毁槽函数就不会被调用。跨线程问题如果你在非主线程创建了QWebSocket或QWebSocketServer必须确保它们在该线程的事件循环中运行。Qt的网络类通常要求在同一线程创建和使用。调试在服务端和客户端的connected、disconnected、error等信号处都加上日志输出可以清晰看到连接状态的变化。6.3 服务端在高负载下崩溃或响应变慢症状连接数增多或消息频率变高后程序崩溃或客户端感觉卡顿。排查内存泄漏使用ValgrindLinux、Dr. MemoryWindows或Qt Creator自带的分析工具检查是否有内存未释放。重点检查QWebSocket对象和通过new创建的业务对象。业务逻辑阻塞主线程如5.2节所述将耗时操作移到子线程。可以使用QElapsedTimer在关键代码段前后计时找出性能瓶颈。消息风暴某个客户端可能以极高速率发送消息。可以在服务端做限流例如记录每个客户端上次处理消息的时间如果间隔太短则丢弃或延迟处理。操作系统资源限制检查系统的文件描述符File Descriptor限制。每个TCP连接都会占用一个。对于Linux可以使用ulimit -n查看和修改。解决实施心跳机制、异步处理、连接和消息限流。对于生产环境考虑使用更专业的网络库如Boost.Asio或框架但Qt WebSockets对于数百个并发连接的本地服务通常是足够的。6.4 Web客户端JavaScript连接本地服务被拒绝症状浏览器控制台报错WebSocket connection to ws://localhost:port/ failed。排查跨域问题CORS如果HTML文件是通过file://协议打开的或者来自不同端口/域浏览器出于安全策略会阻止WebSocket连接。这是最常见的原因。服务未运行或端口错误确认你的Qt服务端程序正在运行并且监听的端口与客户端尝试连接的端口一致。解决针对本地开发最简单的方法是使用一个简单的HTTP服务器来提供你的测试HTML文件而不是直接双击打开。Python有一个快速的方法在HTML文件所在目录打开终端运行python3 -m http.server 8000然后在浏览器访问http://localhost:8000/test.html。这样HTML和WebSocket服务都来自localhost不同端口大多数浏览器允许连接。检查服务端日志确保QWebSocketServer成功监听了QHostAddress::LocalHost或QHostAddress::Any。7. 安全加固与部署建议即使是本地服务安全也不容忽视尤其是当它可能被其他本地应用或未来扩展到局域网访问时。7.1 使用安全模式WSS将服务器初始化的NonSecureMode改为SecureMode。m_pWebSocketServer new QWebSocketServer( QStringLiteral(Secure Server), QWebSocketServer::SecureMode, // 改为安全模式 this );但这需要SSL证书。对于本地开发或内部使用可以生成自签名证书。# 使用OpenSSL生成自签名证书和密钥 openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes然后在代码中加载证书QSslConfiguration sslConfig; QSslKey sslKey(keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); QSslCertificate sslCert(certFile); sslConfig.setPrivateKey(sslKey); sslConfig.setLocalCertificate(sslCert); sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); // 对于自签名证书不验证对端 m_pWebSocketServer-setSslConfiguration(sslConfig);客户端如Web前端连接时需要使用wss://协议。浏览器会对自签名证书发出警告需要手动确认接受。7.2 实现简单的身份验证可以在WebSocket握手阶段通过HTTP头进行简单的身份验证。QWebSocketServer在握手时会发出originAuthenticationRequired信号已过时Qt6中建议用重写verifyOrigin或serverHandshakeInterrupted信号你可以在这里检查请求的Origin或自定义的HTTP头。 更常见的做法是在连接建立后第一个消息里携带认证令牌Token。服务端收到后验证验证失败则立即关闭连接。void WebSocketServer::onTextMessageReceived(const QString message) { QWebSocket *pClient qobject_castQWebSocket *(sender()); if (!m_authenticatedClients.contains(pClient)) { // 未认证第一个消息必须是认证消息 if (message.startsWith(AUTH:)) { QString token message.mid(5); if (validateToken(token)) { m_authenticatedClients.insert(pClient); pClient-sendTextMessage(AUTH_SUCCESS); } else { pClient-close(QWebSocketProtocol::CloseCodePolicyViolated, Invalid token); } } else { pClient-close(QWebSocketProtocol::CloseCodePolicyViolated, Authentication required); } return; // 认证流程中不处理其他业务 } // ... 已认证客户端的正常业务处理 ... }7.3 作为系统服务守护进程运行对于需要长时间运行、开机自启的本地服务在Windows上可以将其注册为系统服务在Linux上可以配置为systemd服务或守护进程。这涉及到服务管理、日志重定向、崩溃重启等运维知识超出了本文范围但这是产品化的重要一步。Qt应用可以方便地编译为无GUI的控制台程序配合相应的服务管理脚本即可。一个在Linux下简单的守护进程化思路是使用fork系统调用或者使用更专业的工具如daemonize。核心是脱离终端、关闭标准文件描述符、在后台运行。