17 KiB
OmniSocketGo 项目导读
本文面向第一次接手 OmniSocketGo 的开发者,目标是帮助你在阅读源码之前先建立整体认知。它不替代根目录的 README.md;README.md 仍然适合作为快速构建和运行说明,这份文档更偏向项目结构、模块职责、消息流转和源码阅读导航。
1. 项目定位
OmniSocketGo 是一个 Linux-only 的 Go 1.22 项目,核心模型是:
- 一个中心
server - 多个连接到
server的peer - 一个独立的时延日志汇总工具
latencysummary
从职责上看:
server负责接收 TCP 连接、校验peer注册、维护在线连接表,并在不同peer之间转发业务消息。peer负责连接server、完成注册、发送文本或文件、接收转发消息,并将接收到的内容持久化到本地目录。latencysummary负责读取原始时延 JSONL 日志,按消息维度聚合为摘要结果,并额外生成一个 HTML 图表,方便观察端到端时延。
这个项目不是点对点直连通信,而是典型的“中心转发”模式。peer-a 想发给 peer-b 时,消息路径是:
peer-a -> server -> peer-b
2. 顶层结构
当前仓库的核心内容可以按下面理解:
OmniSocketGo/
├─ README.md
├─ go.mod
├─ doc/
│ └─ project-guide.md
├─ cmd/
│ ├─ server/
│ │ └─ main.go
│ ├─ peer/
│ │ ├─ main.go
│ │ ├─ interactive.go
│ │ └─ interactive_test.go
│ ├─ latencysummary/
│ │ └─ main.go
│ └─ internal/
│ ├─ protocol/
│ ├─ transport/
│ ├─ server/
│ ├─ peer/
│ └─ latencylog/
└─ latencysummary
其中:
cmd/server、cmd/peer、cmd/latencysummary是 3 个可执行程序入口。cmd/internal/*是这些入口依赖的内部包,真正的核心逻辑都在这里。- 根目录中还存在一个名为
latencysummary的文件;本文聚焦源码结构,因此不把它作为主要源码入口展开。
3. 分层视角看项目
如果从“分层”而不是“目录”来看,项目可以分成 5 层:
- 命令入口层
- 业务层
- 协议层
- 传输层
- 观测与分析层
对应关系如下:
| 层次 | 目录/包 | 主要职责 |
|---|---|---|
| 命令入口层 | cmd/server cmd/peer cmd/latencysummary |
解析参数,组装对象,启动主循环 |
| 业务层 | cmd/internal/server cmd/internal/peer |
注册、转发、发送、接收、持久化 |
| 协议层 | cmd/internal/protocol |
定义消息类型、编码解码、消息合法性校验 |
| 传输层 | cmd/internal/transport |
基于 TCP 发送/接收完整消息,处理并发写和 Linux 时间戳 |
| 观测与分析层 | cmd/internal/latencylog |
记录时延事件、汇总 JSONL、生成图表 |
推荐把它理解为:
protocol决定“消息长什么样”transport决定“消息怎么在 TCP 上被完整收发”server和peer决定“业务如何使用这些消息”latencylog决定“如何观察一次消息从发送到落盘经历了什么”
4. 三个可执行程序分别做什么
4.1 server
入口在 cmd/server/main.go。
它做的事情很直接:
- 解析命令行参数,比如
-listen和-latency-log - 创建
Hub - 监听 TCP 地址
- 每接受一个连接,就交给
Hub.ServeConn处理
server 自身不理解“聊天”“文件同步”之类的上层业务语义,它只知道:
- 首条消息必须是
register - 已注册
peer只能发送text或file - 如果目标
peer不存在,要回一个error - 如果目标连接失效,要清理连接并回一个
error
可以把 server 看成一个中心路由器。
4.2 peer
入口在 cmd/peer/main.go。
它负责:
- 用
Dial连接server - 连接建立后立刻发送
register - 根据参数决定是否发送一条初始消息
- 启动接收循环
- 把收到的文本或文件落盘到
inbox-dir - 如果启用了交互模式,在同一条长连接上反复发送多条消息
它支持的典型模式有两种:
- 一次性模式:启动后发送一条
text或file - 交互模式:启动后进入简单 REPL,持续复用同一条连接发消息
4.3 latencysummary
入口在 cmd/latencysummary/main.go。
它不参与在线通信,只处理离线日志:
- 读取一个或多个原始时延 JSONL 文件
- 按消息聚合事件
- 计算多类时延指标
- 输出汇总 JSONL
- 额外输出一个 HTML 图表
如果说 server 和 peer 是数据面,那么 latencysummary 就是观测面的离线分析工具。
5. 核心内部模块导读
5.1 cmd/internal/protocol
这是协议层,决定消息的结构和线上的编码方式。
主要类型
Message 是整个项目最核心的结构体,字段包括:
TypeIDFromToFileNameBody
支持的消息类型
text:正文按 UTF-8 文本解释file:正文是原始文件字节,必须有FileNameregister:peer向server注册身份error:server返回错误信息
它负责的事情
- 校验不同消息类型的字段约束
- 把结构化消息编码为字节流
- 从字节流还原为结构化消息
- 处理帧边界,避免 TCP 粘包/拆包问题
关键约束
- 所有消息都必须有
From和To text不能带FileNametext的Body必须是合法 UTF-8file必须有FileNameregister的目标必须是serverregister不能带正文error必须由server发出- 单帧最大大小为
8 * 1024 * 1024
5.2 cmd/internal/transport
这是传输层,负责把协议消息稳定地跑在一条 TCP 连接上。
核心类型是 TCPConn,它对 net.Conn 做了封装,提供:
SendReceiveReceiveLoopCloseCloseGracefully
它解决了哪些问题
- 保证发送的是一整条消息,而不是半条
- 通过写锁避免多个 goroutine 并发写时字节流互相交错
- 在接收侧持续读取,直到拿到完整帧
- 在 Linux 上启用 socket timestamping,记录发送和接收链路中的内核时间戳
Linux 相关实现
tcp_linux.go 是这个项目比较有特色的一块。它会尝试打开 Linux 的 timestamping 能力,并记录部分关键事件,例如:
A_TX_SCHEDA_TX_SOFTWAREB_RX_SOFTWARE
这也是为什么项目明确写了 Linux only。不是只有部署目标是 Linux,而是代码本身依赖 Linux 的 socket timestamping 能力。
5.3 cmd/internal/server
这里只有一个核心概念:Hub。
可以把 Hub 理解为“在线连接中心”,它维护:
peer ID -> TCPConn的映射
Hub 的主要职责
- 处理新连接的注册流程
- 拒绝未注册连接直接发业务消息
- 拒绝重复
peer ID - 按目标
peer ID查找连接并转发消息 - 连接关闭或转发失败时清理注册表
Hub 的行为边界
它只负责转发和协议约束,不负责:
- 业务持久化
- 文件存储管理
- 聊天记录管理
- 权限控制
也就是说,目前它是一个很轻量的转发中心,而不是功能复杂的消息中间件。
5.4 cmd/internal/peer
这个包是 peer 端的业务层。
Client
Client 表示一个已经连接并注册到 server 的节点,主要方法包括:
DialSendTextSendFileSendFilePathReceiveReceiveLoopPersistMessageClose
它负责的事情
- 建立到
server的 TCP 连接 - 发送
register完成身份注册 - 为业务消息分配自增
MessageID - 发送文本消息和文件消息
- 接收来自
server的转发消息或错误消息 - 把收到的业务消息落盘
持久化策略
接收侧落盘逻辑在 persist.go:
- 文本消息会被追加到
messages.log - 文件消息会被写成单独文件
- 文件名格式是:
<from>-<messageID>-<baseFileName>
这样做的好处是:
- 文本消息便于顺序追踪
- 文件消息天然避免覆盖
- 文件名里直接带了来源和消息 ID,方便回溯
网络绑定能力
peer 还支持:
-bind-ip:指定本地源 IP-bind-device:指定 Linux 网络设备,例如eth0、wwan0
这对多网卡环境或特殊链路测试比较有用。
5.5 cmd/internal/latencylog
这个包负责“记录”和“分析”两件事。
记录侧
logger.go 定义了:
EventLoggerJSONLLogger- 一组
LogMessageEvent/LogMessageEventAt辅助函数
当前业务上最重要的事件有:
A_APP_PREP_BEGINA_TX_SCHEDA_TX_SOFTWAREB_RX_SOFTWAREB_APP_RECVB_PERSIST_BEGINB_PERSIST_END
其中:
A_*表示发送侧B_*表示接收侧TX/RX更偏内核或传输链路APP/PERSIST更偏应用层
分析侧
summary.go 会把原始事件按消息聚合,并计算:
AProcessingLatencyNSAQueueLatencyNSABTransportPropagationNSBKernelReceivePathLatencyNSBProcessingLatencyNSEndToEndLatencyNSApproxRTTNS
summary_chart.go 则把这些摘要结果渲染成 HTML 页面,方便快速观察不同消息的时延分布。
6. 核心消息流
这一节按一次完整消息生命周期来梳理。
6.1 连接与注册
peer启动后调用Dial(serverAddr, peerID, ...)- 底层建立 TCP 连接
peer立刻发送一条register消息server的Hub.ServeConn先读取首条消息- 如果首条消息不是
register,连接会被拒绝 - 如果
peer ID重复,连接会被拒绝并收到error - 注册成功后,
Hub把该连接加入在线表
注册阶段决定了后续所有转发的寻址基础。
6.2 发送文本或文件
发送侧调用:
SendText(to, body)或SendFile(to, fileName, body)或SendFilePath(to, path)
发送前会生成新的 MessageID,然后:
- 记录发送前的应用层事件
A_APP_PREP_BEGIN - 交给
transport.TCPConn.Send transport调用协议层编码- 编码后的消息被写入 TCP 连接
- Linux 侧尽量采集
A_TX_SCHED和A_TX_SOFTWARE
6.3 server 转发
server 收到消息后:
- 确认消息类型只能是
text或file - 强制把
msg.From改成当前已注册的peer ID - 通过
msg.To查找目标连接 - 找不到目标时返回
error - 找到目标就直接转发
这里有一个重要细节:server 不信任客户端自己填写的 From。即使发送端伪造了 From,Hub 也会用实际注册身份覆盖它。
6.4 接收与落盘
接收侧 peer 的接收循环拿到消息后:
transport在 Linux 下尝试记录B_RX_SOFTWAREClient.Receive/ReceiveLoop记录B_APP_RECV- 根据消息类型调用
PersistMessage - 持久化开始时记录
B_PERSIST_BEGIN - 写盘完成后记录
B_PERSIST_END
因此,一条业务消息从“发送端开始准备”到“接收端落盘完成”形成了相对完整的一条时延链路。
6.5 时延日志汇总
后处理阶段由 latencysummary 完成:
- 用一个或多个
-input指定原始 JSONL 日志 - 加载所有事件
- 按消息聚合
- 计算摘要时延
- 输出一个汇总 JSONL
- 按输出文件名自动生成一个同名 HTML 图表
7. 协议说明
7.1 消息结构
业务层统一使用 protocol.Message:
Type / ID / From / To / FileName / Body
其中:
FileName仅对file消息有意义Body不进入 header JSON,而是作为二进制正文附加在后面
7.2 TCP 上传输的帧格式
从 TCP 视角,完整格式可以理解为:
[4-byte frameLength][4-byte headerLen][header JSON][body bytes]
更细一点说:
WriteFrame负责最外层的frameLengthEncodeMessage负责 payload 内部的headerLen + header JSON + body
这样做的目的很明确:TCP 是字节流,不天然保留消息边界,所以要自己在协议层补齐边界信息。
7.3 错误语义
当前协议里的错误消息由 server 发送,类型是 error。常见场景包括:
- 首条消息不是
register - 重复注册相同
peer ID - 已注册
peer再次发送register - 目标
peer不存在 - 发送了不支持的消息类型
从设计上看,error 仍然走同一条消息通道,而不是额外开一个控制通道。
8. 时延日志机制
8.1 为什么项目里有这套日志
这个仓库不只是做“能发消息”,还明显在关注消息经过网络栈时的细粒度时延。尤其是:
- 应用层开始准备消息的时间
- 消息进入发送调度队列的时间
- 消息进入软件发送路径的时间
- 接收侧内核把数据交给协议栈的时间
- 接收侧应用真正读到消息的时间
- 接收侧写盘完成的时间
这些点能帮助区分:
- 应用侧处理慢
- 发送侧排队慢
- 网络传输慢
- 接收侧内核路径慢
- 接收侧持久化慢
8.2 当前谁在打点
当前实际打点来源主要有两类:
peer应用层:发送、接收、持久化transport传输层:Linux kernel timestamping
server 代码里保留了 WithLogger 和 -latency-log 相关入口,但当前实现仍然把 server 视为黑盒转发器,不主动为转发过程写入业务级端到端事件。这一点从现有测试也能看出来:服务端转发路径默认不产出这类事件。
8.3 汇总结果怎么看
latencysummary 输出的摘要结果按“单条消息”聚合。阅读时可以重点看:
EndToEndLatencyNS:从发送侧准备开始到接收侧写盘完成AQueueLatencyNS:发送端从进入调度到真正进入软件发送路径ABTransportPropagationNS:从发送侧真正发出到接收侧应用读到BProcessingLatencyNS:接收端应用读到后到写盘完成
如果某些事件缺失,摘要里会带 MissingTimestamps,告诉你少了哪些关键时间点。
9. 运行与调试补充
9.1 构建入口
按 README.md 当前给出的方式,主要构建命令是:
go build -o bin/server ./cmd/server
go build -o bin/peer ./cmd/peer
go build -o bin/latencysummary ./cmd/latencysummary
也可以按不同 Linux 架构交叉编译。
9.2 server 常用参数
-listen:监听地址,默认:9000-latency-log:原始时延 JSONL 输出路径
9.3 peer 常用参数
-id:当前节点 ID-server:服务端地址-to:一次性发送时的目标peer-text:一次性发送文本-file:一次性发送文件-inbox-dir:接收内容的落盘目录-bind-ip:本地源 IP-bind-device:本地网络设备-latency-log:原始时延 JSONL 输出路径-interactive:是否启用交互式 REPL,默认开启
9.4 交互命令
交互模式支持:
help
text <peer> <message>
file <peer> <path>
quit
这让你可以在同一条长连接上连续发送多次,而不用每发一条消息就重启一次进程。
9.5 Linux-only 限制
这个项目应当被当作 Linux 项目来理解。
需要注意两层含义:
- 部署目标是 Linux
- 代码实现也依赖 Linux 特性
在当前 Windows 环境下执行 go test ./...,会因为 cmd/internal/transport 中的 Linux 专属实现而构建失败。这属于平台限制,不代表仓库当前代码损坏。换句话说,这个失败更接近“当前平台不支持这套实现”,而不是“代码逻辑错误”。
10. 推荐阅读顺序
如果你刚接手项目,建议按下面顺序读:
README.mdcmd/server/main.gocmd/peer/main.gocmd/internal/protocol/message.gocmd/internal/protocol/codec.gocmd/internal/transport/tcp.gocmd/internal/transport/tcp_linux.gocmd/internal/server/hub.gocmd/internal/peer/client.gocmd/internal/peer/persist.gocmd/internal/latencylog/logger.gocmd/internal/latencylog/summary.gocmd/internal/latencylog/summary_chart.go
这样阅读的好处是:
- 先知道程序怎么启动
- 再知道消息长什么样
- 再知道消息怎么传
- 再知道服务端和客户端各自做什么
- 最后再看时延观测和分析
11. 你接手后最值得先记住的几件事
- 这是一个“中心转发”的系统,不是
peer直连。 register是连接建立后的第一条消息,缺了它后续都不成立。server会用已注册身份覆盖消息里的From,不会信任客户端自报身份。- 文本消息和文件消息共享同一套协议,只是约束不同。
transport不只是收发 TCP,还承担 Linux 时间戳采集。- 接收侧持久化是
peer的职责,不是server的职责。 latencysummary是离线分析工具,不在在线转发链路里。
如果后续你准备改协议、改传输层,或者新增消息类型,建议先把 protocol -> transport -> peer/server -> latencylog 这一整条链路一起过一遍,再开始动代码。