第一章:QT6 QML与Go后端通信的架构演进与风险全景
现代桌面应用正经历从单体架构向前后端分离范式的深刻迁移。在 QT6 生态中,QML 作为声明式 UI 层,天然承担视图职责;而 Go 凭借其并发模型、静态编译与跨平台能力,日益成为高性能本地后端服务的首选。二者协同并非简单桥接,而是一场涉及进程模型、序列化协议、生命周期管理与错误传播路径的系统性重构。
通信模式的代际跃迁
早期 QT5 常依赖 QProcess 启动外部 Go 二进制并基于 stdin/stdout 管道通信,耦合度高且调试困难。QT6 推出 QAbstractSocket 与 QNetworkAccessManager 的现代化网络抽象层,配合 Go 的 net/http 或轻量级 WebSocket 服务(如 gorilla/websocket),催生出三种主流模式:
- HTTP REST/JSON:适合 CRUD 场景,QML 使用
XmlHttpRequest或QtQuick.Network模块; - WebSocket 长连接:适用于实时状态同步(如设备监控仪表盘);
- 本地 Unix Domain Socket(Linux/macOS)或 Named Pipe(Windows):零网络开销,Go 后端监听
/tmp/qml-backend.sock,QML 通过QTcpSocket连接,需启用QT_QPA_PLATFORM=offscreen避免 GUI 线程阻塞。
关键风险矩阵
| 风险类型 | 具体表现 | 缓解策略 |
|---|---|---|
| 进程生命周期失配 | Go 后端崩溃时 QML 未感知,请求挂起 | QML 端实现心跳探测 + onError 回调;Go 端暴露 /health 端点 |
| 类型安全缺失 | JSON 序列化丢失 Go struct tag 映射 | 在 Go 中统一使用 json:"field_name" 标签,并在 QML 中用 JSON.parse() 后校验字段存在性 |
| 线程与上下文穿越 | QML 主线程直接调用阻塞式 HTTP 请求 | 使用 WorkerScript 封装网络调用,或 Go 后端提供异步回调接口 |
快速验证本地 socket 通信示例
在 Go 后端启动 Unix socket 服务:
// main.go
listener, _ := net.Listen("unix", "/tmp/qml-backend.sock")
defer listener.Close()
for {
conn, _ := listener.Accept()
go func(c net.Conn) {
io.WriteString(c, `{"status":"ready","timestamp":`+strconv.FormatInt(time.Now().Unix(), 10)+`}`)
c.Close()
}(conn)
}
QML 中连接并解析响应:
import QtNetwork 6.0
TcpSocket {
id: backendSocket
onConnected: write("PING\n") // 触发握手
onTextMessageReceived: {
const data = JSON.parse(message); // 安全解析 JSON
console.log("Backend status:", data.status);
}
Component.onCompleted: connectToHost("unix:/tmp/qml-backend.sock")
}
第二章:WebSocket实时通信的高危实践与加固方案
2.1 WebSocket协议层握手与跨域安全策略的深度解析
WebSocket 握手本质是 HTTP 协议的“伪装升级”:客户端发送含 Upgrade: websocket 和 Sec-WebSocket-Key 的 GET 请求,服务端校验后返回 101 Switching Protocols 响应,并附带 Sec-WebSocket-Accept(由客户端 key + 固定 GUID 经 SHA-1/Base64 计算得出)。
握手关键字段对照表
| 字段 | 客户端要求 | 服务端响应约束 |
|---|---|---|
Origin |
必含(实际发起页面源) | 严格校验是否在白名单内 |
Sec-WebSocket-Origin |
已废弃(旧草案) | 忽略 |
Access-Control-Allow-Origin |
不可见 | 若为非通配符,必须精确匹配 Origin |
// 客户端发起握手时的 Origin 透传(浏览器自动注入)
const ws = new WebSocket("wss://api.example.com/realtime");
// 浏览器自动添加请求头:Origin: https://app.example.com
此代码中
Origin头不可手动设置,由浏览器根据当前页面协议+主机+端口自动生成;服务端若返回Access-Control-Allow-Origin: *,则拒绝携带凭证(如 cookies)的连接——这是跨域安全的核心权衡。
跨域验证流程(mermaid)
graph TD
A[客户端发起 ws:// 请求] --> B{浏览器检查 Origin 是否合法}
B -->|合法| C[发送含 Origin 的 Upgrade 请求]
B -->|非法| D[直接阻断,不发请求]
C --> E[服务端比对 Origin 与 CORS 白名单]
E -->|匹配| F[返回 101 + Sec-WebSocket-Accept]
E -->|不匹配| G[返回 403 或忽略]
2.2 QML端WebSocket QML类型(QtWebSockets)的内存泄漏陷阱与生命周期管理
常见泄漏场景
WebSocket 实例若在 QML 中未显式关闭并置空,会因 JS 引用+底层 C++ 对象双持有导致内存滞留。
生命周期关键点
- 创建于 Component.onCompleted → 必须配对销毁于 Component.onDestroyed
close()仅终止连接,不释放资源;需配合destroy()或作用域回收
正确实践示例
WebSocket {
id: ws
url: "wss://api.example.com"
onOpened: console.log("Connected")
onClosed: console.log("Socket closed")
// ✅ 安全销毁:显式关闭 + 置空引用
Component.onDestruction: {
if (ws && ws.readyState !== WebSocket.CLOSED) {
ws.close(); // 参数默认为 1000(normal closure)
}
ws = null; // 解除 QML 引用,触发底层析构
}
}
close(code, reason)中code=1000表明正常关闭,避免服务端误判为异常断连;ws = null是关键——QML 引擎仅在无 JS 引用且对象无 parent 时才调用 C++ 析构函数。
内存状态对比表
| 状态 | readyState | JS 引用 | C++ 对象存活 | 是否泄漏 |
|---|---|---|---|---|
| 未 close + 未置空 | CLOSED | ✓ | ✓ | ✅ 是 |
| 已 close + 置空 | CLOSED | ✗ | ✗ | ❌ 否 |
2.3 Go后端gorilla/websocket连接池滥用导致的FD耗尽实战复现
问题诱因:无限制连接复用
当 gorilla/websocket 连接被错误地放入全局连接池(如 sync.Pool[*websocket.Conn])并反复 Dial() 后未关闭底层 net.Conn,会导致文件描述符(FD)持续累积。
复现场景代码
// ❌ 危险:将未清理的 Conn 放入 Pool
var connPool = sync.Pool{
New: func() interface{} {
c, _, _ := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil)
return c // 忘记 defer c.Close() 或未关闭底层 net.Conn!
},
}
逻辑分析:
websocket.Conn内部持有net.Conn,但sync.Pool不会自动调用Close();每次Get()都新建 TCP 连接,旧连接 FD 泄漏。Dialer默认不限制并发,系统级 FD 耗尽(ulimit -n 1024下约 1000+ 并发即触发too many open files)。
FD 消耗对比表
| 操作 | 单次新增 FD 数 | 持久性 |
|---|---|---|
正常 Dial() + Close() |
1 | ✅ 释放 |
Dial() 后仅 Put() 到 Pool |
1 | ❌ 永驻 |
正确处置流程
graph TD
A[客户端 Dial] --> B{是否需复用?}
B -->|否| C[使用后 Close()]
B -->|是| D[封装为带 CloseHook 的 Wrapper]
D --> E[Pool.Put 时触发底层 net.Conn.Close]
2.4 消息序列化选型失当:JSON vs CBOR在QML/Go双端时序一致性中的崩塌案例
数据同步机制
QML前端通过WebSocket与Go后端通信,双方约定以毫秒级时间戳驱动状态机。初始选用JSON序列化,但未统一浮点时间戳精度处理逻辑。
序列化行为差异
| 特性 | JSON(Go json.Marshal) |
CBOR(go-cbor) |
|---|---|---|
| 时间戳编码 | float64 → "1712345678.123"(字符串) |
int64 → 1712345678123(纳秒整数) |
| 浮点舍入误差 | ✅ 存在(IEEE-754双精度) | ❌ 无(整数截断) |
// Go端时间戳序列化片段
ts := time.Now().UnixMilli() // 返回int64
data := map[string]interface{}{"ts": ts, "val": 42}
// JSON: {"ts":1712345678123,"val":42} → QML解析为Number → 隐式转float64 → 精度丢失
// CBOR: 直接编码为uint64 → QML侧通过QCBOR解码得精确int64
该转换导致QML中new Date(ts)生成时间偏移达±3ms,破坏事件因果序。
修复路径
- 统一采用CBOR + 显式
int64时间戳字段 - QML侧绑定
QByteArray而非var接收原始CBOR blob
graph TD
A[QML emitEvent] --> B[Go decode JSON]
B --> C{ts float64?}
C -->|yes| D[Lossy round-trip]
C -->|no| E[CBOR int64 → exact]
2.5 心跳机制缺失引发的QML界面假死与Go服务端连接僵尸化联合调试
现象复现路径
- QML侧
WebSocket.onClosed未触发,UI按钮点击无响应(非主线程阻塞,实为事件循环停滞) - Go服务端
net.Conn连接数持续增长,ss -tn | grep :8080 | wc -l显示大量ESTABLISHED但无收发流量
核心问题定位
心跳缺失导致双向超时策略失配:
- QML
WebSocket默认无自动心跳,依赖 TCP keepalive(默认 2 小时) - Go 服务端启用
SetReadDeadline但未配套发送 ping 帧
Go服务端心跳补全代码
// 启动协程定期向客户端发送 WebSocket ping 帧
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("ping failed: %v", err)
return
}
case <-done:
return
}
}
}()
WriteMessage(websocket.PingMessage, nil)触发底层协议帧封装;30s间隔需 tcp_keepalive_time=7200s),确保在连接被中间设备静默断开前探测活性。
QML侧心跳响应增强
| 组件 | 配置项 | 推荐值 | 说明 |
|---|---|---|---|
| WebSocket | onPingReceived |
必须实现 | 重置本地活跃计时器 |
| Timer | interval |
25000 ms | 比服务端 ping 间隔短 5s |
联合调试流程
graph TD
A[QML界面卡顿] --> B{抓包确认WS帧流}
B -->|无Ping/Pong| C[注入心跳逻辑]
B -->|有Ping但无Pong响应| D[Go端WriteMessage错误处理缺失]
C --> E[验证conn.SetWriteDeadline]
D --> E
第三章:RPC通信模式的现代替代与历史包袱
3.1 Qt Remote Objects(QRO)被弃用的技术动因与Qt6.7+官方迁移路径
Qt 6.7 正式将 Qt Remote Objects(QRO)标记为 deprecated,核心动因在于其与 Qt Core 的耦合过深、跨进程通信抽象层冗余,且难以适配现代异步 I/O 模型(如 QEventDispatcher 重构与 QThreadPool 统一调度需求)。
数据同步机制替代方案
Qt 官方推荐迁移至 QMetaObject::invokeMethod + QRemoteObjectNode 的轻量封装,或直接采用 QtDBus(Linux)与 QLocalSocket(跨平台)组合:
// Qt6.7+ 推荐:基于 QLocalServer/QLocalSocket 的零依赖 IPC
QLocalServer server;
server.listen("myapp-ipc"); // 命名管道/Unix domain socket
// ⚠️ 注意:需手动序列化 QObject 属性,使用 QMetaObject::writeTo()
逻辑分析:
QLocalSocket替代 QRO 的QRemoteObjectRegistryHost,避免了 QRO 内部的Replica/Source元对象反射开销;listen()参数为平台无关的套接字标识符,Windows 下自动映射为命名管道。
官方迁移路径对比
| 方案 | 适用场景 | 是否需重写元对象绑定 | 线程安全 |
|---|---|---|---|
QtDBus |
Linux 桌面环境 | 否(D-Bus introspection) | ✅(通过 connection thread) |
QLocalSocket + QDataStream |
全平台、低延迟 | 是(需显式 serialize/deserialize) | ❌(需加锁或 moveToThread) |
graph TD
A[QRO 应用] --> B{Qt6.7+}
B --> C[QtDBus: Linux]
B --> D[QLocalSocket: Windows/macOS/Linux]
C --> E[dbus-daemon 代理]
D --> F[本地字节流直连]
3.2 Go端gRPC-Web桥接QML的TypeScript中间层必要性论证与轻量实现
QML原生不支持HTTP/2或Protocol Buffers二进制帧,而Go后端暴露的是gRPC-Web(application/grpc-web+proto)服务。直接在QML中发起gRPC-Web请求需绕过Qt Network模块限制,且缺乏类型安全与错误传播机制。
为何必须引入TypeScript中间层?
- QML的
XmlHttpRequest无法正确处理gRPC-Web响应头(如grpc-status、grpc-message) - TypeScript可生成强类型客户端(基于
.proto),自动绑定jspb序列化逻辑 - 提供统一的错误归一化接口,屏蔽底层
415 Unsupported Media Type等协议细节
轻量实现核心代码
// grpc-web-client.ts
import { GreeterClient } from './proto/greeter_grpc_web_pb';
import { HelloRequest } from './proto/greeter_pb';
const client = new GreeterClient('http://localhost:8080', null, {
transport: WebsocketTransport, // 或XHRTransport
});
export const sayHello = (name: string): Promise<string> => {
const req = new HelloRequest().setName(name);
return new Promise((resolve, reject) => {
client.sayHello(req, {}, (err, res) => {
if (err) reject(new Error(err.message));
else resolve(res.getMessage());
});
});
};
逻辑分析:该封装将gRPC-Web原始回调转为Promise,屏蔽了
grpc-web库中onEnd事件流复杂性;transport选项决定是否启用WebSocket降级(对QML WebView兼容更优);null第二参数表示默认元数据为空,避免QML侧手动注入Authorization头引发跨域预检失败。
| 维度 | 原生QML方案 | TypeScript中间层 |
|---|---|---|
| 类型安全 | ❌ 动态JSON解析 | ✅ tsc编译时校验 |
| 错误码映射 | ❌ 需手动解析statusText |
✅ 自动转换grpc-status=14→UNAVAILABLE |
graph TD
A[QML WebView] -->|fetch API| B(TypeScript Bridge)
B -->|gRPC-Web POST| C[Go gRPC-Web Gateway]
C -->|HTTP/1.1 + base64| D[gRPC Server]
3.3 基于ZeroMQ + QMetaObject::invokeMethod的异步RPC伪同步封装实践
在Qt应用中实现跨进程RPC时,直接裸用ZeroMQ易导致线程阻塞或信号槽跨线程调用崩溃。核心解法是:将ZMQ消息收发置于工作线程,再通过QMetaObject::invokeMethod安全回调主线程UI对象。
伪同步调用机制
- 客户端发送请求后立即返回
QFuture<T>(非阻塞) - 工作线程监听ZMQ REP套接字,收到响应后触发
invokeMethod投递至目标对象
// 主线程调用示例
QFuture<QString> future = rpcClient->callAsync("getUserName", 123);
// 后续可链式连接:future.then([](const QString& name) { /* 处理结果 */ });
逻辑分析:
callAsync内部生成唯一request ID并注册QHash<id, QPromise<T>>;ZMQ响应到达工作线程后,通过invokeMethod(obj, method, Qt::QueuedConnection, ...)将结果安全派发至主线程,自动完成QPromise::addResult()。
关键参数说明
| 参数 | 作用 |
|---|---|
Qt::QueuedConnection |
确保invokeMethod跨线程安全执行 |
Q_ARG/Q_RETURN_ARG |
类型安全传递参数与返回值 |
QPromise |
封装异步状态,支持取消与进度通知 |
graph TD
A[UI线程调用callAsync] --> B[生成RequestID+QPromise]
B --> C[工作线程发送ZMQ REQ]
C --> D[ZMQ REP服务响应]
D --> E[工作线程invokeMethod]
E --> F[UI线程QPromise.setFuture]
第四章:进程间共享内存通信的底层攻防视角
4.1 QSharedMemory在Linux/macOS/Windows三平台权限模型差异与QML无法直接访问的根本限制
权限模型本质差异
Linux 使用 shm_open() + fchmod() 控制 POSIX 共享内存对象的文件系统级权限;macOS 虽兼容 POSIX API,但默认禁用 O_EXCL 标志且 /dev/shm 不挂载;Windows 则完全基于 Win32 安全描述符(SECURITY_ATTRIBUTES),依赖 DACL 显式授权。
QML 无法直接访问的根源
QML 引擎运行于 QQmlApplicationEngine 的沙箱上下文,无 QSharedMemory 所需的底层系统调用能力(如 mmap()、CreateFileMappingW),且 QSharedMemory 非 QObject 派生类,不支持元对象系统注册,无法暴露为 QML 类型。
| 平台 | 底层机制 | 默认权限粒度 | QML 可桥接方式 |
|---|---|---|---|
| Linux | POSIX shm /dev/shm | mode_t (rwx) | 需 C++ 封装为 QObject |
| macOS | Mach ports + mmap | 无全局命名空间 | 仅支持进程内匿名映射 |
| Windows | File Mapping Object | ACL 细粒度控制 | 必须通过 Q_INVOKABLE 导出 |
// 示例:跨平台安全初始化(Linux/macOS/Windows)
QSharedMemory shm("my_app_data");
#ifdef Q_OS_WIN
// Windows:需显式设置安全属性
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE);
shm.setNativeKey(reinterpret_cast<quintptr>(&sd)); // 非标准用法,仅示意
#endif
上述代码在 Windows 中尝试注入安全描述符指针,但
QSharedMemory::setNativeKey()实际不接受SECURITY_DESCRIPTOR*—— 这揭示了 Qt 抽象层对原生权限模型的有意屏蔽,导致上层无法精细控制,进一步阻断 QML 直接集成路径。
4.2 Go端使用mmap+unsafe.Pointer构建零拷贝共享环形缓冲区的跨语言ABI对齐技巧
为实现与C/C++/Rust等语言的高效互通,Go需绕过runtime内存管理,直接操作OS映射的共享内存页。
内存布局对齐关键点
- 页对齐(
syscall.Getpagesize())是mmap前提 - 缓冲区头结构体必须显式
//go:packed且字段按ABI要求对齐(如uint64起始偏移需8字节对齐) unsafe.Offsetof()用于校验字段偏移是否跨语言一致
零拷贝环形缓冲区初始化示例
const pageSize = 4096
buf, err := syscall.Mmap(-1, 0, 2*pageSize,
syscall.PROT_READ|syscall.PROT_WRITE,
syscall.MAP_SHARED|syscall.MAP_ANONYMOUS)
if err != nil { panic(err) }
ring := (*RingHeader)(unsafe.Pointer(&buf[0]))
RingHeader含readIdx,writeIdx,mask等uint64字段;mask必须为2^n−1,确保位运算索引不越界;MAP_ANONYMOUS避免文件依赖,MAP_SHARED使多进程可见。
跨语言ABI对齐验证表
| 字段名 | C offsetof |
Go unsafe.Offsetof |
是否一致 |
|---|---|---|---|
readIdx |
0 | 0 | ✅ |
writeIdx |
8 | 8 | ✅ |
data |
16 | 16 | ✅ |
graph TD
A[Go mmap申请共享页] --> B[构造packed RingHeader]
B --> C[用unsafe.Slice绑定data区域]
C --> D[原子操作更新idx,无锁同步]
4.3 QML侧通过C++ Plugin暴露QAbstractListModel接口绑定共享内存数据的实时刷新陷阱
数据同步机制
当QML通过QAbstractListModel绑定共享内存(如QSharedMemory)中的结构化数据时,模型自身不感知外部内存变更,仅依赖显式调用beginInsertRows()/dataChanged()等通知机制。
常见陷阱根源
- ✅ 正确:C++插件在共享内存更新后主动触发
dataChanged(index, index, {Qt::DisplayRole}) - ❌ 错误:仅修改共享内存内容,未调用
QAbstractListModel通知函数
关键代码示例
// 在共享内存写入新数据后必须同步通知QML
QModelIndex idx = index(row, 0);
emit dataChanged(idx, idx, QVector<int>() << Qt::DisplayRole);
逻辑分析:
dataChanged()需传入有效QModelIndex及角色列表;若省略角色或索引越界,QML视图将静默忽略刷新。QVector<int>() << Qt::DisplayRole确保仅重绘显示文本,避免冗余更新。
| 问题现象 | 根本原因 |
|---|---|
| QML列表无响应 | 未触发dataChanged()信号 |
| 部分项刷新异常 | index()返回无效索引 |
graph TD
A[共享内存写入] --> B{C++插件监听到变更?}
B -->|否| C[QML视图停滞]
B -->|是| D[调用dataChanged]
D --> E[QML引擎触发re-render]
4.4 内存屏障缺失导致的QML Property Binding乱序更新与Go写端缓存一致性失效分析
数据同步机制
QML引擎依赖Qt的QQmlPropertyCache进行属性绑定更新,但未在关键路径插入std::atomic_thread_fence(memory_order_acquire/release),导致CPU重排序下绑定回调早于实际值写入。
Go侧写端问题
Go runtime在sync/atomic包外直接使用unsafe.Pointer更新共享结构体字段,绕过内存屏障语义:
// ❌ 危险:无屏障的非原子写入
data.ptr = unsafe.Pointer(&newVal) // 可能被编译器/CPU重排
该赋值不保证newVal内存内容已对其他线程可见,引发QML读到未初始化字段。
根本原因对比
| 维度 | QML Binding侧 | Go写端侧 |
|---|---|---|
| 同步原语缺失 | memory_order_seq_cst |
atomic.StorePointer |
| 触发条件 | 多核+弱内存模型(ARM) | -gcflags="-l"禁用内联 |
graph TD
A[Go写newVal] -->|无屏障| B[CPU重排]
B --> C[QML读ptr]
C --> D[读到旧/newVal未刷缓存]
第五章:通信范式选型决策树与企业级落地建议
核心决策维度拆解
企业在选型时需同步评估五个不可妥协的硬性指标:端到端延迟容忍度(如金融交易系统要求
决策树可视化流程
flowchart TD
A[Q1: 是否需要强事务一致性?] -->|是| B[选分布式事务型:Seata+RocketMQ事务消息]
A -->|否| C[Q2: 消息吞吐量是否 >10万TPS?]
C -->|是| D[选高吞吐流式:Pulsar分层存储+Topic分区预热]
C -->|否| E[Q3: 是否存在异构协议终端?]
E -->|是| F[选协议网关型:NATS JetStream + 自定义WebSocket/HTTP桥接器]
E -->|否| G[选轻量可靠型:RabbitMQ Quorum Queues+Prometheus深度集成]
典型行业落地对照表
| 行业 | 主导范式 | 关键配置参数 | 落地陷阱警示 |
|---|---|---|---|
| 智能制造 | MQTT over TLS 1.3 | QoS=1, Clean Session=false, KeepAlive=60s | 忘记配置Broker端Session Expiry Interval导致断连后状态丢失 |
| 医疗影像平台 | gRPC-Web + HTTP/2 | MaxConcurrentStreams=200, IdleTimeout=30m | 客户端未启用gRPC健康检查探针,K8s Service误判Pod就绪 |
| 政务云平台 | AMQP 1.0 + SASL外部认证 | ContainerId绑定部门OU,MessageAnnotations注入审批流水号 | 未在AMQP Link层启用DeliveryCount重试计数,引发重复审批 |
灰度发布实施清单
- 首阶段:在非关键链路(如用户行为日志上报)部署双通道,旧HTTP API与新gRPC接口并行运行,通过Envoy Header路由标记流量来源;
- 第二阶段:基于Jaeger traceID采样比对,验证新通道P99延迟下降42%且无消息乱序;
- 第三阶段:将订单履约服务切换为Pulsar Schema强制校验模式,利用Avro Schema Registry实现字段级变更审计;
- 第四阶段:全量切流后保留旧通道72小时,但仅接收告警消息,通过Grafana看板实时对比两通道的消息积压曲线斜率差异。
某新能源车企在车机OTA升级系统中,采用该清单第四阶段策略,成功捕获Pulsar Broker因磁盘IO瓶颈导致的偶发ack超时问题,避免了23万台车辆固件升级中断事故。
安全加固强制项
所有生产环境必须启用TLS 1.3双向认证,证书Subject字段需嵌入Kubernetes Namespace和Service Account名称;消息体敏感字段(如身份证号、银行卡号)必须在Producer端调用Apache Shiro Crypto API完成AES-GCM加密,密钥轮换周期严格控制在72小时以内;审计日志需同步写入独立Elasticsearch集群,且索引模板强制开启ILM策略,保留周期按《网络安全法》要求设置为180天。
