Apache mina是一個基於NIO(非阻塞IO)模型的網絡應用框架。詳細資料和下載地址爲:http://mina.apache.org/
遠程客戶端通過IoService建立連接得到session,session將數據傳送到IoFilterChain進行過濾,最後客戶端在IoHandle中進行數據操作,其工作原理如圖所示:
此處記錄的是Android中Apache mina於服務器建立的長連接通信示例。長連接和http短連接底層都是基於TCP/IP協議,長連接通過Socket,ServerSocket與服務器保持連接,服務端一般使用ServerSocket建立監聽,監聽客戶端的連接,客戶端使用Socket,指定端口和IP地址與服務端進行連接。通過長連接,可以實現服務器主動向客戶端推送消息,可以減少客戶端對服務器的輪詢,減少服務器的壓力,而且通信效率高於http,okhttp等。
Apache mina框架核心類:
- IoService接口相關類:監聽管理、session管理等
- IoAcceptor及其相關的類:繼承於IoService接口,實現TCP或UDP協議的監聽器
- IoConnector及其相關的類:繼承於IoService接口,支持TCP或UDP協議與服務器建立連接
- LoggingFilter 記錄mina所有日誌
- ProtocolCodecFilter 數據轉化過濾器
- CompressionFilter 數據壓縮過濾器
- SSLFilter 數據加密與解密傳輸過濾器
- IoSession類:通信管理類
- Handler類:session事件的監聽,消息收發監聽,異常監聽等
mina簡單服務端建立
創建java項目,導入mina-core-2.0.21.jar和slf4j-api-1.7.26.jar兩個庫文件,如不加slf4j包則會出現java.lang.ClassNotFoundException: org.slf4j.LoggerFactory錯誤。
- MinaServer類
public class MinaServer {
static final int PORT = 55555;
public static void main(String[] args) {
IoAcceptor acceptor = new NioSocketAcceptor();
//設置過濾器,主要設置協議的編碼解碼
acceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(
new TextLineCodecFactory(
StandardCharsets.UTF_8,
LineDelimiter.WINDOWS.getValue(),
LineDelimiter.WINDOWS.getValue()
)
));
//添加日誌過濾器
acceptor.getFilterChain().addLast("logger", new LoggingFilter());
//設置讀緩存大小
acceptor.getSessionConfig().setReadBufferSize(2048);
//設置讀寫空閒時間相同,都爲10s
acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
//註冊處理器
acceptor.setHandler(new MyHandler());
// 綁定端口開始進行監聽
try {
acceptor.bind(new InetSocketAddress(PORT));
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("端口 " + PORT + " 啓動成功");
}
}
- MyHandler類
/**
* session對象的創建、消息收發等事件監聽
*/
public class MyHandler extends IoHandlerAdapter {
@Override
public void sessionCreated(IoSession session) {
System.out.println("session被創建,sessionId: " + session.getId());
}
@Override
public void sessionOpened(IoSession session) {
String address = session.getRemoteAddress().toString();
System.out.println("session被打開,address is : " + address);
}
@Override
public void sessionClosed(IoSession session) {
System.out.println("session客戶端關閉,");
}
/**
* 消息的接收
*
* @param session
* @param message
*/
@Override
public void messageReceived(IoSession session, Object message) {
//假如是string類型消息
String strMsg = message.toString();
//向客戶端返回消息
session.write("服務端已收到消息: " + new Date().toString());
System.out.println("接收到消息:" + strMsg);
}
/**
* 消息的發送
*
* @param session
* @param message
* @throws Exception
*/
@Override
public void messageSent(IoSession session, Object message) {
}
}
mina客戶端建立
新建Android項目,拷貝mina-core-2.0.21.jar和slf4j-api-1.7.26.jar兩個庫文件到libs目錄並添加到dependencies,app\build.gradle如圖所示:
- ConnectManage類
/**
* 封裝connection和disconnection方法給外部調用
*/
public class ConnectManage {
private ConnectConfig mConfig;
private WeakReference<Context> mContext;
private NioSocketConnector mConnector;
private IoSession mSession;
private InetSocketAddress mAddress;
private ConnectFuture mConnectFuture;
public ConnectManage(ConnectConfig config) {
this.mConfig = config;
mContext = new WeakReference<>(mConfig.getContext());
init();
}
/**
* 初始化連接配置
*/
private void init() {
// 創建連接對象
mConnector = new NioSocketConnector();
// 設置連接地址
mAddress = new InetSocketAddress(mConfig.getIp(), mConfig.getPort());
mConnector.setDefaultRemoteAddress(mAddress);
// 設置緩衝大小
mConnector.getSessionConfig().setReadBufferSize(mConfig.getReadBufferSize());
// 設置過濾
mConnector.getFilterChain().addLast("logger", new LoggingFilter());
mConnector.getFilterChain().addLast("codec", new ProtocolCodecFilter(new ObjectSerializationCodecFactory()));
// 設置心跳包,斷線重連
KeepAliveMessageFactory heartBeatFactory = new KeepAliveMessageFactoryImpl();
KeepAliveRequestTimeoutHandler heartBeatHandler = new KeepAliveRequestTimeoutHandlerImpl();
KeepAliveFilter heartBeat = new KeepAliveFilter(heartBeatFactory, IdleStatus.BOTH_IDLE, heartBeatHandler);
// 是否回發
heartBeat.setForwardEvent(true);
// 設置當連接的讀取通道空閒的時候,心跳包請求時間間隔
heartBeat.setRequestInterval(mConfig.getHeartInterval());
// 設置心跳包請求後,若超過該時間後則調用KeepAliveRequestTimeoutHandler.CLOSE_NOW
heartBeat.setRequestTimeout(mConfig.getHeartTimeOut());
mConnector.getFilterChain().addLast("heartbeat", heartBeat);
mConnector.getSessionConfig().setUseReadOperation(true);
// 設置連接監聽
mConnector.setHandler(new ConnectHandler(mContext.get(), this));
// 超時設置
mConnector.setConnectTimeoutMillis(mConfig.getConnectionTimeout());
}
/**
* 是否關閉Session
*
* @return
*/
public boolean isCloseSession() {
if (mSession != null) return mSession.isClosing();
return false;
}
/**
* 與服務器建立連接
*
* @return
*/
public boolean connect() {
try {
mConnectFuture = mConnector.connect(mAddress);
//一直等到連接爲止
mConnectFuture.awaitUninterruptibly();
//獲取session對象
mSession = mConnectFuture.getSession();
} catch (Exception e) {
e.printStackTrace();
return false;
}
return mSession != null;
}
/**
* 斷開與服務器的連接
*/
public void disConnect() {
if (mConnector != null) mConnector.dispose();
if (mConnectFuture != null) mConnectFuture.cancel();
mConnector = null;
mSession = null;
mAddress = null;
mContext = null;
mConnectFuture = null;
}
}
- ConnectConfig類
/**
* 連接選項配置
* 通過lombok+構建者模式實現config
*/
@Getter
@Setter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ConnectConfig {
private Context context; //內容
private String ip; //IP地址
private int port; //端口
private int readBufferSize; // 緩存大小
private int connectionTimeout; // 連接超時時間
private int heartInterval; // 心跳間隔時間
private int heartTimeOut; // 心跳請求超時時間
}
- ConnectHandler類
/**
* 業務處理類
*/
public class ConnectHandler extends IoHandlerAdapter {
private Context context;
private ConnectManage connectionManage;
private static final String BROADCAST_ACTION = "mina";
private static final String MESSAGE = "Message";
ConnectHandler(Context context, ConnectManage connectionManage) {
this.context = context;
this.connectionManage = connectionManage;
}
@Override
public void sessionCreated(IoSession session) throws Exception {
super.sessionCreated(session);
System.out.println("---創建連接---");
}
/**
* 將session保存到session manage中,從而可以發送消息到服務器端
*/
@Override
public void sessionOpened(IoSession session) throws Exception {
// 當與服務器連接成功時,將我們的session保存到我們的session manager類中,從而可以發送消息到服務器
SessionManage.getInstance().setIoSession(session);
System.out.println("---打開連接---");
}
@Override
public void sessionClosed(IoSession session) throws Exception {
super.sessionClosed(session);
System.out.println("---關閉連接---");
if (!SessionManage.getInstance().isCloseSession()) {// 關閉後自動重連
if (connectionManage != null && connectionManage.isCloseSession())
connectionManage.connect();
} else {
if (connectionManage != null) connectionManage.disConnect();
connectionManage = null;
}
}
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
super.sessionIdle(session, status);
System.out.println("---空閒---");
}
@Override
public void messageReceived(IoSession session, Object message) throws Exception {
if (context != null) {
//通過Android局部廣播LocalBroadcastManager機制發送消息
Intent intent = new Intent(BROADCAST_ACTION);
intent.putExtra(MESSAGE, message.toString());
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
System.out.println("---接收消息--- " + message);
}
@Override
public void messageSent(IoSession session, Object message) throws Exception {
super.messageSent(session, message);
System.out.println("---發送消息--- " + message);
}
}
- ConnectService類
/**
* 服務管理類
*/
public class ConnectService extends Service {
//負責調用connect manage完成於服務器的連接
private ConnectThread connectThread;
@Override
public void onCreate() {
super.onCreate();
//初始化線程並啓動
connectThread = new ConnectThread("mina");
connectThread.start();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
//斷開連接並釋放線程
connectThread.disConnection();
connectThread = null;
}
}
- ConnectThread類
/**
* 創建線程類來調用connection manage完成於服務器的連接
*/
public class ConnectThread extends HandlerThread {
private ConnectManage mConnectManage;
private boolean isConnection;
ConnectThread(String name) {
super(name);
ConnectConfig connectConfig = new ConnectConfig.ConnectConfigBuilder()
.ip("192.168.1.55")
.port(55555)
.readBufferSize(10240)
.connectionTimeout(10000)
.heartInterval(20)
.heartTimeOut(5).build();
mConnectManage = new ConnectManage(connectConfig);
}
@Override
protected void onLooperPrepared() {
// 利用循環請求連接
while (!isConnection) {
if (mConnectManage == null) break;
//是否於服務器連接成功
boolean isConnection = mConnectManage.connect();
if (isConnection) {
// 當請求成功的時候,跳出循環
System.out.println("----------連接成功");
break;
}
System.out.println("---------連接失敗");
try {
Thread.sleep(2500);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 銷燬服務器的連接
*/
public void disConnection() {
isConnection = true;
if (getLooper() != null) getLooper().quit();
if (mConnectManage != null) {
mConnectManage.disConnect();
mConnectManage = null;
}
}
}
- SessionManage類
/**
* 會話管理類
*/
class SessionManage {
private static SessionManage mSessionManage = null;
private IoSession mSession;
private boolean isCloseSession;
//單例加鎖機制
public static SessionManage getInstance() {
if (mSessionManage == null) {
synchronized (SessionManage.class) {
if (mSessionManage == null)
mSessionManage = new SessionManage();
}
}
return mSessionManage;
}
public void setIoSession(IoSession ioSession) {
this.mSession = ioSession;
}
public boolean isCloseSession() {
return isCloseSession;
}
private void setCloseSession(boolean closeSession) {
isCloseSession = closeSession;
}
/**
* 將對象寫到服務器
*/
public void writeToServer(Object msg) {
if (mSession != null) {
mSession.write(msg.toString());
}
}
/**
* 關閉連接
*/
public void closeSession() {
if (mSession != null) {
mSession.closeNow();
mSession.closeOnFlush();
mSession.getService().dispose();
}
mSession = null;
setCloseSession(false);
}
}
- KeepAliveRequestTimeoutHandlerImpl類
/**
* 當心跳超時時的處理
*/
class KeepAliveRequestTimeoutHandlerImpl implements KeepAliveRequestTimeoutHandler {
@Override
public void keepAliveRequestTimedOut(KeepAliveFilter keepAliveFilter, IoSession ioSession) throws Exception {
ioSession.closeNow();
}
}
- KeepAliveMessageFactoryImpl類
/**
* 我這裏沒做啥操作
*/
class KeepAliveMessageFactoryImpl implements KeepAliveMessageFactory {
@Override
public boolean isRequest(IoSession ioSession, Object o) {
return false;
}
@Override
public boolean isResponse(IoSession ioSession, Object o) {
return false;
}
@Override
public Object getRequest(IoSession ioSession) {
return null;
}
@Override
public Object getResponse(IoSession ioSession, Object o) {
return null;
}
}
- MainActivity測試類
public class MainActivity extends AppCompatActivity {
private Button bt1, bt2;
private TextView view3;
//自定義廣播接收器
private MessageBroadcastReceiver receiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
receiver = new MessageBroadcastReceiver();
registBroadcast();
}
private void initView() {
bt1 = findViewById(R.id.view);
bt2 = findViewById(R.id.view2);
view3 = findViewById(R.id.textView3);
bt1.setOnClickListener(v -> SessionManage.getInstance().writeToServer("這是客戶端發送的消息"));
bt2.setOnClickListener(v -> {
Intent intent = new Intent(MainActivity.this, ConnectService.class);
startService(intent);
});
}
private void registBroadcast() {
IntentFilter filter = new IntentFilter("mina");
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, filter);
}
private void unregistBroadcast() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
}
/**
* 接受數據並更新UI
*/
private class MessageBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
view3.setText(intent.getStringArrayExtra("Message").toString());
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopService(new Intent(this, ConnectService.class));
unregistBroadcast();
}
}
若Android客戶端啓動時報錯:
解決辦法是:在app\build.gradle中增加includeCompileClasspath true。如圖所示:
若報錯信息爲:
解決辦法是:在app\build.gradle中增加java8的編譯。如圖所示:
通常爲防止客戶端連接不上服務端,可以關閉服務端的防火牆再試試。