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与客户端进行通讯的(有点像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 日志截图,可以看到每隔一分钟就会有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实现,示例代码如下:
运行后Server和Client的log如下:
因为建立连接后我通过IoSession向服务器端输入“hello world from java client”,服务器端有相应的输出。服务器端收到消息后执行session.write(new Date()); Java和Android客户端都会打印server端的日期。
最后欢迎check out代码: https://github.com/breakJeff/MinaLongConnection/
- Mina 长连接实践
- apache mina 长连接
- mina长连接ConnectGameServer 类
- Mina长连接Android使用
- Android长连接之mina
- mina实现长连接 推送
- 使用mina实现Android长连接
- Android 基于mina 实现 Socket 长连接
- Android使用Mina建立长连接
- mina长连接ConnectFutrueUntil(获取Iosession工具类)类
- 基于Apache mina 的android 客户端tcp长连接实现
- android socket基于mina框架实现和服务器长连接
- Android长连接--网络框架之mina框架的解析
- Mina长连接框架实现Android客户端与服务器端通信
- Mina长连接框架实现Android客户端与服务器端通信
- 使用mina保持android端和服务端的长连接
- 客户端与服务端长连接Mina框架讲解
- android使用Mina实现与服务器长连接
- Spring的整体架构
- css布局(上下固定,中间自适应)
- 剑指offer——孩子们的游戏
- 各种排序算法时间复杂度,稳定性
- WAFaas modsecurity整体规则的介绍
- Mina 长连接实践
- 移动前端开发不得不了解的html5 head 头标签
- forEach方法遍历数组
- Xcode打印乱起八糟的东西清除
- 集群技术(三)nginx1.3 -- nginx反向代理(负载均衡)
- 简单的银行系统
- 剑指offer 数值的整数次方
- Port 8005 required by Tomcat v7.0 Server at localhost is already in use
- 谈一谈拦截导航控制器返回事件(下)——AOP