572 lines
17 KiB
Markdown
572 lines
17 KiB
Markdown
# 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` 这一整条链路一起过一遍,再开始动代码。
|