专用服务器 · Session · Travel
复制栈讲的是「数据怎么同步」,这篇讲「玩家怎么连上来、怎么换图、服务器怎么部署运维」。DS 与 Listen Server 的差别、那条一步都不能错的连接握手时序、Seamless Travel 为什么能不断线、以及容器编排里 DS 的 Ready-Allocate-Shutdown 生命周期。
一句话结论
DS 无本地玩家无渲染;连接要走 Hello→Login→Join 完整握手;换图用 Seamless Travel 才不断线;容器编排走 Ready-Allocate-Shutdown 生命周期。
场景问题
打个比方:这整套像订场地打球。
DS vs Listen Server:DS 是中立场馆——场地方不下场打,所有人条件对等;Listen Server 是在某位球友家院子里打——他既是主人又是球员,零延迟、天然占优,人少凑局还行,正式比赛不行。
Session 是订场系统:先「发布一个场次」(
CreateSession,写清几人、公开还是私密)→ 别人「搜场次」(FindSessions)→ 「报名加入」(JoinSession)拿到地址过去。Beacon 是先打个电话问一句:还有位置吗、能不能先给我留一个——不用先换衣服进场(不走完整 Login/Join 和地图加载),问完就挂,开销小。握手时序像进场安检:先证明你这个门牌号真能回话(challenge-response 防伪造)→ 报版本号对不对得上(
NMT_Hello)→ 递身份证登记(NMT_Login)→ 场馆告诉你去几号场(NMT_Welcome)→ 你到场报到(NMT_Join)才算真正开始。任何一步卡住,玩家感受都只是「连不上」,所以得知道这条链条上有哪几环。换图两种方式:普通
ServerTravel是清场赶人、换完场地让大家重新排队进来——重连过程可能有人回不来;Seamless Travel 是先把大家领到休息区(过渡关卡),场地布置好再请回去——人一直在馆内,连接不断。而跨图要留的东西得报备(GetSeamlessTravelActorList),否则清场时一并清掉。
「玩家点了『加入房间』到能在场景里跑动,中间发生了什么?」——这条链路上任何一步失败都表现为「连不上」,而报错信息往往很含糊。再往后:比赛结束换下一张图,为什么有的实现玩家会掉线重连、有的无缝切换?最后是运维:一台机器跑几个 DS 实例、K8s 怎么知道哪个实例空闲。这三件事构成 DS 的工程全貌。
实现方案
1. DS vs Listen Server
| Dedicated Server | Listen Server | |
|---|---|---|
| 本地玩家 | 无 | 有(房主既是服务器又是玩家) |
| 渲染 | 无(-nullrhi,不加载渲染资源) | 有 |
NetMode | NM_DedicatedServer | NM_ListenServer |
| 构建 | 独立 Server target,裁掉客户端资源 | 普通客户端构建 |
| 公平性 | 所有玩家延迟对等 | 房主零延迟,天然优势 |
| 适用 | 竞技、商业运营 | 小型合作、局域网 |
DS 启动大致形如:
MyGameServer.exe /Game/Maps/Arena?listen -port=7777 -log
map URL 里的 ?listen 开监听,? 后可带自定义参数(?MaxPlayers=32),服务器侧在 GameMode::InitGame/PreLogin 读取。
2. 完整连接握手时序
每步的常见报错:
- challenge 阶段失败 → 连接超时(防火墙/端口不通/UDP 被拦)。
NMT_Hello后 → 「Version mismatch」(客户端服务器构建版本不一致)。PreLogin拒绝 → 自定义拒绝原因(服务器满、票据校验失败、封禁)。NMT_Join前卡住 → 客户端地图加载失败/缺资源。
握手底层的 Control Channel 与 PacketHandler 细节见 NetDriver/Channel/Bunch;初始复制顺序见 属性复制。
3. 关卡切换:ServerTravel vs Seamless Travel
非无缝(ServerTravel,默认) | Seamless Travel | |
|---|---|---|
| 连接 | 断开重连(客户端走一遍完整握手) | 保持连接,无需重连 |
| 过程 | 服务器直接换图,客户端重新连接加载 | 先切到轻量过渡关卡(transition map),再加载目标图 |
| 体验 | 有「掉线—重连」感,可能连不回来 | 无缝,玩家只看到加载 |
| 开关 | — | GameMode::bUseSeamlessTravel = true |
跨图保留 Actor:Seamless Travel 时大多数 Actor 会被销毁,要跨图保留的(PlayerController、PlayerState、GameInstance 数据)通过 GetSeamlessTravelActorList / AGameModeBase::GetSeamlessTravelActorList 声明。这是「换图后玩家数据还在」的关键。
GameInstance 天然跨关卡存活(见 Gameplay Framework),跨图数据放它最稳。
4. Session 与 Beacon
Session(会话):通过 OnlineSubsystem 的 SessionInterface 做房间的创建—查找—加入:
CreateSession(设置:人数上限/是否公开/自定义键值)
↓
FindSessions(按过滤条件搜索) → 返回 session 列表
↓
JoinSession(选中的 session) → 拿到连接串 → ClientTravel 连上去
OnlineSubsystem 是抽象层,底下可接 Steam、EOS、自研后端等不同实现(OnlineSubsystemNull 用于局域网测试)。业务代码只调统一接口,换平台不改逻辑。
Beacon(信标):轻量的预连接通道,不用完整加入游戏就能和服务器通信:
AOnlineBeaconHost(服务器侧)+AOnlineBeaconClient(客户端侧)。- 用途:预约座位(进图前先占位,防超卖)、查询服务器状态/当前人数、大厅交互。
- 好处:不必走完整的 Login/Join 流程和地图加载,开销小、响应快。
5. 运维侧:容器化与编排
- 打包瘦身:Server target 构建裁掉客户端资源(贴图、音频、渲染管线),镜像可小很多。启动加
-nullrhi/-nosound进一步省资源。 - 一机多实例端口分配:一台机器跑 N 个 DS 进程,每个占不同
-port(如 7777、7778…)。需要端口分配器/编排系统统一管理,避免冲突;容器化下用宿主端口映射。 - 编排生命周期(Agones / GameLift / K8s):DS 是有状态、有生命周期的负载,标准三态:
Ready(就绪,等待分配) → Allocated(已分配,一局进行中) → Shutdown(局结束,退出)
Ready:进程起来、地图加载完,向编排层报告可接受分配。
Allocated:编排层把一批玩家分配给它,此期间不能被回收(有玩家在里面)。
Shutdown:一局结束主动退出,编排层拉起新实例(DS 通常是一局一实例的短生命周期模型,避免状态污染)。
优雅下线:缩容或发版时不能直接杀有玩家的实例。做法是「停止接受新分配 + 等当前局自然结束 + 超时兜底」——和局外服务的优雅下线同理,但粒度是「一局」而不是「一个请求」。
与局外后台对比:局外服务是无状态可随意扩缩(见 接入网关),DS 是强状态、局粒度的,不能中途迁移玩家——这是它与 有状态服务迁移 的关键差异:DS 通常不迁移,而是等局结束。
6. 一句话记住这几组容易混的概念
| 一组 | 差别一句话 | 类比 |
|---|---|---|
| DS vs Listen Server | DS 无本地玩家无渲染、延迟对等;Listen Server 房主零延迟、他退整局崩 | 中立场馆 vs 在球友家院子打 |
| Session vs Beacon | Session 管「房间的建/搜/加入」;Beacon 是不进场就能问话占位的轻通道 | 订场系统 vs 先打个电话问还有没有位 |
ServerTravel vs Seamless Travel | 前者断开重连,后者保持连接、先过渡关卡 | 清场重排队 vs 领到休息区等布置 |
ClientTravel vs ServerTravel | ClientTravel 是一个客户端自己去别处;ServerTravel 是服务器带着全场换图 | 你自己换场馆 vs 全队一起换场 |
PreLogin vs PostLogin | PreLogin 是「准不准进」(满人/封禁/票据校验);PostLogin 是「已经进来了,发装备」(创建 PC、生成 Pawn) | 门口验票 vs 进场发球衣 |
| DS 扩缩 vs 局外服务扩缩 | DS 是局粒度强状态、不中途迁移,等局结束再退;局外服务无状态、随时可换实例 | 一场球打完才收场 vs 随时换收银台 |
一条主线串起来:订场(Session/Beacon)→ 进场安检(握手五步)→ 开打(复制栈接管)→ 中途换场地(Seamless Travel + 报备要留的东西)→ 散场(Shutdown,编排拉新实例)。
为什么这么做
DS 而非 Listen Server 做竞技:Listen Server 的房主零延迟,天然不公平,且房主退出整局崩。DS 让所有玩家延迟对等,且可运维、可扩缩。
完整握手分阶段:challenge 挡伪造源 IP,NMT_Hello 挡版本不匹配,PreLogin 挡满人/封禁/无效票据——尽早拒绝,越靠前的检查越廉价,避免为无效连接分配昂贵资源(PlayerController、地图加载)。
Seamless Travel:换图断线重连是玩家流失点(可能连不回来)。保持连接 + 过渡关卡让换图无感,代价是要显式声明跨图保留的 Actor。
一局一实例 + Ready/Allocated/Shutdown:DS 有状态且状态难清理干净(残留 Actor、内存碎片)。一局结束就退出、由编排拉新实例,是最简单可靠的隔离——用进程边界代替状态清理逻辑。
为什么别的选择不行
- 用 Listen Server 做商业竞技:房主优势 + 房主掉线全崩 + 无法运维。
- 跳过 challenge 直接建连接:伪造源 IP 就能让服务器为大量假连接分配状态被打垮。
- 换图用非无缝 travel(对长时间对局/连续比赛):每次换图全员断线重连,掉线率与体验双输。
- DS 实例长生命周期跑多局:状态残留难清理,一局的 bug 污染下一局;且难做版本滚动更新。
- 直接杀有玩家的实例做缩容:玩家一局白打,投诉。必须走「停新分配 + 等局结束」。
沉淀结论
DS(无本地玩家/无渲染/独立 Server target/延迟对等)vs Listen Server。连接握手:challenge → NMT_Hello(版本)→ NMT_Login(PreLogin 可拒)→ Login → NMT_Welcome → NMT_Join → PostLogin → 初始复制。换图用 Seamless Travel(保持连接 + 过渡关卡 + GetSeamlessTravelActorList 保留 Actor)。Session 走 OnlineSubsystem 抽象(Steam/EOS/自研),Beacon 做轻量预约/查询。运维:Ready → Allocated → Shutdown,一局一实例,优雅下线靠「停新分配 + 等局结束」。
记忆口诀
DS:无本地玩家 / 无渲染 nullrhi / Server target 裁资源 / 延迟对等
握手:challenge 防伪IP → Hello 校版本 → Login(PreLogin可拒) → Welcome 下发图 → Join → PostLogin 生成Pawn
换图:ServerTravel 断线重连 / Seamless 保持连接+过渡关卡 / GetSeamlessTravelActorList 保留
Session:OnlineSubsystem 抽象 / Create-Find-Join / Steam·EOS·自研
Beacon:轻量预连接 / 预约座位防超卖 / 查状态不走完整 Login
运维:Ready→Allocated→Shutdown / 一局一实例 / 优雅下线=停新分配+等局结束
内容来源
综合整理。主要参考:Epic 官方文档 Dedicated Servers、Travelling in Multiplayer、Online Subsystem、Online Beacons、Agones/Amazon GameLift 官方文档的 DS 生命周期模型。
相关专题:握手底层的 Control Channel/PacketHandler 见 NetDriver/Channel/Bunch;跨图保留与 GameInstance 见 Gameplay Framework;局外接入层对比见 接入网关;有状态迁移的差异见 数据迁移;发布与灰度见 发布策略。
自测:合上资料能说清楚吗?
客户端从点「加入」到能在场景跑动,握手经过哪些步骤?
参考答案
ClientTravel → 底层 challenge-response(StatelessConnectHandler 防伪造源 IP,验证前不分配 NetConnection)→ NMT_Hello(引擎/网络版本校验)→ NMT_Login(带 map URL 与 OnlineID/票据)→ GameMode::PreLogin(可拒绝:满人/封禁/票据无效)→ Login(创建 PlayerController)→ NMT_Welcome(下发地图)→ 客户端加载地图 → NMT_Join → PostLogin(生成 Pawn)→ 初始 Actor 复制。设计原则是尽早用廉价检查拒绝无效连接。
Seamless Travel 和普通 ServerTravel 差别?跨图怎么保留数据?
参考答案
普通 ServerTravel 会让客户端断开重连(走一遍完整握手,可能连不回来);Seamless Travel 保持连接,先切到轻量过渡关卡再加载目标图,玩家无感。开关是 GameMode::bUseSeamlessTravel = true。跨图要保留的 Actor(PlayerController、PlayerState)通过 GetSeamlessTravelActorList 声明;跨关卡数据放 GameInstance 最稳(天然跨图存活)。
DS 在容器编排里的生命周期是什么?为什么一局一实例?
参考答案
Ready(进程起来地图加载完,报告可接受分配)→ Allocated(已分配玩家,一局进行中,不能被回收)→ Shutdown(局结束主动退出,编排拉新实例)。一局一实例是因为 DS 强状态且状态难清理干净(残留 Actor、内存碎片),用进程边界代替状态清理逻辑最简单可靠,也便于滚动更新。优雅下线靠「停止接受新分配 + 等当前局自然结束 + 超时兜底」,不能直接杀有玩家的实例。
Beacon 是什么?解决什么问题?
参考答案
轻量的预连接通道(AOnlineBeaconHost + AOnlineBeaconClient),不走完整 Login/Join 和地图加载就能与服务器通信。典型用途:进图前预约座位(防超卖)、查询服务器状态/当前人数、大厅交互。好处是开销小响应快,避免为一次简单查询走完整连接流程。