关于Mina实现的安卓端Socket长连接,我找了很多博客,都只是粗略大概的能够与服务器进行通讯,没有详细谈到长连接保活和性能优化,本篇博客记录了我在封装Mina长连接时候遇到的一些问题和相关代码,以及一些不懂的地方,希望大佬能够指正!
github直接依赖使用
我们在实现Socket长连接需要考虑的问题:
- 何为长连接?
- 长连接断开之后需要怎么重连?
- 与服务端怎么约定长连接?服务端怎么知道我连着还是没有连上?
- 网络不好的时候怎么操作才能既保证长连接及时的连接上,又保证良好的性能(电量优化)?
由于我做的是股票app,股票的实时行情需要在服务端更新数据之后推送给客户端,这样就是我要用到Socket的地方;
- 创建一个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中有几个比较重要的地方
- Servvice的生命周期跟MainActivity绑定,也就是说我是在MainActivity里面启动的这个Service,因为我的app在退出的时候就需要不参与数据的实时更新了;但是当用户按下home键之后,app没有退出,当用户再次通过后台调起app时,如果在后台停留时间过长,Service可能会被杀掉(在老的手机上出现过这种情况,且很频繁,这里service的保活就显得微不足道),这时候会出现各种问题;我参考了一些app的做法就是,在applcation里面去监听app进程,当进程被杀掉,就手动重启整个app.这个方法很凑效,貌似当下只能这么做,后面会给一篇博客写这个小技巧
- 在做心跳监测的时候,当出现网络频繁的断开连接的时候,会出现网络连接正常之后,Mina的Session连接不成功,一直处于重新连接,我猜想可能是因为Session的Buffer导致(google了一些大牛是这么说的,水平有限,未能深入研究),所以这里干脆将整个服务里的线程干掉,重新创建所有对象,相当于service重新启动了一遍
if (null != thread) {
thread.disConnect();
thread.quit();
thread = null;
KLog.w("TAG", "连接被释放,全部重新连接");
}
- 性能优化,当我们的手机处于无网络状态的时候,是连接不上socket的,那么这时候的断开我们就没有必要重连,所以我使用了广播去监听网络连接状态,当广播监听到网络状态断开之后,会自动重连10次,达到10次,如果还是没有网,就彻底不再重连,关闭整个服务,这样能优化一些性能,服务在后台跑,也是有性能消耗的;当广播监听网络连接上之后,就又重新开启服务去重连..
- 由于项目中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,基本能够保证在有网络的情况下长连接,并且能够监听心跳,断开重连,无网络不再重连,节省资源,正常收发内容;整个过程总结如下:
- 使用Service,保证Socket的内容收发;
- 确保Mina几个关键点设置正确,否则无法收发内容;主要就是Session,IoHandler,发送和接收数据编解码的ProtocolCodecFilter,以及监测心跳的KeepAliveFilter和KeepAliveRequestTimeoutHandler;
- 各种综合情况考虑下的重连,包括网络一直连接,网络时断时续,网络彻底断开,数据发送的时机;
- 踩坑,当网络时断时续时,网络连接上之后,发送数据会沾满Buffer导致一直连接不上,重置整个连接,目前为止能够解决;
疑点:
mConnection.getSessionConfig().setBothIdleTime(mConfig.getIdleTimeOut());//设置客户端空闲时间
mConnection.getSessionConfig().setKeepAlive(true);//设置心跳
mConnection.setConnectTimeoutCheckInterval(mConfig.getConnetTimeOutCheckInterval());//设置连接超时时间
heartBeat.setRequestInterval(mConfig.getRequsetInterval());//设置心跳间隔时间
- 这几个时间我在config中配置了,貌似不起作用,按正常情况来说,java的时间都是以毫秒计算,比如把客户端空闲时间设置成了30*1000这种,也就是30秒,但是在客户端空闲时发送心跳的时间跟我设置的对不上,我设置了30秒,但是空闲我测了一下好像10秒就开始发送;纠结...
- 心跳间隔时间也不对,我设置了10秒,也就是客户端空闲30秒之后,每10秒发送一次心跳给服务端,但是时间上貌似都不对
请大佬解答!