第 19 章 联机、登录与你的 Go 服务:客户端怎么和服务器说话

前面 18 章,你学的是”一台机器上,怎么把世界画出来、发出声、做出效果”。但从你说”想做个联机塔防/大富翁、要和我的 Go 服务链接”那一刻起,问题变了一个量级:

世界不再只活在你这一台机器里。它要活在很多台机器上,而且每台机器看到的,得是同一个世界。

这一章是你整个目标的”总开关”。它要讲清楚:联机游戏到底在解决什么、你的 Go 后端站在哪、登录和实时通信分别怎么走、以及——你作为一个 Go 程序员,这里不是从零学,是把你已有的本事接进来。


19.1 联机游戏到底在解决什么”老问题”

单机游戏里,“世界状态”就存在你这台电脑的内存里:怪有 100 血、塔在 (3,0,5)、玩家有 50 金币。一切你说了算,因为没有别人。

联机呢?A 玩家打了怪,B 玩家屏幕上那怪也得掉血;C 玩家建了塔,D 也得看见。于是冒出三个躲不掉的麻烦:

  • 延迟:消息从 A 传到服务器再到 B,要走网络,不是瞬移。
  • 丢包:消息可能半路丢了,得有重发/补救。
  • 一致性:多份状态要保证”最终大家看到一样的东西”。

这三个麻烦,是联机游戏一切设计的源头。记住它们,你后面看任何网络方案都不会迷路。

两种世界观先分清楚:

  • 服务器权威(Server Authoritative):所有关键逻辑(谁死了、加多少分、怪往哪走)由服务器算,客户端只是”把服务器说的画出来”。防作弊、好调试,新手强推。
  • 对等(P2P):玩家机器之间直接通信,没有中心服务器。省成本但难防作弊、难一致。

你的情况特殊且幸运:你本来就是写服务器的。所以直接用”服务器权威”——你的 Go 服务就是那个”说了算”的中心。这比你从头学一套游戏服务器框架顺得多。


19.2 你的 Go 服务,在整个架构里站哪

一个联机游戏,通常分成两层,职责完全不同:

第一层:账号 / 登录 / 数据(你熟,用 HTTP)

  • 注册、登录、token 发放、存档、排行榜、充值(若有)。
  • 特点:无实时要求,QPS 不高,逻辑重、要落数据库。
  • 这正是你写 Go Web 服务的强项。你现有的 gin / echo / 标准库 net/http handler,直接就是这层的 endpoint。

第二层:游戏内实时同步(要学,用 WebSocket / 现成方案)

  • 怪物移动、塔开火、玩家位置、聊天、房间状态。
  • 特点:高频、低延迟、长连接。
  • 可以仍在你的 Go 里用 gorilla/websocket 起一个 ws 服务;也可以用 Unity 官方 Netcode + 中继,或第三方(Photon 等)。塔防/大富翁这类实时性”中等”的游戏,自己用 WebSocket 写完全撑得住。

关键认知:登录/数据用 HTTP(你已会),游戏实时用 WebSocket 或网络库(你要学)。两者职责分开,不要混在一个连接里硬搞。


19.3 登录流程长什么样(你写过的 handler 就是这边的 endpoint)

这是你最该有”熟悉感”的一段。把 Unity 客户端当成”另一个前端”,你的 Go 就是后端,流程和你写过的任何 Web 登录一模一样。

步骤拆解:

  1. 客户端收集账号密码,用 UnityWebRequest 发 POST https://你的域名/login,body 是 JSON:{"username":"老哥","password":"******"}。
  2. 你的 Go 接口:查库校验账号密码 → 对了就生成一个 token(JWT 之类,带 userId 和过期时间)→ 返回 {"token":"xxx","userId":42}。
  3. 客户端存 token:存内存或 PlayerPrefs(注意:别存明文密码;token 也别长期裸存,最好内存持有 + 过期刷新)。
  4. 后续每次请求,在 HTTP Header 带 Authorization: Bearer <token>。
  5. 你的 Go 中间件:每个需要登录的接口,先校验 token,解出 userId,才知道”这是谁在操作”。

看明白没?这套东西你闭着眼都能写。 Unity 端只是把”浏览器 fetch”换成了”UnityWebRequest”,你的 Go 端一行都不用改思路。

反问:你 Go 里写 JWT 中间件时,是从 token 解出 userId 再查权限。那”玩家在游戏里建塔花了 100 金币”这个请求,你的中间件除了验 token,还得额外检查什么,才能防止作弊?(提示:光验”是谁”够吗?)


19.4 Unity 里怎么发 HTTP 请求(异步,别卡界面)

这里有个后端思维要改:游戏主线程一旦被网络 IO 阻塞,整个画面就冻住了(第 1 章那个”16 毫秒一帧”的约束,卡一帧就掉帧)。

所以同步 HTTP 在游戏里是禁忌。两种异步写法:

写法一:协程(Coroutine)+ UnityWebRequest(Unity 传统做法)

IEnumerator Login(string url, string json) {
    using var req = UnityWebRequest.Post(url, json);
    yield return req.SendWebRequest();   // 这一行把"等网络"交出去,不卡主线程
    string result = req.downloadHandler.text;
    // 解析 JSON,拿到 token
}
// 启动:StartCoroutine(Login(url, json));

写法二:C# async/await + HttpClient(更现代,和你写 Go 的异步手感接近)

async Task LoginAsync() {
    var resp = await httpClient.PostAsync(url, content);
    var body = await resp.Content.ReadAsStringAsync();
}

JSON 解析:Unity 自带 JsonUtility(够用,但要求 C# 类字段和 JSON 键对应、类要标 [Serializable]);功能强可用 Newtonsoft.Json(NuGet 引入)。你 Go 里 json.Unmarshal 很熟了——这边思路一样,只是把 JSON 反序列化成 C# 类,字段要对得上。

思维衔接:你 Go 里 struct { Username string } + json.Unmarshal;Unity 里 [Serializable] class LoginReq { public string username; } + JsonUtility.FromJson<LoginReq>(json)。连”字段要对得上”这个坑都一模一样。


19.5 WebSocket:游戏内实时怎么连

HTTP 是一问一答,适合登录这种”偶尔来一次”的事。但游戏里服务器要主动告诉客户端”怪移动到 (x,y) 了""玩家 B 建了塔”——这是服务器推,HTTP 干不了,得上 WebSocket(一条长连接,双方随时互发)。

你的 Go 端(你已经会):

// 用 gorilla/websocket 起 ws endpoint
upgrader := websocket.Upgrader{}
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    conn, _ := upgrader.Upgrade(w, r, nil)
    // 每个连接 = 一个玩家,存进连接池
    for {
        _, msg, _ := conn.ReadMessage()
        // 解析消息:谁、想干啥
        // 处理 + 广播给房间内其他连接
    }
})

Unity 端:用 WebSocket 客户端库(如 NativeWebsocket、WebSocketSharp),在 OnMessage 回调里处理服务器推来的状态。

你们自己定的消息协议(JSON 即可起步):

{"type":"spawn_enemy","x":3.0,"y":0,"hp":100}
{"type":"build_tower","playerId":42,"x":5,"y":2}
{"type":"sync_hp","enemyId":7,"hp":30}

服务器权威的关键一句:客户端发”我想在 (x,y) 建塔” → 服务器校验”这玩家有钱吗、这位置空吗、合法吗” → 合法才广播”建塔成功”。永远别信客户端发来的”我杀了怪给我加 100 分”——分数由服务器算。


19.6 状态怎么同步:状态同步 vs 帧同步(思想层)

这是联机架构的”分水岭”,新手先懂思想:

  • 状态同步(State Synchronization):服务器定期把”每个物体的位置/血量/状态”发给客户端,客户端照着画。简单、好防作弊。塔防、大富翁都适合。
  • 帧同步(Lockstep / Frame Sync):所有客户端跑同一套逻辑,只同步”输入”(谁按了什么键)。省带宽,但一个客户端算错就全员不一致,极难调试。

对你:选 状态同步 + 服务器权威。你的 Go 服务器算得快、算得准,客户端只负责”把服务器说的画出来”——前面学的摄像机、雾、光照、声音,全成了”画”的材料,网络层是”指挥”。


19.7 连回你已经学过的概念

这张图你要在脑子里扎根:

        你的 Go 服务器(逻辑层 / 指挥)
                  │  同步状态
                  ▼
    ┌───────────────────────────────────┐
    │  Unity 客户端(表现层 / 画师)        │
    │  网络组件拿到状态 → 更新 Transform    │
    │  → 摄像机画(第7章)                  │
    │  → 光照上色(第6章)                  │
    │  → 雾藏远处(第8章)                  │
    │  → AudioSource 按位置发声(第12章)   │
    └───────────────────────────────────┘
  • 网络层 = 逻辑层:决定”世界现在是什么状态”。
  • 摄像机/雾/光照/声音 = 表现层:把状态”画/放”给玩家看。
  • 你第 12 章学的 AudioSource 听位置——联机时”声音从哪来”也由服务器同步的位置决定。
  • 所以前面每一章都不是白学的:它们是表现层手里的材料,网络层是它们的指挥。

反问:既然”客户端只负责画服务器说的”,那如果网络延迟 200ms,客户端这 200ms 里画面该干嘛?直接卡住等?还是”猜”一下?这引出了”插值/预测”——你后面做联机必碰,先有个印象。


19.8 新手最容易踩的坑(提前告诉你,省得掉坑)

  1. 主线程做网络/IO → 卡顿掉帧。用协程或 async,别同步阻塞。
  2. token / 密码存明文 → 至少别存密码;token 用内存持有 + 过期刷新;敏感平台用安全存储。
  3. 客户端算分数/血量 → 被改内存就作弊。服务器权威,客户端只提交”意图”。
  4. 不处理断线重连 → 玩家掉线世界就乱。至少要”重连后服务器把当前房间状态全量推一份”。
  5. 忽略 HTTPS / CORS → 真机/PC 构建宽松,但 WebGL 构建受限;生产环境一律 HTTPS,Go 端配好 CORS 允许游戏域名。
  6. 消息协议随意加字段不版本化 → 客户端/服务器版本不一致就炸。早期就给消息带个 version 字段。

19.9 你作为 Go 程序员的”天然优势”

最后给你点信心。很多人学 Unity 联机,卡在”服务器怎么写”。你不一样:

  • 你 already 写过 HTTP handler、JWT、数据库 CRUD——登录/数据层对你零门槛。
  • 你懂并发:Go 的 goroutine 天然适合”每连接一个 goroutine”的 WebSocket 服务;你比只学前端的 Unity 新人更懂”多玩家状态怎么隔离、怎么广播”。
  • 你懂系统设计:服务器权威、状态一致性、防作弊,这些是你写后端就有的直觉。
  • 你懂接口契约:你定 JSON 消息协议,会和写 Go API 一样严谨。

所以你这条路其实很短:补齐 Unity 客户端(C# + 组件 + 编辑器)+ 把实时同步用 WebSocket 接上你已有的 Go 后端,就成了。下一章给你一张”塔防/大富翁项目蓝图”,把这条路画成可执行的步骤。

【思考题】

  1. 登录请求里,你的 Go 中间件除了校验 token 解出 userId,还得额外检查什么,才能防止”玩家建塔花 100 金币”这种作弊?(提示:光验”是谁”够吗?)
  2. 为什么游戏里不能用”同步 HTTP”做实时同步?如果强行用,玩家体验会怎样?
  3. 状态同步和帧同步,哪个更适合你做的塔防?为什么大富翁(回合制)哪怕用最慢的同步方式也问题不大?
  4. “客户端只负责画服务器说的”这句话,遇到 200ms 网络延迟时会有什么矛盾?业界一般怎么缓解(说出思路即可,不用写代码)?

【参考答案】

  1. 光验”是谁”不够。还得在服务端重新校验这次操作的合法性:该玩家金币是否 ≥100、目标位置是否空闲且合法、是否在他的权限内。因为客户端发来的”我花 100 建塔”可能是伪造的(改内存发假请求)。服务器必须按自己掌握的真实状态再算一遍,扣金币、写库、再广播结果。这才是服务器权威的真谛:客户端只提交”意图”,服务器裁定”结果”。
  2. 同步 HTTP 会阻塞游戏主线程等待网络往返(几十到几百毫秒),期间画面冻结、输入无响应,玩家直接卡死感。而且 HTTP 一问一答,服务器无法主动推送”怪移动了”。所以实时必须用异步(协程/async)+ 长连接(WebSocket)。
  3. 塔防用状态同步最稳:怪多、位置频繁变、要防作弊,服务器统一算状态广播最简单可靠。大富翁是回合制,一回合才动一次,对实时零要求,哪怕用最慢的 HTTP 轮询(每隔一会问一次”轮到谁了”)都毫无问题——它根本不需要毫秒级同步。
  4. 矛盾:服务器状态变了,但消息还在路上,客户端画的还是 200ms 前的旧状态,玩家看到”敌人瞬移/迟顿”。缓解思路:插值(Interpolation)——客户端不立刻跳到新位置,而是用一小段时间”平滑移动”过去;预测(Prediction)——客户端先按本地输入预估下一步,等服务器确认再修正。这正是第 19.7 节反问里埋的”插值/预测”,是联机手感的核心技术。