Android基于Mina实现的Socket长连接(二)

关于Mina实现的安卓端Socket长连接,我找了很多博客,都只是粗略大概的能够与服务器进行通讯,没有详细谈到长连接保活和性能优化,本篇博客记录了我在封装Mina长连接时候遇到的一些问题和相关代码,以及一些不懂的地方,希望大佬能够指正!

github直接依赖使用

我们在实现Socket长连接需要考虑的问题:

  • 何为长连接?
  • 长连接断开之后需要怎么重连?
  • 与服务端怎么约定长连接?服务端怎么知道我连着还是没有连上?
  • 网络不好的时候怎么操作才能既保证长连接及时的连接上,又保证良好的性能(电量优化)?

由于我做的是股票app,股票的实时行情需要在服务端更新数据之后推送给客户端,这样就是我要用到Socket的地方;

  1. 创建一个Service,这个Service就是Socket发送和接收数据的核心,这个Service需要最大限度的保证它的存活率,参考了一些文章,做了一些保活的(zhuang)策略(bi),其实也没啥卵用,像小米这种手机,要杀还是分分钟杀掉我的进程,除非跟QQ微信一样加入白名单,进程保活参考文章

以下是我Service的部分代码,都做了详细的注释

public class BackTradeService extends Service {
    private static final String TAG = "BackTradeService";
    private ConnectionThread thread;
    public String HOST = "127.0.0.1";
    public String PORT = "2345";
    private ConnectServiceBinder binder = new ConnectServiceBinder() {
        @Override
        public void sendMessage(String message) {
            super.sendMessage(message);
            SessionManager.getInstance().writeTradeToServer(message);//通过自定义的SessionManager将数据发送给服务器
        }

        @Override
        public void changeHost(String host, String port) {
            super.changeHost(host, port);
            releaseHandlerThread();
            startHandlerThread(HOST, PORT);
        }
    };


    @Override
    public IBinder onBind(Intent intent) {
        Bus.register(this);
        SocketCommandCacheUtils.getInstance().initTradeCache();
        KLog.i(TAG, "交易服务绑定成功--->");
        HOST = intent.getStringExtra("host");
        PORT = intent.getStringExtra("port");
        startHandlerThread(HOST, PORT);
        return binder;
    }


    @Override
    public boolean onUnbind(Intent intent) {
        Bus.unregister(this);
        SocketCommandCacheUtils.getInstance().removeAllTradeCache();
        KLog.i(TAG, "交易行情服务解绑成功--->");
        releaseHandlerThread();
        return super.onUnbind(intent);
    }

//这里是创建连接的配置,端口号,超时时间,超时次数等
    public void startHandlerThread(String host, String port) {
        ConnectionConfig config = new ConnectionConfig.Builder(getApplicationContext())
                .setIp(host)
                .setPort(MathUtils.StringToInt(port))
                .setReadBufferSize(10240)
                .setIdleTimeOut(30)
                .setTimeOutCheckInterval(10)
                .setRequestInterval(10)
                .builder();
        thread = new ConnectionThread("BackTradeService", config);
        thread.start();
    }

    public void releaseHandlerThread() {
        if (null != thread) {
            thread.disConnect();
            thread.quit();
            thread = null;
            KLog.w("TAG", "连接被释放,全部重新连接");
        }
    }

    /***
     * 心跳超时,在此重启整个连接
     * @param event
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(ConnectClosedEvent event) {
        if (event.getColseType() == SocketConstants.TRADE_CLOSE_TYPE) {
            KLog.w("TAG", "BackTradeService接收到心跳超时,重启整个推送连接");
            releaseHandlerThread();
            startHandlerThread(HOST, PORT);
        }
    }

    /***
     * 无网络关闭所有连接,不再继续重连
     * @param event
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(ConnectCloseAllEvent event) {
        if(event.isCloseAll()){
            releaseHandlerThread();
        }
    }

    /***
     * 连接成功之后,在这里重新订阅所有交易信息
     * @param event
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(ConnectSuccessEvent event) {
        if (event.getConnectType() == SocketConstants.TRADE_CONNECT_SUCCESS) {
            ArrayList tradeCache = SocketCommandCacheUtils.getInstance().getTradeCache();
            if (null != tradeCache) {
                for (int i = 0; i < tradeCache.size(); i++) {
                    String tm = String.valueOf(System.currentTimeMillis());
                    String s = ...json //这里是发送的数据格式,与后台约定好
                    SessionManager.getInstance().writeTradeToServer(s);
                }
            }
        }
    }

    class ConnectionThread extends HandlerThread {

        TradeConnectionManager mManager;

        public ConnectionThread(String name, ConnectionConfig config) {
            super(name);
            if (null == mManager)
                mManager = new TradeConnectionManager(config,SocketConstants.TRADE_CLOSE_TYPE);
        }

        @Override
        protected void onLooperPrepared() {
            if (null != mManager)
                mManager.connnectToServer();
        }

        public void disConnect() {
            if (null != mManager)
                mManager.disContect();
        }
    }

Service中有几个比较重要的地方

  1. Servvice的生命周期跟MainActivity绑定,也就是说我是在MainActivity里面启动的这个Service,因为我的app在退出的时候就需要不参与数据的实时更新了;但是当用户按下home键之后,app没有退出,当用户再次通过后台调起app时,如果在后台停留时间过长,Service可能会被杀掉(在老的手机上出现过这种情况,且很频繁,这里service的保活就显得微不足道),这时候会出现各种问题;我参考了一些app的做法就是,在applcation里面去监听app进程,当进程被杀掉,就手动重启整个app.这个方法很凑效,貌似当下只能这么做,后面会给一篇博客写这个小技巧
  2. 在做心跳监测的时候,当出现网络频繁的断开连接的时候,会出现网络连接正常之后,Mina的Session连接不成功,一直处于重新连接,我猜想可能是因为Session的Buffer导致(google了一些大牛是这么说的,水平有限,未能深入研究),所以这里干脆将整个服务里的线程干掉,重新创建所有对象,相当于service重新启动了一遍
if (null != thread) {
            thread.disConnect();
            thread.quit();
            thread = null;
            KLog.w("TAG", "连接被释放,全部重新连接");
        }
  1. 性能优化,当我们的手机处于无网络状态的时候,是连接不上socket的,那么这时候的断开我们就没有必要重连,所以我使用了广播去监听网络连接状态,当广播监听到网络状态断开之后,会自动重连10次,达到10次,如果还是没有网,就彻底不再重连,关闭整个服务,这样能优化一些性能,服务在后台跑,也是有性能消耗的;当广播监听网络连接上之后,就又重新开启服务去重连..
  2. 由于项目中Socket订阅是通过特定的commond去触发的,比如我发送2,服务器就会给我返回当前开市情况,发送3,服务器就返回公告信息;所以当我启动Service,在某个特定的页面(一般在页面的生命周期,如onCreat)向服务器一次发送多条订阅,此时有可能与服务器恰好断开了连接,正在重连,那么重连成功之后,不可能再走那个生命周期,所以需要将订阅的command缓存,重新连接之后,再次发送一遍,确保服务器接收到了订阅的内容
 /***
     * 连接成功之后,在这里重新订阅所有交易信息
     * @param event
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(ConnectSuccessEvent event) {
        if (event.getConnectType() == SocketConstants.TRADE_CONNECT_SUCCESS) {
            ArrayList tradeCache = SocketCommandCacheUtils.getInstance().getTradeCache();
            if (null != tradeCache) {
                for (int i = 0; i < tradeCache.size(); i++) {
                    String tm = String.valueOf(System.currentTimeMillis());
                    String s = ...json //这里是发送的数据格式,与后台约定好
                    SessionManager.getInstance().writeTradeToServer(s);
                }
            }
        }
    }

  • 连接管理类,这个类处理了Socket连接,发送数据,接收数据,长连接监听
public class TradeConnectionManager {
    private final int closeType;
    private ConnectionConfig mConfig;
    private WeakReference mContext;
    private NioSocketConnector mConnection;
    private IoSession mSession;
    private InetSocketAddress mAddress;

    private enum ConnectStatus {
        DISCONNECTED,//连接断开
        CONNECTED//连接成功
    }

    private ConnectStatus status = ConnectStatus.DISCONNECTED;

    public ConnectStatus getStatus() {
        return status;
    }

    public void setStatus(ConnectStatus status) {
        this.status = status;
    }

    public TradeConnectionManager(ConnectionConfig config, int closeType) {
        this.mConfig = config;
        this.mContext = new WeakReference<>(config.getContext());
        this.closeType = closeType;
        init();
    }

    private void init() {
        mAddress = new InetSocketAddress(mConfig.getIp(), mConfig.getPort());
        mConnection = new NioSocketConnector();
        mConnection.getSessionConfig().setReadBufferSize(mConfig.getReadBufferSize());
        mConnection.getSessionConfig().setKeepAlive(true);//设置心跳
        //设置超过多长时间客户端进入IDLE状态
        mConnection.getSessionConfig().setBothIdleTime(mConfig.getIdleTimeOut());
        mConnection.setConnectTimeoutCheckInterval(mConfig.getConnetTimeOutCheckInterval());//设置连接超时时间
        mConnection.getFilterChain().addLast("Logging", new LoggingFilter());
        mConnection.getFilterChain().addLast("codec", new ProtocolCodecFilter(new MessageLineFactory()));
        mConnection.setDefaultRemoteAddress(mAddress);
        //设置心跳监听的handler
        KeepAliveRequestTimeoutHandler heartBeatHandler = new KeepAliveRequestTimeoutHandlerImpl(closeType);
        KeepAliveMessageFactory heartBeatFactory = new TradeKeepAliveMessageFactoryImpm();
        //设置心跳
        KeepAliveFilter heartBeat = new KeepAliveFilter(heartBeatFactory, IdleStatus.BOTH_IDLE, heartBeatHandler);
        //是否回发
        heartBeat.setForwardEvent(false);
        //设置心跳间隔
        heartBeat.setRequestInterval(mConfig.getRequsetInterval());
        mConnection.getFilterChain().addLast("heartbeat", heartBeat);
        mConnection.setHandler(new DefaultIoHandler());
    }

    /**
     * 与服务器连接
     *
     * @return
     */
    public void connnectToServer() {
        int count = 0;
        if (null != mConnection) {
            while (getStatus() == ConnectStatus.DISCONNECTED) {
                try {
                    Thread.sleep(3000);
                    ConnectFuture future = mConnection.connect();
                    future.awaitUninterruptibly();// 等待连接创建成功
                    mSession = future.getSession();
                    if (mSession.isConnected()) {
                        setStatus(ConnectStatus.CONNECTED);
                        SessionManager.getInstance().setTradeSeesion(mSession);
                        KLog.e("TAG", "trade连接成功:mSession-->" + mSession);
                        Bus.post(new ConnectSuccessEvent(SocketConstants.TRADE_CONNECT_SUCCESS));
                        break;
                    }
                } catch (Exception e) {
                    count++;
                    KLog.e("TAG", "connnect中连接失败,trade每三秒重新连接一次:mSession-->" + mSession + ",count" + count);
                    if (count == 10) {
                        Bus.post(new ConnectClosedEvent(closeType));
                    }
                }
            }
        }
    }

    /**
     * 断开连接
     */
    public void disContect() {
        setStatus(ConnectStatus.CONNECTED);
        mConnection.getFilterChain().clear();
        mConnection.dispose();
        SessionManager.getInstance().closeSession(closeType);
        SessionManager.getInstance().removeSession(closeType);
        mConnection = null;
        mSession = null;
        mAddress = null;
        mContext = null;
        KLog.e("tag", "断开连接");
    }

    /***
     * Socket的消息接收处理和各种连接状态的监听在这里
     */
    private class DefaultIoHandler extends IoHandlerAdapter {

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

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

        }

        @Override
        public void sessionCreated(IoSession session) throws Exception {
            super.sessionCreated(session);
            KLog.e("tag", "sessionCreated:" + session.hashCode());
        }

        @Override
        public void sessionClosed(IoSession session) throws Exception {
            super.sessionClosed(session);
            KLog.e("tag", "sessionClosed,连接断掉了,需要在此重新连接:" + session.hashCode());
            setStatus(ConnectStatus.DISCONNECTED);
            Bus.post(new ConnectClosedEvent(closeType));
        }

        @Override
        public void messageSent(IoSession session, Object message) throws Exception {
            super.messageSent(session, message);
            KLog.e("tag", "messageSent");
        }

        @Override
        public void inputClosed(IoSession session) throws Exception {
            super.inputClosed(session);
            KLog.w("tag", "server or client disconnect");
            Bus.post(new ConnectClosedEvent(closeType));
        }

        @Override
        public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
            super.sessionIdle(session, status);
            KLog.e("tag", "sessionIdle:" + session.toString() + ",status:" + status);
            if (null != session) {
                session.closeNow();
            }
        }
    }

以上代码中,最核心的是IoHandlerAdapter ,我们自定义的DefaultIoHandler 继承自这个IoHandlerAdapter,所有处理连接成功,连接失败,失败重连,接收服务器发回的数据都在这里处理

这里可以看一下messageSent和messageReceived两个方法,分别是发送数据给服务器和接收服务器的数据,这也就是Mina的高明之处(数据层与业务层剥离,互不干涉)

还有一个核心,就是自定义过滤器

mConnection.getFilterChain().addLast("Logging", new LoggingFilter());
mConnection.getFilterChain().addLast("codec", new ProtocolCodecFilter(new MessageLineFactory()));

上面一个是日志过滤器,规范写法,下面这个就是我们与服务器约定好的编码格式和一些数据截取,如报头,报文,心跳,数据,等等,需要我们去自定义;这也突出了Mina的核心,使用过滤器去将业务层与数据包分离;

  • 自定义的数据编码器,Mina的规范写法
public class MessageLineEncoder implements ProtocolEncoder {
    @Override
    public void encode(IoSession ioSession, Object message, ProtocolEncoderOutput protocolEncoderOutput) throws Exception {
        String s = null ;
        if(message instanceof  String){
            s = (String) message;
        }
        CharsetEncoder charsetEncoder = (CharsetEncoder) ioSession.getAttribute("encoder");
        if(null == charsetEncoder){
            charsetEncoder = Charset.defaultCharset().newEncoder();
            ioSession.setAttribute("encoder",charsetEncoder);
        }

        if(null!=s){
            IoBuffer buffer = IoBuffer.allocate(s.length());
            buffer.setAutoExpand(true);//设置是否可以动态扩展大小
            buffer.putString(s,charsetEncoder);
            buffer.flip();
            protocolEncoderOutput.write(buffer);
        }
    }

    @Override
    public void dispose(IoSession ioSession) throws Exception {

    }
}
  • 数据解码器,需要根据与服务器约定的格式来编写,编码格式,数据截取等都是约定好的
public class MessageLineCumulativeDecoder extends CumulativeProtocolDecoder {
    @Override
    protected boolean doDecode(IoSession ioSession, IoBuffer in, ProtocolDecoderOutput protocolDecoderOutput) throws Exception {
        int startPosition = in.position();
        while (in.hasRemaining()) {
            byte b = in.get();
            if (b == '\n') {//读取到\n时候认为一行已经读取完毕
                int currentPosition = in.position();
                int limit = in.limit();
                in.position(startPosition);
                in.limit(limit);
                IoBuffer buffer = in.slice();
                byte[] bytes = new byte[buffer.limit()];
                buffer.get(bytes);
                String message = new String(bytes);
                protocolDecoderOutput.write(message);
                in.position(currentPosition);
                in.limit(limit);
                return true;
            }
        }
        in.position(startPosition);
        return false;
    }
}
  • 最后是编解码工厂类
public class MessageLineFactory implements ProtocolCodecFactory {
    private MessageLineCumulativeDecoder messageLineDecoder;
    private MessageLineEncoder messageLineEncoder;

    public MessageLineFactory() {
        messageLineDecoder = new MessageLineCumulativeDecoder();
        messageLineEncoder = new MessageLineEncoder();
    }

    @Override
    public ProtocolEncoder getEncoder(IoSession ioSession) throws Exception {
        return messageLineEncoder;
    }

    @Override
    public ProtocolDecoder getDecoder(IoSession ioSession) throws Exception {
        return messageLineDecoder;
    }
}
  • 长连接中心跳的监测,Mina使用KeepAliveRequestTimeoutHandler来为我们实现了心跳的监听,开发者只需要实现KeepAliveRequestTimeoutHandler,重写keepAliveRequestTimedOut方法,就能够接收到之前设置好的心跳超时的回调
//设置心跳监听的handler
KeepAliveRequestTimeoutHandler heartBeatHandler = new KeepAliveRequestTimeoutHandlerImpl(closeType);
KeepAliveMessageFactory heartBeatFactory = new MarketKeepAliveMessageFactoryImpm();
 //设置心跳
KeepAliveFilter heartBeat = new KeepAliveFilter(heartBeatFactory, IdleStatus.BOTH_IDLE, heartBeatHandler);

心跳超时的回调

public class KeepAliveRequestTimeoutHandlerImpl implements KeepAliveRequestTimeoutHandler {

    private final int closeType;

    public KeepAliveRequestTimeoutHandlerImpl(int closeType) {
        this.closeType = closeType ;
    }

    @Override
    public void keepAliveRequestTimedOut(KeepAliveFilter keepAliveFilter, IoSession ioSession) throws Exception {
        KLog.e("TAG","心跳超时,重新连接:"+closeType);
        Bus.post(new ConnectClosedEvent(closeType));
    }
}
  • 有一个更巧妙的地方就是,Mina能够将心跳内容跟业务内容通过KeepAliveMessageFactory区分开来,心跳内容可以在客户端空闲一段时间之后自动发送给服务端,服务端发回一段特殊内容(一般固定不变)给客户端,表明此时连接正常;这样就不需要客户端和服务端来区分哪些包是心跳包,哪些是业务内容;
public class MarketKeepAliveMessageFactoryImpm implements KeepAliveMessageFactory {
    /***
     * 行情心跳包的request
     */
    public  final String marketHeartBeatRequest = "[0,0]\n";
    /***
     * 行情心跳包的response
     */
    public  final String marketHeartBeatResponse = "[0,10]\n";

    @Override
    public boolean isRequest(IoSession ioSession, Object o) {
        if (o.equals(marketHeartBeatRequest)) {
            return true;
        }
        return false;
    }

    @Override
    public boolean isResponse(IoSession ioSession, Object o) {
        if (o.equals(marketHeartBeatResponse)) {
            return true;
        }
        return false;
    }

    @Override
    public Object getRequest(IoSession ioSession) {
        return marketHeartBeatRequest;
    }

    @Override
    public Object getResponse(IoSession ioSession, Object o) {
        return marketHeartBeatResponse;
    }
}

request是发送过去的心跳包内容,response是服务器返回的心跳内容,开发者只需要判断服务器返回的内容是约定的心跳答复内容,那就表明当前连接完全正常

    @Override
    public boolean isResponse(IoSession ioSession, Object o) {
        if (o.equals(marketHeartBeatResponse)) {
            return true;
        }
        return false;
    }
总结:

以上就是我在项目中使用Mina封装的Socket,基本能够保证在有网络的情况下长连接,并且能够监听心跳,断开重连,无网络不再重连,节省资源,正常收发内容;整个过程总结如下:

  1. 使用Service,保证Socket的内容收发;
  2. 确保Mina几个关键点设置正确,否则无法收发内容;主要就是Session,IoHandler,发送和接收数据编解码的ProtocolCodecFilter,以及监测心跳的KeepAliveFilter和KeepAliveRequestTimeoutHandler;
  3. 各种综合情况考虑下的重连,包括网络一直连接,网络时断时续,网络彻底断开,数据发送的时机;
  4. 踩坑,当网络时断时续时,网络连接上之后,发送数据会沾满Buffer导致一直连接不上,重置整个连接,目前为止能够解决;
疑点:
mConnection.getSessionConfig().setBothIdleTime(mConfig.getIdleTimeOut());//设置客户端空闲时间
mConnection.getSessionConfig().setKeepAlive(true);//设置心跳 
mConnection.setConnectTimeoutCheckInterval(mConfig.getConnetTimeOutCheckInterval());//设置连接超时时间
heartBeat.setRequestInterval(mConfig.getRequsetInterval());//设置心跳间隔时间
  1. 这几个时间我在config中配置了,貌似不起作用,按正常情况来说,java的时间都是以毫秒计算,比如把客户端空闲时间设置成了30*1000这种,也就是30秒,但是在客户端空闲时发送心跳的时间跟我设置的对不上,我设置了30秒,但是空闲我测了一下好像10秒就开始发送;纠结...
  2. 心跳间隔时间也不对,我设置了10秒,也就是客户端空闲30秒之后,每10秒发送一次心跳给服务端,但是时间上貌似都不对

请大佬解答!

你可能感兴趣的:(Android基于Mina实现的Socket长连接(二))