關(guān)于Mina實(shí)現(xiàn)的安卓端Socket長(zhǎng)連接,我找了很多博客,都只是粗略大概的能夠與服務(wù)器進(jìn)行通訊,沒(méi)有詳細(xì)談到長(zhǎng)連接编土罚活和性能優(yōu)化,本篇博客記錄了我在封裝Mina長(zhǎng)連接時(shí)候遇到的一些問(wèn)題和相關(guān)代碼,以及一些不懂的地方,希望大佬能夠指正!
我們?cè)趯?shí)現(xiàn)Socket長(zhǎng)連接需要考慮的問(wèn)題:
- 何為長(zhǎng)連接?
- 長(zhǎng)連接斷開(kāi)之后需要怎么重連?
- 與服務(wù)端怎么約定長(zhǎng)連接?服務(wù)端怎么知道我連著還是沒(méi)有連上?
- 網(wǎng)絡(luò)不好的時(shí)候怎么操作才能既保證長(zhǎng)連接及時(shí)的連接上,又保證良好的性能(電量?jī)?yōu)化)?
由于我做的是股票app,股票的實(shí)時(shí)行情需要在服務(wù)端更新數(shù)據(jù)之后推送給客戶端,這樣就是我要用到Socket的地方;
- 創(chuàng)建一個(gè)Service,這個(gè)Service就是Socket發(fā)送和接收數(shù)據(jù)的核心,這個(gè)Service需要最大限度的保證它的存活率,參考了一些文章,做了一些笨垓撸活的(zhuang)策略(bi),其實(shí)也沒(méi)啥卵用,像小米這種手機(jī),要?dú)⑦€是分分鐘殺掉我的進(jìn)程,除非跟QQ微信一樣加入白名單,進(jìn)程苯刃冢活參考文章
以下是我Service的部分代碼,都做了詳細(xì)的注釋
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);//通過(guò)自定義的SessionManager將數(shù)據(jù)發(fā)送給服務(wù)器
}
@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, "交易服務(wù)綁定成功--->");
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, "交易行情服務(wù)解綁成功--->");
releaseHandlerThread();
return super.onUnbind(intent);
}
//這里是創(chuàng)建連接的配置,端口號(hào),超時(shí)時(shí)間,超時(shí)次數(shù)等
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", "連接被釋放,全部重新連接");
}
}
/***
* 心跳超時(shí),在此重啟整個(gè)連接
* @param event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onEventMainThread(ConnectClosedEvent event) {
if (event.getColseType() == SocketConstants.TRADE_CLOSE_TYPE) {
KLog.w("TAG", "BackTradeService接收到心跳超時(shí),重啟整個(gè)推送連接");
releaseHandlerThread();
startHandlerThread(HOST, PORT);
}
}
/***
* 無(wú)網(wǎng)絡(luò)關(guān)閉所有連接,不再繼續(xù)重連
* @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 //這里是發(fā)送的數(shù)據(jù)格式,與后臺(tái)約定好
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中有幾個(gè)比較重要的地方
- Servvice的生命周期跟MainActivity綁定,也就是說(shuō)我是在MainActivity里面啟動(dòng)的這個(gè)Service,因?yàn)槲业腶pp在退出的時(shí)候就需要不參與數(shù)據(jù)的實(shí)時(shí)更新了;但是當(dāng)用戶按下home鍵之后,app沒(méi)有退出,當(dāng)用戶再次通過(guò)后臺(tái)調(diào)起app時(shí),如果在后臺(tái)停留時(shí)間過(guò)長(zhǎng),Service可能會(huì)被殺掉(在老的手機(jī)上出現(xiàn)過(guò)這種情況,且很頻繁,這里service的笨瞥蓿活就顯得微不足道),這時(shí)候會(huì)出現(xiàn)各種問(wèn)題;我參考了一些app的做法就是,在applcation里面去監(jiān)聽(tīng)app進(jìn)程,當(dāng)進(jìn)程被殺掉,就手動(dòng)重啟整個(gè)app.這個(gè)方法很湊效,貌似當(dāng)下只能這么做,后面會(huì)給一篇博客寫(xiě)這個(gè)小技巧
- 在做心跳監(jiān)測(cè)的時(shí)候,當(dāng)出現(xiàn)網(wǎng)絡(luò)頻繁的斷開(kāi)連接的時(shí)候,會(huì)出現(xiàn)網(wǎng)絡(luò)連接正常之后,Mina的Session連接不成功,一直處于重新連接,我猜想可能是因?yàn)镾ession的Buffer導(dǎo)致(google了一些大牛是這么說(shuō)的,水平有限,未能深入研究),所以這里干脆將整個(gè)服務(wù)里的線程干掉,重新創(chuàng)建所有對(duì)象,相當(dāng)于service重新啟動(dòng)了一遍
if (null != thread) {
thread.disConnect();
thread.quit();
thread = null;
KLog.w("TAG", "連接被釋放,全部重新連接");
}
- 性能優(yōu)化,當(dāng)我們的手機(jī)處于無(wú)網(wǎng)絡(luò)狀態(tài)的時(shí)候,是連接不上socket的,那么這時(shí)候的斷開(kāi)我們就沒(méi)有必要重連,所以我使用了廣播去監(jiān)聽(tīng)網(wǎng)絡(luò)連接狀態(tài),當(dāng)廣播監(jiān)聽(tīng)到網(wǎng)絡(luò)狀態(tài)斷開(kāi)之后,會(huì)自動(dòng)重連10次,達(dá)到10次,如果還是沒(méi)有網(wǎng),就徹底不再重連,關(guān)閉整個(gè)服務(wù),這樣能優(yōu)化一些性能,服務(wù)在后臺(tái)跑,也是有性能消耗的;當(dāng)廣播監(jiān)聽(tīng)網(wǎng)絡(luò)連接上之后,就又重新開(kāi)啟服務(wù)去重連..
- 由于項(xiàng)目中Socket訂閱是通過(guò)特定的commond去觸發(fā)的,比如我發(fā)送2,服務(wù)器就會(huì)給我返回當(dāng)前開(kāi)市情況,發(fā)送3,服務(wù)器就返回公告信息;所以當(dāng)我啟動(dòng)Service,在某個(gè)特定的頁(yè)面(一般在頁(yè)面的生命周期,如onCreat)向服務(wù)器一次發(fā)送多條訂閱,此時(shí)有可能與服務(wù)器恰好斷開(kāi)了連接,正在重連,那么重連成功之后,不可能再走那個(gè)生命周期,所以需要將訂閱的command緩存,重新連接之后,再次發(fā)送一遍,確保服務(wù)器接收到了訂閱的內(nèi)容
/***
* 連接成功之后,在這里重新訂閱所有交易信息
* @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 //這里是發(fā)送的數(shù)據(jù)格式,與后臺(tái)約定好
SessionManager.getInstance().writeTradeToServer(s);
}
}
}
}
- 連接管理類,這個(gè)類處理了Socket連接,發(fā)送數(shù)據(jù),接收數(shù)據(jù),長(zhǎng)連接監(jiān)聽(tīng)
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,//連接斷開(kāi)
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);//設(shè)置心跳
//設(shè)置超過(guò)多長(zhǎng)時(shí)間客戶端進(jìn)入IDLE狀態(tài)
mConnection.getSessionConfig().setBothIdleTime(mConfig.getIdleTimeOut());
mConnection.setConnectTimeoutCheckInterval(mConfig.getConnetTimeOutCheckInterval());//設(shè)置連接超時(shí)時(shí)間
mConnection.getFilterChain().addLast("Logging", new LoggingFilter());
mConnection.getFilterChain().addLast("codec", new ProtocolCodecFilter(new MessageLineFactory()));
mConnection.setDefaultRemoteAddress(mAddress);
//設(shè)置心跳監(jiān)聽(tīng)的handler
KeepAliveRequestTimeoutHandler heartBeatHandler = new KeepAliveRequestTimeoutHandlerImpl(closeType);
KeepAliveMessageFactory heartBeatFactory = new TradeKeepAliveMessageFactoryImpm();
//設(shè)置心跳
KeepAliveFilter heartBeat = new KeepAliveFilter(heartBeatFactory, IdleStatus.BOTH_IDLE, heartBeatHandler);
//是否回發(fā)
heartBeat.setForwardEvent(false);
//設(shè)置心跳間隔
heartBeat.setRequestInterval(mConfig.getRequsetInterval());
mConnection.getFilterChain().addLast("heartbeat", heartBeat);
mConnection.setHandler(new DefaultIoHandler());
}
/**
* 與服務(wù)器連接
*
* @return
*/
public void connnectToServer() {
int count = 0;
if (null != mConnection) {
while (getStatus() == ConnectStatus.DISCONNECTED) {
try {
Thread.sleep(3000);
ConnectFuture future = mConnection.connect();
future.awaitUninterruptibly();// 等待連接創(chuàng)建成功
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));
}
}
}
}
}
/**
* 斷開(kāi)連接
*/
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", "斷開(kāi)連接");
}
/***
* Socket的消息接收處理和各種連接狀態(tài)的監(jiān)聽(tīng)在這里
*/
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", "接收到服務(wù)器端消息:" + 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 繼承自這個(gè)IoHandlerAdapter,所有處理連接成功,連接失敗,失敗重連,接收服務(wù)器發(fā)回的數(shù)據(jù)都在這里處理
這里可以看一下messageSent和messageReceived兩個(gè)方法,分別是發(fā)送數(shù)據(jù)給服務(wù)器和接收服務(wù)器的數(shù)據(jù),這也就是Mina的高明之處(數(shù)據(jù)層與業(yè)務(wù)層剝離,互不干涉)
還有一個(gè)核心,就是自定義過(guò)濾器
mConnection.getFilterChain().addLast("Logging", new LoggingFilter());
mConnection.getFilterChain().addLast("codec", new ProtocolCodecFilter(new MessageLineFactory()));
上面一個(gè)是日志過(guò)濾器,規(guī)范寫(xiě)法,下面這個(gè)就是我們與服務(wù)器約定好的編碼格式和一些數(shù)據(jù)截取,如報(bào)頭,報(bào)文,心跳,數(shù)據(jù),等等,需要我們?nèi)プ远x;這也突出了Mina的核心,使用過(guò)濾器去將業(yè)務(wù)層與數(shù)據(jù)包分離;
- 自定義的數(shù)據(jù)編碼器,Mina的規(guī)范寫(xiě)法
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);//設(shè)置是否可以動(dòng)態(tài)擴(kuò)展大小
buffer.putString(s,charsetEncoder);
buffer.flip();
protocolEncoderOutput.write(buffer);
}
}
@Override
public void dispose(IoSession ioSession) throws Exception {
}
}
- 數(shù)據(jù)解碼器,需要根據(jù)與服務(wù)器約定的格式來(lái)編寫(xiě),編碼格式,數(shù)據(jù)截取等都是約定好的
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時(shí)候認(rèn)為一行已經(jīng)讀取完畢
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;
}
}
- 長(zhǎng)連接中心跳的監(jiān)測(cè),Mina使用KeepAliveRequestTimeoutHandler來(lái)為我們實(shí)現(xiàn)了心跳的監(jiān)聽(tīng),開(kāi)發(fā)者只需要實(shí)現(xiàn)KeepAliveRequestTimeoutHandler,重寫(xiě)keepAliveRequestTimedOut方法,就能夠接收到之前設(shè)置好的心跳超時(shí)的回調(diào)
//設(shè)置心跳監(jiān)聽(tīng)的handler
KeepAliveRequestTimeoutHandler heartBeatHandler = new KeepAliveRequestTimeoutHandlerImpl(closeType);
KeepAliveMessageFactory heartBeatFactory = new MarketKeepAliveMessageFactoryImpm();
//設(shè)置心跳
KeepAliveFilter heartBeat = new KeepAliveFilter(heartBeatFactory, IdleStatus.BOTH_IDLE, heartBeatHandler);
心跳超時(shí)的回調(diào)
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","心跳超時(shí),重新連接:"+closeType);
Bus.post(new ConnectClosedEvent(closeType));
}
}
- 有一個(gè)更巧妙的地方就是,Mina能夠?qū)⑿奶鴥?nèi)容跟業(yè)務(wù)內(nèi)容通過(guò)KeepAliveMessageFactory區(qū)分開(kāi)來(lái),心跳內(nèi)容可以在客戶端空閑一段時(shí)間之后自動(dòng)發(fā)送給服務(wù)端,服務(wù)端發(fā)回一段特殊內(nèi)容(一般固定不變)給客戶端,表明此時(shí)連接正常;這樣就不需要客戶端和服務(wù)端來(lái)區(qū)分哪些包是心跳包,哪些是業(yè)務(wù)內(nèi)容;
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是發(fā)送過(guò)去的心跳包內(nèi)容,response是服務(wù)器返回的心跳內(nèi)容,開(kāi)發(fā)者只需要判斷服務(wù)器返回的內(nèi)容是約定的心跳答復(fù)內(nèi)容,那就表明當(dāng)前連接完全正常
@Override
public boolean isResponse(IoSession ioSession, Object o) {
if (o.equals(marketHeartBeatResponse)) {
return true;
}
return false;
}
總結(jié):
以上就是我在項(xiàng)目中使用Mina封裝的Socket,基本能夠保證在有網(wǎng)絡(luò)的情況下長(zhǎng)連接,并且能夠監(jiān)聽(tīng)心跳,斷開(kāi)重連,無(wú)網(wǎng)絡(luò)不再重連,節(jié)省資源,正常收發(fā)內(nèi)容;整個(gè)過(guò)程總結(jié)如下:
- 使用Service,保證Socket的內(nèi)容收發(fā);
- 確保Mina幾個(gè)關(guān)鍵點(diǎn)設(shè)置正確,否則無(wú)法收發(fā)內(nèi)容;主要就是Session,IoHandler,發(fā)送和接收數(shù)據(jù)編解碼的ProtocolCodecFilter,以及監(jiān)測(cè)心跳的KeepAliveFilter和KeepAliveRequestTimeoutHandler;
- 各種綜合情況考慮下的重連,包括網(wǎng)絡(luò)一直連接,網(wǎng)絡(luò)時(shí)斷時(shí)續(xù),網(wǎng)絡(luò)徹底斷開(kāi),數(shù)據(jù)發(fā)送的時(shí)機(jī);
- 踩坑,當(dāng)網(wǎng)絡(luò)時(shí)斷時(shí)續(xù)時(shí),網(wǎng)絡(luò)連接上之后,發(fā)送數(shù)據(jù)會(huì)沾滿Buffer導(dǎo)致一直連接不上,重置整個(gè)連接,目前為止能夠解決;
疑點(diǎn):
mConnection.getSessionConfig().setBothIdleTime(mConfig.getIdleTimeOut());//設(shè)置客戶端空閑時(shí)間
mConnection.getSessionConfig().setKeepAlive(true);//設(shè)置心跳
mConnection.setConnectTimeoutCheckInterval(mConfig.getConnetTimeOutCheckInterval());//設(shè)置連接超時(shí)時(shí)間
heartBeat.setRequestInterval(mConfig.getRequsetInterval());//設(shè)置心跳間隔時(shí)間
- 這幾個(gè)時(shí)間我在config中配置了,貌似不起作用,按正常情況來(lái)說(shuō),java的時(shí)間都是以毫秒計(jì)算,比如把客戶端空閑時(shí)間設(shè)置成了30*1000這種,也就是30秒,但是在客戶端空閑時(shí)發(fā)送心跳的時(shí)間跟我設(shè)置的對(duì)不上,我設(shè)置了30秒,但是空閑我測(cè)了一下好像10秒就開(kāi)始發(fā)送;糾結(jié)...
- 心跳間隔時(shí)間也不對(duì),我設(shè)置了10秒,也就是客戶端空閑30秒之后,每10秒發(fā)送一次心跳給服務(wù)端,但是時(shí)間上貌似都不對(duì)
請(qǐng)大佬解答!