2011.08.15和2011.08.16
12号给的那个,逻辑和方法上是没问题,可是那个string的split不知为啥偶尔会漏掉分隔符,明明里面有分隔符,确没有找到。后来换了种字符窜处理方法也是这样。
上周五python程序来了,在他的点拨下,最终还是用二连包,第一个包长的包必须用writeInt(占4字节)或writeUnsignedInt(无符号)或writeShort(占2字节)发,第二个包体的包最好用writeBytes。这样读取时,先用readInt或readUnsignedInt或readShort就行了,后面就用readBytes或者readByte就行了,不然还要约定好第二个包体用什么方法读取,第一个包长也要和服务器约定好是用int,uint还是short。服务器也应该这样发包,二连包,先发int或uint或short包长的包,再发byte型包体的包。
上代码,这次经过了严格的测试,没有任何问题了。
原来flush()方法,就算socket没有连接,flush的数据也会缓存起来,等连接后,已经会按没有连接前flush数据的顺序来发送数据。
http://apsay.gearhostpreview.com/?p=1171
2011.08.11和2011.08.12
天呀,该死的bytesAvailable,这个死玩意,对于ByteArray来说,原来是和position关联的,此消彼长。
writeBytes也不知怎么回事,第一次把一个ByteArray写到我的另外一个ByteArray没有问题,可以通过offset length随便截取。可以第二次还是这个BA,全写可以,改变offset只有遇到空格就挂了,老是说我RangeError: Error #2006: The supplied index is out of bounds.我对比第一次和第二次,写前写后,源和目标的ByteArray的所有属性全部一样。无解。
有了点空,咬牙花了一天时间,总算较完美解决了socket的数据传输问题。经过用Timer,1毫秒发送一次请求并连续发送1000次,服务器也同样频率发送数据测试来看,大数据小数据都没有任何问题。现在我能想到的出意外可能就是,自定义分隔符正好被分到两次data事件里了,这样就没办法了。不过好像如果服务器是用的self.transport.write(value)后跟self.transport.write(‘^end’),这种方式发的数据,而且后面的write数据很少,flash好像不会把这个自定义分隔符分开发。
这个项目的Python程序几天没来了,其他人也不好问,不知道如果专门发个int型的包,flash是不是绝对不会把它分成两次data事件也就是分到两个包发过来。如果也会分开那就太悲催了。我看被人读int包都直接用socket.redInt.如果一次data事件发过来的数据有多个int包呢。反正我试了没有int包的情况,返回一个巨长的负数。
我想最完美的方式,就用一个字节的分割符。找个冷僻的。这样绝对不会被分到两次事件里。然后服务器可以对数据预处理。如果有这个字节就替换成其他。客户端接完后再替换回来。这个替换字符可以约定好。
这样就完美了。
反正现在来看我客户端这边能做到这样,已经很好了。
08.12
Python通过网络与其他语言类型进行转换的问题。就用标准模块struct。
http://blog.csdn.net/JGood/article/details/4290158
http://docs.python.org/library/struct.html
self.transport.write(struct.pack(‘>I’, 10))
一定要加>,不然是用little-endian,而不是flash的默认的big-endian。会造成readUnsignedInt读的数出错。
我觉得按flash socket的机制完全有可能,这个uint数被分到两次事件的数据包中。而且这个uint包必须在一次数据事件开头,才能用this.readUnsignedInt();直接读到,不然还要用position来定位读取。
觉得还是用一个字节数的自定义分隔符最保险。
今天花了上午,可以说彻底解决了flash socket的数据传输问题。
比较郁闷的是Python程序写的那个server.py,看了半天,也没找到怎么在每个包后加了个\r\n。有些讨厌。最终的数据如果合并后可能会出现些不期望的结果,不过也能处理。
private var _byte:ByteArray = new ByteArray(); private static const SOCKET_END:String = "^"; /** *让服务器先发个自定义分隔符过了,就可以实现动态自定义分隔符。适应数据中确实有^end数据的情况。 */ private var count:uint = 0; private function readResponse(event:ProgressEvent):void { count++; //trace(count); //var len:int = this.readUnsignedInt(); //trace(len); var bytedata:ByteArray = new ByteArray(); this.readBytes(bytedata); var byteLen:uint = bytedata.length; bytedata.position = 0; //bytedata.uncompress(); trace(byteLen); var proofread:String = ""; for (var i:uint = byteLen; i > 0; i--) { var single:String = bytedata.readUTFBytes(1); proofread = proofread.concat(single); } var resultsArray:Array = proofread.split(ClientSocket.SOCKET_END); //split会在自定义分隔符后多一个空字符窜的数组元素,无解。注意去掉。 var rlen:uint = resultsArray.length - 1; //trace(rlen); if (rlen) { if (rlen==1) { if (! resultsArray[j] && resultsArray[j] == "\r\n" && resultsArray[j] == "") { if (_byte.length != 0) { parseDataReceived(_byte); } return; } } /*上面是处理自定义分隔符是在本次事件数据包的第一个字节的各种情况*/ var endPosition:uint = 0; var newPosition:uint = 0; for (var j:uint = 0; j < rlen; j++) { if (j!=0) { newPosition += (endPosition + ClientSocket.SOCKET_END.length); } endPosition = resultsArray[j].toString().length; //trace(newPosition); bytedata.position = newPosition; bytedata.readBytes(_byte,_byte.length,endPosition); parseDataReceived(_byte); } } else { bytedata.position = 0; bytedata.readBytes(_byte,_byte.length); } } private function parseDataReceived( byte:ByteArray):void { byte.position = 0; var resultsData:String = byte.readUTFBytes(byte.length); byte.clear(); trace(resultsData+";\n"); try { /*var xml:XML = new XML(resultsData);*/ /*trace(xml.toXMLString());*/ } catch (err:Error) { logger(err.toString()); } }
2011.08.09
如果是用循环var single:String =bytedata.readUTFBytes(1); proofread = proofread.concat(single);获得的字符数就算有\r\n,也是和字节数相等的。这种方法取得字符只有\0会造成不相等.
重新写了下验证自定义分隔符的方式,并且理论上解决了自动合并的情况。不过我还没碰到自动合并的情况。倒是如果在一个期望的完整数据就没接收完毕的情况,疯狂的请求服务器,然后服务台不停发数据,会出现丢失数据的情况,无解。只能避免一个期望的完整大数据接收完之前就不停请求发送数据。服务器消息队列?或先接收包长,再判断自定义分隔符之前所有数据是否和包长相等或大于?,反正小于就return,放弃这个大数据避免报错。
发现用了服务器用了zlib.compress后,效率极高,而且数据也不会丢失。
private static const SOCKET_END:String = "^end"; private var _end:Array = ClientSocket.SOCKET_END.split(''); /** *让服务器先发个自定义分隔符过了,就可以实现动态自定义分隔符。适应数据中确实有^end数据的情况。 */ private function readResponse(event:ProgressEvent):void { //var len:int = this.readInt(); var bytedata:ByteArray = new ByteArray(); this.readBytes(bytedata); //bytedata.uncompress(); var bytelen:uint = bytedata.length; var proofread:String = ""; bytedata.position = 0; for (var i:uint = bytelen; i > 0; i--) { var single:String = bytedata.readUTFBytes(1); proofread = proofread.concat(single); } var endPosition:int = proofread.lastIndexOf(ClientSocket.SOCKET_END); if (endPosition != -1) { var newPosition:uint = bytelen - endPosition - ClientSocket.SOCKET_END.length; _byte.writeBytes(bytedata,0,endPosition); //if(_byte.length < len) _byte.clear(); return; _byte.position = 0; var data:String = _byte.readUTFBytes(_byte.length); _byte.clear(); if (newPosition!=0) { _byte.writeBytes(bytedata,newPosition); } parseDataReceived(data); } else { _byte.writeBytes(bytedata); } }
2011.08.08晚上
奋战一天,终于搞定啦,哈哈。
如果数据太大,哪怕服务器是一次发的数据,flash socket也会自动根据情况分割数个小包发送,并激活相应次数的SOCKET_DATA事件。根据测试来看,分割机制似乎是基于网速,可能还有其他。
我没有测试服务器快速发送小数据时,flash socket又会把它们合并发送的情况。不过根据网上的资料来看,是会这样的,就是那个20000次数据而rogressEvent.SOCKET_DATA事件只产生了2000多次的文章,网上到处都是。
也就是说你完全不要指望用服务器发一次数据,SOCKET_DATA事件就会触发一次的妄想,来处理你的数据。SOCKET_DATA事件触发的次数有可能大于服务器发数据的次数(比如数据大网速慢的情况),也可能小于(比如数据小网速快的情况)。
用readUTFBytes把ByteArray转换为字符窜再合并为期望的完整数据时,有很大机率中文会成为乱码。所以必须用ByteArray合并为期望的完整ByteArray再用readUTFBytes转成字符。
不能直接用ByteArray来查找自定义的分隔符。所以要分两部分并行处理,用一个全局ByteArray用来存储期望的完整数据,并用这个ByteArray转成字符窜输出给需要这个数据的方法。
用个临时的ByteArray来存储本次SOCKET_DATA事件触发而得到数据,并把这个ByteArray转成字符窜再查找自定义分隔符,这个自定义分隔符出现的次数,才是真正服务器发送数据的次数。也就是你期望的完整数据的个数。
经过自己测试,\0会增加bytesAvailable的一个字节数,但不会增加readUTFBytes后的字符数。\r或\n不会增加bytesAvailable字节数,但是会增加一个字符数。\r\n连一起会增加一个字节数和两个字符数。
如果没有中文字符,socket的bytesAvailable字节数和readUTFBytes后的字符数是相等的。每一个中文字,字节数为3,字符数为1.
下面给的代码只解决了socket自动分割,也就是SOCKET_DATA事件触发的次数有可能大于服务器发数据的次数(比如数据大网速慢的情况)。对于socket自动合并,也就是小于(比如数据小网速快的情况),没处理。如果没有中文字符的话,其实方案也出来了,因为没有中文字符并且没有\r\n\0等字符的话,字节数和字符数是相等的,这样只要在字符窜中搜索自定义分隔符的位置,然后用这个字符位置,去分割ByteArray就行。
但是有中文就麻烦了,因为socket是随机分割合并的,有时候真好一个汉字被分到两个包里,这时候toString就是乱码。当然你也可以用while结合readUTFBytes(1)来定位自定义分隔符,这样就能知道自定义分隔符的字节位置。但是我不喜欢while。效率太低而且还容易超出15s挂掉。
最好是能发二连包先给出一个int型包长,可是那个python代码搞了半天,也不知怎么发个int型数据。self.transport.write(len(value))报错,如果self.transport.write(srt(len(value)))又没有意义了。我们项目的python程序又没来。唉。只能这样了。都晚上九点了,明天就要忙其他事了,要完善只能等以后了。
private var _byte:ByteArray = new ByteArray(); private function readResponse(event:ProgressEvent):void { //var len:int = this.readInt(); var bytedata:ByteArray = new ByteArray(); //trace(this.bytesAvailable); this.readBytes(bytedata); trace(bytedata.length); //trace(this.bytesAvailable); trace("test"); var data:String = bytedata.readUTFBytes(bytedata.length); //trace(data.length); //trace(data.substr(-3)); //trace(data+'test\r\n'); //parseDataReceived(bytedata); _byte.writeBytes(bytedata); trace(_byte.length); if(data.lastIndexOf(":end") != -1) { parseDataReceived(_byte); } } private function parseDataReceived( bytedata:ByteArray ):void { //var data:String = bytedata.toString().replace(":end",""); //这个结果也一样OK; bytedata.position = 0; var data:String = bytedata.readUTFBytes(bytedata.length).replace(":end",""); //trace(data); bytedata.clear(); }
2011.08.08
周末想的那个也不行,今天又找了资料看了看。才理解了ProgressEvent.SOCKET_DATA事件。
AS3.0中使用Socket使用tcp服务器协议,它是一种流协议,不停的将分片传输给客户端,P作为流,发包是不会整包到达的,而是源源不断的。它不同于UDP服务器协议,UDP作为数据包协议,整包到达。也就是说服务器一次性发送的总共的数据,可能是几个包,也可能是几个半包。
6号写的,完全是基于udp形式的方案。还有在发包长度时也没必要人为加:号,而是发两次包,二连包,第一个包长的包必须用writeInt(占4字节)或writeUnsignedInt(无符号)或writeShort(占2字节)发,第二个包体的包最好用writeBytes。这样读取时,先用readInt或readUnsignedInt或readShort就行了,后面就用readBytes或者readByte就行了,不然还要约定好第二个包体用什么方法读取,第一个包长也要和服务器约定好是用int,uint还是short。服务器也应该这样发包,二连包,先发int或uint或short包长的包,再发byte型包体的包。
发包如下:
//发送数据 private function sendData(data:String):void{ if(socket == null) return; if(data==null||data.length==0) return; var byte:ByteArray = new ByteArray(); byte.writeUTFBytes(data); //下面注意: socket.writeInt(byte.length); //先写入数据的长度 socket.writeBytes(byte); //再写入内容 socket.flush(); }
说实话,我不了解纯底层的socket,比用了amf的socket效率高多少,但我认为,纯底层的socket最好只用于需要实时并频繁的小量数据传输这种情况。其他其他情况完全没必要,不然问题太多了,实现想不到在网速慢的情况下大量数据在发包却不会整包到达的,而是源源不断的到达,而且最崩溃的是SOCKET_DATA事件的触发完全不可控的情况下的解决方案。当这次请求的数据传输缓慢时,迟迟等不到最后的数据包时,是该再发次请求让服务器再重发数据,还算继续等待?怎么确定这个临界点。
我觉得kinglong的BaseSocket类-可以提升Socket数据传输效率已经可以适用于绝大多数情况了。
因为对服务器端语言完全不精通,而且还有其他工作要做,本来socket这个问题,是另一个AS同事和python程序员在负责,他们暂时用xmlsocket跑起来了,他们也懒的继续弄。今天也是我最后在这上面花时间了。以后如何碰到上心的后端程序员希望能一起把这个问题弄透弄明白。
下面是转自汇程网的文章,有时候可以好好研究。
http://www.4ucode.com/Study/Topic/614962
正文
关于粘包问题,到网上找了一下,发现解决方法其实很多:
AMF3
http://www.klstudio.com/post/202.html
MINA自带的sumup例子
http://mina.apache.org/documentation.html
Apache SSHD
http://mina.apache.org/sshd/
L potato中讨论AS3出现的粘包问题
http://code.google.com/p/lpotato/source/browse/trunk/java/toolkit/WebContent/test/flash/SocketBridge.as?r=13
hudo实现sgs客户端时处理ProgressEvent.SOCKET_DATA事件的方法
http://code.google.com/p/hudo/
darkstar-as3也处理过类似的问题,不过方法略有不同,似乎叫payload(意思叫有效载荷)
http://code.google.com/p/darkstar-as3/
总而言之,处理二进制流数据时不写循环是不严谨的,由于naggle算法的影响
http://zh.wikipedia.org/zh-cn/%E7%B4%8D%E6%A0%BC%E7%AE%97%E6%B3%95
网速会导致数据包的大小是无法预计的,因此读数据算法需要慎重考虑(尤其是非文本协议)。
MINA和AS3存在类似的问题,你可以延迟读出和连续读出,维护一个状态值,但基于性能问题,
最好不要在这方面使用耗时的操作——MINA的receive会阻塞会话,而flash也会阻塞enterFrame。
方法其实还有很多,例如使用分界字符。NIO使socket和线程不再是一对一而是一对多,而flash的异步网络读写更是让人耳目一新,觉得自己要熟练掌握这些技术还需要付出更大的努力。
20100720 update:
* 使用base64,直接用文本模式通信(服务器和客户端需要做base64的编解码)
可以参考f3server的代码
http://code.google.com/p/f3server/
按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)
代价:数据量大了,只能防君子。
好处:用文本传输任意二进制数据,不再限于文本。
f3server没有添加ssl的支持(估计很想这么做),如果程序开发者觉得安全不重要,可以考虑用这种base64的方法避开mina的粘包问题。(题外话——我对base64和字符串操作没信心,对于正则表达式完全是白痴,还是用纯二进制传输好了,虽然写mina的自定义协议很让人抓狂)
20100819 update:
如果你对客户端数据解析有复杂需求,并且需要实时处理,可以参考《Actionscript 3.0宝典》介绍的一个开源项目的代码。
FVNC
http://code.google.com/p/fvnc/
这是一个远程桌面的flash客户端。
它实现数据接收队列和循环协议解析。
由于实现得过于繁琐而且考虑大数据传输,
个人认为这不是一种实用的解决方法。
但如果对数据结构有高级别的认识(我级别还不够高= =b)可以考虑用svn下载这份代码。
另外,它对网络协议层的封装方法也比较规范和标准。
20100830 update:
as3chat
http://code.google.com/p/as3chat/
客户端as中接收与发送数据,貌似是socket+xml,但不太明白为什么不直接使用xmlsocket?
20101013 update:
还发现一个写得很规范的AS3库,用于解析sgs的pack格式socket数据包:
yadaa (Yet Another Darkstar ActionScript API)
http://code.google.com/p/yadaa/
收数据的关键代码在com.plamentotev.yadaa.client.PDSClient
的processServerMessage()
和onData监听器中,
特点是使用while轮询消除粘包。
使用Logger记录日志,
使用if(this.dataBuffer.bytesAvailable < msgSize)和倒退position延迟处理实现包的合并。
processServerMessage()只能获得包的复制内存。
每次轮询读包后总是要分配新的ByteArray,即使还有剩余数据,也要转移到新的ByteArray。
20110130 update
Adobe Flex SDK的fdb调试器svn在
http://opensource.adobe.com/svn/opensource/flex/sdk/trunk/modules/debugger/src/java/flash/tools/debugger/concrete/DProtocol.java
使用早期的同步机制(好像没有用并发库和ByteBuffer),使用循环消除分裂的接收包。
使用包长度和命令整数机制。
使用小对象缓存(用不同长度byte[]的DMessage缓存不同长度的收包)。
———————————————-
2011.08.06
我觉得我想到解决方法了。周一到公司测试下。
在给服务器发请求时应该是字节长度或resume加冒号加字节加冒号加结束符。服务器分割数据调用相应方法返回对应数据。比如1000: bar: end和resume: 1000: end。服务器返回数据字节长度加冒号加数据内容加结束符。或要求续传。如1000:bar: end resume: 1000: end
客户端用循环一个一个字节读取并保存到变量。用条件判断是否读到冒号。如读到则退出循环。
先判断变量是否为resume.是则读取并保存为字符串并判断最后是否为: end,是则发送按差值截取的上次数据发送,这个差值记得去掉: end可能还有\ 0的字节数。否则发送resume: 0: end,意思是全部重发。因resume数据很少一般不会丢包断包,如真碰到这极端情况,请求全部重发就行不会浪费流量。
如不是resume,则判断剩下字节数是否和已保存变量相等或大于,比较前先转换为数字型。如果是则读取剩下数据并保存为字符串并去掉最后: 后的字符,如相等而不是大于应该就丢了: 及后面数据,正好省了处理。输出。
如果小于则计算差值,发送差值给服务器比如resume:100: end。同时保存已接收数据但不输出。
服务器接到iresume: 即明白是差值,然后把上次发送的数据按差值截取发送。客户端接受并继续走上次同样的逻辑流程。如果接收完整则合并上次已接收数据输出。
注意结束字符\0,可能会造成些问题。
———————————————-
2011.08.05下面是以前的- / / /
同事的负责这部分,可是出了很多问题,昨天自己把pyamf上的socket例子的flex mxml改成了as3的,然后把twistd pyamf socket跑起来了,解决了socket的安全策略问题。
今天又用这个文章的例子解决了,同事写的as socket丢包的问题。很遗憾,还是没有解决。实在没办法了,实在不行还是用pyamf得了,纯socket问题太多了,特别是网速超慢而数据量大的情况下。项目负责服务器端的python程序员虽然很强可是太忙,而且对as3完全不了解,也不太打算了解。所以只能这样了。要解决这个问题还真得是精通服务器和客户端程序的人,或者两边的人都真心想解决这问题,一起下功夫。
转载自
http://flashapi.com/main/read.php?tid=61
對於as3的socket的使用,頗有一番鬱悶。有幾個哥們在群裡問,as3 怎麼經常接受不到數據!,是不是as3 的效率比 java c++低,所以老是丟包?我鬱悶了,這問題我反過來 讓他 使用as3 向後台 使用socket來個for循環發上個千條數據,看看 後台有沒收到?結果,嘿嘿。這哥們的公司沒做 分包處理。
socket粘包,是多種原因造成的,一般經過粘包處理後,在用戶不斷線的情況下,基本上不會出現丟包問題。對於字符串通信的socket,一般 使用 類似XmlSocket的分包處理功能。比如在遊戲中,”event=move&x=100&y=100\0″;這樣,服務端讀取時,每次讀完socket緩衝區後,對字符串進行分割處理.然後分發數據。客戶端(as3)在接受到 這種數據包時,也使用類似的方法讀取,然後 派發給顯示層。
package com.hengry.servers{ import flash.events.Event; public class DataEvent extends Event { /** * 當socket讀取到一個完整的數據包時,派發 SOCKET_DATA事件 * */ public static const SOCKET_DATA:String="socket_data"; //數據包 public var data:String=null; public function DataEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false) { super(type, bubbles, cancelable); } } } package com.hengry.servers { import flash.events.Event; import flash.events.ProgressEvent; import flash.net.Socket; /* * utf-8字符串通信的 socket封裝,對數據分包進行處理 * 請偵聽 DataEvent.SOCKET_DATA接受並讀取數據包。 */ public class StringSocket extends Socket { //緩存 數據 private var _buffer:String=""; //數據分割符 private var _key:String="\0"; private var _host:String; private var _port:int; public function StringSocket(key:String="\0") { super(); _key=key; this.addEventListener(ProgressEvent.SOCKET_DATA,dataHandler); } override public function connect(host:String, port:int):void { this._host=host; this._port=port; super.connect(host,port); } private function dataHandler(event:Event):void { event.stopImmediatePropagation();//阻止此事件 //盡力將緩存區給讀乾淨,如果這裡超過15s,那 說明 寫的數據 不是常用遊戲常用通信了。 while(this.bytesAvailable!=0) { this._buffer=this._buffer.concat(this.readUTFBytes(1)); } if(this._buffer.search(this.key)!=-1){ parseAndDispatchData(this._buffer); this._buffer=this._buffer.slice(this._buffer.lastIndexOf(key)+1); //这里暂时也不确定,为啥不是this._buffer=””;我pan的疑问 } } /** * 解析 當前緩存的 字符串。數據 * 拼裝 數據包,派發事件 * @param data:String 當前緩存的數據 * */ private function parseAndDispatchData(data:String):void { var arr:Array=data.split(this.key); var len:uint=data.length-1; var itemdata:String; var sevent:DataEvent; for(var i:int=0;i<len;i++) { itemdata=arr; //这里暂时不确定,应该是arr[i]吧.我pan的疑问 // 派發數據接受事件,則派發 sevent=new DataEvent(DataEvent.SOCKET_DATA); sevent.data=itemdata; dispatchEvent(sevent); } } public function get host():String { return this._host; } public function get port():int { return this._port; } public function get key():String { return _key; } } }
本文遵循署名-非商业性使用共享协议,转载请注明。