Mina粘包,断包问题处理(附完整实例,客户端,服务端)

1.什么是断包,粘包?

在讲断包,粘包之前,先说下消息保护边界和无消息保护边界。
1.保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息.也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包.
2.而面向流则是无消息保护边界的,如果发送端连续发送数据, 接收端有可能在一次接收动作中,会接收两个或者更多的数据包。

而tcp是面向流的,需要在消息接收端处理消息边界问题。

接收端在接受数据时有可能会遇到下面四种情况

A.先接收到dataA然后接收到dataB.
B.先接收到dataA的部分数据,然后接收到dataA余下的部分以及dataB的全部.
C.先接收到了dataA的全部数据和dataB的部分数据,然后接收到了dataB的余下的数据.
D.一次性接收到了dataA和dataB的全部数据.

A为正常情况,无粘包或断包。
B为断包+粘包。
C为粘包+断包。
D为粘包。

2.如何处理Mina中遇到的粘包和断包问题

在Mina框架中有个CumulativeProtocolDecoder 累积性的协议解码器,专门用来处理粘包和断包问题。doDecode()的返回值有重要作用。

A.你的doDecode()方法返回true 时,CumulativeProtocolDecoder 的decode()方法会首先判断你是否在doDecode()方法中从内部的IoBuffer 缓冲区读取了数据,如果没有,则会抛出非法的状态异常,也就是你的doDecode()方法返回true 就表示你已经消费了本次数据(相当于聊天室中一个完整的消息已经读取完毕),进一步说,也就是此时你必须已经消费过内部的IoBuffer 缓冲区的数据(哪怕是消费了一个字节的数据)。如果验证过通过,那么CumulativeProtocolDecoder 会检查缓冲区内是否还有数据未读取,如果有就继续调用doDecode()方法,没有就停止对doDecode()方法的调用,直到有新的数据被缓冲。

B. 当你的doDecode()方法返回false 时,CumulativeProtocolDecoder 会停止对doDecode()方法的调用,但此时如果本次数据还有未读取完的,就将含有剩余数据的IoBuffer 缓冲区保存到IoSession 中,以便下一次数据到来时可以从IoSession 中提取合并。如果发现本次数据全都读取完毕,则清空IoBuffer 缓冲区(让父类进行接收下一个包)。简而言之,当你认为读取到的数据已经够解码了,那么就返回true,否则就返回false。这个CumulativeProtocolDecoder 其实最重要的工作就是帮你完成了数据的累积,因为这个工作是很烦琐的。也就是说返回true,那么CumulativeProtocolDecoder会再次调用decoder,并把剩余的数据发下来;(意思就是会把剩余数据给doDecode()处理,剩余数据就是remaining()的数据),返回false就不处理剩余的,(不把剩余数据给doDecode()处理)当有新数据包来的时候就把剩余的数据和新的数据拼接在一起,然后再调用decoder。

下面附上一个完整的实例
1.消息的格式
包头+消息长度(int)+消息内容(json字符串)+包尾,包头包尾是十六进制字符串00 aa bb cc,转化成字节数组0, -86, -69, -52四个字节,下面的完整实例有客户端,服务端,将会解析数据,获取其中的消息内容(Json字符串)并且打印处理,消息以字节数组的方式在服务端,客户端之间传递。

服务端代码

package com.my.mina;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.Date;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.serialization.ObjectSerializationCodecFactory;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

/**
 * mina的Service端
 * 
 * @author linbin
 *
 */
public class MinaService {

    public static void main(String[] args) {

        // 创建一个非阻塞的server端的Socket
        IoAcceptor acceptor = new NioSocketAcceptor();
        // 添加日志过滤器
        acceptor.getFilterChain().addLast("logger", new LoggingFilter());
        acceptor.getFilterChain().addLast("codec",
                new ProtocolCodecFilter(new ByteArrayCodecFactory(Charset.forName("UTF-8"))));// 自定义解编码器

        // 设置Handler
        acceptor.setHandler(new DemoServerHandler());
        // 设置读取数据的缓存区大小
        acceptor.getSessionConfig().setReadBufferSize(2048);
        // 读写通道10秒内无操作进入空闲状态
        acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
        try {
            // 绑定端口
            acceptor.bind(new InetSocketAddress(20000));
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("启动服务");
    }

    /**
     * @ClassName: DemoServerHandler
     * @Description: 负责session对象的创建和监听以及消息的创建和接收监听
     * @author chenzheng
     * @date 2016-12-9 下午3:57:11
     */
    private static class DemoServerHandler extends IoHandlerAdapter {

        // 服务器与客户端创建连接
        @Override
        public void sessionCreated(IoSession session) throws Exception {
            System.out.println("服务器与客户端创建连接...");
            super.sessionCreated(session);
        }

        @Override
        public void sessionOpened(IoSession session) throws Exception {
            System.out.println("服务器与客户端连接打开...");
            super.sessionOpened(session);
        }

        // 消息的接收处理
        @Override
        public void messageReceived(IoSession session, Object message) throws Exception {
            // TODO Auto-generated method stub
            super.messageReceived(session, message);// 消息的接受

            // 传递自定义解编码器传递数组和解析数组丢包断包的
            String a = (String) message;
            System.out.println("接收到的数据:" + a);
            session.write(a);

        }

        // 消息发送后调用
        @Override
        public void messageSent(IoSession session, Object message) throws Exception {
            // TODO Auto-generated method stub
            super.messageSent(session, message);
            System.out.println("服务器发送消息成功...");
        }

        // session关闭
        @Override
        public void sessionClosed(IoSession session) throws Exception {
            // TODO Auto-generated method stub
            super.sessionClosed(session);
            System.out.println("断开连接:");
        }
    }

}

编码器


package com.my.mina;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolEncoderAdapter;
import org.apache.mina.filter.codec.ProtocolEncoderOutput;

import java.nio.charset.Charset;

/**
 * 编码器
 * 
 */
public class ByteArrayEncoder extends ProtocolEncoderAdapter {

    private final Charset charset;

    public ByteArrayEncoder(Charset charset) {
        this.charset = charset;

    }

    /**
     * 直接将数据发出去,数据格式,包头+消息长度(int)+消息内容(json字符串)+包尾 包头包尾是十六进制字符串00 aa bb cc,转化成字节数组0,
     * -86, -69, -52四个字节
     *
     * @param session
     * @param message
     * @param out
     * @throws Exception
     */
    @Override
    public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception {
        // 仿项目,解决断包,粘包问题
        String value = (message == null ? "" : message.toString());// 消息值
        byte[] content = value.getBytes(charset);// 消息内容,字节数组
        IoBuffer buf = IoBuffer.allocate(38 + content.length).setAutoExpand(true);// 缓冲区容量大小38字节加上字符长度
        buf.put(new byte[] { 0, -86, -69, -52 });// 输入包开头固定值十六进制00 aa bb cc,转化成字节数组
        buf.putUnsignedInt(content.length);// int为4字节,一个字节等于2个16进制字符,所以有八位 00 00 00 0c,内容长度。
        buf.put(content);// 消息内容
        buf.put(new byte[] { 0, -86, -69, -52 });// 包尾
        buf.flip();
        out.write(buf);// 写入
    }

}

解码器,重点,解决Mina断包,丢包问题

package com.my.mina;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

import java.nio.charset.Charset;

/**
 * 自定义解码器,确保能读到完整的包
 */
public class ByteArrayDecoder extends CumulativeProtocolDecoder {

    private final Charset charset;

    public ByteArrayDecoder(Charset charset) {
        this.charset = charset;

    }

    @Override
    protected boolean doDecode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput protocolDecoderOutput)
            throws Exception {
        // 丢包,断包处理
        if (ioBuffer.remaining() > 4)// 有包头,包头足够
        {
            ioBuffer.mark();// 标记当前position的快照标记mark,以便后继的reset操作能恢复position位置,开始是0
            byte[] l = new byte[4];
            ioBuffer.get(l);// 读取包头,占4个字节
            if (ioBuffer.remaining() < 4)// 内容长度的4个字节不够,断包
            {
                ioBuffer.reset();
                return false;//
            } else {// 内容长度的4个字节数组足够
                byte[] bytesLegth = new byte[4];// 内容长度
                ioBuffer.get(bytesLegth);// 读取内容长度,int类型,占四个字节
                int len = MinaUtil.byteArrayToInt(bytesLegth);// 内容长度有多少
                if (ioBuffer.remaining() < len)// 内容不够,断包
                {
                    ioBuffer.reset();
                    return false;//

                } else { // 消息内容足够

                    byte[] bytes = new byte[len];
                    ioBuffer.get(bytes, 0, len);
                    protocolDecoderOutput.write(new String(bytes, charset));// 读取内容,并且发送

                    if (ioBuffer.remaining() < 4) {// 包尾不够
                        ioBuffer.reset();
                        return false;//

                    } else {// 包尾足够
                        byte[] tails = new byte[4];
                        ioBuffer.get(tails);// 读取包尾
                        if (ioBuffer.remaining() > 0)// 最后如果粘了包,会再次调用doDeocde()方法,把剩余数据给doDeocde()方法处理
                        {
                            return true;
                        }

                    }
                }

            }

        }
        return false;// 断包,或者执行完,

    }
}

解编码工厂

package com.my.mina;

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;

import java.nio.charset.Charset;

/**
 * 自定义解编码器工厂
 *
 */

public class ByteArrayCodecFactory implements ProtocolCodecFactory {

    private ByteArrayDecoder decoder;
    private ByteArrayEncoder encoder;

    public ByteArrayCodecFactory() {
        this(Charset.defaultCharset());
    }

    public ByteArrayCodecFactory(Charset charSet) {
        encoder = new ByteArrayEncoder(charSet);
        decoder = new ByteArrayDecoder(charSet);
    }

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

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

}

注意:客户端,服务端需要和服务端有同样的解码器,编码器,解编码工厂这三个类。

客户端核心代码


package com.example.mina.minaapplication.view;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.example.mina.minaapplication.R;
import com.example.mina.minaapplication.mina.ByteArrayCodecFactory;

import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.SocketSessionConfig;
import org.apache.mina.transport.socket.nio.NioSocketConnector;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

/**
 * Mina客户端
 */
public class MainActivity extends Activity {
    /**
     * 线程池,避免阻塞主线程,与服务器建立连接使用,创建一个只有单线程的线程池,尽快执行线程的线程池
     */
    private static ExecutorService executorService = Executors.newSingleThreadExecutor();


    /**
     * 连接对象
     */
    private NioSocketConnector mConnection;
    /**
     * session对象
     */
    private IoSession mSession;
    /**
     * 连接服务器的地址
     */
    private InetSocketAddress mAddress;

    private ConnectFuture mConnectFuture;


    public static final int UPADTE_TEXT = 1;
    /**
     * 服务端返回的信息
     */
    private TextView tvShow;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvShow = findViewById(R.id.tv_show);
        initConfig();
        connect();
        findViewById(R.id.send).setOnClickListener(new View.OnClickListener() {//发送消息数据


            @Override
            public void onClick(View view) {
                if (mConnectFuture != null && mConnectFuture.isConnected()) {//与服务器连接上
                    mConnectFuture.getSession().write("{\"id\":11,\"name\":\"ccc\"}");//发送json字符串
                }

            }
        });
    }

    /**
     * 初始化Mina配置信息
     */
    private void initConfig() {
        mAddress = new InetSocketAddress("192.168.0.1", 20000);//连接地址,此数据可改成自己要连接的IP和端口号
        mConnection = new NioSocketConnector();// 创建连接
        // 设置读取数据的缓存区大小
        SocketSessionConfig socketSessionConfig = mConnection.getSessionConfig();
        socketSessionConfig.setReadBufferSize(2048);
        socketSessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 4);//设置4秒没有读写操作进入空闲状态
        mConnection.getFilterChain().addLast("logging", new LoggingFilter());//logging过滤器
        mConnection.getFilterChain().addLast("codec", new ProtocolCodecFilter(new ByteArrayCodecFactory(Charset.forName("UTF-8"))));//自定义解编码器
        mConnection.setHandler(new DefaultHandler());//设置handler
        mConnection.setDefaultRemoteAddress(mAddress);//设置地址


    }

    /**
     * 创建连接
     */

    private void connect() {

        FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
            @Override
            public Void call() {//

                try {
                    while (true) {
                        mConnectFuture = mConnection.connect();
                        mConnectFuture.awaitUninterruptibly();//一直等到他连接为止
                        mSession = mConnectFuture.getSession();//获取session对象
                        if (mSession != null && mSession.isConnected()) {
                            Toast.makeText(MainActivity.this, "连接成功", Toast.LENGTH_SHORT).show();
                            break;
                        }
                        Thread.sleep(3000);//每隔三秒循环一次
                    }

                } catch (Exception e) {//连接异常


                }
                return null;
            }
        });
        executorService.execute(futureTask);//执行连接线程
    }


    /**
     * Mina处理消息的handler,从服务端返回的消息一般在这里处理
     */
    private class DefaultHandler extends IoHandlerAdapter {


        @Override
        public void sessionOpened(IoSession session) throws Exception {
            super.sessionOpened(session);

        }

        /**
         * 接收到服务器端消息
         *
         * @param session
         * @param message
         * @throws Exception
         */
        @Override
        public void messageReceived(IoSession session, Object message) throws Exception {
            Log.e("tag", "接收到服务器端消息:" + message.toString());

            Message message1 = new Message();
            message1.what = UPADTE_TEXT;
            message1.obj = message;
            handler.sendMessage(message1);
        }


        @Override
        public void sessionIdle(IoSession session, IdleStatus status) throws Exception {//客户端进入空闲状态.
            super.sessionIdle(session, status);

        }
    }

    /**
     * 更新UI
     */
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case UPADTE_TEXT:
                    String message = (String) msg.obj;
                    tvShow.setText(message);
                    break;
            }
        }
    };
}

客户端截图:

客户端截图

服务端截图:

服务端截图

本文完整项目代码地址(欢迎来star):
https://github.com/lb1207087645/Android-Mina-master

参考资源:

浅谈TCP粘包、断包问题与解决方案

mina自定义编解码器接收处理byte数组(同时解决数据传输中的粘包、缺包问题)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容