UE5.8 Dedicated Server 与 Listen Server:从一场凌晨吵架说起

📑 目录

一场凌晨两点的争吵

凌晨两点,小林把键盘一推,说受不了了。

他们团队正在用 UE5.8 做一款四人合作撤离射击游戏。玩法已经验证完:搜物资、打 AI、坐直升机撤离。Demo 阶段一直用 Listen Server,也就是让其中一个玩家当主机。美术和策划联机打得挺开心,直到前天晚上一次测试,阿杰突然把客户端关了,整个房间直接消失,另外三个人全掉线。

小林Gameplay出身,一直主张 Dedicated Server。阿杰是网络老炮,总惦记着 Listen Server 省钱。俩人坐在会议室里,谁也不肯走。

小林指着屏幕:你关游戏,房间就没了,这能接受?

阿杰反问:你才四个人,上专用服务器,一个月云主机费用你出?

老赵端着咖啡进来,说别吵了,天亮前给结论。

这篇文章,就是那次争吵之后我整理的笔记。我会把 Dedicated Server 与 Listen Server 的差异、UE5.8 里的网络模式、专用服务器怎么打包、服务器怎么部署、P2P 与中心化架构、安全反作弊、性能优化,以及平台多人服务怎么接入,尽量说透。

Dedicated Server 与 Listen Server 到底差在哪

Listen Server 的游戏进程里同时运行着客户端逻辑和服务器逻辑。主机玩家拥有世界权威,其他玩家通过直连到主机。主机退游戏,世界就没了,因为世界只存在于他的机器上。

Dedicated Server 则是一个不渲染画面、不接收本地玩家输入的独立进程。它只负责模拟世界状态,并通过 Replication 把所有状态同步给各个客户端。所有客户端对它来说是对等的,没有谁拥有特殊权限。

flowchart TD
    subgraph player_a ["玩家 A 主机"]
        host_client["客户端 + 服务器逻辑"]
    end
    subgraph player_b ["玩家 B"]
        client_b["纯客户端"]
    end
    subgraph player_c ["玩家 C"]
        client_c["纯客户端"]
    end
    subgraph ds_player_a ["玩家 A"]
        ds_client_a["纯客户端"]
    end
    subgraph ds_player_b ["玩家 B"]
        ds_client_b["纯客户端"]
    end
    subgraph ds_server ["数据中心"]
        dedicated["UE5 Dedicated Server"]
    end
    client_b --> host_client
    client_c --> host_client
    ds_client_a --> dedicated
    ds_client_b --> dedicated

左边是 Listen Server,右边是 Dedicated Server。左边的主机身兼两职,右边的服务器躲在数据中心里,谁都不偏袒。

Listen Server 最大的好处是省服务器。尤其是主机性能不错、玩家之间距离很近的小规模合作游戏,几乎零成本。Epic 的很多官方多人原型都用它。

Dedicated Server 最大的好处是稳定、公平、可控。主机掉线不影响别人,反作弊更容易做,网络质量也更统一。竞技类和需要长期房间的游戏通常选它。

还有一个容易被忽略的点:网络拓扑对延迟的影响。Listen Server 下,其他玩家的数据先到主机,再转发给彼此;Dedicated Server 下,数据先到服务器,再分发。如果主机和某个玩家之间物理距离远,那个玩家就会感觉特别卡。Dedicated Server 可以把服务器放在离玩家群体更近的机房,减少这种不公平。

四种网络模式

UE5.8 在 ENetMode 里定义了四种网络模式。理解它们是后续所有决策的前提。

flowchart TD
    A["启动 UE 进程"] --> B{选择网络模式}
    B -->|NM_Standalone| C["本地单人 无网络"]
    B -->|NM_ListenServer| D["玩家机器同时当服务器"]
    B -->|NM_DedicatedServer| E["无渲染专用服务器"]
    B -->|NM_Client| F["只连接别人 不做主机"]
    C --> G["适合单机剧情或编辑器预览"]
    D --> H["适合小房间合作或局域网对战"]
    E --> I["适合竞技、长期房间、反作弊"]
    F --> J["适合所有客户端玩家"]

NM_Standalone 就是没有网络,所有 Actor 的 Role 都是 ROLE_Authority。编辑器里点 Play 默认就是这个模式。

NM_ListenServer 是本机既当服务器又当客户端。GetWorld()->GetNetMode() 在主机上返回 NM_ListenServer,在加入的玩家上返回 NM_Client。主机本地的 PlayerController 拥有 Authority,远程玩家的 Controller 是 Autonomous 或 Simulated。

NM_DedicatedServer 永远不渲染、不播放声音、不加载客户端资源。启动时通常加 -server 参数,并指定 -log 和端口。这个进程里的所有 Actor 都是 Authority,但没有本地玩家。

NM_Client 是加入别人房间的客户端。它只能拥有本地玩家控制的 Pawn,其他所有状态都来自服务器同步。

判断当前运行环境最稳的办法不是看命令行,而是看 GetNetMode()。代码里经常这么写:

// 只在服务器上执行权威逻辑
if (GetNetMode() != NM_Client)
{
    ApplyDamageToMonster();
}

专用服务器打包

UE5.8 打包 Dedicated Server 需要在 Build 时指定 Server 目标。假设你的项目叫 Extraction,那么打包命令大致如下:

# 生成 Visual Studio 项目文件后编译 Editor + Server
RunUAT.bat BuildCookRun -project="D:\Extraction\Extraction.uproject" -noP4 -platform=Win64 -serverconfig=Shipping -cook -allmaps -stage -pak -archive -archivedirectory="D:\Extraction\Builds\Server"

Linux 服务器更常见,因为云主机便宜。构建 Linux Server 之前要确保安装了交叉编译工具链。命令里把平台改成 Linux:

RunUAT.sh BuildCookRun -project="/home/ci/Extraction/Extraction.uproject" -noP4 -platform=Linux -serverconfig=Shipping -cook -allmaps -stage -pak -archive

打包出来的目录里有一个 ExtractionServer.exeExtractionServer。启动它:

./ExtractionServer -server -log -port=7777

Listen Server 的启动更简单,直接在客户端命令行里加 ?Listen

./ExtractionGame.exe /Game/Maps/Lobby?Listen -log

专用服务器打包有几个坑。第一是默认不会把客户端资源Cook进服务器,但服务器加载地图时仍需要地图本身和碰撞网格。第二是 Dedicated Server 不会初始化渲染设备,所以任何依赖 GEngine->GameViewport 的代码都会崩溃,必须加 IsDedicatedServer() 保护。第三是默认的 DefaultEngine.iniNetDriver 配置要确认,UE5.8 默认用 UENetDriver,大规模项目可以切到 WebSocketNetDriver 或自定义后端。

命令行参数备忘

启动 Dedicated Server 时,常用的参数有这么几个:

-server 表示以服务器模式启动,不加载客户端渲染。-log 让日志输出到控制台,调试时很有用。-port=7777 指定监听端口。-MaxPlayers=32 限制最大玩家数。-Map=/Game/Maps/Level_01 指定初始地图。生产环境一般还会加 -RenderOffScreen-nullrhi,确保服务器在没有显卡的机器上也能跑。

Linux 上如果遇到字体或音频初始化失败,可以在启动脚本里设置 SDL_VIDEODRIVER=dummy,让 UE 认为有一个虚拟显示设备。很多云主机没有声卡,还要把 DefaultEngine.ini 里的 AudioDevice 设为 NullAudioDevice,否则启动时会卡住。

P2P 与中心化架构

Listen Server 本质上是一种 P2P 变体。每个客户端都需要与主机建立连接,主机负责转发和仲裁。Dedicated Server 是中心化架构,所有客户端只与服务器通信,彼此不知道对方的存在。

P2P 的连接数会随玩家数平方增长。如果每个玩家都要与其他所有玩家直接通信,连接数为:

Cp2p=n(n1)2C_{p2p} = \frac{n \cdot (n - 1)}{2}

n=8n = 8 时,Cp2p=28C_{p2p} = 28。当 n=64n = 64 时,Cp2p=2016C_{p2p} = 2016。Listen Server 稍微好一点,因为 n1n - 1 个客户端只与主机连接,主机承担全部转发压力。

Dedicated Server 的连接数是 nn。每个客户端只维护一条到服务器的连接,服务器同时维护 nn 条连接。复杂度从 O(n2)O(n^2) 降到 O(n)O(n)

graph LR
    subgraph Listen["Listen Server 8 人房"]
        H["主机"] --- C1["玩家1"]
        H --- C2["玩家2"]
        H --- C3["玩家3"]
        H --- C4["玩家4"]
        H --- C5["玩家5"]
        H --- C6["玩家6"]
        H --- C7["玩家7"]
    end
    subgraph Dedicated["Dedicated Server 64 人房"]
        S[Server] --- D1["玩家"]
        S --- D2["玩家"]
        S --- D3["玩家"]
        S --- D4["玩家"]
        S -.- Dn["...玩家"]
    end

这个图很直观。Listen Server 的拓扑像自行车轮,Dedicated Server 也是自行车轮,但轮毂在数据中心,辐条更粗也更稳。

P2P 有个天然缺陷:主机玩家更容易作弊。因为他本地就有 Authority,修改内存里的速度、血量、后坐力,其他客户端根本没法验证。Dedicated Server 把权威逻辑搬到了服务器上,客户端只能发送输入,服务器才决定结果。

服务器部署基础

Dedicated Server 跑起来之后,下一步是让它能被玩家找到并连上。最基础的做法是云主机加公网 IP。

我用过的最小配置大概是这样:

; DefaultEngine.ini 片段
[/Script/OnlineSubsystemUtils.IpNetDriver]
NetServerMaxTickRate=60
MaxNetTickRate=120
MaxInternetClientRate=15000

一台 4 核 8G 的 Linux 云主机,通常能带 20 到 40 个玩家,具体看你游戏里复制的东西有多少。每个 Actor 每帧都在问自己:我要不要同步?同步给谁?这些问题回答得不好,带宽会爆。

服务器需要开放端口。UE 默认用 UDP 7777、7778,Steam 则要 27015。如果是 Kubernetes 部署,需要把 Service 类型设为 LoadBalancer 或 NodePort。如果是 AWS,安全组里记得放行 UDP。

心跳和保活要自己做。UE 自带的 UNetConnection 会发心跳包,但如果服务器被云厂商回收,还是要有一个外部进程监控。我习惯用 systemd 跑服务器进程,崩溃后自动重启,再用一个 Go 小服务把当前房间状态写到 Redis,供匹配服务读取。

匹配服务可以用 Epic Online Services 的 Sessions 接口,也可以自己写。自己写的话,核心逻辑就三步:玩家请求对局、后端分配一个空闲 DS、把 IP 端口返回给客户端。客户端拿到后用 ClientTravel 连接:

const FString Url = FString::Printf(TEXT("%s:%d"), *ServerIP, ServerPort);
PlayerController->ClientTravel(Url, TRAVEL_Absolute);

一次完整的进房流程

玩家点击匹配后,背后大概发生了这些事:

sequenceDiagram
    participant C as "客户端"
    participant M as "匹配后端"
    participant DS as "Dedicated Server"
    C->>M: 提交匹配请求
    M-->>C: 返回房间地址 IP:Port
    C->>DS: 发送 NMT_Hello
    DS-->>C: 回复 NMT_Welcome 与 PlayerId
    C->>DS: 加载地图并请求初始同步
    DS-->>C: 发送世界快照
    C->>DS: 玩家输入开始流动

这个流程在 Listen Server 里也类似,只是匹配后端换成主机玩家自己。Dedicated Server 的好处是后端可以预先分配好资源,玩家进入时直接连,不用等主机加载。

反作弊与安全性

回到阿杰和小林的争吵。小林最担心的问题是:Listen Server 下主机开修改器怎么办?

UE5.8 里,Actor 的 Role 决定了谁有权威。客户端上的 Pawn 通常是 ROLE_AutonomousProxy,自己只能预测移动;服务器上的 Pawn 是 ROLE_Authority,它才决定最终位置。这个设计叫 Client-Server Authoritative。Listen Server 的主机本地角色就是 Authority,绕过验证的空间大得多。

Dedicated Server 把 Authority 完全收走。客户端发送移动输入,服务器用同样的逻辑计算,再把结果同步回来。如果客户端发了一个不可能的速度,服务器直接拒绝。只要服务器代码写得严谨,普通修改器很难生效。

但 Dedicated Server 不是万能盾牌。经验不足的开发者会直接在客户端上处理伤害判定,再把结果 RPC 到服务器。这就等于把 Authority 拱手让人。正确做法是所有伤害、血量、掉落、经济、匹配结果都在服务器上算。

另一个常见漏洞是数据包重放。客户端发送的 RPC 如果可被截获并重放,玩家就能反复触发同一个动作。UE 的 FUniqueNetId 和会话令牌可以减轻这个问题,关键 RPC 最好加序列号或时间戳校验。

加密方面,UE5.8 支持 DTLS 和 SSL 的插件扩展。竞技游戏通常还会接入第三方反作弊,比如 Easy Anti-Cheat。EAC 在 Dedicated Server 上部署更容易,因为服务器环境干净,不会像玩家机器那样有各种注入软件。

反作弊还要考虑状态校验。服务器不能只看客户端发来的位置,还要检查从上一个合法位置到当前位置是否可能。常用做法是服务器保存最近几帧的位置历史,用速度上限和碰撞体积做合理性检查。如果某帧玩家移动速度超过了角色最大速度的 1.5 倍,就直接拉回并记录日志。这个倍数可以根据游戏节奏调整,撤离类游戏通常设得严格一些。

服务器性能优化

性能是小林和阿杰都没有意识到的大坑。UE5.8 的 Replication 默认很暴力,它会试着同步所有 Replicates=true 的 Actor。一个房间里如果有 1000 个可拾取道具,每个客户端每秒都会收到大量状态包。

优化的第一步是控制 Net Update Frequency。远处的敌人不需要每帧同步,玩家视线外的物体可以降低频率。在 C++ 里设置:

GetCharacterMovement()->NetUpdateFrequency = 60.0f;
GetCharacterMovement()->MinNetUpdateFrequency = 20.0f;

远处的 AI 可以降到 10Hz,甚至 5Hz。玩家靠近再拉回来。

第二步是 Network Relevance。服务器每帧都会计算哪些 Actor 对哪个客户端是相关的。默认距离阈值在 AGameNetworkManager 里配置。超出范围的 Actor 不会被复制。UE5.8 还引入了 Replication Graph,专门为大规模游戏优化相关性判断。把高频同步的对象注册到 UReplicationGraph 上,能显著降低 CPU 和带宽。

Actor 休眠也很实用。把不常变化的 Actor 设成 Dormant,服务器会跳过它们的常规复制,只有显式唤醒时才同步。道具、门、被破坏后的掩体都适合休眠。

带宽的上限可以用一个简单公式估算:

Bserver=i=1nRiSiTi+OheaderB_{server} = \sum_{i=1}^{n} \frac{R_i \cdot S_i}{T_i} + O_{header}

RiR_i 是第 ii 个客户端的复制频率,SiS_i 是平均包大小,TiT_i 是压缩或批处理间隔,OheaderO_{header} 是协议头开销。如果每个玩家每秒同步 60 次、每次 300 字节,20 个玩家的服务器出口带宽就是 20×60×30036020 \times 60 \times 300 \approx 360 KB/s,约 2.9 Mbps。加上头开销和广播,实际可能翻倍。

CPU 优化主要靠减少服务器端的物理模拟。Dedicated Server 不需要布料、粒子、面部动画。把这些 Component 在服务器上关掉。用 SetReplicates(false) 把纯装饰性对象排除在复制之外。

Tick 优化也很关键。默认 PrimaryActorTick.bCanEverTick = true 的 Actor 越多,服务器越慢。能事件驱动就别 Tick,能用 Timer 就别 Tick。

Replication Graph 的配置分两步。第一步是在 DefaultEngine.ini 里把 ReplicationGraph 设为默认网络驱动。第二步是在项目里新建一个 UReplicationGraph 子类,重写 InitGlobalActorClassSettings,给不同 Actor 类设置默认复制频率。比如玩家 Pawn 每帧同步,投掷物每 10 毫秒同步,静态道具直接不复制。启动时通过 SetReplicationDriverClass 注册。

平台多人服务集成

现代多人游戏很少从零搭后端。UE5.8 的 Online Subsystem 抽象层已经接入了 Epic Online Services、Steam、PlayFab、GameLift 等平台。

Epic Online Services 是 UE 的亲儿子。EOS 提供身份验证、好友、会话、排行榜、统计、语音和反作弊。UE5.8 的 OnlineSubsystemEOS 插件启用后,IOnlineSessionPtr 接口基本不用改代码就能从 Steam 切到 EOS。

Steam 的 Online Subsystem 老牌但局限多。它只能给 Steam 玩家用,主机平台不支持。Steam 的 P2P 中继对 Listen Server 很友好,Dedicated Server 也能用 Steam 做认证。

PlayFab 和 Azure PlayFab 更偏后端即服务。玩家账号、物品、经济、成就、匹配都能托管。UE5.8 可以通过 HTTP 插件或 PlayFab SDK 调用它的 REST API。如果团队没有后端程序员,PlayFab 能省很多事。

AWS GameLift 和 Google Agones 是 Dedicated Server 托管方案。GameLift 帮你把服务器镜像部署到全球实例,提供匹配、扩缩容、会话管理。Agones 基于 Kubernetes,更适合已经有 K8s 运维经验的团队。

平台接入最麻烦的不是 API,而是账号互通。如果游戏上 Steam 又上 Epic,两边玩家能不能一起玩?跨平台需要统一的身份系统和会话协议。UE5.8 的 EOS 身份服务可以把 Steam、Epic、PlayStation、Xbox 的账号映射到同一个 FUniqueNetId 下,这是跨平台匹配的基础。

如果游戏要上主机平台,Sony 和 Microsoft 都要求使用各自的在线服务进行身份验证。UE5.8 的 OnlineSubsystemPS5OnlineSubsystemLive 插件分别处理这部分。主机版本通常不能直接用 Steam 或 EOS 做账号,但可以用 EOS 做跨平台好友和匹配的中间层。

那笔选型账

天亮的时候,小林和阿杰终于坐下算了笔账。

如果游戏是 4 人合作、房间生命周期短、对反作弊要求不高,Listen Server 足够。它能省掉服务器费用,开发也快。Demo 和 EA 阶段用它验证玩法没问题。

如果游戏有排位、有经济系统、有长期存档,或者玩家超过 8 人,Dedicated Server 是底线。多出的是云主机和运维成本,换来的是不掉房、难作弊、好扩展。

UE5.8 两个方案都支持得不错。Listen Server 到 Dedicated Server 的迁移成本主要在代码的权威性假设上。前期如果所有关键逻辑都按 Authoritative Server 写,后面切 Dedicated Server 会轻松很多。

老赵最后拍了板:Demo 用 Listen Server,正式上线前必须切 Dedicated Server。预算不够就先少开几个区,至少核心逻辑不能依赖主机。

结语

那天晚上之后,团队决定先保留 Listen Server 做 Demo,但所有伤害、掉落、撤离判定都按服务器权威来写。等上线测试阶段,再一键切换到 Dedicated Server。

Dedicated Server 与 Listen Server 的选择没有标准答案。看清自己的玩家规模、反作弊需求、延迟敏感度和预算,再对照 UE5.8 提供的四种网络模式,自然能得出靠谱的结论。

如果你也在凌晨两点为这事儿吵架,希望这篇文章能给你一点底气。祝你的服务器永不宕机,玩家不掉线。