ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flash游戏服务器搭建全指南:Nginx静态托管与XMLSocket联机通信

Flash游戏服务器搭建全指南:Nginx静态托管与XMLSocket联机通信 简介一份面向网络编程学习者的实战资源包围绕Flash游戏客户端与服务器端通信展开涉及TCP连接与select I/O多路复用模型。压缩包内包含一个SWF赛车游戏、一个控制台版服务器程序以及解释双方数据包格式的PPT材料可直接运行exe查看效果也可阅读cpp源码理解select模型如何并发处理多个客户端连接。资源共15个文件以C源码、工程配置文件和调试辅助文件为主既有可执行程序也有pdb、obj等中间产物便于对照学习编译与排错整体仅649KB内容精炼。目前已有235人学习浏览适合正在学习Windows网络编程、或需要参考select模型做课程设计/毕业设计的开发者。通过分析服务器与Flash游戏的数据包交互可以掌握自定义协议头、数据体设计以及非阻塞I/O事件处理的常见写法具有较强的实践参考价值。1. flash游戏还在服务器怎么接住它浏览器早就自动停用了Flash插件但老游戏站点上那一批SWF小游戏并没有消失。很多从业者手里还有这类老资产要么是怀旧游戏站的站长要么是网吧维护、学校机房管理想把一台服务器重新架起来让局域网里的人还能打开这些游戏。这件事可以拆成两半一半是静态托管也就是把SWF文件放到Web服务器上让浏览器能加载另一半是联机通信也就是Flash客户端通过Socket或HTTP去访问一个后端服务做登录、对战、排行榜这类交互。本文就是沿着这两条线把“Flash游戏 服务器”从选型、配置到排错讲清楚目标是让新手照着做能跑通让熟手看完能避开那些老生常谈的坑。2. 静态托管SWFNginx的MIME与局域网访问配置2.1 SWF文件在服务器上是什么角色MIME与浏览器加载路径把Flash游戏部署到服务器本质上和部署一个普通网页没有太大区别服务器提供一个HTTP地址浏览器通过object或embed标签加载SWF。但在实际运维里最容易翻车的恰恰是最不起眼的MIME类型。浏览器在拿到服务器的响应时会看Content-Type头来决定怎么处理这个文件。SWF的标准MIME是application/x-shockwave-flashNginx自带的mime.types文件里通常已经有这一项。但有些精简安装、或者手动编译的Nginxmime.types不全或者服务器管理员把所有未知文件都按application/octet-stream返回这时浏览器会把SWF当成二进制下载而不是交给Flash插件渲染。排查方法很简单用curl看响应头curl -I http://127.0.0.1/game.swf正常情况应该看到Content-Type: application/x-shockwave-flash如果看到的是application/octet-stream或text/plain就需要在Nginx配置里强制指定MIME。我一般会加上这段location ~* \.swf$ { root /data/flash-games; add_header Content-Type application/x-shockwave-flash always; add_header Cache-Control public, max-age86400; }add_header ... always里的always参数表示不管响应状态码是什么都带上这个头能避免某些重定向场景下头丢失。Cache-Control设置一天的缓存是因为SWF体积往往不小用户重复访问时服务器压力能小一些。这里有个细节如果站里SWF文件名带版本号比如game_v2.swf那缓存时长可以拉到七天如果文件名不变但内容经常更新建议缓存时间短一些或者用Cache-Control: no-cache否则用户端拿到的永远是旧文件。2.2 Nginx最小配置让局域网内的人都能打开游戏如果只是本机玩直接双击SWF就行。但“Flash游戏以及服务器”这个需求里真实场景多数是让局域网内其他电脑访问这就涉及监听地址、目录权限、防火墙三件事。先写一个完整的、能跑起来的最小server块server { listen 80; server_name _; root /data/flash-games; index index.html; location / { try_files $uri $uri/ 404; } location ~* \.swf$ { add_header Content-Type application/x-shockwave-flash always; add_header Cache-Control public, max-age86400; } }server_name _是Nginx里“匹配所有Host头”的写法局域网用户拿IP直接访问时不需要改hosts文件。root指向SWF所在的物理目录try_files $uri $uri/ 404确保找不到文件时返回404而不是目录列表。这里要特别注意默认情况下Nginx的index只对/请求生效如果用户访问http://192.168.1.10/game.swf走的是location /SWF文件能正常返回但如果用户访问的是http://192.168.1.10/Nginx会去找index.html而这个文件不存在就返回404所以目录里至少要放一个跳转页或者直接用绝对路径访问SWF。局域网访问还有一个高频坑监听的是127.0.0.1还是0.0.0.0。Nginx默认监听80端口没有写IP时实际上绑定的是所有网卡也就是0.0.0.0局域网访问没问题。但有些发行版自带的Nginx配置文件里写了listen 127.0.0.1:80;这时候局域网其他机器就访问不到需要在配置里改成listen 80;或listen 0.0.0.0:80;。改完配置记得先测试再重载nginx -t systemctl reload nginx2.3 验证与快速换服curl、hosts与wamp的差异配置完成后不要急着开浏览器先用curl在服务器本机做三步验证。第一步验证文件存在第二步验证MIME第三步验证大小curl -sI http://127.0.0.1/game.swf | head -n 1 curl -sI http://127.0.0.1/game.swf | grep -i content-type curl -sI http://127.0.0.1/game.swf | grep -i content-length如果本机能通局域网内其他机器却打不开依次检查三件事一是服务器防火墙是否有放行80端口二是客户端访问的IP是否正确三是Nginx监听的地址是否为0.0.0.0。我处理过的案例里最典型的是阿里云安全组放行了80但系统防火墙没放行两边都要看。放行命令在不同发行版上不一样CentOS用systemctl管理firewalldUbuntu用ufw但排查思路一致都是先确认监听、再确认防火墙规则。有人会用wamp在Windows上搭服务器逻辑和Nginx完全一样但Apache的目录权限配置和Nginx差别较大。Apache 2.4默认对没有Require指令的目录是拒绝访问的必须显式加Directory D:/flash-games Options Indexes AllowOverride All Require ip 192.168.1.0/24 /Directory这段配置的意思是允许192.168.1.0这个网段访问其他来源一律拒绝。这里的Require ip写的是你局域网的实际网段如果路由器的DHCP分配的网段是10.0.0.x那就得写10.0.0.0/24照抄别人的配置最容易在这里翻车。3. 让Flash游戏联机XMLSocket客户端与Python服务器从握手到收发3.1 Flash的网络能力边界为什么联机游戏都用Socket而不是HTTP大部分单机Flash游戏只需要静态托管就够了但一旦涉及排行榜、多人在线、实时对战就得让SWF和服务器通信。Flash提供了三种网络能力URLLoader走HTTP适合登录、提交分数这种低频请求XMLSocket走TCP长连接适合实时交互早期还有AMFAction Message Format走RTMP协议配合Adobe的Media Server或Red5使用。常见做法是联机Flash游戏优先用XMLSocket因为HTTP每次请求都要重新建立连接服务器要处理大量无意义的握手开销。XMLSocket建立后保持长连接服务器主动推送消息到客户端延迟低得多。它的协议也简单数据以\n换行符作为消息分隔符服务端按行读取客户端按行解析。缺点是没有框架帮你做序列化和路由所有消息都要自己定义格式比如login|username|room、move|x|y。在动手写代码之前先明确XMLSocket的一个重要特性它不能跨域连接也不允许连接除843端口以外任意端口的安全策略请求。换句话说客户端连接服务器的任意端口之前Flash播放器会先到目标服务器的843端口去拉取crossdomain.xml如果拉不到或者内容不允许Socket连接会直接被拦截并抛出SecurityError。这个问题在3.4节单独展开因为它是联机Flash项目里最常见、也最容易让人误判的问题。3.2 服务端Python Socket服务器的最小实现先实现一个最小的Socket服务器功能是接收客户端的文本行消息回一个确认。选择Python是因为它的标准库socket就能完成不需要安装第三方依赖。代码如下import socket import threading HOST 0.0.0.0 PORT 9100 def handle(conn, addr): print(f新连接: {addr}) buffer b try: while True: data conn.recv(1024) if not data: break buffer data # 按 \n 拆行Flash XMLSocket 默认行分隔协议 while b\n in buffer: line, buffer buffer.split(b\n, 1) text line.decode(encodingutf-8, errorsignore) print(f收到: {text}) conn.sendall(bpong\n) except Exception as e: print(f连接异常: {e}) finally: conn.close() srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(5) print(fFlash Socket 服务器已启动: {HOST}:{PORT}) while True: conn, addr srv.accept() threading.Thread(targethandle, args(conn, addr), daemonTrue).start()这段代码里有几个参数需要说明。HOST设为0.0.0.0表示监听所有网卡这样局域网内任何一台机器都能连上来如果只想允许本机连接才用127.0.0.1。PORT9100是自定义的范围只要不是80、443这种常用端口就行但要注意别和服务器上其他服务冲突。recv(1024)是每次读取最多1024字节实际能处理的游戏消息已经足够因为Flash的XMLSocket消息体通常只有几百字节。SO_REUSEADDR用来解决一个问题服务器崩溃退出后端口还处于TIME_WAIT状态不设置这个选项几秒内重启会报“Address already in use”。行拆分逻辑里的buffer.split(b\n, 1)很重要。TCP是流式协议客户端发送hello\nworld\n服务器可能第一次recv就收到完整的两行也可能只收到半行剩下半行在第二次recv才到。所以必须把未拆完的数据留在buffer里等下一次数据到达时继续拆这就是循环里while b\n in buffer的意义。如果漏了这一步消息一多就会出现乱拼、黏包的现象这也是很多新手写TCP程序最容易踩的坑。3.3 客户端AS3里连服务器并处理数据帧服务端就绪后写一个简单的AS3客户端。这段代码用纯AS3编写不需要任何第三方库也刻意避开了Flex框架的UI组件package { import flash.display.Sprite; import flash.events.Event; import flash.events.IOErrorEvent; import flash.events.ProgressEvent; import flash.events.SecurityErrorEvent; import flash.net.Socket; import flash.utils.ByteArray; public class GameClient extends Sprite { private var sock:Socket; public function GameClient() { sock new Socket(); sock.timeout 5000; sock.addEventListener(Event.CONNECT, onConnect); sock.addEventListener(IOErrorEvent.IO_ERROR, onIOError); sock.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSecurityError); sock.addEventListener(ProgressEvent.SOCKET_DATA, onData); // 这里改成你服务器的实际IP sock.connect(192.168.1.10, 9100); } private function onConnect(e:Event):void { trace(已连接服务器); // 发一条登录消息以 \n 结尾 sock.writeUTFBytes(login|player1\n); sock.flush(); } private function onData(e:ProgressEvent):void { var buf:ByteArray new ByteArray(); sock.readBytes(buf, 0, sock.bytesAvailable); var line:String buf.readUTFBytes(buf.length); trace(收到: line); } private function onIOError(e:IOErrorEvent):void { trace(IO错误: e.text); } private function onSecurityError(e:SecurityErrorEvent):void { trace(安全沙箱错误: e.text); } } }这里几个参数值得展开。sock.timeout 5000是连接超时时间单位毫秒超过5秒连不上就触发IOError。writeUTFBytes(login|player1\n)后面的flush()必须写Socket在写入时内部有缓冲区不flush就不能保证数据立刻发出去。ProgressEvent.SOCKET_DATA是Flash监听socket数据的唯一正确事件不能用Event.DATA那是旧版写法。收到数据时sock.bytesAvailable表示缓冲区里有多少字节可读readUTFBytes按UTF-8编码读完整个缓冲区。这个写法适合每条消息都比较短的场景。如果消息可能很长或者一个事件里包含多条消息就需要像Python服务端那样维护一个buffer在AS3里用indexOf(\n)去拆行。AS3编译需要Flex SDK或旧版Flash Builder命令行编译的命令是mxmlc -use-networktrue GameClient.as -output GameClient.swf-use-networktrue这个参数非常关键。默认的编译选项里如果SWF要访问网络必须显式开启这个标记否则Flash Player会认为这个SWF只在本地使用所有网络功能都被禁用连connect都会静默失败。3.4 crossdomain.xml与安全沙箱联机失败的第一嫌疑联机失败时十个里有八个是沙箱问题。Flash Player默认不允许一个来自A域的SWF去连接B域的Socket需要B域通过跨域策略文件显式授权。这个文件就是crossdomain.xml要放在被连接服务器的843端口上。很多人搞混两种情况如果Flash用URLLoader请求HTTP数据crossdomain.xml可以放在Web服务器的80端口根目录下但如果用XMLSocket连接TCP端口Flash Player会先请求目标服务器的843端口策略文件必须由服务端直接返回。开发环境最省事的办法是在一个临时脚本里监听843端口并返回策略文件import socket POLICY b?xml version1.0? cross-domain-policy allow-access-from domain* / /cross-domain-policy srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((0.0.0.0, 843)) srv.listen(10) print(policy server start at 843) while True: conn, addr srv.accept() conn.sendall(POLICY) conn.close()生产环境不要用domain*这里只为开发方便上线时要改成具体的域名或IP。这条843策略服务器可以直接和业务服务器跑在同一台机器上互不影响。如果Flash报SecurityError 2046但脚本又确认策略文件返回正常还有个容易被忽略的点策略文件里的allow-access-from标签写法错误。注意是cross-domain-policy是根元素allow-access-from的子节点中间漏掉一行都会让Flash解析失败。4. 服务器选型和协议取舍Flash时代留下的三类方案4.1 静态HTTP轮询最简单但不适合实时如果游戏只是单机玩法加一个排行榜服务端用HTTP就够。AS3里的URLLoader配合URLRequestPOST一个JSON或键值对到服务器服务器接收后写进数据库然后返回结果。这种方案的优点是没有长连接服务器实现简单任何语言都能做Nginx加上一个PHP脚本就能跑。缺点也明显Flash客户端要定时去请求服务器拉数据时间间隔短了服务器压力大时间间隔长了实时性差。Flash时代的页游里很多聊天功能就是这么做的两秒轮询一次服务器挂着几百个连接反复请求CPU和带宽消耗都很可观。这种方案最早期的部署其实不需要单独写Socket服务器直接用已有的Web服务器就行。适合的场景是几个好友之间玩个联机答题或者比赛的最终结果只在结束页展示不需要落地的实时交互。4.2 XMLSocket/文本行协议联机小游戏的主路径第3章里的Python服务器就是一条主路径。文本行协议最大的好处是调试直观服务端收到的就是明文遇到问题能立刻看出是哪条消息出问题。网络游戏服务器开发里有一个通用做法叫“消息路由”即每条消息的第一个字段是消息类型比如login、move、attack服务器根据类型分发到不同的处理函数。这个思路用XMLSocket实现起来很自然login|player1 move|player1|100|200 attack|player1|player2文本协议的缺陷是消息体积比二进制大一条move消息可能四五十字节如果服务器每秒处理几千条消息网络开销会明显上去。对Flash小游戏来说这不算问题但如果做的是一整个MMORPG的主干通信就应该考虑用AMF或直接自定义二进制格式。文本协议的另一个坑是处理中文编码Flash端writeUTFBytes按UTF-8编码Python端decode(utf-8)也要对应两边的编码如果一边UTF-8一边GBK就等着看乱码吧。4.3 AMF/Red5/WebSocket中大型Flash项目的遗产Flash联机游戏曾经有一套更“正统”的服务器方案Adobe的Flash Media ServerFMS和开源的Red5。这套方案用的是RTMP协议客户端连上后可以发布视频流、共享对象、远程调用服务器方法比XMLSocket封装的层级更高。Red5是Java写的部署时动手改的通常是web.xml里配置的端口和JVM内存参数。对现在的读者来说如果不是接手一个老项目不建议从零用Red5因为Java环境、Red5版本、Flash播放器兼容性三个条件凑齐并不容易。WebSocket在Flash里也能用AS3有一个WebSocket类需要Flash Player 11.0以上版本。但浏览器禁用Flash之后WebSocket方案的价值更多体现在把老Flash游戏迁移到HTML5的方向上很少有人还会新写一个Flash客户端去连WebSocket服务器。这里要特别说明如果你是在维护一个还在运营的Flash页游服务端是Java或C写的自定义Socket协议客户端是AS3最稳妥的做法是保持XMLSocket不动只改服务器IP和端口配置不要去改协议。4.4 三种方案对比方案实时性服务端复杂度适用场景缺点HTTP轮询低秒级延迟低普通Web服务器即可排行榜、非实时交互服务器压力大不适合高频通信XMLSocket文本协议高毫秒级中需自行处理拆包粘包联机对战、聊天室消息体积大需要定义协议AMF/Red5/RTMP高高Java环境部署复杂老页游、视频直播类运维重客户端依赖Flash停用后难维护这个表格不是一个静态结论而是选型前的思考框架。接手的项目如果已经是AMF体系迁移成本远大于维护成本那就继续用如果是新项目我的判断是文本协议最容易落地出了问题也最好排查。任何协议都有代价关键是代价是否在你能控制的范围里。5. Flash游戏服务器搭建的常见问题排查2046到跨域策略5.1 一连接就触发SecurityErrorcrossdomain.xml没生效现象AS3代码在本地测试能连上服务器的9100端口但把SWF放到Nginx上之后客户端连接直接报SecurityError: Error #2046。原因Flash Player认为SWF所在的域Nginx的IP和连接的Socket服务器不是同一个源需要跨域策略授权。而策略文件要么没有部署要么部署到了80端口而不是843端口。XMLSocket和URLLoader的跨域策略请求端口是不同的后者走HTTP的80端口根目录前者固定走843端口。解决在目标服务器的843端口上跑一个返回cross-domain-policyallow-access-from domain*//cross-domain-policy的服务。先用浏览器访问http://服务器IP/crossdomain.xml确认文件内容没问题再用命令行工具去测843端口有没有响应。我一般是先在本机测通再让客户端从另一台机器连这样能快速区分是策略问题还是网络问题。5.2 局域网连不上服务器防火墙、IP和沙箱现象服务器本机访问游戏页正常局域网里其他电脑却一直转圈打不开或者打开页面但Socket连不上。原因第一种是Nginx监听地址不对配置里写了127.0.0.1而不是0.0.0.0。第二种是系统防火墙没放行端口CentOS的firewalld会拦截非本机IP的入站连接。第三种是Flash播放器本身的本地沙箱限制这个最隐蔽如果客户端SWF是直接在浏览器地址栏输入file:///D:/game.swf打开的Flash Player会认为这是一个本地文件Socket默认连不到远程服务器除非在Flash Player设置里把该文件加入受信任目录。解决先改Nginx监听地址并重载再检查防火墙放行规则firewall-cmd --permanent --add-port80/tcp firewall-cmd --permanent --add-port9100/tcp firewall-cmd --reload最后把SWF放到Web服务器上通过HTTP访问而不是用File协议直接打开。这一步最大的价值在于开发时要尽早让客户端从Web服务器加载而不是本地双击测完之后再移上去两者的沙箱行为完全不同。5.3 搜索里被反复提起的2046错误先查MIME再查缓存现象网上很多人在搜“flash 2046错误”实际报错文本是Error #2046位置在网络加载或Socket连接阶段。原因2046这个错误代码在Flash Player里涉及网络和跨域访问的多个场景。最常见的是跨域策略文件缺失或解析失败其次是加载SWF时服务器返回的MIME类型不对导致Flash Player拒绝执行。还有一种情况是Nginx开了缓存客户端拿到旧策略文件但服务器策略已经换了。解决按先后顺序排查先确认SWF能通过HTTP正常下载且Content-Type正确再看843端口的策略文件是否能被访问最后清掉浏览器缓存强制刷新重新加载一次。如果这都已经做了仍然报2046用Charles或Fiddler抓包看Flash Player实际请求了哪些地址、收到了什么响应十有八九是策略文件的域名和Flash内的连接地址不匹配。比如SWF里写的是localhost但用户访问用的是局域网IP策略文件里却没有放行这个IP的规则。5.4 浏览器直接拦截Flash插件三条退路现象Google Chrome、Microsoft Edge在2020年底之后默认禁用Flash如果用户双击一个包含SWF的老网页会看到“Adobe Flash Player已不再受支持”的提示。原因Adobe宣布生命周期终止浏览器厂商也把NPAPI插件支持移除这是生态层面的问题不是服务器配置问题。解决三条常见路径按维护成本从低到高排列。第一用Flash Player Projector也就是独立播放器双击SWF文件就能本地播放服务器端只需要把SWF下载下来。第二用第三方兼容方案最常见的是Ruffle它用Rust写的Flash模拟器通过WebAssembly在浏览器里运行对ActionScript 1和2的老游戏支持得比较好ActionScript 3的Socket和网络功能目前还受限做联机调试时要谨慎。第三做迁移把游戏重新用HTML5实现。前两条只能解决“能玩”第三条才能解决“能长期维护”。很多老游戏站现在的策略是保留服务器端逻辑不动前端用Ruffle播放SWF核心逻辑从AS3迁到TypeScript。6. 浏览器停用后用Projector把SWF客户端重新跑起来6.1 把SWF加进受信任目录再运行Flash Player Projector是Adobe官方出过的独立播放器双击SWF就能运行不需要浏览器。但直接从网上下载的SWF用Projector打开访问本地服务器Socket时同样会触发沙箱限制。解决办法是在Flash Player设置管理器里把这个SWF所在目录加入“受信任的位置”。具体操作路径是右键点击播放器画面打开“全局设置”切到“高级”标签页找到“受信任位置”添加SWF所在文件夹。所有受信任目录里的SWF会被视为可信内容允许网络访问。6.2 用flashvars把服务器地址参数化服务器运维里最常见的需求是联调时切换测试服和正式服。如果SWF里写死了服务器IP每次都要重新编译。常见做法是让SWF在启动时读取flashvars参数var params:Object LoaderInfo(this.root.loaderInfo).parameters; var serverHost:String params.host || 127.0.0.1; var serverPort:int int(params.port || 9100); sock.connect(serverHost, serverPort);Projector播放器本身不直接传flashvars这时候可以做一个简单的HTML页用object标签播放SWF并传参数也可以直接用swf方式配合参数但浏览器已经不能播放所以更省事的做法是改SWF的启动逻辑如果params.host为空默认走项目配置里的地址。我手里维护过的老Flash项目都用这种办法做到了一套SWF部署多种环境省去了大量重复编译。关于把旧SWF从浏览器缓存里捞出来这一步很多怀旧站用过浏览器虽然不显示Flash但缓存目录里仍有历史下载的SWF文件。找的时候用文件头部特征SWF未压缩时以FWS开头压缩时以CWS开头。可以在缓存目录里搜索这两个标记grep -rl CWS\|FWS ~/.cache/google/chrome/Default/Cache 2/dev/null | head找到文件后改成.swf后缀再用Projector打开。这个方法对接手老站的运维人员很有用省得去旧服务器重新导数据。做Flash游戏服务器这件事最大的教训就是别高估兼容性。浏览器会更新、插件会停用、老协议会被淘汰能留住的只有那批SWF资源和服务器上的数据。我自己的习惯是做这类老项目尽量把逻辑和资源都放到自己能控制的环境里要么用Projector跑本地播放要么把核心逻辑迁出去。想起来要升级的时候手里有退路。希望帮到你。本文还有配套的精品资源点击获取
返回列表