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