# 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` 这一整条链路一起过一遍，再开始动代码。
