实测环境
本卷全部代码在下列环境中实测通过:
| 组件 | 版本 | 说明 |
|---|---|---|
| Go | 1.26.0(darwin/arm64) | 主干语言,全部 L1 代码的运行时 |
| Go 模块代理 | https://goproxy.cn,direct | 本机唯一可达的代理,见下文 |
| Docker client | 29.7.2 | 用于跑 Java 对照 |
| Docker engine | 29.5.2(Colima,Ubuntu 24.04) | 6 vCPU / 10 GB |
| Java(对照) | Temurin 21.0.12.1 LTS | 经 eclipse-temurin:21-jdk 容器运行 |
| 操作系统 | macOS(Darwin 25.3.0,Apple Silicon) | 10 CPU / 32 GB RAM |
Go 1.26 是本卷的硬性要求,因为用到了一些较新的标准库行为(time 的单调钟语义、net 的 Accept 错误分类)。更低版本大多也能跑,但正文里的实测数字是在 1.26 上取得的。
为什么主干是 Go
选 Go 做主干的理由有三条,每一条都直接服务于「从零实现」这个目标。
第一,goroutine-per-connection 的写法让网络代码读起来像同步代码。 你不需要手写状态机来管理「这条连接读到一半了,还差三个字节」——io.ReadFull 会把短读补齐,代码是线性的。
第二,标准库够用。 net、encoding/binary、crypto/sha1、crypto/hmac、encoding/base64、time——写一个完整的协议栈、KCP、WebSocket 握手,不需要引入任何第三方库。这对「从零」是决定性的:读者能看见每一行在做什么,而不是在框架的抽象层里绕。
第三,工具链自带。 go test、go vet、-race、-benchmem 都在标准安装里,本卷的每一层验收都能用它们完成,不需要额外装测试框架。
Java 对照怎么跑
本机只装了 Java 8(Zulu 1.8.0_482),既没有虚拟线程,也没有现代 GC。所以本卷所有 Java 代码都经 Docker 拉取的 JDK 21 运行。
docker pull eclipse-temurin:21-jdk
docker run --rm eclipse-temurin:21-jdk java -version
实测输出:
openjdk version "21.0.12.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-21.0.12.1+1 (build 21.0.12.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.12.1+1 (build 21.0.12.1+1-LTS, mixed mode, sharing)
这里有一个必须提前说清楚的坑。 本机用的是 Colima 作为 Docker 运行时,而 Colima 默认只把 $HOME 挂进虚拟机,/tmp 挂不进去。实测把 /tmp 下的目录挂进容器,容器里看到的是一个空目录:
# 错误示范:挂 /tmp,容器里是空的
docker run --rm -v /tmp/mmobook/java-probe:/w -w /w alpine ls -la
# total 8
# drwxr-xr-x 2 root root 4096 ... .
# drwxr-xr-x 1 root root 4096 ... ..
所以本卷的 Java 代码一律放在 $HOME 下,再挂进容器:
mkdir -p "$HOME/.aegis-java/ch02"
# 把 .java 文件放进去
docker run --rm -v "$HOME/.aegis-java/ch02:/w" -w /w eclipse-temurin:21-jdk \
sh -c "javac FrameProbe.java && java FrameProbe"
本卷的 Java 代码只用 JDK 自带的类(java.nio.ByteBuffer、java.util.concurrent、java.net),不引入 Netty 或其他第三方 jar——这样对照代码能原样跑,不需要配 Maven 或 Gradle。正文在需要时会说明 Netty 的对应实现在做什么。
从零搭建工作区
本卷每一章都有一个独立的 Go module,互不干扰,方便你按章实验:
mkdir -p ~/aegis-playground && cd ~/aegis-playground
go mod init aegis
go.mod 里只需要一行 module 声明,加一行 Go 版本:
module aegis
go 1.26
第 6 章手写 WebSocket 时也只用标准库(crypto/sha1、encoding/base64),所以整卷 61 篇里只有极少数地方需要联网拉依赖,其余全部离线可跑。
代码块怎么读
每个代码块的前三行是它的元信息:
// file: internal/proto/frame.go
// tier: L1
// verify: go test ./internal/proto/
// file:是这个文件在工作区里的相对路径。带// file:的块都是完整可编译文件,包含package声明和全部 import,可以原样复制出去。// tier:是这一块的层级(见下节)。// verify:是验证命令,从 module 根目录直接执行即可。- 不参与验证的纯展示片段用
// snippet:标注。 - 无注释语言(JSON / YAML / Dockerfile / SQL / proto)把元信息写在围栏之前的 HTML 注释里。
可运行性分级
本卷把每一块代码标上一个层级。这不是装饰——它明确告诉你这一块能不能在你机器上跑出来。
| 层级 | 含义 | 验收方式 | 本卷占比 |
|---|---|---|---|
| L1 | 纯逻辑,不依赖外部进程 | go test 直接跑绿 | 绝大多数 |
| L2 | 真起进程或真连端口 | 自写 harness 做端到端断言 | 第 2、6、17 章 |
| L2J | Java 对照实现 | 经 Docker 的 JDK 21 真跑 | 约 10 节 |
| L3 | 仅静态(清单、配置) | 只做语法与结构校验 | 少量 |
| L4 | 不可复现(公网、真机、DDoS) | 正文显式标注,命令行不可复现 | 极少 |
| B | 基准 | 产出数字,软告警不硬失败 | 每章 0–2 节 |
凡是 L4 的段落,正文都会在显眼处写明「本节需真实环境,命令行不可复现」。 如果你看到一段代码没有层级标注,那是我们的疏忽——请以附录 B 的版本为准。
基准的三条纪律
本卷有若干节会给出实测数字(延迟分布、吞吐、内存增量)。这些数字都遵守三条纪律,你在自己机器上复现时也请照做:
- 进程隔离。 被测服务端单独起进程,压测客户端单独起进程,不要让它们抢同一批 CPU。
- 预热到稳定。 丢弃前 20% 的样本,确认滚动中位数变化小于 5% 才开始记录。
- 工作集超过缓存。 如果测的是内存或吞吐,数据规模要大到超出 CPU 缓存,否则测的是缓存命中率而不是算法。
并且——所有数字都只代表「同机相对量级」,绝对数值不可移植。 你的机器跑出来比书里快一倍或慢一半都正常。要核对的是方向和结构(谁比谁快、快在哪个环节),不是小数位。本卷正文不会出现「快 N 倍」这类绝对断言。
关于依赖:哪些要联网,哪些不用
「从零实现」不只是写作风格,也是依赖策略。本卷 61 篇里,绝大多数代码离线可跑,少数需要联网的地方都集中且可预期:
| 类别 | 依赖 | 出现位置 |
|---|---|---|
| 标准库 | net / encoding/binary / crypto/* / time | 全卷主干,无需联网 |
| 测试 | testing / testing/quick | 全卷 L1 验收,无需联网 |
| WebSocket | github.com/gorilla/websocket(仅第 6 章对照) | 第 6 章一处,缓存已有 |
| fuzz | Go 原生 testing.F(1.18+) | 第 16 章,无需联网 |
| Java | eclipse-temurin:21-jdk 镜像 | 双栈对照节 |
也就是说,只有第 6 章的一处对照和全部 Java 块需要联网,其余代码只要 go.mod 里一行 module aegis 就能跑。如果你在完全离线的环境里读这本书,跳过那几块即可,主线不受影响。
为什么不引第三方库。 写 MMO 服务端当然可以用现成的框架——本卷最后也会和 skynet、pitaya、leaf 做对照。但读者从框架里学不到「为什么」。用 gorilla/websocket 你一行 Upgrader.Upgrade 就完成了握手,可你永远不知道 Sec-WebSocket-Accept 是怎么从 Sec-WebSocket-Key 算出来的,也不知道掩码为什么必须存在。手写一遍之后,再用框架你会知道它在替你做什么、代价在哪。
五个常见误解
开始之前,先把几个最容易让人走偏的预期纠正掉。
误解一:以为「从零」等于「重新发明轮子」。 本卷不是要你去生产环境手写 KCP,而是让你看得懂 KCP。手写是手段,理解是目的。真到项目里,你大概率会直接用成熟库——但那时的选择是有依据的选择,而不是抄一段博客。
误解二:以为 Go 就是「更简单的 Java」。 不是。Go 的 netpoller 与 JVM 的 NIO/虚拟线程是两套不同的并发模型,它们对「一万条连接该开几个线程」的回答不一样。第 2.3 节会把两者的实测摆在一起,你会看到它们在延迟分布上的真实差异。
误解三:以为延迟高就是代码慢。 大部分时候不是。延迟高往往是排队——消息在邮箱里排、在 TCP 发送缓冲里排、在调度器的 runqueue 里排。本卷反复用「队列长度」这个视角看问题,而不是只看函数耗时。
误解四:以为服务器权威就是把所有逻辑搬服务端。 搬过去只会让服务器变慢。权威的本质是划一条线:线内的事服务端说了算,线外的事客户端可以自己算但会被校验。第 15 章整章都在讲这条线怎么划。
误解五:以为这本书读完就能上线。 不能。它给的是零件和判断力,不是成品。《晨曦大陆》是一个测试床,aegis 是一个教学脚手架。上线还需要你补上本卷不涉及的部分——真实运维、合规、支付、客服——那些在卷四会碰一部分,但绝不完整。
环境踩坑清单
下面几条是本卷写作过程中在本机实测撞到的,提前告诉你,省得你卡住。
macOS 的 accept backlog 只有 128
sysctl kern.ipc.somaxconn
# kern.ipc.somaxconn: 128
这是内核允许的最大半连接队列长度。当你一口气发起上千条 TCP 连接时,超出队列的部分会被直接 RST,压测结果会莫名其妙地少掉一批连接。本卷所有千连接压测都错峰发起(例如把 1000 条连接摊到 10 秒里建立),正文里的 bot 工具有一个 -stagger 参数就是干这个的。如果确实需要,也可以临时调大:
sudo sysctl -w kern.ipc.somaxconn=1024
文件描述符这边反而不紧张,实测:
ulimit -n # 1048576
sysctl kern.maxfilesperproc # 122880
docker compose 子命令在本机不可用
本机的 Docker 是 Colima 提供的,docker compose 子命令跑不起来,但独立的 docker-compose 可用。本卷的验证命令一律用 docker run,不依赖 compose。
统计行数别用 wc -l
本机的命令行环境装了一层命令代理,grep 和 wc -l 的行为会被改写,结果不可信。本卷统计 Markdown 行数一律用:
python3 -c "print(open('file.md').read().count(chr(10)))"
不要连别人已经在跑的中间件
本机有几个已经在跑着的 Redis 与 PostgreSQL 实例(端口 16379 / 16380 / 15432 / 15434 / 15435),那是别的项目在用的。本卷需要中间件时一律另起独立容器并映射到不同的端口:
docker run -d --rm -p 26379:6379 redis:7-alpine
用完 docker rm -f 清理掉。
高精度计时用 time.Now(),别信 time.Since 的墙钟
本卷大量测延迟,计时一律用 Go 的 time.Now()——它同时带墙钟和单调钟读数,Sub 出来的差值不受系统时间调整影响。不要用 time.Now().UnixNano() 相减去算耗时:那读的是墙钟,NTP 校正或手动改时间会让差值为负或突然变大。
// snippet: 高精度计时的正确写法
start := time.Now()
// ... 被测代码 ...
elapsed := time.Since(start) // 单调,安全
-race 会显著拖慢,但必须跑
数据竞争的代码在单线程压测里可能完全不报错,上多核才炸。本卷所有并发相关的 L1 代码都要求额外跑一次:
go test -race ./...
-race 会让程序慢 5–20 倍,不要在它下面测性能数字。它的用途只有一个:抓竞争。抓不到不等于没有,但抓到了就一定有问题。
端口占用要先探
本卷的 L2 端到端测试会真起监听端口。如果 8080、9000 之类被别的进程占了,测试会以「connection refused」或「address already in use」失败,看起来像代码 bug 其实不是。起服务前先探一下:
lsof -nP -iTCP:9000 -sTCP:LISTEN
本卷的 harness 一律用高位随机端口(net.Listen("tcp", "127.0.0.1:0") 让内核分配),正文里出现的固定端口只是示意。
目录结构约定
aegis 工作区在卷末会长成这样(每一章只用到其中一小部分):
aegis/
├── go.mod
├── cmd/
│ ├── aegis/ # 服务端入口(第 17 章装配)
│ └── bot/ # 自写压测机器人(第 17 章)
└── internal/
├── proto/ # 帧格式与协议版本(第 3 章)
├── codec/ # 二进制编解码(第 4 章)
├── kcp/ # 可靠 UDP(第 5 章)
├── ws/ # WebSocket 接入(第 6 章)
├── session/ # 会话表(第 7 章)
├── router/ # 消息路由(第 8 章)
├── timing/ # 时间轮与世界时钟(第 9 章)
├── actor/ # 邮箱与实体(第 10 章)
├── idem/ # 幂等与去重(第 11 章)
├── sync/ # 状态同步(第 12 章)
├── predict/ # 预测与校正(第 13 章)
├── clock/ # 时间同步(第 14 章)
├── authority/ # 服务器权威校验(第 15 章)
└── guard/ # 协议防护(第 16 章)
环境没问题了,从第 1 章 1.1 分层与职责边界 开始。
阅读上一节:序
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。