简介:这份资源是在AS3.0环境下实现的WebSocket客户端库,面向使用Flash Professional、Flex Builder等工具开发交互式网页、游戏或移动应用的ActionScript开发者,帮助其在Flash环境中建立客户端与服务器之间的长连接,实现低延迟的双向实时通信,适用于在线游戏、实时聊天、股票行情推送等场景。压缩包共133个文件,以115个as源码类文件为主体,另含mxml示例、project与prefs工程配置、xml描述文件、swc编译产物及md说明文档等,整体约392KB,结构紧凑便于查阅。库已对底层TCP套接字进行封装,开发者无需处理复杂网络细节,导入后创建WebSocket对象、指定服务器地址与端口,即可通过send方法发送数据并监听onMessage、onOpen、onClose、onError等事件完成收发与状态管理。目前已有222人学习下载,结合源码、示例与文档可快速集成WebSocket功能,提升实时网络应用的开发效率与用户体验。
1. as3.0 websocket:Flash 退场之后,这套老技术为什么还在被翻出来
如果你现在还在搜 as3.0 websocket,大概率不是要做一个全新的 Flash 网站,而是手里有一份跑了很多年的 ActionScript 3.0 工程,需要让它跟后端保持长连接。可能是某个工业看板、某套内网教学系统、某个还在服役的互动展示程序,前端是 AS3 写的,后端已经换成了 WebSocket 服务,中间这层协议怎么接上,就是全部问题所在。
AS3 原生只给了 Socket 和 XMLSocket,没有 WebSocket 类。想在 AS3 里用 WebSocket,绕不开两条路:一是自己按 RFC 6455 在 Socket 上实现握手和帧解析,二是用现成的 as3-websocket 类库。这篇文章就把这两条路都拆开讲清楚,包括握手报文怎么拼、掩码怎么算、帧怎么拆、心跳怎么保、断线怎么重连,以及那些只有真跑过才会遇到的坑。适合手上真有 AS3 工程、需要落地长连接的开发者,也适合想理解 WebSocket 协议底层的人拿它当练手样本。
2. AS3 里接 WebSocket 的两条路:自研帧解析还是用现成库
2.1 为什么 AS3 没有原生 WebSocket,只能自己补
Flash Player 和 AIR 的网络 API 里,能建立 TCP 连接的是flash.net.Socket,能发文本的是XMLSocket。WebSocket 是建立在 TCP 之上的应用层协议,握手阶段用 HTTP Upgrade,之后是二进制帧。AS3 的 Socket 给你的是原始字节流,readUTFBytes、writeUTFBytes、readByte这些方法足够你手动完成整个协议。
浏览器里的 JS 有new WebSocket(url),Flash 没有对应封装,这是历史原因:WebSocket 标准定稿时 Flash 已经进入生命周期后段,Adobe 没有动力再往运行时里加新 API。所以 AS3 侧的一切都要自己写,或者依赖社区库。
这里有个关键判断:如果你的后端是标准 WebSocket 服务(比如基于某开源框架实现的 ws 服务),那 AS3 必须严格按 RFC 6455 来,握手头、帧格式一个字节都不能错,否则服务端会直接断开。如果后端是你自己能改的,理论上可以自定义一套更简单的协议,但那样就失去了用 WebSocket 的意义,不推荐。
2.2 自研实现的最小骨架:Socket 连接与握手
先看握手。WebSocket 握手本质是一个 HTTP GET 请求,带几个特定头:
// AS3 手动发起 WebSocket 握手 // 假设已建立 socket 连接,connected 事件已触发 private function sendHandshake():void { // 1. 生成 16 字节随机数,Base64 编码作为 Sec-WebSocket-Key var nonce:ByteArray = new ByteArray(); for (var i:int = 0; i < 16; i++) { nonce.writeByte(Math.floor(Math.random() * 256)); } var key:String = Base64.encodeByteArray(nonce); // 2. 拼接握手请求行与头部 var req:String = "GET /chat HTTP/1.1\r\n" + "Host: " + host + ":" + port + "\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Key: " + key + "\r\n" + "Sec-WebSocket-Version: 13\r\n" + "\r\n"; // 3. 以 UTF 字节写入 socket socket.writeUTFBytes(req); socket.flush(); }这段代码的逻辑是:先生成客户端随机 key,再按 HTTP 报文格式拼出请求,最后通过 Socket 发出。参数上要注意三点。第一,Sec-WebSocket-Key必须是 16 字节随机数的 Base64,长度固定 24 字符,服务端会拿它和固定 GUID 拼接后做 SHA-1 再 Base64,返回在Sec-WebSocket-Accept里,客户端应当校验这个值。第二,Host头必须和实际连接的主机端口一致,很多服务端会校验。第三,行尾必须是\r\n,最后要有一个空行,少一个\r服务端就认为报文没结束。
握手响应回来后,你需要从 Socket 的字节流里读出 HTTP 响应头,找到101 Switching Protocols和Sec-WebSocket-Accept,校验通过后,后续字节就都是 WebSocket 帧了。这里有个容易翻车的点:握手响应和第一帧数据可能在同一批字节里到达,如果你读完响应头就把缓冲区清了,第一帧就丢了。正确做法是读完头部后,把剩余字节留在缓冲区里交给帧解析器。
2.3 帧解析:掩码、操作码和分片怎么处理
WebSocket 帧格式是自研实现里最需要耐心的部分。一个帧的头部至少 2 字节,结构如下:
| 字段 | 位数 | 含义 |
|---|---|---|
| FIN | 1 | 是否为消息最后一帧 |
| RSV1-3 | 3 | 扩展位,通常为 0 |
| Opcode | 4 | 帧类型,1 文本、2 二进制、8 关闭、9 ping、10 pong |
| Mask | 1 | 客户端发往服务端必须为 1 |
| Payload len | 7 | 载荷长度,126 表示后跟 2 字节长度,127 表示后跟 8 字节 |
| Masking-key | 32 | 掩码密钥,Mask 为 1 时存在 |
| Payload data | 变长 | 实际数据,需与掩码异或还原 |
客户端发数据必须加掩码,服务端发数据不能加掩码。掩码算法是:对载荷第 i 个字节,与 masking-key 的第 i%4 个字节做异或。解析时反过来做一次就还原了。
// 解析一个 WebSocket 帧(简化版,假设数据已完整到达) private function parseFrame(buf:ByteArray):void { buf.position = 0; var b0:int = buf.readUnsignedByte(); var b1:int = buf.readUnsignedByte(); var fin:Boolean = (b0 & 0x80) != 0; var opcode:int = b0 & 0x0F; var masked:Boolean = (b1 & 0x80) != 0; var len:int = b1 & 0x7F; // 长度扩展:126 读 2 字节,127 读 8 字节 if (len == 126) { len = buf.readUnsignedShort(); } else if (len == 127) { // AS3 没有无符号 64 位,实际项目里载荷很少超过 65535 var high:uint = buf.readUnsignedInt(); var low:uint = buf.readUnsignedInt(); len = low; // 简化处理,真实场景需判断 high 是否为 0 } // 读取掩码密钥 var maskKey:ByteArray = new ByteArray(); if (masked) { buf.readBytes(maskKey, 0, 4); } // 读取并还原载荷 var payload:ByteArray = new ByteArray(); buf.readBytes(payload, 0, len); if (masked) { for (var i:int = 0; i < len; i++) { payload[i] = payload[i] ^ maskKey[i % 4]; } } // 按 opcode 分发 switch (opcode) { case 0x1: // 文本帧 handleText(payload); break; case 0x2: // 二进制帧 handleBinary(payload); break; case 0x8: // 关闭帧 socket.close(); break; case 0x9: // ping,需回 pong sendPong(payload); break; } }这段解析里,fin和opcode决定消息边界。如果收到opcode=0的续帧,说明上一条消息还没结束,要把载荷拼接到上一条消息的缓冲区里,直到fin=1才算完整。很多自研实现只处理单帧消息,遇到分片就乱码,这是最常见的翻车点之一。
参数上,len的三种编码方式必须都支持,虽然实际业务里超过 65535 字节的消息不多,但一旦后端发了大消息而你没处理 127 分支,解析就会错位,后面所有帧全乱。掩码密钥是 4 字节,异或时用i % 4取对应字节,这个细节写错会导致中文乱码。
2.4 用现成库的取舍:省事但要看维护状态
如果不想自己啃协议,社区里有 as3-websocket 这类库,封装了握手、帧解析、事件回调。用库的好处是省掉上面所有字节级操作,坏处是这类库大多停止维护多年,遇到新版本服务端的某些行为(比如强制 permessage-deflate 压缩扩展)可能不兼容。
选型时我一般看三点:库是否处理了分片、是否支持 ping/pong 心跳、是否暴露了原始字节接口。前两点决定稳定性,第三点决定你遇到协议层问题时能不能自己排查。如果库把 Socket 完全封装死,出问题时你连抓到的字节都看不到,那就成了黑匣子,排查成本反而更高。
3. 把连接跑起来:心跳、重连与二进制消息的落地细节
3.1 心跳保活:为什么 30 秒是个常见阈值
WebSocket 连接建立后,中间可能经过多层网络设备,空闲连接会被静默断开。AS3 侧需要主动发 ping 帧保活。常见做法是每 30 秒发一次 ping,服务端回 pong,连续两次没收到 pong 就判定连接已死,触发重连。
// 心跳定时器:每 30 秒发一次 ping private var heartbeatTimer:Timer = new Timer(30000); private var missedPong:int = 0; private function startHeartbeat():void { heartbeatTimer.addEventListener(TimerEvent.TIMER, onHeartbeat); heartbeatTimer.start(); } private function onHeartbeat(e:TimerEvent):void { if (missedPong >= 2) { // 连续两次没收到 pong,判定断线 heartbeatTimer.stop(); reconnect(); return; } sendPing(); missedPong++; } // 收到 pong 帧时重置计数 private function onPong():void { missedPong = 0; }30 秒这个值不是随便定的。太短会增加无谓流量,太长则断线发现不及时。多数反向代理和负载均衡的空闲超时在 60 秒左右,心跳间隔取它的一半比较稳妥。如果你的服务端自己也会发 ping,那客户端只要负责回 pong 就行,不必再主动发。
注意 ping 帧的载荷可以是空的,也可以带应用数据,服务端必须原样回在 pong 里。有些实现把 ping 当业务消息处理,这是误用。
3.2 断线重连:指数退避比固定间隔靠谱
断线后立刻重连,如果服务端还没恢复,会形成大量无效连接。指数退避是更稳的做法:第一次 1 秒后重连,失败则 2 秒、4 秒、8 秒,上限 30 秒。
private var retryDelay:int = 1000; private const MAX_DELAY:int = 30000; private function reconnect():void { var timer:Timer = new Timer(retryDelay, 1); timer.addEventListener(TimerEvent.TIMER, function(e:TimerEvent):void { openSocket(); }); timer.start(); // 下次延迟翻倍,封顶 30 秒 retryDelay = Math.min(retryDelay * 2, MAX_DELAY); } // 连接成功后重置延迟 private function onConnected(e:Event):void { retryDelay = 1000; sendHandshake(); }这里的关键是连接成功后必须把retryDelay重置回 1 秒,否则下次断线会直接从 30 秒开始等。另外重连时要先清理旧的 Socket 对象和定时器,不然会出现多个连接同时存在的诡异状态,消息重复处理。
3.3 二进制消息:ByteArray 的读写位置陷阱
AS3 的ByteArray有个position指针,读写都会移动它。解析帧时如果忘了重置position,读出来的数据就是错的。上面parseFrame开头那句buf.position = 0就是防这个。
发二进制消息时,要按帧格式组装:
// 发送二进制帧(客户端需加掩码) private function sendBinary(data:ByteArray):void { var frame:ByteArray = new ByteArray(); frame.writeByte(0x82); // FIN=1, opcode=2 var len:int = data.length; if (len < 126) { frame.writeByte(0x80 | len); // Mask=1 } else if (len < 65536) { frame.writeByte(0x80 | 126); frame.writeShort(len); } // 4 字节掩码密钥 var mask:ByteArray = new ByteArray(); for (var i:int = 0; i < 4; i++) { mask.writeByte(Math.floor(Math.random() * 256)); } frame.writeBytes(mask); // 载荷异或掩码 data.position = 0; for (var j:int = 0; j < len; j++) { frame.writeByte(data[j] ^ mask[j % 4]); } socket.writeBytes(frame); socket.flush(); }参数上,0x82是 FIN 加二进制 opcode,文本消息则是0x81。掩码位必须置 1,否则服务端会按协议错误关闭连接。data.position = 0这行不能省,如果这个 ByteArray 之前被读过,position 停在末尾,循环就从越界位置开始,发出去的全是空。
3.4 用 Wireshark 验证帧格式对不对
自研实现出问题时,最有效的排查手段是抓包。在本地跑一个 WebSocket 服务,用 Wireshark 过滤tcp.port == 你的端口,看握手请求和响应,再看后续帧的 FIN、opcode、mask 位是否符合预期。
如果服务端返回400 Bad Request,多半是握手头拼错,重点检查Sec-WebSocket-Key长度和行尾。如果握手成功但消息乱码,重点看掩码异或和长度字段。如果连接几秒后断开,看是不是没回 pong。抓包能看到字节级真相,比在代码里加日志猜要快得多。
4. as3.0 websocket 避坑:五条血泪经验
4.1 握手响应和首帧粘包,首条消息丢失
现象:连接建立成功,但服务端发的第一条欢迎消息客户端收不到,从第二条开始正常。
原因:握手响应和第一帧数据在同一批 TCP 字节里到达,代码读完 HTTP 头后把整个缓冲区清空,首帧被丢弃。
解决:读完响应头后,计算已消耗的字节数,把剩余字节保留在缓冲区,直接交给帧解析器处理,不要清空。
4.2 中文消息乱码,英文正常
现象:服务端发的中文显示为乱码,英文和数字正常。
原因:掩码异或时用了错误的索引,或者文本帧没有按 UTF-8 解码,直接当单字节字符处理。
解决:确认异或索引用i % 4,文本载荷用payload.readUTFBytes(payload.length)读取,不要用readByte逐个转字符。
4.3 分片消息只收到第一片
现象:大消息只收到开头一部分,后续内容丢失或解析报错。
原因:解析器只处理fin=1的帧,遇到opcode=0的续帧没有拼接逻辑。
解决:维护一个消息缓冲区,收到opcode=1或2时开始累积,收到opcode=0时追加,直到fin=1才交付业务层。
4.4 重连后消息重复处理
现象:断线重连后,同一条消息被处理了两次。
原因:旧 Socket 的事件监听没有移除,新旧两个 Socket 同时收到消息。
解决:重连前调用socket.removeEventListener清理所有监听,把 socket 置空,再创建新对象。
4.5 心跳发了但服务端不回 pong
现象:客户端按时发 ping,但一直收不到 pong,误判断线频繁重连。
原因:服务端实现只处理文本帧,把 ping 帧当未知类型忽略,或者服务端要求客户端回 pong 而不是主动发 ping。
解决:先确认服务端的心跳策略,如果服务端主动发 ping,客户端只需在收到 ping 时回 pong,不要自己再发。抓包确认 ping 帧确实到达服务端。
5. 进阶:用 AS3 做 WebSocket 代理层,把老工程接进新后端
手上有一份庞大的 AS3 业务代码,直接改网络层风险太大,更稳的做法是在 AS3 里做一个薄代理层:上层业务照旧调用原来的接口,代理层负责把调用翻译成 WebSocket 帧,收到帧再翻译回业务事件。这样协议细节被隔离在一层里,业务代码几乎不用动。
代理层的核心是一个请求-响应映射表。每条发出去的消息带一个自增的seq,代理层把seq和回调函数存进字典,收到响应时按seq找回回调。
private var pending:Object = {}; // seq -> callback private var seq:int = 0; public function request(cmd:String, params:Object, callback:Function):void { seq++; pending[seq] = callback; var msg:Object = { seq: seq, cmd: cmd, params: params }; sendText(JSON.stringify(msg)); } private function onMessage(text:String):void { var msg:Object = JSON.parse(text); var cb:Function = pending[msg.seq]; if (cb != null) { delete pending[msg.seq]; cb(msg.data); } }这个模式的好处是业务层完全不用关心 WebSocket 的帧、掩码、心跳,只面对request和回调。参数上,seq用自增整数即可,注意在重连后要清空pending字典,否则旧请求的回调会永远挂着,造成内存泄漏。
验证代理层是否可靠,我一般做两件事:一是模拟服务端延迟响应和乱序响应,看回调是否按seq正确匹配;二是模拟断线重连,确认重连后新请求正常、旧请求被清理。这两步过了,代理层基本就能扛住日常使用。
最后说个我自己的习惯:每次改完帧解析代码,先不接业务,而是写一个回环测试,客户端发什么服务端原样回什么,用不同长度的消息(1 字节、125 字节、126 字节、70000 字节)各跑一遍。长度边界是帧解析最容易错的地方,回环测试能在接业务之前就把问题暴露出来。这套方法帮我省了很多事后排查的时间,希望帮到你。
本文还有配套的精品资源,点击获取