Mina 长连接实践

来源:互联网 发布:屋顶告白大会 知乎 编辑:程序博客网 时间:2024/05/16 17:23

- Mina介绍

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

官网流程图

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

这里写图片描述

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

服务器端示例代码

从示例代码可以看出,实现和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<Long, IoSession> 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<Context> 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如下:
服务器与android客户端运行截图
第一张是服务器端运行后日志截图,第二张是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实现,示例代码如下:
普通Java连接
运行后Server和Client的log如下:
Java客户端运行日志
因为建立连接后我通过IoSession向服务器端输入“hello world from java client”,服务器端有相应的输出。服务器端收到消息后执行session.write(new Date()); Java和Android客户端都会打印server端的日期。
最后欢迎check out代码: https://github.com/breakJeff/MinaLongConnection/

原创粉丝点击