简介:一份围绕Flash游戏与服务器通信的完整学习资源,面向希望了解网络编程、TCP连接及select I/O多路复用模型的开发者。压缩包内含一个Flash赛车游戏(SWF)、对应的C++服务器控制台程序,以及讲解双方数据包格式的PPT,可直接运行并对照源码学习。服务器采用TCP协议,并基于select模型实现多客户端并发处理,是理解经典网络服务架构的很好范例。
资源共15个文件,主要包括.cpp/.h源码、.obj/.pdb编译中间文件、.exe可执行程序等,压缩包整体649KB。通过源码与可执行程序,可以观察select模型如何同时监控多个套接字、处理并发请求,并理解Flash客户端与服务器之间数据包如何封装与解析——包括包头、标识符、数据体等关键部分。PPT则进一步梳理了通信格式设计思路,便于从零开始搭建类似联机小游戏。
已有235人学习下载,适合正在学习网络编程、准备开发小型联机游戏或想深入理解非阻塞并发模型的技术人员参考。
1. Flash 游戏以及服务器在扛什么:一个没被淘汰的老协议栈
说到 Flash 游戏以及服务器,很多人的第一反应是“都 2026 年了,这东西还存在吗”。手头恰好有一条老游戏线的登录通道还在跑:SWF 用 AMF 协议往 9123 端口发包,服务端校验完返回角色存档。它没死在 Flash Player 停更那天,反而因为没人敢动,变成整个业务里最忌讳碰的黑匣子。
Flash 游戏的服务器本质上不是一台放 SWF 的 Web 站点,而是一台要处理登录、存档、广播、跨域策略的游戏接入机。它既要认得出 AMF 或二进制 Socket 包,又要按 Flash Player 的安全沙箱规则回传 crossdomain.xml,还得在玩家集中登录时把广播消息压下去。这篇文章写给三类人:要接盘老项目的人、想从零搭一台可复现服务的人,以及准备把 Flash 服务搬上虚拟化或容器环境的人。可以先记住一个结论:Flash 客户端可能过时了,但服务端协议设计里的日志、超时、半包处理和状态广播,放到今天的 WebSocket 项目里依然完全适用。
2. 先从协议和服务端框架下手:Flash 游戏服务器凭什么“认人”
2.1 Flash 游戏服务器的三种常见链路:HTTP、长连接和媒体流
Flash 客户端能用的网络连接方式并不多,AS3 里实际也就三条路:URLLoader走 HTTP、Socket/XMLSocket走长连接、NetConnection走 RTMP。选哪种,直接决定服务端的形态和运维方式。
| 链路 | 典型场景 | 服务端常见形态 |
|---|---|---|
| HTTP + URLLoader | 登录、公告、排行榜、拉取配置 | Nginx / PHP / Java Web,最省事 |
| Socket / XMLSocket | 实时战斗、聊天、在线状态广播 | Node.js、Java Netty、SmartFoxServer 一类游戏框架 |
| RTMP | 直播观战、语音视频、流媒体型互动 | nginx-rtmp、Red5 |
HTTP 链路最简单,Flash 每次请求都新建连接,服务端不需要维护玩家在线状态。代价是实时性差,做不了大厅里的坐标广播。Socket 链路正好相反,一次连接建立后可以持续双向收发,适合做战斗同步,但需要自己处理分包、粘包、心跳和断线重连。RTMP 本质上是带通道复用的长连接,Flash 端的NetStream和SharedObject都建立在它之上,适合对时序要求更高的媒体互动。
我给老项目做维护时的直观感受是:登录和存档走 HTTP 或者轻量二进制接口都行,真正让服务器“活起来”的永远是那条 Socket 长连接。很多 Flash 游戏之所以卡在“进房间刷列表慢”,不是因为服务器性能差,而是整个服务端长连接模型没有建好。
2.2 Flash Remoting 与 AMF 协议:老项目为什么还留着它
AMF 是 Flash 时代专门为远程调用设计的一套二进制序列化协议,分 AMF0 和 AMF3 两个版本。AS3 里的NetConnection.call("Service.method", responder, args)就是通过 Flash Remoting 把参数编码成 AMF 发给服务端,服务端返回的对象再自动反序列化成 AS3 对象。这套机制的最大优点是省掉了手工拼 XML 或 JSON 的过程,在 2008 年前后非常流行。
当时最常见的服务端实现是 PHP 搭配 AMFPHP,把 PHP 类方法直接暴露成远程网关。开发者只需要在 PHP 里写一个UserService::login($uid, $pwd),Flash 端就能用serviceName.methodName调用它,几乎不需要写解析代码。AMF 里对数组、对象、字符串都有固定类型标记,比如 AMF0 里字符串是0x06,数字是0x00,对象是0x03,类型标记后面再跟长度和内容。看起来像格式规范,实际上正是这些二进制标记决定了服务端字段不能随意变更。
老项目一直没迁走 AMF,通常不是因为性能,而是因为改造成本高。Flash 端 AS3 类里定义了强类型字段,服务端 PHP 返回的stdClass一旦少了字段或改了类型,客户端大概率反序列化成 null 而不是报错。这种静默失败比直接报错更可怕,线上表现就是“部分玩家存档突然少了装备”,查日志却看不到异常。如果手里有这类老服务,第一原则是服务端字段只增不改,降低 AMF 反序列化的兼容风险。
2.3 最小可复现的登录握手服务器:Node.js Socket 半包处理
很多 Flash 游戏最终会抛弃 Flash Remoting,改走自定义二进制 Socket 协议。原因是登录、移动、战斗这种高频小包,AMF 的编码开销和 HTTP 的握手成本都太高。下面给一个我能直接复现的最小登录认证服务端,协议帧设计成四个部分:magic固定 0x46,cmd是命令号,len是 body 长度,body是具体数据。
const net = require("net"); const server = net.createServer((socket) => { // 每个 socket 单独维护半包缓冲区 socket.recvBuffer = Buffer.alloc(0); socket.on("data", (chunk) => { socket.recvBuffer = Buffer.concat([socket.recvBuffer, chunk]); // 循环取出当前缓冲区里的完整帧 while (socket.recvBuffer.length >= 4) { const magic = socket.recvBuffer.readUInt8(0); if (magic !== 0x46) { socket.end(); return; } const bodyLen = socket.recvBuffer.readUInt16BE(2); if (socket.recvBuffer.length < 4 + bodyLen) { // 半包:数据还没到齐,留在缓冲区等下个 chunk return; } const body = socket.recvBuffer.subarray(4, 4 + bodyLen); socket.recvBuffer = socket.recvBuffer.subarray(4 + bodyLen); handlePacket(socket, body); } }); }); function handlePacket(socket, body) { if (body.length < 3) return; const uid = body.readUInt16BE(0); const pwd = body.toString("utf8", 2); if (pwd === "letmein") { const reply = Buffer.alloc(4); reply.writeUInt8(0x46, 0); reply.writeUInt8(0x01, 1); // cmd=1 表示登录成功 reply.writeUInt16BE(0, 2); // body 长度为 0 socket.write(reply); } else { const deny = Buffer.from([0x46, 0xff, 0x00, 0x00]); socket.write(deny); } } server.listen(9123, "0.0.0.0", () => { console.log("Flash legacy server listening on 9123"); });这段代码里最重要的一行是Buffer.concat和 while 循环。Flash 端Socket.write发出的一个ByteArray,在 TCP 上可能被拆成两次 data 事件送达,也可能两个包被合并成一次送达。不做半包处理,登录包一多就会出现“第一次能连,第二次卡死”的玄学问题。readUInt16BE表示按大端序读取两个字节,Flash 端 ByteArray 默认写入整数时用的是writeShort,同样是大端序,所以这里的字节序必须两边一致。
参数说明集中在几个位置:9123是监听端口,可以按区服划分成 9124、9125;0x46是自定义 magic 字节,防止客户端连错服务;cmd=0xff保留给服务端拒绝包。实际项目里还会在 body 前面加 session token,服务端在登录成功后生成随机串,后续所有包先校验 token 再处理业务。这样即使有人从抓包里复制了登录请求,也不能在没有 token 的情况下重放。
3. 单机扛不动玩家时:服务器虚拟化、集群和时区校准的三种改造
3.1 先虚拟化再谈扩容:KVM 和容器的取舍
Flash 游戏服务端有个特点:每个区服往往是独立进程、独立端口,玩家数据也按区服隔离。单台物理机上直接跑多个区服进程当然可以,但一旦某个区的 GC 停顿把 CPU 打满,其他区会跟着卡,玩家只会骂服务器,不会怪同机房的邻居。常见做法是通过 KVM 给服务器做系统,把每个 Flash 区服放进独立虚拟机,vCPU 和内存配额都固定下来,互不挤占。
如果项目已经在容器化环境里,Docker 是我更推荐的方案。Flash 老服务通常没有优雅下线机制,容器重启后要能立刻监听原始端口。以 Node.js 版服务为例,容器启动时加一行 host 网络模式最省事:
docker run -d --name flash-srv \ --network host \ -e FLASH_ZONE_ID=3 \ -v /srv/flash-data:/data \ flash-server:1.2用--network host而不是-p 9123:9123,是因为 Flash Player 连接 Socket 前可能先向同端口发策略文件请求,bridge 模式只映射业务端口时,容易出现策略请求落到别的服务上。host 模式直接复用宿主机网络栈,端口和源 IP 都透明,排错时少一层 NAT 干扰。
不过虚拟化不是越多越好。Flash 实时战斗对 CPU 抖动非常敏感,宿主机上其他虚拟机争抢 CPU 会让毫秒级广播变成几十毫秒延迟。我的做法是给游戏服务所在虚拟机做 CPU 绑定,或者在容器编排里设置 CPU 独占。KVM 方案用virsh vcpupin,容器方案给docker run加--cpuset-cpus,保证进程尽量跑在固定的物理核上。
3.2 服务器集群的真相:Flash 长连接会话比 HTTP 难住百倍
HTTP 服务做集群很容易,负载均衡器随便轮询就能把请求分到不同后端。Flash 的 Socket 长连接做不到这一点:玩家连上 A 网关后,整个战斗期间的移动包都走这条连接,如果网关重启,客户端不会自动重连到 B 网关,而是直接黑屏。
所以在做服务器集群前,要先给 Flash 协议设计一个“区服重连”机制。常见做法是玩家登录时先请求一个 HTTP 接口,接口返回当前可用网关地址列表及一个短期 ticket,客户端再用 Socket 连其中一个网关,连接成功后提交 ticket。网关拿到 ticket 后到 Redis 或数据库里校验,通过才允许进入游戏逻辑。这样负载均衡只需要作用在 HTTP 登录接口上,长连接网关靠 ticket 做校验,断线后客户端再向登录接口重新要一份地址列表。
集群化之后最大的坑是存档一致性。Flash 老代码里经常出现“登录时读数据库,下线时写数据库”的简单模型,一旦同一个玩家同时从两台设备登录,就会互相覆盖存档。我一般会把存档写入收敛到单玩家队列,或者给每份存档加版本号,写库前比较版本,低于当前版本的直接拒绝。这个逻辑听起来简单,但老代码里通常没有版本字段,加版本号又要同步 Flash 端的协议,属于典型的“牵一发动全身”。
3.3 时间服务器和时区:Flash 登录签名为什么总差八小时
Flash 端Date.getTime()返回的是自 1970 年 1 月 1 日以来的 UTC 毫秒数,不是北京时间,也不是本地时间。服务端如果直接用 PHP 的date()或 Java 的new Date()来比对过期时间,就会把服务器本地时区混进去。服务器设成 UTC,玩家是东八区,签到代码里只要有一次DateTime转字符串的操作,日期就偏移了八小时。
应对思路很简单:服务端一律不存本地时间,所有时间戳统一用 epoch 秒或毫秒存储,只有展示时才转成玩家时区。Flash 端也不要把本地时间拼进签名,而是用Date.now()拿到 epoch 毫秒,再带上 uid 做签名。服务端校验的是签名时间和当前服务器时间的差值,只要差值在容忍范围内就放行,这样即使两台服务器时间偏差一两秒也不会立刻误伤玩家。
时间同步层面,很多从 Windows 环境转过来的人习惯问“Windows 时间服务器用哪个”。Windows 自带的 W32Time 服务主要为域认证设计,默认精度不高,不适合当游戏服务器的时间基准。Linux 后端一般用 chrony,配置指向内网 NTP 或公网 NTP 池即可。典型配置如下:
sudo timedatectl set-timezone UTC sudo systemctl enable --now chronyd在/etc/chrony.conf里写入:
pool ntp.aliyun.com iburst driftfile /var/lib/chrony/drift makestep 1 3iburst参数让 chronyd 在启动后快速发包校准时间,而不是等多久才动一次;makestep 1 3的意思是系统时间偏差超过 1 秒且前三次同步都超差时才直接跳变。对闪存玩家来说,跳变瞬间服务端交易时间会出现负值,所以一般不建议把makestep放宽到瞬间大跳。验证是否同步成功,用chronyc sources -v,看到输出行首为*就说明当前源已被采用。若多台服务器需要互相对表,可以把其中一台设为内网时间服务器,其他服务器把它写进 pool,内网延迟比公网低一个数量级。
4. 上线那天的三件套:RTMP 推流、Web 服务器安全和线上运维动作
4.1 RTMP 推流服务器搭建:给 Flash 直播游戏做直播通道
不少 Flash 游戏不只有玩法,还内置了主播观战或教学直播。RTMP 是 Flash 时代最通用的推流协议,服务端常见做法是给 Nginx 编译nginx-rtmp模块,用它来接收推流并转 HLS。只做推流转发的配置并不复杂,核心是一个 rtmp 块:
rtmp { server { listen 1935; application live { live on; record off; hls on; hls_path /tmp/hls; hls_fragment 2; hls_playlist_length 6; allow publish 127.0.0.1; deny publish all; allow play 127.0.0.1; deny play all; } } }这里1935是 RTMP 默认端口,application live对应推流地址里的live路径,Flash 端调用NetConnection.connect("rtmp://server-ip/live")时就是连到它。hls_fragment 2是每两秒生成一个切片,切片越小延迟越低,但会带来更多文件写入;hls_playlist_length 6表示播放列表保留最近 6 秒的切片,配合hls_fragment 2刚好够低延迟观战。
allow publish 127.0.0.1和deny publish all是上线前最容易被忽略的两行。如果不加限制,任何知道推流地址的人都能往同一个流名推流,把正在直播的主播直接顶掉。生产环境更稳妥的做法是把127.0.0.1换成编码服务器内网 IP,并给推流地址拼接随机 token,Nginx 再用on_publish回调做二次鉴权。RTMP 失败在 Flash 端通常表现为NetConnection.Connect.Failed或NetStream.Play.StreamNotFound,第一反应先看 Nginx 错误日志里是 403 还是 404,403 一般就是权限配置问题,不是服务没起来。
4.2 Web 服务器安全:响应头、跨域策略和隐藏服务器版本
Flash 游戏通常还会搭配一个 Web 站点用来加载 SWF、crossdomain.xml 和游戏公告。这里的 Web 服务器安全,首先不是防注入,而是别让攻击者通过最基本的响应头猜到软件版本。
Nginx 默认会在 400、500 错误页里显示Server: nginx/x.y.z,玩家传一个畸形请求给站点,响应体里就可能返回服务器版本信息。攻击者拿到版本号后可以精准匹配历史漏洞。更气人的是,很多攻击脚本根本不关心业务漏洞,先扫响应头再看 404 页面特征,识别出你还在跑十年前的老版本系统。
一组最基础的加固配置如下:
server { listen 80; server_name flash.example.com; server_tokens off; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; location /crossdomain.xml { root /srv/www/flash; default_type application/xml; } location ~ /\. { deny all; } }server_tokens off关掉 Nginx 错误页里的版本号,同时反向代理层也要把上游的X-Powered-By头剥掉,不然 PHP 版本还是会漏出去。X-Content-Type-Options: nosniff是防浏览器把crossdomain.xml当文本渲染,X-Frame-Options: SAMEORIGIN是防止游戏页面被嵌入到别的站点钓鱼。location ~ /\.这一段是在禁止访问.git、.env、.svn这类点开头文件,很多老项目把备份压缩包放在 Web 根目录下,扫描器一抓一个准。
加这些配置时有一个 Flash 时代的坑:crossdomain.xml必须放在目标域根路径,而且 Content-Type 不能改成text/plain,否则 Flash Player 可能识别失败。升级站点服务时,我会先用curl -I验证 crossdomain.xml 返回 200 和正确的 Content-Type,再推进下一步。
4.3 服务器运维:systemd、日志轮转和端口连接数监控
Flash 游戏服务端的运维重点不是写新功能,而是保证老进程能稳定跑住。我见过太多“开发环境好好的,线上跑一个月就崩”的案例,最后定位下来都是文件句柄或端口资源耗尽。所以上线前先把服务托管给 systemd,并明确打开文件数限制:
[Unit] Description=flash legacy gateway After=network.target [Service] User=flashops WorkingDirectory=/opt/flash-gw ExecStart=/usr/bin/node server.js Restart=on-failure RestartSec=3 LimitNOFILE=65535 [Install] WantedBy=multi-user.targetRestart=on-failure解决进程异常退出时的自动拉起,但要注意不是所有失败都要立刻重启,比如启动时校验数据库失败,RestartSec 设太短会疯狂重启。LimitNOFILE=65535非常关键,Flash 长连接场景下每个玩家占一个 socket,文件描述符默认 1024 的话,几百个在线玩家就会把服务拖垮。
监控端口和连接数时,我常用的命令是ss -lntp和netstat -an。看到大量 TIME_WAIT 的不要急着改tcp_tw_reuse,TIME_WAIT 是 TCP 正常状态,真正要关注的是tcp_max_tw_buckets有没有触发警告,以及源端口是否耗尽。日志轮转可以用系统自带的 logrotate,给 Node 进程的 stdout 输出指定到/var/log/flash/server.log,然后按天切割、保留 14 天,避免一个日志文件涨到几十 GB。线上半夜出问题时的后悔药就是这些日志,平时不轮转,关键时刻连查问题都无从下手。
5. Flash 服务器避坑指南:2046 错误、策略沙箱和时区偏差
5.1 IOError #2046:先怀疑 HTTP 层,再怀疑协议层
现象:Flash 端加载某个接口时报IOError: Error #2046,游戏功能中断,但服务端日志看不到任何异常。
原因:AS3 的URLLoader对非 HTTP 200 返回非常敏感,只要服务端返回 400、403、500,Flash 就会抛 2046 而不是像浏览器那样显示具体状态码。常见的触发点有两个:一是接口返回了 400 错误且响应体里还附带服务器信息,二是 Flash 请求头里带了浏览器不允许的字符,Nginx 直接拒绝。
解决:先在浏览器或 curl 里复现同一个 URL,确认状态码和响应体。Flash 报 2046 时,把URLLoader换成URLStream并不解决问题,真正要查的是服务端错误日志。如果在 Nginx 的 error log 里看到 400,优先检查 URL 是不是包含未转义的中文或空格。服务端返回 JSON 时还要注意响应头Content-Type必须是 Flash 能识别的格式,纯文本接口偶尔会因为这个解析失败。
5.2 crossdomain.xml 的玄学:本地能连,线上连不上
现象:SWF 在本机测试时连接服务器一切正常,部署到线上域名后,所有 Socket 请求都在连接阶段被 Flash Player 拦截。
原因:Flash Player 有安全沙箱,浏览器里的 SWF 访问目标服务器的 Socket 端口前,必须先读到该服务器域名根路径下的crossdomain.xml。本地测试时可能因为 SWF 和服务器同源,或者用了 Flash 调试器放行参数,掩盖了这个问题。线上域名是game.example.com,服务器在api.example.com,Flash Player 不认它们属于同一个域。
解决:在服务端根路径提供 crossdomain.xml,并给 Socket 端口显式声明权限。一个最小可用的策略文件长这样:
<cross-domain-policy> <allow-access-from domain="*" to-ports="843,9123,1935" /> </cross-domain-policy>这里有三个坑。第一,domain="*"在生产环境建议收窄到自己的游戏域名,否则外部恶意 SWF 也能连上你的服务端口。第二,to-ports必须覆盖实际业务端口和策略端口,很多项目只写了 9123,漏了 843,结果 Flash 先请求 843 端口拿策略,拿不到就放弃连接。第三,如果 Flash 端连的是同端口策略请求,也就是连接 9123 后先发送<policy-file-request/>,服务端必须在收到这个字符串后返回策略内容,而不是直接进入业务协议。
5.3 时区和夏令时:Flash 时间戳的八小时幽灵
现象:玩家签到日期偶尔错一天,技能冷却时间差 8 小时,服务端日志里的时间和客户端显示时间对不上。
原因:老服务通常在数据库里用DATETIME存本地时间,没存时区信息。Flash 客户端发来的是 UTC 毫秒,服务端转成东八区后写入DATETIME,看起来正常。但到了夏令时切换区域或服务器迁移时,系统时区一变,数据库里所有历史时间整体偏移,Flash 端再读出来就乱了。
解决:新接口全部改用 epoch 毫秒或TIMESTAMP存储,不依赖数据库会话时区。读取时再由服务端统一换算成玩家时区,Flash 端不做Date.setHours这类手工偏移。同时把服务器系统时区固定为 UTC,避免localtime()函数混入东八区偏移。迁移老数据时要先做离线脚本,把历史DATETIME字段按旧时区解析成 epoch,再写回新字段,不能直接ALTER TABLE改类型。
5.4 同一网关同时混跑新老协议,导致状态互相覆盖
现象:Flash 老客户端和 Web 新客户端同时在线时,玩家的在线状态在 Redis 里一会儿有、一会儿没有,掉线提示莫名其妙。
原因:Flash 网关的在线列表是一张全局 HashMap,老协议用玩家 ID 做 key,新协议用 session ID 做 key,两个客户端同时登录同一个账号时,后登录的把他踢下线。踢下线动作本身又不是用协议层合法通知,直接把 TCP 连接断掉,Flash 端就弹出“连接被服务器关闭”。
解决:网关统一账号层踢人策略,同一个玩家 ID 只允许一个活跃连接,新连接上线时先向旧连接发送带 reason code 的重连通知,让客户端主动请求重新登录。更稳妥的做法是在登录 token 里追加设备类型,区分 Flash 端和 Web 端,两边同时存在时以 Web 端为准,Flash 端禁用登录。这套逻辑必须在网关里做,不能依赖客户端自觉。
6. 退役之后再续命:存档迁移、WebSocket 桥接和协议兼容验证
6.1 存档迁移:把 AMF 二进制当成一种可转译的序列化格式
Flash Player 虽然退役,但玩家的存档数据还活在数据库里。老项目里最常见的存档形态是数据库 BLOB 字段存 AMF 二进制,游客户端登录时拉出来反序列化。迁移这类数据的关键是先离线解析,再落 JSON,不要直接在游戏进程里改协议。
一个只读解析的开头长这样:
function extractPlayer(rawAmfBuffer) { const marker = rawAmfBuffer.readUInt8(0); if (marker !== 0x0A) throw new Error("不是 AMF3 对象,需要换 AMF0 解析器"); // AMF3 对象从这里开始:读 U29 长度,再按 key-value 循环 return parseAmfObject(rawAmfBuffer.subarray(1)); }AMF3 的对象结构与 JSON 不同,字符串有引用表,对象属性名可能复用索引,所以刻不能只按顺序读一遍。我的习惯是先写一个只读解析器,把解析结果输出成 JSON 文件,人工比对几条样本后,再启动迁移任务。迁移过程中原 BLOB 字段保留一个月,新字段放 JSON,两边并行写入,等玩家反馈稳定后再删旧字段。
6.2 用 WebSocket 给新前端配一个桥
如果你还希望老 SWF 里的玩法能在新浏览器里继续跑,常见做法是用 Ruffle 这类开源 Flash 运行时模拟器加载 SWF。但模拟器对 Socket 的支持通常有限,更可靠的做法是让新前端直接走 WebSocket,再由网关把 WebSocket 帧转换成老协议发往后端。
桥接层不负责业务逻辑,只做协议翻译和心跳保持。新前端这边把登录命令按老协议的字节序组装好,原样发给网关:
async function bridgeToLegacy(uid, password) { const ws = new WebSocket("wss://yourhost/gateway"); ws.binaryType = "arraybuffer"; ws.onopen = () => { const body = new Uint8Array(2 + password.length); const dv = new DataView(body.buffer); dv.setUint16(0, uid); // 与 2.3 的 readUInt16BE 对应 new TextEncoder().encodeInto(password, body.subarray(2)); ws.send(body); }; }网关收到后,把这段Uint8Array转成 Buffer,再复用老协议连接池发给后端,返回的数据原样回传。这样后端不感知协议变化,新前端也不需要理解 AMF。迁移时我会同时保留两条通道:老 Flash 客户端直接连老端口,新前端走 WebSocket 网关,两条通道在网关层共用同一套鉴权和踢人逻辑。
6.3 验证方法:新旧协议对同一组字节跑出相同结果
桥接做完后,不要直接切流量。先准备一组固定的登录请求字节,分别发给老端口和 WebSocket 网关,对比返回帧的前几个字节。再用线上日志重放一个高峰时段的 Socket 数据,确认新网关的响应延迟没有明显升高。
我做这类迁移翻过车:直接在生产库改存档字段,结果一部分老客户端读到了负数,登录后角色背包直接打不开。后来养成的习惯是先做只读网关,观察一周日志,确认新旧协议帧和存档解析完全一致后,再放开写流量。改动老服务端时的第一条纪律永远是备份进程、备份数据库、备份 SWF 原文件,这个习惯帮我在夜里少接了很多电话。希望帮到你。
本文还有配套的精品资源,点击获取