《MMO 服务器开发》附录 B 环境与复现速查

《MMO 服务器开发》卷一附录 B:把「在本机把这套书跑一遍」所需的一切写成可照抄的清单。给出 Go 1.26、Colima/Docker、Temurin 21 的实测版本表,从建工作区到跑 L1、L2、Java 对照、机器人压测读 JSON 的五步复现流程,附 macOS accept backlog 与文件描述符等踩坑,以及一页可直接粘贴的命令序列。

附录 B 环境与复现速查

前言 交代了本卷「为什么选 Go、Java 对照怎么跑」,本附录把它落成一张可以照抄的清单。两者分工明确:前言讲取舍与理由,本附录只给命令、版本号和实测输出。凡是本附录列出的命令,都在写作机上真跑过;跑不了的会显式标注。

如果你只是想确认「我的机器能不能复现这本书」,直接从 B.4 的一页命令序列开始;如果你已经卡在某个环境问题上,看 B.3 的踩坑清单。

一句话版本:装好 Go 1.26、能跑 Docker(Colima 即可)、把 Java 代码放到 $HOME 下,本卷 61 篇里除第 6 章一处 WebSocket 对照外全部离线可跑。

B.1 环境实测表

下表是写作机的实测版本,也是本卷所有数字的来源环境。你的版本可以不同,但主干 Go 建议 1.26 及以上——正文用到的 net 的 Accept 错误分类和 time 单调钟语义在这个版本上才完整。

组件实测版本验证命令
Gogo1.26.0 darwin/arm64go version
Go 模块代理https://goproxy.cn,directgo env GOPROXY
Docker client29.7.2docker --version
Docker engine29.5.2(Colima,aarch64,6 vCPU / 10 GiB)docker version --format '{{.Server.Version}}'
Java(对照)Temurin 21.0.12.1 LTSdocker run --rm eclipse-temurin:21-jdk java -version
docker-compose5.5.0(独立二进制)docker-compose version
docker compose不可用docker compose version(报 unknown command)
宿主机macOS 26.3(Darwin 25.3.0),10 核 / 32 GiBsw_vers / sysctl -n hw.ncpu

三条实测输出,供你对照:

go version
go env GOPROXY
docker --version
go version go1.26.0 darwin/arm64
https://goproxy.cn,direct
Docker version 29.7.2, build a7dcaa6fdb

GOPROXY 是本机唯一可达的代理。 如果你的网络能直连 proxy.golang.org,可以不改;本卷只有第 6 章的 github.com/gorilla/websocket 一处需要联网拉依赖,其余全部标准库。依赖已在模块缓存里时,go build 完全离线。

B.2 从零复现五步

下面五步把「拿到源码 → 看到真实数字」串成一条线。每一步都给可执行命令与实测输出。

B.2.1 建工作区

本卷每章一个独立 Go module,互不 import,方便按章实验。module 名统一叫 aegis:

mkdir -p ~/aegis-playground && cd ~/aegis-playground
go mod init aegis

生成的 go.mod 只需两行:

module aegis

go 1.26

B.2.2 跑 L1:go test

标 L1 的代码块都是纯逻辑,从 module 根直接跑:

go vet ./...
go test ./...
go test -race ./...

第 17 章装配完成后,本机实测:

$ go test ./...
?   	aegis/cmd/aegis	[no test files]
?   	aegis/cmd/bot	[no test files]
ok  	aegis/internal/gateway	(cached)
?   	aegis/internal/proto	[no test files]

$ go test -race ./...
ok  	aegis/internal/gateway	3.283s

-race 一定要单独跑一次。 它让程序慢 5–20 倍,不能和基准数字混在一起,但它是唯一能抓出数据竞争的手段。

B.2.3 跑 Java 对照(L2J)

本机只装了 Java 8,所有 Java 对照都经 Docker 的 JDK 21 跑。关键约束: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 Oct 12 02:34 .
# drwxr-xr-x    1 root     root          4096 Oct 12 04:06 ..

所以 Java 代码一律放 $HOME/.aegis-java/chNN/:

mkdir -p "$HOME/.aegis-java/ch02"
cp java/Spawn100k.java "$HOME/.aegis-java/ch02/"
docker run --rm -v "$HOME/.aegis-java/ch02:/w" -w /w eclipse-temurin:21-jdk \
  sh -c "javac Spawn100k.java && java Spawn100k"

镜像 eclipse-temurin:21-jdk 已在本机,无需 pull。验证一个最小程序,实测输出:

java.version = 21.0.12.1
virtual thread = true

不要用本机 javac 冒充验证——那是 Java 8,没有虚拟线程,跑出来的结论是错的。

B.2.4 跑多进程 L2

L2 是真起进程、真连端口的端到端验证。第 17 章装配出同一个二进制 aegis,用 -role 扮演 world 与 gateway 两种角色:

go build -o bin/aegis ./cmd/aegis
go build -o bin/bot ./cmd/bot

./bin/aegis -role=world -listen=127.0.0.1:19001 &
./bin/aegis -role=gateway -listen=127.0.0.1:19002 -upstream=127.0.0.1:19001 &
sleep 1

两个进程启动后各自打印可解析的地址行,实测:

2026/10/12 12:10:16 [world] listening on 127.0.0.1:19131
ADDR=127.0.0.1:19131 ROLE=world
2026/10/12 12:10:17 [gateway] listening on 127.0.0.1:19141 upstream=127.0.0.1:19131
ADDR=127.0.0.1:19141 ROLE=gateway

ADDR= 那一行是给 harness 和运维脚本解析用的,端口写成 0 时会替换成内核分配的真实端口。用一个机器人跑冒烟测试,实测走完「登录 → 进入世界 → 邻居可见 → 合法移动 → 瞬移被拒」整条链路:

[alpha] 登录应答 {"ok":true,"id":1,"name":"alpha"}
[alpha] 进入世界 (x=8.0)
[alpha] 可见 2 个邻居: [beta gamma]
[alpha] 合法移动 -> MoveResp
[alpha] 瞬移到 x=9999 -> 被拒(ErrorNotify)
smoke OK

B.2.5 跑机器人压测并读 JSON

机器人 bot 是自写的:站内没有 k6/wrk/hey,所以本卷自己写了一个。两个要点:-stagger 把连接摊开建立绕开 accept backlog;每个机器人顺序发请求测 RTT,最后汇总 P50/P99:

./bin/bot -mode=bench -addr=127.0.0.1:19002 \
  -conns=200 -reqs=10 -stagger=2s -out=bench.json

-out 落一份 JSON,-stagger=2s 把 200 条连接摊到 2 秒里建立。本机实测输出:

{
  "conns": 200,
  "requests": 2000,
  "failed_conns": 0,
  "elapsed_ms": 1999,
  "qps": 1000.5002501250625,
  "p50_ms": 0.605,
  "p90_ms": 2.415,
  "p99_ms": 5.326,
  "max_ms": 28.093,
  "mean_ms": 1.029730000000001
}

读 JSON 不必手数,一行 python 取出关键字段:

python3 -c "import json;d=json.load(open('bench.json'));print('qps=%.0f p50=%.2fms p99=%.2fms'%(d['qps'],d['p50_ms'],d['p99_ms']))"
# qps=1001 p50=0.60ms p99=5.33ms

记住基准三纪律:进程隔离、预热到稳定、工作集超过缓存。并且——这些数字只代表同机相对量级,绝对数值不可移植。换台机器跑出两倍差距很正常,要核对的是方向与结构,不是小数位。

B.3 复现踩坑清单

下面是写作过程中在本机实测撞到的坑,按「你大概率先遇到哪个」排序。

① macOS 的 accept backlog 只有 128。 任何千连接压测都必须错峰,否则超出队列的连接被直接 RST:

sysctl kern.ipc.somaxconn   # kern.ipc.somaxconn: 128
sudo sysctl -w kern.ipc.somaxconn=1024   # 需要时临时调大

bot 的 -stagger 就是干这个的:把 1000 条连接摊到 10 秒里建立。文件描述符这边不紧张,实测 ulimit -n = 1048576、kern.maxfilesperproc = 122880。

② 高精度计时别读墙钟。 测延迟一律用 Go 的 time.Now(),它同时带墙钟和单调钟,Sub 出来的差值不受系统调时间影响。不要用 time.Now().UnixNano() 相减——NTP 校正会让差值为负或突然变大。

③ -race 会显著拖慢,但必须跑。 数据竞争在单线程压测里可能完全不报错,上多核才炸。它的用途只有一个:抓竞争。抓到就一定有问题;抓不到不等于没有。

④ 起服务前先探端口。 端口被占时测试会以 connection refused / address already in use 失败,看着像代码 bug 其实不是:

lsof -nP -iTCP:19002 -sTCP:LISTEN

本卷 harness 一律用高位随机端口(net.Listen("tcp", "127.0.0.1:0")),正文里的固定端口只是示意。

⑤ docker compose 子命令在本机不可用。 Colima 提供的 Docker 跑不起 docker compose,但独立的 docker-compose(5.5.0)可用。本卷验证命令一律用 docker run,不依赖 compose:

$ docker compose version
docker: unknown command: docker compose
$ docker-compose version
Docker Compose version 5.5.0

⑥ 统计行数别用 wc -l、别信 grep。 本机命令行装了一层命令代理,grep 与 wc -l 的行为会被改写。统计 Markdown 行数一律用:

python3 -c "print(open('file.md').read().count(chr(10)))"

⑦ 不要连别人已经在跑的中间件。 本机有别的项目在用的 Redis(16379/16380)与 PostgreSQL(15432/15434/15435)。本卷需要中间件时一律另起独立容器并映射到不同端口,用完 docker rm -f 清理。

B.4 一页可粘贴命令序列

把 B.1–B.3 合成一段,从头到尾照抄即可。假设你已经把源码放进 ~/aegis-playground:

set -e
cd ~/aegis-playground

# 1. 环境核对
go version                          # 期望 go1.26.0 darwin/arm64
go env GOPROXY                      # 期望 https://goproxy.cn,direct
docker --version                    # 期望 29.7.2

# 2. L1:纯逻辑单测
go vet ./...
go test ./...
go test -race ./...

# 3. Java 对照(Colima 只挂 $HOME,/tmp 不挂)
mkdir -p "$HOME/.aegis-java/ch02"
cp java/Spawn100k.java "$HOME/.aegis-java/ch02/"
docker run --rm -v "$HOME/.aegis-java/ch02:/w" -w /w eclipse-temurin:21-jdk \
  sh -c "javac Spawn100k.java && java Spawn100k"

# 4. 多进程 L2:world + gateway 两个进程
go build -o bin/aegis ./cmd/aegis
go build -o bin/bot ./cmd/bot
./bin/aegis -role=world   -listen=127.0.0.1:19001 &
./bin/aegis -role=gateway -listen=127.0.0.1:19002 -upstream=127.0.0.1:19001 &
sleep 1

# 5. 机器人压测 + 读 JSON(-stagger 绕开 somaxconn=128)
./bin/bot -mode=bench -addr=127.0.0.1:19002 \
  -conns=200 -reqs=10 -stagger=2s -out=bench.json
python3 -c "import json;d=json.load(open('bench.json'));print('qps=%.0f p50=%.2fms p99=%.2fms'%(d['qps'],d['p50_ms'],d['p99_ms']))"

# 6. 收尾
kill %1 %2

这一段在本机逐条跑过,最终一行实测输出:

qps=1001 p50=0.60ms p99=5.33ms
SEQUENCE OK

复现成功的判据

把上面的序列跑完,你应该看到四个信号,缺一不可:

  1. go test ./... 与 go test -race ./... 全绿,没有 DATA RACE。
  2. Java 对照经 Docker 跑出 java.version = 21.0.12.1、virtual thread = true。
  3. world 与 gateway 两个进程都打印了 ADDR=... ROLE=...,且机器人冒烟以 smoke OK 结束。
  4. bench.json 的 failed_conns 为 0,p50_ms / p99_ms 有值。

四个信号齐了,环境就算通了。数字对不上不重要——方向和结构对上就行。若某一项跑不通,先查 附录 D 常见错误与 FAQ ,再看 附录 C 实测数据汇总 里同机量级做对照。想回看装配细节,从 17.3 机器人客户端与第一组承载数字 进入。

阅读上一节:附录 A aegis 卷一 API 与模块速查 · 阅读下一节:附录 C 实测数据汇总

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「mmo」更多文章

  1. 《MMO 服务器开发》工程化与运营 · 目录
  2. 《MMO 服务器开发》9.3 上线检查清单
  3. 《MMO 服务器开发》9.2 性能预算达标