第一章:Go语言协议实现的核心范式与设计哲学
Go语言在协议实现层面摒弃了传统面向对象的继承与接口强制绑定,转而拥抱组合优先、显式契约与零分配抽象的设计哲学。其核心范式体现为“小接口、大组合、隐式实现”——接口仅定义最小行为集合(如 io.Reader 仅含 Read(p []byte) (n int, err error)),任何类型只要实现该方法即自动满足接口,无需显式声明。
接口即契约,而非类型分类
Go接口是纯粹的行为契约,不携带状态或实现细节。例如,实现自定义HTTP中间件协议时,只需定义:
// Middleware 是处理 HTTP 请求链的函数契约
type Middleware func(http.Handler) http.Handler
// 示例:日志中间件,完全独立于具体 Handler 实现
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("REQ: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r) // 委托给下游处理器
})
}
此代码无继承、无泛型约束(Go 1.18前)、无反射调用,仅靠函数签名匹配完成协议适配。
组合优于继承的工程实践
协议扩展通过结构体嵌入自然达成。例如,为 net.Conn 添加超时与加密能力:
type SecureTimedConn struct {
net.Conn // 嵌入基础连接,自动获得 Read/Write 等方法
timeout time.Duration
}
func (c *SecureTimedConn) Read(b []byte) (int, error) {
if c.timeout > 0 {
c.Conn.SetReadDeadline(time.Now().Add(c.timeout))
}
return c.Conn.Read(b) // 直接委托,语义清晰,开销为零
}
协议实现的三原则
- 显式性:所有依赖必须在函数签名或结构体字段中明确定义;
- 可测试性:接口粒度小,便于用内存模拟(如
bytes.Buffer替代io.Writer); - 零拷贝友好:
[]byte和unsafe.Slice等原生支持避免协议层冗余内存分配。
| 范式特征 | 传统OOP方案 | Go协议实现方式 |
|---|---|---|
| 类型扩展机制 | 继承链 | 结构体嵌入 + 方法重写 |
| 接口绑定时机 | 编译期强制声明 | 运行时隐式满足 |
| 协议演化成本 | 修改基类影响全继承树 | 新增小接口,旧实现仍有效 |
第二章:底层网络协议栈的Go化构建
2.1 基于net.Conn的TCP/UDP协议封装与生命周期管理
Go 标准库 net.Conn 是面向连接通信的统一抽象,但 TCP 与 UDP 在语义上存在本质差异:TCP 是流式、有状态、全双工;UDP 是无连接、消息边界明确、无内置重传。
封装设计原则
- TCP 封装需管理连接建立、保活、优雅关闭(
SetKeepAlive,CloseWrite) - UDP 封装需适配
net.PacketConn,通过ReadFrom/WriteTo维护地址上下文
生命周期关键状态
| 状态 | TCP 可达 | UDP 可达 | 说明 |
|---|---|---|---|
| 初始化 | ✅ | ✅ | net.Dial/Listen 或 net.ListenPacket |
| 活跃传输 | ✅ | ✅ | Read/Write 或 ReadFrom/WriteTo |
| 关闭中 | ✅ | ❌ | TCP 支持半关闭,UDP 无状态不可“关闭中” |
// TCP 连接封装示例:带超时与错误恢复的读写器
func (c *TCPConn) ReadMsg(p []byte) (n int, err error) {
c.mu.Lock()
defer c.mu.Unlock()
c.conn.SetReadDeadline(time.Now().Add(30 * time.Second))
return c.conn.Read(p) // 参数 p:用户提供的缓冲区,长度决定单次最大读取字节数;返回 n 为实际读取字节数
}
该方法在临界区加锁保障并发安全,设置读超时避免永久阻塞,并复用底层 net.Conn.Read 接口——体现了对标准接口的轻量增强而非替代。
2.2 自定义二进制协议编解码器:binary.Read/Write与unsafe优化实践
在高频低延迟场景中,标准 encoding/gob 或 JSON 序列化开销过大。基于 binary.Read/binary.Write 构建轻量二进制协议是常见选择。
核心结构体示例
type Order struct {
ID uint64
Price int32
Side byte // 'B' or 'S'
Symbol [8]byte // fixed-length symbol
}
binary.Read(r, binary.BigEndian, &o)按字段顺序、字节对齐逐字段解析;需确保结构体无填充间隙(可使用//go:packed或手动对齐)。
unsafe 优化路径
- 使用
unsafe.Slice(unsafe.Pointer(&o), size)直接映射内存块 - 避免
[]byte复制,但需严格保证生命周期安全
性能对比(10K 次序列化)
| 方式 | 耗时 (ns/op) | 分配内存 (B/op) |
|---|---|---|
binary.Write |
245 | 48 |
unsafe 批量写入 |
89 | 0 |
graph TD
A[原始结构体] --> B[binary.Write]
A --> C[unsafe.Slice + memcpy]
B --> D[安全但有拷贝开销]
C --> E[零分配但需内存管理]
2.3 TLS 1.3协议扩展支持:crypto/tls配置定制与ALPN协商实战
TLS 1.3 默认禁用不安全扩展,但 ALPN(Application-Layer Protocol Negotiation)作为关键扩展被强制保留,用于 HTTP/2、h3 等协议的无缝协商。
ALPN 协商流程
config := &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
MinVersion: tls.VersionTLS13,
}
NextProtos指定客户端支持的应用层协议优先级列表,服务端从中选择首个匹配项;MinVersion: tls.VersionTLS13强制仅启用 TLS 1.3,避免降级至含脆弱扩展的旧版本。
常见 ALPN 协议标识对照
| 协议 | ALPN 字符串 | 用途 |
|---|---|---|
| HTTP/2 | h2 |
加密二进制帧传输 |
| HTTP/1.1 | http/1.1 |
兼容性兜底 |
| QUIC/HTTP3 | h3 |
基于 UDP 的新标准 |
graph TD A[ClientHello] –>|Includes ALPN extension| B[ServerHello] B –>|Selects first match| C[Proceed with h2 or h3]
2.4 ICMPv6与Raw Socket权限控制:syscall.Socket与平台兼容性攻坚
权限差异的本质根源
Linux、macOS 和 Windows 对 AF_INET6 + SOCK_RAW 的权限策略截然不同:
- Linux:需
CAP_NET_RAW(非 root 用户可通过setcap cap_net_raw+ep ./bin授予) - macOS:仅允许 root 或启用了
net.inet6.ip6.forwarding=1的特权上下文 - Windows:依赖
SeCreateGlobalPrivilege,且需管理员 manifest 声明
syscall.Socket 调用的跨平台陷阱
// Go 中创建 ICMPv6 raw socket 的典型调用
fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_RAW, syscall.IPPROTO_ICMPV6, 0)
if err != nil {
log.Fatal(err) // 在 macOS 上常返回 "operation not permitted"
}
该调用直接映射到 socket(2) 系统调用。参数含义:
AF_INET6:启用 IPv6 地址族;SOCK_RAW:绕过内核协议栈封装,需自行构造 ICMPv6 头;IPPROTO_ICMPV6:指定协议号 58,但部分内核版本(如 macOS 13+)对此值校验更严格,需确认sys/protosw.h实际定义。
兼容性决策矩阵
| 平台 | 是否支持非 root ICMPv6 Raw Socket | 替代方案 |
|---|---|---|
| Linux | ✅(CAP_NET_RAW) | setcap 或容器 securityContext |
| macOS | ❌(仅 root) | 使用 net.DialUDP + ipv6-icmp 伪设备(需 sudo ifconfig lo0 inet6 fe80::1%lo0 prefixlen 64) |
| Windows | ⚠️(需管理员+Manifest) | WinPCap/Npcap 驱动层封装 |
graph TD
A[调用 syscall.Socket] --> B{OS 检查 CAP/Privilege}
B -->|Linux| C[成功返回 fd]
B -->|macOS| D[errno=EPERM]
B -->|Windows| E[触发 UAC 提权或失败]
D --> F[降级为 UDP socket + ping 库模拟]
2.5 协议状态机建模:stateless FSM库集成与goroutine安全迁移策略
stateless 是一个轻量、无状态的 Go FSM 库,天然契合协议层对确定性与并发隔离的要求。
核心集成要点
- 状态与事件定义为
string或int,避免指针共享 - 所有转换逻辑纯函数化,不持有外部可变状态
FSM实例本身不可并发写入,需封装保护
goroutine 安全迁移方案
type ProtocolFSM struct {
mu sync.RWMutex
fsm *stateless.FSM
}
func (p *ProtocolFSM) Trigger(event string, args ...interface{}) error {
p.mu.Lock() // 写锁仅保护触发动作(非状态持久化)
defer p.mu.Unlock()
return p.fsm.Event(event, args...)
}
此封装将并发控制收敛至单点:
Lock()保障Event()调用原子性;因stateless.FSM.Event()本身不修改内部结构(仅读取映射+执行回调),实际开销极低。所有状态变更通过回调函数注入,业务逻辑与状态机解耦。
| 迁移策略 | 适用场景 | 安全边界 |
|---|---|---|
| 读写锁封装 | 高频事件、低延迟要求 | 事件触发阶段 |
| Channel 串行化 | 强顺序一致性协议 | 全生命周期 |
| 每连接独立 FSM | 连接粒度隔离需求明确 | 无共享状态,零锁 |
graph TD
A[Client Event] --> B{ProtocolFSM.Trigger}
B --> C[acquire Lock]
C --> D[stateless.FSM.Event]
D --> E[exec callbacks]
E --> F[release Lock]
第三章:Wireshark深度联动调试体系搭建
3.1 Go协议流量注入:pcapgo + gopacket生成可回放PCAP文件
构建可复现的网络测试流量,需精准控制协议栈各层字段。gopacket 提供完整的数据包组装能力,pcapgo 负责序列化为标准 PCAP 格式。
核心依赖与初始化
import (
"os"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/pcapgo"
)
gopacket抽象了链路层到传输层的封装逻辑;pcapgo.Writer支持纳秒级时间戳写入,兼容 Wireshark 回放。
构建 TCP SYN 包示例
// 构造以太网帧 → IP → TCP 层
eth := layers.Ethernet{SrcMAC: srcMAC, DstMAC: dstMAC}
ip := layers.IPv4{SrcIP: net.ParseIP("192.168.1.100"), DstIP: net.ParseIP("192.168.1.200")}
tcp := layers.TCP{SrcPort: 12345, DstPort: 80, SYN: true, Seq: 1000}
buf := gopacket.NewSerializeBuffer()
gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true}, ð, &ip, &tcp)
SerializeOptions{FixLengths: true}自动填充 IP/TCP 头部长度与校验和;buf.Bytes()输出原始字节流,可直接写入 PCAP。
写入 PCAP 文件流程
graph TD
A[构造Layer栈] --> B[序列化为[]byte]
B --> C[pcapgo.Writer.WritePacket]
C --> D[磁盘PCAP文件]
| 组件 | 作用 |
|---|---|
gopacket.PacketBuilder |
分层构建与校验 |
pcapgo.Writer |
支持微秒/纳秒时间戳、链路类型设置 |
layers.Ethernet |
可显式控制 MAC 地址与 EtherType |
3.2 自定义Wireshark Dissector开发:Lua插件编写与字段注册全流程
Wireshark 的 Lua Dissector 允许在不编译 C 代码的前提下解析私有协议。核心流程包括:协议定义、字段注册、解码逻辑实现与 dissector 注册。
协议与字段注册
-- 定义协议对象
local myproto = Proto("myproto", "My Custom Protocol")
-- 注册可显示字段(用于Packet Details面板)
local f_length = ProtoField.uint16("myproto.length", "Length", base.DEC)
local f_magic = ProtoField.bytes("myproto.magic", "Magic Bytes")
myproto.fields = {f_length, f_magic}
ProtoField.uint16 创建 16 位无符号整数字段,"myproto.length" 是唯一过滤器名称;base.DEC 指定十进制显示;ProtoField.bytes 用于原始字节展示。
解析逻辑与注册
function myproto.dissector(buffer, pinfo, tree)
if buffer:len() < 4 then return end
pinfo.cols.protocol = "MYPROTO"
local subtree = tree:add(myproto, buffer(), "My Protocol Data")
subtree:add(f_length, buffer(0,2)):set_text("Length: " .. buffer(0,2):uint())
subtree:add(f_magic, buffer(2,2))
end
-- 绑定到 TCP 端口 0x1337(4919)
DissectorTable.get("tcp.port"):add(4919, myproto)
| 步骤 | 关键动作 | 说明 |
|---|---|---|
| 1 | Proto 实例化 |
声明协议名与描述 |
| 2 | ProtoField 注册 |
定义可搜索/可显示的协议字段 |
| 3 | dissector() 实现 |
解析字节流并构建树形结构 |
| 4 | DissectorTable.add() |
将 dissector 关联到传输层端口或协议 |
graph TD A[定义Proto对象] –> B[注册ProtoField字段] B –> C[实现dissector函数] C –> D[绑定至DissectorTable]
3.3 实时抓包-日志双向映射:Go runtime trace与tshark -V输出结构化解析
为实现网络请求与 Go 程序执行轨迹的精准对齐,需将 tshark -V 的协议层解析结果与 go tool trace 中的 Goroutine、Netpoll、Syscall 事件建立时间戳+语义双维度映射。
数据同步机制
采用纳秒级单调时钟(runtime.nanotime() + clock_gettime(CLOCK_MONOTONIC))统一校准两端时间源,消除系统时钟漂移。
结构化解析示例
# tshark -V -T json -o "json.pretty:TRUE" -Y "http.request" capture.pcapng | jq '.[] | {ts: .frame.time_epoch, src: .ip.src, method: .http.request_method}'
该命令提取 HTTP 请求的绝对时间戳、源 IP 与方法,作为映射锚点;time_epoch 为 Unix 纳秒精度浮点数,可直接与 trace 中 procStart/gStart 时间字段比对。
| 字段 | trace 来源 | tshark 来源 | 对齐方式 |
|---|---|---|---|
ts_ns |
ev.Ts |
.frame.time_epoch * 1e9 |
四舍五入到纳秒 |
goroutine_id |
ev.G |
— | 关联 net/http handler goroutine 标签 |
conn_fd |
ev.Args[0] (syscall) |
.tcp.stream |
通过 TCP 流索引反查 fd |
映射流程
graph TD
A[tshark -V 输出 JSON] --> B[解析 frame.time_epoch + http fields]
C[go tool trace] --> D[提取 Goroutine 创建/阻塞/唤醒事件]
B & D --> E[按 ±10μs 窗口关联事件]
E --> F[生成 traceID ↔ streamID 双向索引表]
第四章:面向协议的Fuzzing框架开源实录
4.1 go-fuzz驱动层改造:支持自定义协议输入语料的Corpus序列化协议
为适配物联网设备二进制私有协议(如TLV+CRC变体),需在 go-fuzz 驱动层注入协议感知的语料序列化逻辑。
Corpus 序列化核心接口
type ProtocolCorpusSerializer interface {
// 将原始字节流按协议规则封装为可 fuzz 的语料单元
Serialize(raw []byte) ([]byte, error) // 输出含协议头、校验、填充的完整帧
Deserialize(fuzzed []byte) ([]byte, error) // 剥离协议外壳,返回有效载荷供目标函数消费
}
Serialize() 负责构造合法协议帧(如添加 0x55AA 同步头、2字节长度域、CRC16-CCITT 校验),确保语料通过协议解析前置校验;Deserialize() 则逆向提取 payload,避免 fuzz 引擎干扰协议结构。
支持的协议特征映射表
| 协议要素 | 实现方式 | 是否必需 |
|---|---|---|
| 帧头识别 | 固定魔数或可配置字节序列 | 是 |
| 长度字段 | 1/2/4 字节,大端/小端可选 | 是 |
| 校验算法 | CRC16-CCITT / XOR8 / 无校验 | 否 |
| 载荷加密标识 | TLV 中 type=0x80 表示 AES-GCM | 否 |
数据流改造示意
graph TD
A[原始语料字节] --> B[ProtocolCorpusSerializer.Serialize]
B --> C[合规协议帧]
C --> D[go-fuzz 输入队列]
D --> E[目标函数调用前<br>Deserialize 提取 payload]
4.2 基于AST的协议语法模糊变异引擎:protobuf/gRPC IDL与自定义DSL双模式支持
该引擎以编译器前端技术为基底,将 .proto 文件与自定义 DSL 源码统一解析为统一抽象语法树(UAST),实现语义一致的变异操作。
双模式解析架构
- protobuf/gRPC 模式:复用
protoc的--plugin接口,通过libprotocAPI 提取FileDescriptorProto并映射为 UAST 节点 - 自定义 DSL 模式:基于 ANTLR v4 构建轻量级语法分析器,生成带位置信息与类型上下文的 AST
核心变异策略示例(protobuf 字段级)
# 对 message 中的 repeated int32 字段注入边界值变异
def mutate_repeated_field(node: AstNode):
if node.type == "FIELD" and node.label == "repeated" and node.type_name == "int32":
return [
node.with_modifier("max_size=0"), # 空列表
node.with_modifier("max_size=65536"), # 超限触发 gRPC 流控
]
逻辑分析:
node.with_modifier()不修改原始 AST,而是生成带元数据标记的新节点;max_size参数被后续序列化器识别并注入 wire-level 边界约束,确保变异在传输层生效。
模式切换能力对比
| 特性 | protobuf/gRPC 模式 | 自定义 DSL 模式 |
|---|---|---|
| 语法校验粒度 | 编译期强类型(.proto) |
运行时动态语义检查 |
| 变异可扩展性 | 需重编译 protoc 插件 | 热加载 Python 变异规则类 |
graph TD
A[输入IDL源码] --> B{文件后缀判断}
B -->|*.proto| C[protoc + Descriptor → UAST]
B -->|*.dsl| D[ANTLR Parser → UAST]
C & D --> E[统一变异规则引擎]
E --> F[输出变异后IDL/二进制schema]
4.3 覆盖率引导增强:__sanitizer_cov_trace_pc_guard集成与协议字段级覆盖率反馈
__sanitizer_cov_trace_pc_guard 是 LLVM SanitizerCoverage 提供的核心回调钩子,用于在每条基本块入口处注入轻量级覆盖率采样。
// 在目标协议解析器中插入guard变量与回调
static uint32_t guard_var = 0;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (*guard == 0) {
__atomic_fetch_add(guard, 1, __ATOMIC_RELAXED);
// 触发字段级反馈:此处可关联当前PC到协议字段(如IPv4.ttl)
report_field_coverage(__builtin_return_address(0));
}
}
该函数通过原子操作避免竞态,*guard 初始为 0,首次执行时递增并触发字段映射逻辑。__builtin_return_address(0) 提供调用上下文,支撑协议字段(如 TCP.seq, TLS.content_type)的语义标注。
字段-PC 关联策略
- 解析器每进入一个字段解析分支,静态分配唯一
guard地址 - 运行时将
guard地址哈希映射至字段标识符(如0xabc123 → "HTTP.status_code")
覆盖率反馈粒度对比
| 粒度层级 | 覆盖单元 | 协议测试有效性 |
|---|---|---|
| 基本块级 | 编译器生成BB | 中(无法区分字段变异) |
| 字段级(本节) | guard ↔ 字段语义 |
高(精准驱动fuzzer变异) |
graph TD
A[解析器入口] --> B{解析IPv4头?}
B -->|是| C[guard_0x100: version/ihl]
B -->|是| D[guard_0x104: ttl]
C --> E[记录字段级覆盖事件]
D --> E
4.4 漏洞POC自动化提取:panic堆栈符号化解析与协议上下文快照保存机制
panic堆栈的符号化还原
当内核触发panic时,原始调用栈常含未解析的十六进制地址(如 ffff88800012a3b4)。需结合vmlinux符号表与kallsyms进行动态映射:
# 解析示例:addr2line + kallsyms联合定位
def symbolize_stack(raw_lines):
symbols = load_kallsyms("/proc/kallsyms") # {addr: "tcp_v4_rcv+0x12/0x300"}
for line in raw_lines:
addr = extract_addr(line)
if addr in symbols:
print(f"{line} → {symbols[addr]}") # ffff88800012a3b4 → tcp_v4_rcv+0x12/0x300
逻辑说明:
load_kallsyms构建地址→符号映射哈希表;extract_addr使用正则匹配[<.*>]内地址;输出含偏移量的函数名,精准定位漏洞触发点。
协议上下文快照机制
在panic前10ms内捕获关键协议状态:
| 字段 | 来源 | 用途 |
|---|---|---|
skb->data |
__netif_receive_skb_core |
原始报文载荷 |
sk->sk_state |
inet_csk_get_port |
TCP连接状态 |
skb->cb[] |
自定义填充 | POC构造标记 |
自动化提取流程
graph TD
A[触发panic] --> B[捕获raw stack]
B --> C[符号化解析]
C --> D[关联最近skb/sk]
D --> E[序列化为POC YAML]
第五章:从协议实现到云原生网络中间件的演进路径
现代云原生架构中,网络中间件已不再是简单的流量转发组件,而是承载服务发现、熔断限流、零信任鉴权、可观测性注入等关键能力的运行时基础设施。这一演进并非线性叠加,而是由协议栈深度重构驱动的范式迁移。
协议实现层的解耦实践
在某大型电商中台项目中,团队将 HTTP/2 和 gRPC 的帧解析逻辑从 Nginx 模块中剥离,封装为独立的 Rust 编写的 proto-bridge 库。该库通过 WASM 插件机制嵌入 Envoy,支持动态加载 TLS 1.3 扩展(如 ESNI)和自定义 ALPN 协商策略。以下为实际部署中启用双向 mTLS 的配置片段:
tls_context:
common_tls_context:
tls_certificates:
- certificate_chain: { filename: "/etc/certs/tls.crt" }
private_key: { filename: "/etc/certs/tls.key" }
validation_context:
trusted_ca: { filename: "/etc/certs/ca.pem" }
verify_certificate_spki: ["d7k9aXJvZmFjZS1jZXJ0LXNwa2k="]
控制平面与数据平面的协同演进
随着 Istio 1.18 引入 WorkloadGroup 与 Telemetry API 的融合,运维团队在金融核心系统中实现了细粒度遥测策略下发。下表对比了传统 Sidecar 注入模式与新型 eBPF 加速模式的关键指标:
| 指标 | 传统 Envoy Sidecar | eBPF + Cilium 1.14 |
|---|---|---|
| TCP 连接建立延迟 | 8.2 ms | 1.7 ms |
| 内存占用(每 Pod) | 45 MB | 3.1 MB |
| 策略更新生效时间 | 2.4 s | 180 ms |
多集群服务网格的协议适配挑战
某跨国物流企业采用基于 QUIC 的跨大洲服务调用方案。其核心难点在于 UDP 分片重传与 BGP Anycast 的冲突——当边缘节点位于不同 AS 域时,QUIC 连接 ID 在 NAT64 网关处频繁失效。解决方案是开发 quic-gateway-adaptor 组件,在 Kubernetes Ingress Controller 层注入 QUIC 连接池管理器,并利用 ClusterIP Service 的 EndpointSlice 实现连接 ID 映射持久化。
安全协议栈的运行时热插拔
在政务云平台升级中,为满足等保2.1对国密算法的强制要求,团队未重建整个网关层,而是基于 OpenSSL 3.0 提供的 Provider 架构,构建了 sm2-sm4-provider 动态模块。该模块通过 Envoy 的 extension_config_provider 接口注册,在不重启数据平面的前提下完成 TLS 握手算法切换。Mermaid 流程图展示了证书协商阶段的协议路由逻辑:
flowchart LR
A[Client Hello] --> B{ALPN = “h3”?}
B -->|Yes| C[QUIC Transport Layer]
B -->|No| D[HTTP/2 over TLS]
C --> E[SM2 Key Exchange]
D --> F[RSA/ECC Negotiation]
E --> G[SM4-GCM Encryption]
F --> H[AES-256-GCM Encryption]
可观测性协议的标准化落地
某在线教育平台将 OpenTelemetry Collector 改造成协议感知型中间件:其 otlp_http receiver 不仅接收 trace 数据,还解析 X-Service-Version 和 X-Deployment-ID 自定义 header,自动关联 K8s Deployment 的 Git SHA 标签,并通过 Prometheus Remote Write 将服务版本维度注入 metrics 标签族。此设计使故障定位平均耗时从 17 分钟缩短至 210 秒。
