Mina 长连接实践

- Mina介绍

最近项目需要使用长连接,而Mina应该是个不错的选择。个人在Mina的长连接的集成过程中碰到一些问题解决,现在和大家探讨下。言归正传,要使用Mina首先需要看看Mina的官网,特别是他的开发文档需要阅读下(http://mina.apache.org/mina-project/documentation.html)。个人觉得以下二张图是比重要的(来自于http://mina.apache.org/mina-project/resources/ACAsia2006.pdf)。

Mina 长连接实践_第1张图片

从上张图可以看出Mina的客户端有三个比较重要的类,一个是IoSession, 客户端和服务器端建立连接后会返回ConnectFuture,里面就包含这个类,IoHandler是建立连接后的回调接口。第二个是IoFilterChain,在与服务器建立连接前,可以将IoFilterChain的实现注入到IoFilterChainBuilder。因为所有I/O请求和Event都会经过filter,可以通过IoFilterChain进行操作,比如LoggingFilter用来打印所有请求,KeepAliveFilter 用来对服务器发送心跳请求,当然你也可以自定义IoFilterChain进行特需的操作,这个实现个人觉得可以参考OkHttp的源码。最后一个就是IOService,客户端与服务端的通讯都是通过它进行的,对客户端而言,可以通过实现new NioSocketConnector()新建一个IoService:

Mina 长连接实践_第2张图片

第二个比较重要的图就是:
Mina 长连接实践_第3张图片
从上图可以看出,服务器端也是通过IoService与客户端进行通讯的(有点像Java的里面的RMI Stub/Skeleton)。针对该IoService,服务器端有相应的TCP, UDP, SOCKET实现(关于VmPipe我不是很了解,知道的同学可以留言),客户端亦然。服务器端代码如下:

Mina 长连接实践_第4张图片

从示例代码可以看出,实现和Server socket很相似,只是对其做了二次封装而已。基本介绍完毕,下面我来看看具体的实现。

Mina服务器端实现

首先服务器端,你可以上Mina的官网,下载二个jar包(mina-core-2.0.16+slf4j-api-1.7.21), 示例代码如下:

        ioAcceptor = new NioSocketAcceptor();
        ioAcceptor.getFilterChain().addLast("logger", new LoggingFilter());
        ioAcceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(new      ObjectSerializationCodecFactory()));
        ioAcceptor.setHandler(new ConnectionHandler());
        ioAcceptor.getSessionConfig().setReadBufferSize(connectionConfig.getBufferSize());
        ioAcceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, connectionConfig.getIdleTime());
        try {
            ioAcceptor.bind(new InetSocketAddress(connectionConfig.getPort()));
        } catch (IOException e) {
            e.printStackTrace();
        }

需要说明的是服务器端需要实现IoHandler,这样就可以拿到IoSession对客户端发送数据了。示例代码如下:

private static class ConnectionHandler extends IoHandlerAdapter {

        @Override
        public void sessionCreated(IoSession session) throws Exception {
            super.sessionCreated(session);
            InetSocketAddress remoteAddress = (InetSocketAddress) session.getRemoteAddress();
            String clientIp = remoteAddress.getAddress().getHostAddress();
            System.out.println("session created with IP: " + clientIp);
        }

        @Override
        public void sessionOpened(IoSession session) throws Exception {
            super.sessionOpened(session);
            InetSocketAddress remoteAddress = (InetSocketAddress) session.getRemoteAddress();
            String clientIp = remoteAddress.getAddress().getHostAddress();
            System.out.println("session opened with IP: " + clientIp);
            SessionManager.getManager().add(session);
        }

        @Override
        public void sessionClosed(IoSession session) throws Exception {
            super.sessionClosed(session);
            System.out.println("session closed ");
            SessionManager.getManager().remove(session);
        }

        @Override
        public void messageReceived(IoSession session, Object message) {
            System.out.println("message received with message: " + message.toString());
        }

        @Override
        public void sessionIdle(IoSession session, IdleStatus status) {
            System.out.println("session in idle");
        }

        @Override
        public void exceptionCaught(IoSession session, Throwable cause) {
            System.out.println("exception");
            session.closeOnFlush();
            SessionManager.getManager().remove(session);
        }
    }

其中SessionManager是一个简单的IoSession简单容器,可以添加/删除 session,连接建立后可以将session添加到容器中,如果客户session结束或者抛异常,sessionClosed 事件会发出,容器里面的IoSession也被移除。除此之外,SessionManager也可以通过遍历Session对客户端发送消息来用将消息从服务器端发送给客户端。

package com.connection;

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

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SessionManager {
    private final static Map sessions = new ConcurrentHashMap<>();
    private final static SessionManager manager = new SessionManager();

    public static SessionManager getManager() {
        return manager;
    }

    private SessionManager() {}

    public void add(IoSession ioSession) {
        if (ioSession == null) return;

        sessions.put(ioSession.getId(), ioSession);
    }

    public void remove(IoSession ioSession) {
        if (ioSession == null) return;

        sessions.remove(ioSession);
    }

    public void removeAll() {
        if (sessions.size() == 0) return;

        sessions.clear();
    }

    public void update(Object message) {
        for (IoSession ioSession: sessions.values()) {
            ioSession.write(message);
        }
    }
}

Android客户端实现

为什么方便测试,个人实现了二个客户端,一个基于android,一个基于JDK。android客户端除了引入core的jar包,还需要导入slf4j-android-1.6.1-RC1包用于日志输出,具体的jar包可以从官网下载。考虑到连接客户端与服务端比较耗时,用一个service来建立连接。而且在运行过程中,网络有可能断开,注册NetworkReceiver用来监听网络状况。当网络由连接状态变成断开时,stop service, 当网络由断开状态变成连接状态时, start service。还有一点需要说明的是,为了保持长连接,client通过KeepAliveMessageFactory向服务端发送心跳消息维持连接,Mina已有具体的实现,示例代码如下:

        KeepAliveFilter heartBeat = new KeepAliveFilter(new HeartbeatFactory());
        heartBeat.setForwardEvent(true);
        heartBeat.setRequestTimeoutHandler(KeepAliveRequestTimeoutHandler.LOG);
        heartBeat.setRequestInterval(connectionConfig.getTimeInterval());
        chain.addLast(HEART_BEAT, heartBeat);
  private static class HeartbeatFactory implements KeepAliveMessageFactory {
        private final WeakReference weakReference;

        HeartbeatFactory(Context context) {
            this.weakReference = new WeakReference<>(context);
        }

        @Override
        public boolean isRequest(IoSession ioSession, Object o) {
            return true;
        }

        @Override
        public boolean isResponse(IoSession ioSession, Object o) {
            return false;
        }

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

        @Override
        public Object getResponse(IoSession ioSession, Object message) {
            System.out.println("received message is = " + message);
            Intent intent = new Intent(ACTION);
            intent.putExtra(KEY, (Serializable) message);
            LocalBroadcastManager.getInstance(weakReference.get()).sendBroadcast(intent);
            return null;
        }
    }

有必要解释下KeepAliveMessageFactory,isRequest 用来确定是客户端发送心跳包还是服务器发送心跳包,相应的如果是客户端需要通过getRequest是发送具体的数据(本例发送String heartbeat)。如果是服务器端,需要实现getResponse,示例中我先拿到服务器端发出的消息,发送本地广播将主页面的TextView值进行修改。个人在实践过程中发现一个奇怪的情况。如果在客户端实现HeartbeatFactory,所有服务器端发过来的消息都走HeartbeatFactory 的getResponse而不是IoHandler的messageReceived (各位看官如果碰到相同的情况,请留言)。具体的log如下:
Mina 长连接实践_第5张图片
第一张是服务器端运行后日志截图,第二张是android 日志截图,可以看到每隔一分钟就会有heartbeat字段从客户端发出用来保持客户端和服务器端的长连接(KeepAliveFilter的setRequestInterval 是一分钟),并且当客户端和服务器端建立连接后,服务端的IoHandlerAdapter sessionCreated与sessionOpened先后执行。最后一张是模拟器的截图,Connect按钮用来启动service与服务器端建立连接,Send按钮用来想服务器发送一条消息,对应的代码分别如下:

 public void onClick(View v) {
        int id = v.getId();
        switch (id) {
            case R.id.send: {
                SessionHandler.getSessionHandler().writeToServer("hello world from android client");
                break;
            }
            case R.id.connect: {
                Intent intent = new Intent(this, MinaConnectionService.class);
                startService(intent);
                break;
            }
        }
    }

具体的代码介绍完了,还有一点需要知道的是由于客户的网络不是很稳定,在MinaConnectionService connect的过程中,客户端会先检查网络状态,只有在网络连接的状态下才尝试与服务器建立连接。同时在App 启动的时候设置网络监听器,具体代码如下:

 @Override
        protected void onLooperPrepared() {
            while (!isConnect) {
                if (NetworkUtils.isNetworkAvailable(context)) {
                    isConnect = connectionManager.connect();
                    if (isConnect) {
                        SessionHandler.getSessionHandler().setIoSession(connectionManager.getIoSession());
                        break;
                    }
                    try {
                        Thread.sleep(SLEEP_IN_MILLIS);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("network is unavailable");
                    break;
                }

            }
        }
 @Override
    public void onReceive(Context context, Intent intent) {
        if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(intent.getAction())) {
            Parcelable networkExtra = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
            if (networkExtra != null) {
                NetworkInfo networkInfo = (NetworkInfo) networkExtra;
                NetworkInfo.State state = networkInfo.getState();
                startService(state == NetworkInfo.State.CONNECTED, context);
            }
        } else if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
            NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
            if (info != null) {
                startService(NetworkInfo.State.CONNECTED == info.getState() && info.isAvailable(), context);
            }
        }
    }

    private static void startService(boolean isConnect, Context context) {
        if (isConnect) {
            Intent minaIntent = new Intent(context, MinaConnectionService.class);
            minaIntent.putExtra(MinaConnectionService.FORCE_TO_RECONNECT, true);
            context.startService(minaIntent);
            System.out.println("NetworkConnectChangedReceiver connected");
        } else {
            System.out.println("oops, no network");
            Intent minaIntent = new Intent(context, MinaConnectionService.class);
            context.stopService(minaIntent);
        }
    }

Java客户端实现

最后介绍JDK实现,示例代码如下:
Mina 长连接实践_第6张图片
运行后Server和Client的log如下:
Mina 长连接实践_第7张图片
因为建立连接后我通过IoSession向服务器端输入“hello world from java client”,服务器端有相应的输出。服务器端收到消息后执行session.write(new Date()); Java和Android客户端都会打印server端的日期。
最后欢迎check out代码: https://github.com/breakJeff/MinaLongConnection/

你可能感兴趣的:(java后台开发)