Mina使用AMF3与Flash Socket通信的"粘包"问题

项目结构图如下:

方式: 服务端通过把要发送的数据(这里发送的是对象)编码为amf3格式, 再通过java.util.zip.Deflater 进行压缩,再把压缩后的数据发往客户端.
         客户端通过ByteArray.uncompress() 方法解压, 再通过
ByteArray. readObject()方法读取数据(服务端发送的是对象).
mina的文章网上很多, 此项目不再解释.
复制代码即可运行.
说明: 由于使用的nio流在进行数据的传输,所以当服务端同时向客户端发送多次数据时(假如同时发送10次),它并不是发送10次,而是将数据进行整合发送,也就是一般发送少于10次. 如下图:
 
(服务端发送了若干数据,但最终被整合成2个数据段发往客户端. 会触发flash的两次 ProgressEvent.SOCKET_DATA事件)
这个时候客户端在收到"数据段1"后: 通过ByteArray.readObject() 读取对象数据时, 一次只会读取一个数据, 所以要循环读取.
但 这里有个问题, 如果是在服务端先通过amf3output.writeObject(...)写入ByteArrayOutputStream中, 再对ByteArrayOutputStream进行压缩, 那么在客户端解压后, 会出现数据丢失的情况, 即解压后只保留了"数据1", 后面的丢失.
所以服务端要处理为: 先压缩, 再把压缩后的数据写入ByteArrayOutputStream, 再发送给客户端. 客户端接收到后, 循环取出每个数据, 再解压获取真正的数据.
服务端:
AMF3CodecFactory:

package com.test.filter.amf3;

import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFactory;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolEncoder;

public class AMF3CodecFactory implements ProtocolCodecFactory {
private ProtocolEncoder encoder;
private ProtocolDecoder decoder;

public AMF3CodecFactory() {
encoder = new AMF3Encoder();
decoder = new AMF3Decoder();
}

public ProtocolDecoder getDecoder(IoSession session) throws Exception {
return decoder;
}

public ProtocolEncoder getEncoder(IoSession session) throws Exception {
return encoder;
}
}

AMF3Decoder:
package com.test.filter.amf3;

import java.util.zip.InflaterInputStream;
import java.io.DataInputStream;
import java.io.UnsupportedEncodingException;

import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.Amf3Input;
import flex.messaging.io.amf.ASObject;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
/**
 * <style type="text/css">body{background:#C7EDCC;}</style>
 * 解码
 * AMF3Decoder.java
 * @author yan
 *
 */
public class AMF3Decoder extends CumulativeProtocolDecoder {
private final AttributeKey POLICY = new AttributeKey(this.getClass(), "policy");
private final String security = "<policy-file-request/>";
private final SerializationContext context = new SerializationContext();
private final Amf3Input amf3in;
public AMF3Decoder() {
amf3in = new Amf3Input(context);
}
protected boolean doDecode(IoSession session, IoBuffer in,
ProtocolDecoderOutput out) throws Exception {
if(this.isSecurityRequest(session,in)){
out.write(security);
in.free();
return true;
}else{
in.position(0);
//获取flash socket传入的信息.此时客户端传入的是一个as对象
amf3in.setInputStream(new InflaterInputStream(new DataInputStream(in.asInputStream())));
Object message = amf3in.readObject();
if(message instanceof ASObject){
out.write(message);
in.free();
return true;
}else{
in.free();
return false;
}
}
}
/**
* <style type="text/css">body{background:#C7EDCC;}</style>
* 是否为策略文件请求
* @param session
* @param in
* @return
*/
private boolean isSecurityRequest(IoSession session, IoBuffer in){
Boolean policy = (Boolean)session.getAttribute(POLICY);
if(policy != null){
return false;
}
String request = this.getRequest(in);
boolean result = false;
if(request != null){
result = request.startsWith(security);
}  
session.setAttribute(POLICY,new Boolean(result));
return result;
}
private String getRequest(IoBuffer in){
byte[] bytes = new byte[in.limit()];
in.get(bytes);//从IoBuffer中获取数据并放入bytes中
String request=null;
try {
request = new String(bytes,"UTF-8");
} catch (UnsupportedEncodingException e) {
request = null;
}  
return request;
}

}

AMF3Encoder:
package com.test.filter.amf3; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.Deflater; import flex.messaging.io.SerializationContext; import flex.messaging.io.amf.Amf3Output; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.session.AttributeKey; import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.codec.ProtocolEncoder; import org.apache.mina.filter.codec.ProtocolEncoderOutput; /** * <style type="text/css">body{background:#C7EDCC;}</style> * 编码 * AMF3Encoder.java * @author yan * <p>Sep 13, 2010 11:00:53 AM</p> * */ public class AMF3Encoder implements ProtocolEncoder { //压缩时使用的byte大小 private static int cachesize = 1024; private final AttributeKey DEFLATER = new AttributeKey(getClass(),"deflater"); private final SerializationContext context = new SerializationContext(); private final Amf3Output amf3out; public AMF3Encoder() { amf3out = new Amf3Output(context); } public void dispose(IoSession session) throws Exception { try { if (!isStrMsg && amf3out != null){ amf3out.close(); } } catch (Exception e) { e.printStackTrace(); } } private boolean isStrMsg = false; public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception { IoBuffer buffer; if (message instanceof String) { isStrMsg = true; byte[] bytes = ((String) message).getBytes("UTF-8"); buffer = IoBuffer.allocate(bytes.length + 1); buffer.put(bytes); buffer.put((byte) 0x0);//要在末尾写入0,表示一个流信息的结束 buffer.flip(); out.write(buffer); } else { isStrMsg = false; //1. 转为amf3格式. 先压缩数据 Amf3Output amf3out2 = new Amf3Output(new SerializationContext()); ByteArrayOutputStream stream2 = new ByteArrayOutputStream(); amf3out2.setOutputStream(stream2); amf3out2.writeObject(message); int len = stream2.toByteArray().length; System.out.println("oldLen: "+len); byte compressBytes[] = this.compress(session, stream2.toByteArray()); System.out.println("compressedLen: "+compressBytes.length+"/n"); amf3out2.flush(); //2. 再把压缩后的数据写入ByteArrayOutputStream ByteArrayOutputStream stream = new ByteArrayOutputStream(); amf3out.setOutputStream(stream); //amf3out.writeInt(len); //这里是把压缩后的数据作为对象放入stream中, 在客户端取得后,要转为as的ByteArray类型才可用 amf3out.writeObject(compressBytes); amf3out.flush(); //如果在此使用压缩,flash在解压后,将会把数据截断 //byte bytes[] = this.compress(session, stream.toByteArray()); //3. 最后把装有压缩数据的ByteArrayOutputStream发往客户端 byte bytes[] = stream.toByteArray(); buffer = IoBuffer.allocate(bytes.length, false); buffer.put(bytes); buffer.flip(); out.write(buffer); out.flush(); buffer.free(); } } // private Deflater deflater = new Deflater(); /** * <style type="text/css">body{background:#C7EDCC;}</style> * 压缩 * @param session * @param inputs * @return */ private byte[] compress(IoSession session, byte[] inputs) { Deflater deflater = (Deflater) session.getAttribute(DEFLATER); if (deflater == null) { deflater = new Deflater(); session.setAttribute(DEFLATER, deflater); } deflater.reset(); deflater.setInput(inputs); deflater.finish(); byte outputs[] = new byte[0]; ByteArrayOutputStream stream = new ByteArrayOutputStream(inputs.length); byte[] bytes = new byte[cachesize]; int value; while (!deflater.finished()) { value = deflater.deflate(bytes); stream.write(bytes, 0, value); } outputs = stream.toByteArray(); try { stream.close(); } catch (IOException e) { // e.printStackTrace(); } return outputs; } }

ServerHandler:
package com.test.socket.handler;

import java.util.Map;
import java.util.HashMap;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

import com.test.util.SocketUtil;
import com.test.vo.DataCenter;
import com.test.vo.RoleInfo;

import flex.messaging.io.amf.ASObject;

public class ServerHandler extends IoHandlerAdapter {
@Override
public void sessionClosed(IoSession session) throws Exception {
super.sessionClosed(session);
}
public void messageReceived(IoSession session, Object message) throws Exception {
try {
if(message instanceof ASObject){
ASObject object = (ASObject)message;
reqHandler(object,session);
}else{
session.write("");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
System.out.println("客户端 "+session.getRemoteAddress()+" 关闭了连接");
session.close(false);
}

@SuppressWarnings("unchecked")
private void reqHandler(ASObject obj,IoSession session){
String head = (String)obj.get("head");
if(head==null){
return;
}
//新的连接进行登记
if("checkin".equals(head)){
String name = (String)obj.get("name");
DataCenter.Roles.put(name,session);
System.out.println(name+" 登记进入游戏");
Map m1 = new HashMap();
m1.put("head", "m1111111111111111111111");
Map m2 = new HashMap();
//发送个小点的数据
m2.put("head", "m2");
Map m3 = new HashMap();
m3.put("head", "m3333333333333333333333");
RoleInfo m4 = new RoleInfo();
m4.setHead("roleInfo_head");
m4.setName("roleInfo_name_dddd");
SocketUtil.sendSomes(m1,name);
SocketUtil.sendSomes(m2,name);
SocketUtil.sendSomes(m3,name);
SocketUtil.sendSomes(m4,name);
}
}
}

ServerPolicyHandler:
package com.test.socket.handler;

import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;

import flex.messaging.io.amf.ASObject;

public class ServerPolicyHandler extends IoHandlerAdapter {
@Override
public void sessionClosed(IoSession session) throws Exception {
super.sessionClosed(session);
}
public void messageReceived(IoSession session, Object message) throws Exception {
try {
if(message instanceof ASObject){
}else{
sendSecurity(session);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
session.close(false);
}
private void sendSecurity(IoSession session){
String security = "<?xml version=/"1.0/" encoding=/"utf-8/"?>/n" +
" <!DOCTYPE cross-domain-policy SYSTEM /"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd/">/n" +
" <cross-domain-policy>/n" +
" <site-control permitted-cross-domain-policies=/"all/"/>/n" +
" <allow-access-from domain=/"*/" to-ports=/"9000-12000/" />/n" +
" </cross-domain-policy>/n";
session.write(security);
}
}

MServer:
package com.test.socket.server;

import java.io.IOException;
import java.net.InetSocketAddress;

import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.SocketAcceptor;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

import com.test.filter.amf3.AMF3CodecFactory;
import com.test.socket.handler.ServerHandler;
import com.test.socket.handler.ServerPolicyHandler;

public class MServer {
// 服务器端绑定的端口
static int bindPort = 10000;

public static void main(String[] args) throws IOException {
// 创建一个非阻塞的Server端Socket,用NIO
SocketAcceptor acceptor = new NioSocketAcceptor();
// 创建接收数据的过滤器
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("amf3", new ProtocolCodecFilter(new AMF3CodecFactory()));
// chain.addLast("logger", new LoggingFilter() );
// 设定服务器端的消息处理器,
acceptor.setHandler(new ServerHandler());

// 绑定端口,启动服务器
acceptor.bind(new InetSocketAddress("192.168.1.83", bindPort));
System.out.println("Server is Listing on = " + bindPort);
startPolicyServer();
}
/**
* <style type="text/css">body{background:#C7EDCC;}</style>
* 开启策略文件服务端
* @throws IOException
*/
private static void startPolicyServer() throws IOException {
SocketAcceptor acceptor = new NioSocketAcceptor();
// 设定服务器端的消息处理器,
acceptor.setHandler(new ServerPolicyHandler());
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("amf3", new ProtocolCodecFilter(new AMF3CodecFactory()));
// 绑定端口,启动服务器
acceptor.bind(new InetSocketAddress("192.168.1.83", 11000));
System.out.println("Policy Server is Listing on = " + 11000);
}
}

SocketUtil:
package com.test.util;

import java.util.Iterator;
import java.util.Map;

import org.apache.mina.core.session.IoSession;

import com.test.vo.DataCenter;

public class SocketUtil {
/**
* 发送给一些人
* @param names
*/
public static void sendSomes(Object resData,String... names){
if(names==null)return;
for (int i = 0; i < names.length; i++) {
IoSession session = DataCenter.Roles.get(names[i]);
session.write(resData);
}
}
}

DataCenter:
package com.test.vo;

import java.util.HashMap;
import java.util.Map;

import org.apache.mina.core.session.IoSession;

public class DataCenter {
public static Map<String, IoSession> Roles = new HashMap<String, IoSession>();
}

RoleInfo:
package com.test.vo;

public class RoleInfo {
private String name;
private String head;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getHead() {
return head;
}

public void setHead(String head) {
this.head = head;
}
}

客户端:
package com.test.socket{
import flash.display.Sprite;
import flash.events.*;  
    import flash.net.Socket;  
    import flash.net.ObjectEncoding;
    import flash.utils.ByteArray;  
import flash.system.Security;
public class TestAMF extends Sprite{
private var _socket:Socket;
public function TestAMF(){
Security.allowDomain("*");
Security.allowInsecureDomain("*");
Security.loadPolicyFile("xmlsocket://192.168.1.83:11000");
            _socket = new Socket();
//表示socket将采用AMF3进行编码
            _socket.objectEncoding = ObjectEncoding.AMF3;
            configureListeners(_socket);  
            _socket.connect("192.168.1.83",10000);
}
private function configureListeners(dispatcher:IEventDispatcher):void {
            dispatcher.addEventListener(Event.CLOSE, handler);  
            dispatcher.addEventListener(Event.CONNECT, handler);  
            dispatcher.addEventListener(IOErrorEvent.IO_ERROR, handler);  
            dispatcher.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handler);  
            dispatcher.addEventListener(ProgressEvent.SOCKET_DATA, handler);  
        }
private function handler(event:Event):void {
switch(event.type) {
case ProgressEvent.SOCKET_DATA:
trace("======接收到数据段,数据段有效长度: "+_socket.bytesAvailable);
var bytes:ByteArray = new ByteArray();
_socket.readBytes(bytes,0,_socket.bytesAvailable);
try{
//循环取出每个数据
while(true){
try{
//var len:int = bytes.readInt();
var dataBytes:ByteArray = bytes.readObject() as ByteArray;
trace("***当前数据块解压前长度: "+dataBytes.bytesAvailable);
dataBytes.uncompress();
trace("---当前数据块解压后长度: "+dataBytes.bytesAvailable);
var o:Object = dataBytes.readObject();
}catch(e:Error){
return;
}
//得到数据后的操作
this.op(o);
//数据读取完毕后跳出循环
if(bytes.bytesAvailable<=0){
break;
}
}
}catch(e:Error){
trace(e.message);
}
break;
                case Event.CLOSE:
trace("连接关闭");
break;  
                case Event.CONNECT:
trace("连接成功");
var sendbytes:ByteArray = new ByteArray();
sendbytes.writeObject({head:"checkin",name:"def"});
sendbytes.compress();
_socket.writeBytes(sendbytes);
_socket.flush();
break;
                case IOErrorEvent.IO_ERROR:
                case SecurityErrorEvent.SECURITY_ERROR:
trace("连接失败");
break;  
}
}
private function op(o:Object):void{
trace("o.head: "+o.head+"/n");
}
}
}
运行客户端, 会看到如下打印(但不一定是如此):

连接成功
======接收到数据段,数据段有效长度: 68
***当前数据块解压前长度: 21
---当前数据块解压后长度: 33
o.head: m1111111111111111111111

***当前数据块解压前长度: 20
---当前数据块解压后长度: 12
o.head: m2

***当前数据块解压前长度: 21
---当前数据块解压后长度: 33
o.head: m3333333333333333333333

======接收到数据段,数据段有效长度: 60
***当前数据块解压前长度: 58
---当前数据块解压后长度: 68
o.head: roleInfo_head

服务端发送了4次数据, 客户端只触发了2次ProgressEvent.SOCKET_DATA事件, 但都正确收到了4个数据.
但注意红色部分(对应到服务端是发送的一个很小的数据), 会看到解压前是20个字节, 但解压后却是12个字节. 说明真实数据比压缩后还要小, 即压缩后反而变大了.
经过多次测试,这会有个临界点(这个是相对的,请自行测试), 原始数据大于此临界点时,才有压缩到这个临界点的大小,否则会膨胀到接近这个临界点的值(...真拗口,自行测试观察吧).
不过在大多数项目中, 传输的数据基本都还是比较多的,所以总体来说压缩后能提高传输效率.

你可能感兴趣的:(Mina使用AMF3与Flash Socket通信的"粘包"问题)