Files
OmniSocketGo/doc/project-guide.md
2026-03-24 12:05:23 +08:00

572 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
- 文件消息会被写成单独文件
- 文件名格式是:`<from>-<messageID>-<baseFileName>`
这样做的好处是:
- 文本消息便于顺序追踪
- 文件消息天然避免覆盖
- 文件名里直接带了来源和消息 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 <peer> <message>
file <peer> <path>
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` 这一整条链路一起过一遍,再开始动代码。