第一章:Go语言即时战斗游戏开发概览
Go语言凭借其轻量级协程(goroutine)、高效的并发调度、静态编译与跨平台能力,正成为高性能网络游戏后端开发的新兴选择。在即时战斗(Real-Time Combat, RTC)类游戏中,低延迟响应、高吞吐连接管理、确定性逻辑更新与状态同步是核心挑战,而Go的标准库net、sync、time/ticker及生态中成熟的WebSocket库(如gorilla/websocket)可自然支撑这些需求。
即时战斗的关键技术特征
- 帧同步或状态同步模型:Go适合实现服务端权威的状态同步架构,避免客户端作弊;也可通过定时
time.Ticker驱动固定频率的游戏逻辑帧(如60Hz) - 高并发连接处理:单机轻松承载万级WebSocket连接,配合
sync.Pool复用消息结构体,显著降低GC压力 - 热重载与快速迭代:利用
air或fresh工具实现代码修改后自动重启,加速战斗逻辑调试周期
快速启动一个战斗服务端骨架
# 1. 初始化模块
go mod init rtc-game-server
# 2. 安装WebSocket依赖
go get github.com/gorilla/websocket
// main.go:基础连接管理示例
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, // 生产环境需严格校验
}
func handleConnection(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("升级WebSocket失败: %v", err)
return
}
defer conn.Close()
// 启动独立goroutine处理该玩家输入
go func() {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Printf("读取消息失败: %v", err)
break
}
// 此处解析战斗指令(如"attack|targetID=123"),交由战斗引擎处理
log.Printf("收到指令: %s", string(msg))
}
}()
}
func main() {
http.HandleFunc("/ws", handleConnection)
log.Println("战斗服务端已启动,监听 :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Go与常见游戏技术栈对比
| 维度 | Go | Node.js | C++ |
|---|---|---|---|
| 并发模型 | 原生goroutine + channel | Event Loop + async/await | 手动线程池/Actor模型 |
| 内存安全 | 高(无指针算术,GC管理) | 高 | 低(需手动管理) |
| 编译部署 | 单二进制文件,零依赖 | 需Node运行时 | 需链接器与动态库 |
| 热更新支持 | 通过进程替换+连接平滑迁移 | 原生支持 | 通常需插件化或重启服务 |
Go不追求图形渲染,而是聚焦于战斗规则执行、状态同步、匹配调度与反作弊验证——这恰恰是即时战斗系统最不可妥协的“大脑”所在。
第二章:高并发网络通信架构设计
2.1 基于net/http与fasthttp的协议选型与压测对比
性能差异根源
net/http 遵循标准 HTTP/1.1 规范,内置连接池与上下文管理;fasthttp 则通过复用 byte.Buffer 和避免堆分配实现零拷贝解析。
压测关键指标(16核/32GB,10K并发)
| 框架 | QPS | 平均延迟 | 内存占用 | GC 次数/秒 |
|---|---|---|---|---|
| net/http | 18,200 | 54 ms | 420 MB | 12.3 |
| fasthttp | 47,600 | 19 ms | 185 MB | 2.1 |
示例服务启动代码
// fasthttp 版本:无中间件开销,直接操作 RequestCtx
func main() {
h := func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(200)
ctx.SetBodyString("OK")
}
log.Fatal(fasthttp.ListenAndServe(":8080", h))
}
该实现跳过 http.Handler 接口抽象与 net.Conn 封装,直接操作底层字节流;ctx 生命周期由池管理,避免频繁 GC。
协议兼容性权衡
- ✅ fasthttp 支持 HTTP/1.1 全特性(含 Keep-Alive、Chunked)
- ❌ 不支持 HTTP/2、Server-Sent Events 及部分
http.Header语义(如多值合并)
graph TD A[HTTP 请求] –> B{协议栈选择} B –> C[net/http: 安全/标准/易维护] B –> D[fasthttp: 极致性能/有限生态]
2.2 WebSocket长连接管理与心跳保活实战实现
WebSocket 连接易受网络抖动、NAT超时或代理中断影响,需主动维护连接有效性。
心跳机制设计原则
- 客户端定时发送
ping消息(如每30s) - 服务端收到后立即响应
pong - 若连续2次未收到
pong,触发重连
客户端心跳实现(JavaScript)
const ws = new WebSocket('wss://api.example.com/ws');
let heartbeatTimer;
function startHeartbeat() {
heartbeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
}
}, 30000);
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
console.log('Heartbeat acknowledged');
}
};
逻辑分析:
setInterval启动周期性心跳,仅在OPEN状态下发送;onmessage监听服务端pong响应,避免无效发送。ts字段用于端到端延迟诊断。
常见心跳参数对比
| 参数 | 推荐值 | 说明 |
|---|---|---|
| ping间隔 | 30s | 平衡及时性与资源开销 |
| pong超时阈值 | 10s | 防止短暂抖动误判断连 |
| 最大失败次数 | 2 | 避免瞬时故障引发频繁重连 |
graph TD
A[客户端启动] --> B[建立WebSocket连接]
B --> C{连接是否OPEN?}
C -->|是| D[启动心跳定时器]
C -->|否| E[触发重连策略]
D --> F[每30s发送ping]
F --> G[等待pong响应]
G --> H{10s内收到?}
H -->|否| I[计数+1]
H -->|是| D
I --> J{失败≥2次?}
J -->|是| E
J -->|否| F
2.3 消息序列化方案:Protocol Buffers vs JSON性能实测与封装
序列化开销对比(1KB典型消息)
| 指标 | Protobuf(binary) | JSON(UTF-8) |
|---|---|---|
| 序列化耗时(μs) | 12.4 | 89.7 |
| 序列化后体积(B) | 312 | 1024 |
| 反序列化耗时(μs) | 18.9 | 156.3 |
核心封装层设计
# protobuf 封装示例(带类型安全校验)
class MessageCodec:
@staticmethod
def encode(msg: ProtoMessage) -> bytes:
return msg.SerializeToString() # 无schema冗余,零拷贝序列化
@staticmethod
def decode(data: bytes) -> ProtoMessage:
msg = ProtoMessage()
msg.ParseFromString(data) # 强类型解析,失败抛InvalidProtocolBufferError
return msg
SerializeToString()直接输出二进制流,省去文本编码/解码步骤;ParseFromString()基于预编译.protoschema执行内存安全反序列化,规避JSON中常见的字段缺失或类型误判。
性能瓶颈归因
- JSON:字符串解析、Unicode转义、动态对象构建带来显著CPU与GC压力
- Protobuf:紧凑二进制编码 + 预生成静态类,消除运行时反射开销
graph TD
A[原始结构体] --> B{序列化入口}
B --> C[Protobuf:Schema驱动二进制编码]
B --> D[JSON:通用AST树遍历+字符串拼接]
C --> E[低延迟/小体积]
D --> F[高可读性/弱类型]
2.4 连接池与协程安全的ConnManager设计与边界测试
协程安全的核心挑战
高并发场景下,多个 goroutine 同时获取/归还连接易引发竞争:连接泄漏、重复关闭、状态错乱。传统 sync.Mutex 粗粒度锁会成为性能瓶颈。
ConnManager 设计要点
- 使用
sync.Pool缓存空闲连接,降低 GC 压力 - 每个连接绑定唯一
context.Context实现超时与取消隔离 - 连接生命周期由
atomic.Int32状态机管理(Idle → Acquired → Released → Closed)
type ConnManager struct {
pool *sync.Pool
closed atomic.Bool
}
func (cm *ConnManager) Get() (*Conn, error) {
if cm.closed.Load() {
return nil, ErrClosed
}
conn := cm.pool.Get().(*Conn)
if !conn.acquire() { // CAS 状态校验
return cm.newConn() // 状态非法时新建
}
return conn, nil
}
acquire() 内部通过 atomic.CompareAndSwapInt32(&conn.state, Idle, Acquired) 保证原子性;失败即说明该连接已被其他协程抢占或已失效,需重建。
边界测试覆盖维度
| 测试类型 | 触发条件 | 验证目标 |
|---|---|---|
| 并发获取峰值 | 10k goroutines 同时调用 Get | 连接数 ≤ MaxOpen && 无 panic |
| 快速释放风暴 | 获取后立即 Close + 归还 | 连接复用率 ≥95% |
| 上下文取消注入 | Get 时传入已 cancel 的 ctx | 立即返回 ErrCanceled |
graph TD
A[Get] --> B{pool.Get?}
B -->|hit| C[acquire CAS]
B -->|miss| D[newConn]
C -->|success| E[return Conn]
C -->|fail| D
D --> E
2.5 网络层熔断、限流与异常连接自动驱逐机制
网络层需在协议栈早期拦截故障传播。熔断基于连接成功率与RTT双指标动态决策,限流采用令牌桶+连接数双维度控制,异常连接则通过心跳超时、FIN/RST异常序列及TCP retransmit rate > 3次/秒自动标记驱逐。
驱逐策略触发条件
- 连续3次心跳无响应
- 单连接10秒内重传包占比 ≥ 40%
- FIN后未收到ACK且存在乱序RST包
熔断状态机(mermaid)
graph TD
Closed -->|错误率>50%且持续5s| Open
Open -->|休眠期结束+探测请求成功| HalfOpen
HalfOpen -->|半开探测全成功| Closed
HalfOpen -->|任一失败| Open
限流配置示例(Go)
// 基于连接数与QPS的复合限流器
limiter := NewCompositeLimiter(
WithConnLimit(1000), // 全局并发连接上限
WithRateLimit(500, time.Second), // 每秒新建连接数
)
WithConnLimit防止资源耗尽;WithRateLimit抑制连接风暴,二者协同避免雪崩。
第三章:实时战斗逻辑引擎构建
3.1 帧同步模型与服务端权威校验的Go实现
帧同步要求所有客户端在相同逻辑帧执行输入指令,服务端作为唯一权威对输入合法性与状态一致性进行裁决。
数据同步机制
服务端按固定 tick(如 30Hz)推进逻辑帧,广播帧号与校验摘要:
type FrameState struct {
FrameID uint64 `json:"frame_id"`
Checksum uint32 `json:"checksum"` // 输入哈希 + 状态快照 CRC32
Inputs []Input `json:"inputs"` // 经签名验证的客户端输入
}
// 校验逻辑:仅当所有客户端输入已收齐且签名有效时才推进帧
func (s *Server) advanceFrame() {
if len(s.pendingInputs[s.currentFrame]) == s.expectedPlayers {
s.executeFrame(s.currentFrame)
s.broadcast(FrameState{
FrameID: s.currentFrame,
Checksum: s.computeFrameChecksum(),
Inputs: s.pendingInputs[s.currentFrame],
})
}
}
advanceFrame 依赖输入集合完整性与数字签名验证——未通过 ed25519.Verify() 的输入直接丢弃。Checksum 保障帧间状态可复现,防止客户端篡改本地模拟结果。
权威校验流程
graph TD
A[客户端提交带签名的Input] --> B[服务端验证签名与序列号]
B --> C{是否重复/越界?}
C -->|否| D[存入pendingInputs[frame]]
C -->|是| E[拒绝并记录异常]
D --> F[帧输入收齐?]
F -->|是| G[执行确定性逻辑+生成Checksum]
F -->|否| B
关键参数说明
| 字段 | 含义 | 典型值 |
|---|---|---|
FrameID |
单调递增逻辑帧序号 | uint64,起始0 |
TickInterval |
帧间隔毫秒 | 33ms(≈30 FPS) |
InputTTL |
输入有效窗口(帧数) | ±2帧防网络抖动 |
3.2 状态同步与快照压缩算法(Delta Snapshot)编码实践
数据同步机制
传统全量快照在高频更新场景下带宽开销巨大。Delta Snapshot 仅传输自上次同步以来的变更部分,显著降低网络负载。
核心编码流程
def delta_encode(base_state: dict, current_state: dict) -> dict:
delta = {}
for key, new_val in current_state.items():
old_val = base_state.get(key)
if new_val != old_val:
delta[key] = {"op": "update", "val": new_val}
if old_val is None:
delta[key]["op"] = "insert"
elif new_val is None:
delta[key]["op"] = "delete"
return delta
该函数以字典为状态单元,逐键比对生成操作型差分。op 字段标识变更语义,val 携带有效载荷;时间复杂度 O(n),空间复杂度 O(Δn)。
压缩策略对比
| 方法 | 压缩率 | 解码开销 | 适用场景 |
|---|---|---|---|
| JSON diff | 中 | 低 | 结构稳定小对象 |
| Protocol Buffers + delta | 高 | 中 | 高频实时同步 |
graph TD
A[Base Snapshot] --> B[Current State]
B --> C[Key-wise Diff]
C --> D[Op-annotated Delta]
D --> E[Binary Encoding]
3.3 战斗事件驱动架构:基于channel与Fan-in/Fan-out模式的事件总线
在高并发实时战斗场景中,事件解耦与有序分发至关重要。Go 的 chan 天然适配事件总线模型,配合 Fan-in(多源聚合)与 Fan-out(单源广播)可构建低延迟、高吞吐的响应链。
数据同步机制
战斗状态变更(如伤害、位移、Buff)作为事件流,经统一 eventBus channel 聚合:
// 事件总线定义(带缓冲提升吞吐)
type EventBus struct {
in chan Event // Fan-in 输入通道
out chan Event // Fan-out 输出通道
}
func NewEventBus() *EventBus {
return &EventBus{
in: make(chan Event, 1024), // 缓冲防阻塞
out: make(chan Event, 1024),
}
}
in 通道接收各战斗模块(技能系统、AI、物理引擎)推送的事件;out 供所有监听器并发消费。缓冲容量需权衡内存与背压风险——过小易丢事件,过大增延迟。
架构拓扑
graph TD
A[技能系统] -->|Fan-in| C[EventBus.in]
B[AI决策] -->|Fan-in| C
C -->|Fan-out| D[伤害计算]
C -->|Fan-out| E[动画同步]
C -->|Fan-out| F[网络广播]
核心优势对比
| 特性 | 传统轮询 | Channel事件总线 |
|---|---|---|
| 延迟 | ≥50ms(固定周期) | |
| 扩展性 | 修改主循环耦合度高 | 新监听器仅需 go listen(bus.out) |
- 事件类型通过接口
Event统一抽象,支持运行时动态注册监听器; - Fan-in 由 goroutine 安全聚合,Fan-out 借助
for range bus.out实现无锁广播。
第四章:分布式战斗服核心组件集成
4.1 基于etcd的动态服务注册与发现机制
传统静态配置难以应对容器化环境下的服务弹性伸缩。etcd 作为强一致、高可用的分布式键值存储,天然适配服务注册与发现的核心诉求。
注册流程:TTL租约驱动
服务启动时创建带 TTL 的 Lease,并将服务元数据(IP、端口、健康状态)以 PUT 操作写入 /services/{service-name}/{instance-id} 路径:
# 创建10秒TTL租约,并绑定服务实例
ETCDCTL_API=3 etcdctl lease grant 10
# 输出:lease 326c5e9a87f3d7b7
ETCDCTL_API=3 etcdctl put --lease=326c5e9a87f3d7b7 \
/services/api-gateway/inst-001 \
'{"host":"10.1.2.3","port":8080,"weight":100}'
逻辑分析:
--lease参数将 key 绑定至租约,租约过期则 key 自动删除;/services/...路径结构支持前缀监听,便于客户端订阅变更。
发现机制:Watch + 健康感知
客户端通过 watch 监听 /services/api-gateway/ 前缀,实时获取增删改事件。配合定期心跳续租,实现故障自动剔除。
| 特性 | 说明 |
|---|---|
| 强一致性 | Raft 协议保障读写线性一致 |
| 低延迟监听 | gRPC Watch 流式推送变更 |
| 多维度元数据支持 | JSON 值可扩展 tags、version 等 |
graph TD
A[服务实例] -->|PUT + Lease| B[etcd集群]
C[客户端] -->|Watch prefix| B
B -->|Event Stream| C
C --> D[负载均衡器更新路由表]
4.2 Redis集群支撑的跨服战斗状态共享与原子操作
跨服战斗需多服实例实时同步战斗状态(如血量、技能CD、胜负标记),单点Redis易成瓶颈且缺乏容错。采用Redis Cluster分片架构,将战斗ID哈希至16384个slot,确保同一战斗的所有键路由至相同节点。
数据同步机制
使用Redis Pipeline + WATCH/MULTI/EXEC保障状态更新原子性:
# 原子扣减HP并校验存活
pipe = redis_client.pipeline()
pipe.watch(f"battle:{bid}:player:{pid}:hp")
current_hp = int(pipe.get(f"battle:{bid}:player:{pid}:hp") or "0")
if current_hp > damage:
pipe.multi()
pipe.set(f"battle:{bid}:player:{pid}:hp", current_hp - damage)
pipe.setex(f"battle:{bid}:log:{ts}", 3600, f"damage:{damage}")
pipe.execute() # 成功则提交,失败重试
WATCH监听HP键防止并发覆盖;MULTI/EXEC包裹写操作形成事务边界;setex为日志添加TTL避免堆积。
关键设计对比
| 特性 | 单实例Redis | Redis Cluster |
|---|---|---|
| 故障恢复 | 全局不可用 | 分片级自动主从切换 |
| 写吞吐上限 | ~10万 QPS | 线性扩展至百万级 |
| 跨节点原子操作支持 | ✅(单key) | ❌(需Lua或客户端协调) |
状态一致性流程
graph TD
A[客户端发起攻击] --> B{读取当前HP}
B --> C[WATCH目标HP键]
C --> D[校验是否存活]
D -->|是| E[MULTI执行扣血+日志]
D -->|否| F[返回已阵亡]
E --> G[EXEC成功?]
G -->|是| H[广播战斗事件]
G -->|否| B
4.3 Kafka消息队列在战斗日志审计与行为回溯中的应用
日志生产与分区策略
战斗事件(如PlayerAttack, SkillCast, BuffApply)按player_id % 16哈希分区,保障同一玩家行为严格有序:
ProducerRecord<String, String> record = new ProducerRecord<>(
"battle-audit-log",
String.valueOf(playerId), // key → 决定分区
JsonUtils.toJson(event) // value: { "ts": 1712345678900, "type": "PlayerAttack", ... }
);
逻辑分析:playerId作为key确保同玩家日志落于同一分区,Kafka内部分区顺序写入+单分区FIFO语义,为后续按玩家精确回溯提供时序基础;battle-audit-log主题配置retention.ms=604800000(7天),满足合规审计窗口。
实时消费与状态重建
消费者组battle-retracer启用enable.auto.commit=false,手动提交offset以保障幂等回放:
| 组件 | 配置值 | 作用 |
|---|---|---|
isolation.level |
read_committed |
过滤未提交事务日志 |
max.poll.records |
100 |
控制单次拉取量,防OOM |
group.id |
battle-retracer-2024 |
支持多实例并行回溯 |
行为链路还原流程
graph TD
A[战斗事件产生] --> B[Kafka分区写入]
B --> C{消费者组拉取}
C --> D[按player_id聚合事件流]
D --> E[构建时间线状态机]
E --> F[输出可追溯行为快照]
4.4 分布式锁与一致性哈希在多节点技能释放冲突控制中的落地
在高并发战斗服务中,多个游戏服务器节点可能同时处理同一玩家的技能释放请求,导致状态不一致(如重复扣蓝、技能CD重置)。
核心设计思路
- 使用 Redisson 的
RLock实现可重入分布式锁,以玩家 ID 为锁粒度 - 结合一致性哈希将玩家路由到固定节点,降低锁竞争频次
一致性哈希路由示例
// 基于玩家ID计算归属节点(使用Redisson内置一致性哈希)
String playerId = "player_123456";
int slot = Hashing.murmur3_32().hashString(playerId, StandardCharsets.UTF_8).asInt() % nodeCount;
// slot 决定请求转发至哪台技能处理节点
逻辑分析:Murmur3 哈希确保相同 playerID 总映射到同一节点;
% nodeCount避免哈希环未启用时的简单取模,参数nodeCount为当前在线技能节点数,需配合服务发现动态更新。
锁执行流程
graph TD
A[客户端发起技能请求] --> B{按playerId哈希路由}
B --> C[目标节点尝试获取RLock]
C -->|成功| D[校验CD/资源/冷却状态]
C -->|失败| E[返回“操作繁忙”]
D --> F[执行技能逻辑并持久化]
对比方案性能指标
| 方案 | 平均延迟 | 锁冲突率 | 节点扩缩容影响 |
|---|---|---|---|
| 全局锁(playerId为key) | 18ms | 32% | 无 |
| 一致性哈希+局部锁 | 9ms | 7% | 需重新分片 |
第五章:性能调优、监控与线上运维体系
核心指标定义与黄金信号实践
在真实电商大促场景中,我们以“黄金信号”(Golden Signals)为基线构建监控体系:延迟(P99 25%)。某次双十一大促前压测发现订单服务P99延迟突增至2.4s,经火焰图分析定位到MyBatis二级缓存未配置readOnly=false导致序列化阻塞,修复后延迟回落至620ms。
JVM调优实战案例
某支付网关JVM频繁Full GC(每15分钟一次),GC日志显示老年代持续增长。通过jstat -gc <pid>确认OU已达98%,结合jmap -histo:live发现大量com.alipay.sdk.util.MD5对象堆积。最终调整JVM参数为:
-XX:+UseG1GC -Xms4g -Xmx4g -XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=1M -XX:G1ReservePercent=15
GC频率降至每周1次,STW时间从1.8s压缩至120ms以内。
Prometheus+Grafana告警闭环流程
采用Prometheus采集指标,Grafana构建多维看板,并通过Alertmanager实现分级告警:
- P0级(立即响应):HTTP 5xx错误率 > 5% 持续2分钟 → 企业微信+电话双通道
- P1级(2小时内处理):Redis连接池使用率 > 90% → 钉钉群自动推送+关联慢查询日志链接
- P2级(24小时内优化):Kafka消费延迟 > 10万条 → 自动生成Jira工单并分配至SRE组
graph LR
A[应用埋点] --> B[Prometheus抓取]
B --> C[Grafana可视化]
C --> D{阈值触发?}
D -->|是| E[Alertmanager路由]
E --> F[企业微信/电话/邮件]
F --> G[Ops平台自动执行预案]
G --> H[记录MTTR并生成复盘报告]
线上灰度发布与熔断机制
使用Spring Cloud Gateway配合Sentinel实现动态灰度:将1%流量导向新版本订单服务,同时配置熔断规则——当异常比例超60%持续10秒即自动切断该路由。2023年Q3某次库存服务升级中,灰度集群因DB连接池配置错误触发熔断,主流量零感知切换,故障影响范围控制在0.8%用户内。
日志链路追踪标准化
统一接入SkyWalking v9.4,要求所有微服务必须注入trace_id至SLF4J MDC,并在Nginx入口层添加X-Trace-ID头透传。日志格式强制规范:
[%d{ISO8601}] [%X{trace_id}] [%t] [%p] [%c{1}] - %m%n
使跨12个服务的订单创建链路可在3秒内完成全路径检索,平均排障时间从47分钟缩短至6.2分钟。
基础设施监控覆盖矩阵
| 监控层级 | 工具栈 | 关键指标示例 | 数据采集周期 |
|---|---|---|---|
| 应用层 | SkyWalking + Micrometer | JVM内存、线程池活跃数、SQL耗时 | 15s |
| 中间件层 | Redis Exporter | connected_clients、evicted_keys | 30s |
| 容器层 | cAdvisor | container_cpu_usage_seconds_total | 10s |
| 主机层 | Node Exporter | node_load1、node_disk_io_time_ms | 20s |
故障自愈脚本库建设
在Ansible Playbook中沉淀23类高频故障自愈剧本,例如:
redis-cluster-rebalance.yml:自动检测槽位倾斜并触发reshardk8s-pod-eviction.yml:当节点磁盘使用率>95%时驱逐非核心Podmysql-slow-query-clear.yml:解析slow_log并kill运行超300s的事务
某次凌晨MySQL主库IO等待飙升至280ms,自愈脚本自动启用pt-kill --busy-time 300并重启慢查询日志轮转,业务恢复耗时仅47秒。
