關于Mina實現的安卓端Socket長連接,我找了很多博客,都只是粗略大概的能夠與服務器進行通訊,沒有詳細談到長連接保活和性能優化,本篇博客記錄了我在封裝Mina長連接時候遇到的一些問題和相關代碼,以及一些不懂的地方,希望大佬能夠指正!
我們在實現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<Integer> 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<Integer> 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<Context> 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秒發送一次心跳給服務端,但是時間上貌似都不對
請大佬解答!