《MMO 服务器开发》前言:技术选型与环境准备

《MMO 服务器开发》卷一前言:交代本卷的技术选型与环境准备。列出实测的 Go 1.26 与 JDK 21 版本、Docker 拉取 Java 对照环境的完整命令与挂载坑、从零搭建 aegis 工作区的步骤,解释代码块头的 file/tier/verify 三行约定与 L1–L4 加 B 的可运行性分级,并给出 macOS 文件描述符上限、accept backlog、命令统计工具等实测踩坑清单。

实测环境

本卷全部代码在下列环境中实测通过:

组件版本说明
Go1.26.0(darwin/arm64)主干语言,全部 L1 代码的运行时
Go 模块代理https://goproxy.cn,direct本机唯一可达的代理,见下文
Docker client29.7.2用于跑 Java 对照
Docker engine29.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 章
L2JJava 对照实现经 Docker 的 JDK 21 真跑约 10 节
L3仅静态(清单、配置)只做语法与结构校验少量
L4不可复现(公网、真机、DDoS)正文显式标注,命令行不可复现极少
B基准产出数字,软告警不硬失败每章 0–2 节

凡是 L4 的段落,正文都会在显眼处写明「本节需真实环境,命令行不可复现」。 如果你看到一段代码没有层级标注,那是我们的疏忽——请以附录 B 的版本为准。

基准的三条纪律

本卷有若干节会给出实测数字(延迟分布、吞吐、内存增量)。这些数字都遵守三条纪律,你在自己机器上复现时也请照做:

  1. 进程隔离。 被测服务端单独起进程,压测客户端单独起进程,不要让它们抢同一批 CPU。
  2. 预热到稳定。 丢弃前 20% 的样本,确认滚动中位数变化小于 5% 才开始记录。
  3. 工作集超过缓存。 如果测的是内存或吞吐,数据规模要大到超出 CPU 缓存,否则测的是缓存命中率而不是算法。

并且——所有数字都只代表「同机相对量级」,绝对数值不可移植。 你的机器跑出来比书里快一倍或慢一半都正常。要核对的是方向和结构(谁比谁快、快在哪个环节),不是小数位。本卷正文不会出现「快 N 倍」这类绝对断言。

关于依赖:哪些要联网,哪些不用

「从零实现」不只是写作风格,也是依赖策略。本卷 61 篇里,绝大多数代码离线可跑,少数需要联网的地方都集中且可预期:

类别依赖出现位置
标准库net / encoding/binary / crypto/* / time全卷主干,无需联网
测试testing / testing/quick全卷 L1 验收,无需联网
WebSocketgithub.com/gorilla/websocket(仅第 6 章对照)第 6 章一处,缓存已有
fuzzGo 原生 testing.F(1.18+)第 16 章,无需联网
Javaeclipse-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 分层与职责边界 开始。

阅读上一节:序

继续阅读

探索更多技术文章

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

全部文章 返回首页

「mmo」更多文章

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