朝花夕拾之socket的基本使用以及mina框架簡單介紹

靚仔凌霄發表於2019-03-28

工欲善其事,必先利其器,從網際網路誕生到現在,基本上所有的程式都是網路程式,很少有單機版的程式了。 而網路程式設計的本質是兩個裝置之間的資料交換,當然,在計算機網路中,裝置主要指計算機。我們現在進行網路程式設計,基本上都是使用已經封裝好的框架,畢竟自己實現維護一套網路程式設計框架,費時費力不說,做出來之後還不一定好用,有那時間多保養保養頭髮。

朝花夕拾之socket的基本使用以及mina框架簡單介紹

Socket簡介

記得大學學習java的時候,並沒有接觸網路程式設計相關的基礎知識,一直都是單機版程式,沒有見識到網路程式設計的美妙,所以對socket這個東西既熟悉又陌生。熟悉的原因是在MFC實驗課中接觸到socket知識的,沒錯,就是那門古老的開發語言,現在已經銷聲匿跡了,陌生的原因大概也就不言而喻了。好了,說了那麼多,那socket到底是什麼呢?

Socket是應用層與TCP/IP協議族通訊的中間軟體抽象層,它是一組介面 。很多同學會把tcp、udp協議和socket搞混,其實Socket只是一種連線模式,不是協議。tcp、udp是兩個最基本的協議,很多其它協議都是基於這兩個協議。

用socket可以建立tcp連線,也可以建立udp連線,這意味著,用socket可以建立任何協議的連線。簡單的來說,socket相當於一艘船,你把目的地(ip+埠)告訴它,把要運輸的貨物搬上去,然後它將貨物送往目的地。這個貨物具體需要怎麼運輸,運輸完成之後是否還要通知你已經到達目的地,這就是實現協議的區別了。

Socket使用

首先來回顧一下socket的基本使用,建立一個服務端所需步驟

  1. 建立ServerSocket物件繫結監聽埠。
  2. 通過accept()方法監聽客戶端的請求。
  3. 建立連線後,通過輸入輸出流讀取客戶端傳送的請求資訊。
  4. 通過輸出流向客戶端傳送請求資訊。
  5. 關閉相關資源。

嗯。。。Talk is cheap,Show me the code:

public class SocketServer {
    public static void main(String[] args) {
        //第一步
        SocketServer socketServer = new SocketServer();
        socketServer.startServer(2333);
    }
    private void startServer(int port) {
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("伺服器已啟動,等待客戶連線...");
            //第二步 呼叫accept()方法開始監聽,等待客戶端的連線 這個方法會阻塞當前執行緒
            Socket socket = serverSocket.accept();
            System.out.println("客戶端連線成功");
            //第三步 建立輸入輸出流讀資料
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            String receivedMsg;
            while ((receivedMsg = bufferedReader.readLine()) != null && !("end").equals(receivedMsg)) {
                System.out.println("客戶端:" + receivedMsg);
                //第四步 給客戶端傳送請求
                String response = "hello client";
                System.out.println("我(服務端):" + response);
                bufferedWriter.write(response+ "\n");
                bufferedWriter.flush();
            }
            //關閉相關資源
            socket.close();
            serverSocket.close();
            bufferedWriter.close();
            bufferedReader.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

建立一個客戶端所需步驟,其實和服務端程式碼差不多:

  1. 建立Socket物件,指明需要連線的伺服器的地址和埠。
  2. 建立連線後,通過輸出流向伺服器傳送請求資訊。
  3. 通過輸入流獲取伺服器的響應資訊。
  4. 關閉相關資源
public class SocketClient {
    public static void main(String[] args){
        SocketClient socketClient = new SocketClient();
        socketClient.startClient(2333);
    }
    void startClient(int port){
        try {
            Socket clientSocket = new Socket("localhost",port);
            System.out.println("客戶端已啟動");
            //給伺服器發訊息
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
            //接收伺服器傳過來的訊息
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            //鍵盤輸入訊息  傳送給服務端
            BufferedReader inputReader = new BufferedReader(new InputStreamReader(System.in));
            String readLine = null;
            while (!(readLine = inputReader.readLine()).equals("bye")){
                System.out.println("我(客戶端):" + readLine);
                //將鍵盤輸入的訊息傳送給伺服器
                bufferedWriter.write(readLine+"\n");
                bufferedWriter.flush();
                String response = bufferedReader.readLine();
                System.out.println("服務端: " + response);
            }
            bufferedWriter.close();
            inputReader.close();
            clientSocket.close();
        }catch (IOException e){
            e.printStackTrace();
        }
        
    }
}
複製程式碼

以上對於socket的示例比較簡單,只實現了客戶端給伺服器傳送訊息,伺服器收到訊息並回復客戶端。現實的情況其實很複雜,如伺服器高併發的處理,客戶端怎麼監聽伺服器隨時發來的訊息,客戶端斷線重連機制,心跳包的處理,還有在對訊息的拆包、粘包的處理等等。而引入mina框架,我們不必關注複雜的網路通訊的實現,只需專注於具體的業務邏輯。

Mina框架簡介

MINA框架是對java的NIO包的一個封裝,簡化了NIO程式開發的難度,封裝了很多底層的細節,讓開發者把精力集中到業務邏輯上來。可能有一些同學不知道NIO是什麼,這裡簡單介紹一下,NIO就是new IO,是jdk1.4引入的,它是同步非阻塞的,比如一個伺服器多個客戶端,客戶端傳送的請求都會註冊到伺服器的多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。它適用於連線數目多且連線比較短的架構,比如聊天伺服器。

mina簡單使用

Mina 的API 將真正的網路通訊與我們的應用程式隔離開來,你只需要關心你要傳送、接收的資料以及你的業務邏輯即可。如果想要使用mina,需要先去apache下載mina的jar包:mina下載地址,我使用的是2.0.16版本,下載解壓之後,只需要使用mina-core-2.0.16.jar和slf4j-android.jar這兩個包即可.

先看看我們實現的效果圖(由於是視訊轉gif,可能不太好看出來,就是一個簡單的文字通訊):

server
client

建立服務端

前面說mina將網路通訊與我們的應用程式隔離開來,那我們看看怎麼樣實現一個TCP的服務端,最近在學習kotlin,以後的程式碼應該都用kotlin展示了:

//建立一個非阻塞的service端的socket
val acceptor = NioSocketAcceptor()
//設定編解碼器  ProtocolCodecFilter攔截器 網路傳輸需要將物件轉換為位元組流
acceptor.filterChain.addLast("codec",ProtocolCodecFilter(TextLineCodecFactory()))
//設定讀取資料的緩衝區大小
acceptor.sessionConfig.readBufferSize = 2048
//讀寫通道10秒內無操作進入空閒狀態 
acceptor.sessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 10)
//繫結埠
acceptor.bind(InetSocketAddress(8080))
複製程式碼

這段程式碼我們就初始化了一個TCP服務端,其中,編解碼器使用的是mina自帶的換行符編解碼器工廠,設定編解碼器是因為在網路上傳輸資料時,傳送端傳送資料需要將物件轉換為位元組流進行傳輸,接收端收到資料後再將位元組流轉換回來。相當於雙方約定一套規則,具體規則可以自己定,也可以用現成的。我這裡只需要傳送文字,就用內建的啦。

網路通訊已經實現,那傳送、接收資料呢?我們具體的業務邏輯都在IoHandler這個類中進行處理,編寫一個類繼承IoHandlerAdapter ,並重寫它的幾個方法,記得在bind埠之前呼叫acceptor.setHandler(MyIoHandlerAdapter()),不然無法監聽到具體事件 :

/**
     * 向客戶端傳送訊息後會呼叫此方法
     * @param session
     * @param message
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun messageSent(session: IoSession?, message: Any?) {
        super.messageSent(session, message)
        LogUtils.i("伺服器傳送訊息成功")
    }

    /**
     * 從埠接受訊息,會響應此方法來對訊息進行處理
     * @param session
     * @param message
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun messageReceived(session: IoSession?, message: Any?) {
        super.messageReceived(session, message)
        val msg = message!!.toString()
        LogUtils.i("伺服器接收訊息成功:$msg")
    }

    /**
     * 伺服器與客戶端建立連線
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionCreated(session: IoSession?) {
        super.sessionCreated(session)
        LogUtils.i("伺服器與客戶端建立連線")
    }

    /**
     * 伺服器與客戶端連線開啟
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionOpened(session: IoSession?) {
        super.sessionOpened(session)
        LogUtils.i("伺服器與客戶端連線開啟")
    }

    /**
     * 關閉與客戶端的連線時會呼叫此方法
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionClosed(session: IoSession?) {
        super.sessionClosed(session)
        LogUtils.i("關閉與客戶端的連線時會呼叫此方法")
    }

    /**
     * 伺服器進入空閒狀態
     * @param session
     * @param status
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
        super.sessionIdle(session, status)
        LogUtils.i("伺服器進入空閒狀態")
    }

    /**
     * 異常
     * @param session
     * @param cause
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun exceptionCaught(session: IoSession?, cause: Throwable) {
        super.exceptionCaught(session, cause)
        LogUtils.i("伺服器異常$cause")
    }
複製程式碼

大家應該注意到IoSession這個東西了,每一個方法引數裡都有它,那它具體是幹什麼的呢?

IoSession是一個介面,這個介面用於表示Server 端與Client 端的連線,IoAcceptor.accept()的時候返回例項。這個介面有如下常用的方法:

  1. WriteFuture write(Object message):這個方法用於寫資料,該操作是非同步的。
  2. CloseFuture close(boolean immediately):這個方法用於關閉IoSession,該操作也是非同步的,引數指定true 表示立即關閉,否則就在所有的寫操作都flush 之後再關閉。
  3. Object setAttribute(Object key,Object value):這個方法用於給我們向會話中新增一些屬性,這樣可以在會話過程中都可以使用,類似於HttpSession 的setAttrbute()方法。IoSession 內部使用同步的HashMap 儲存你新增的自定義屬性。
  4. SocketAddress getRemoteAddress():這個方法獲取遠端連線的套接字地址。
  5. void suspendWrite():這個方法用於掛起寫操作,那麼有void resumeWrite()方法與之配對。對於read()方法同樣適用。
  6. ReadFuture read():這個方法用於讀取資料, 但預設是不能使用的, 你需要呼叫IoSessionConfig 的setUseReadOperation(true)才可以使用這個非同步讀取的方法。一般我們不會用到這個方法,因為這個方法的內部實現是將資料儲存到一個BlockingQueue,假如是Server 端,因為大量的Client 端傳送的資料在Server 端都這麼讀取,那麼可能會導致記憶體洩漏,但對於Client,可能有的時候會比較便利。
  7. IoService getService():這個方法返回與當前會話物件關聯的IoService 例項。

換言之,拿到IoSession就能夠進行兩端打call了,什麼?你問我怎麼打call?

fun sendText(message: String,client IoSession){
    var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
    ioBuffer.put(message.toByteArray())
    ioBuffer.flip()
    client.write(ioBuffer)
}
複製程式碼
建立客戶端

無論是Server 端還是Client 端,在Mina中的執行流程都是一樣的。唯一不同的就是IoService 的Client 端實現是IoConnector。

val connector = NioSocketConnector()
// 設定連結超時時間
connector.connectTimeoutMillis = 15000
// 新增過濾器
connector.filterChain.addLast("codec",ProtocolCodecFilter(TextLineCodecFactory()))
connector.setDefaultRemoteAddress(InetSocketAddress(ip, port))
val future = connector.connect()
future.awaitUninterruptibly()// 等待連線建立完成
var session = future.session// 獲得IoSession
複製程式碼

IoHandlerAdapter 和服務端一樣,這裡不做過多介紹。

最後貼上服務端程式碼:

class MinaServer : IoHandlerAdapter(){
    private val acceptor: NioSocketAcceptor
    private var isConnected = false
    private var handler: Handler by Delegates.notNull()
    init {
        //建立一個非阻塞的service端的socket
        acceptor = NioSocketAcceptor()
        //設定編解碼器  ProtocolCodecFilter攔截器 網路傳輸需要將物件轉換為位元組流
        acceptor.filterChain.addLast("codec",
                ProtocolCodecFilter(TextLineCodecFactory()))
        //設定讀取資料的緩衝區大小
        acceptor.sessionConfig.readBufferSize = 2048
        //讀寫通道10秒內無操作進入空閒狀態
        acceptor.sessionConfig.setIdleTime(IdleStatus.BOTH_IDLE, 10)
        handler = Handler()

    }

    fun connect(port: Int): MinaServer {
        if (isConnected)
            return this
        thread {
            try {
                //註冊回撥 監聽和客戶端之間的訊息
                acceptor.handler = this
                acceptor.isReuseAddress = true
                //繫結埠
                acceptor.bind(InetSocketAddress(port))
                isConnected = true
                handler.post {
                    connectCallback?.onOpened()
                }
            } catch (e: Exception) {
                e.printStackTrace()
                handler.post {
                    connectCallback?.onError(e)
                }
                LogUtils.i("伺服器連線異常")
                isConnected = false
            }
        }
        return this
    }

    fun sendText(message: String){
        for (client in acceptor.managedSessions.values){
            var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
            ioBuffer.put(message.toByteArray())
            ioBuffer.flip()
            client.write(ioBuffer)
        }
    }

    /**
     * 向客戶端傳送訊息後會呼叫此方法
     * @param session
     * @param message
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun messageSent(session: IoSession?, message: Any?) {
        super.messageSent(session, message)
        LogUtils.i("伺服器傳送訊息成功")
        connectCallback?.onSendSuccess()
    }

    /**
     * 從埠接受訊息,會響應此方法來對訊息進行處理
     * @param session
     * @param message
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun messageReceived(session: IoSession?, message: Any?) {
        super.messageReceived(session, message)
        handler.post {
            connectCallback?.onGetMessage(message)
        }
        val msg = message!!.toString()
        LogUtils.i("伺服器接收訊息成功:$msg")
    }

    /**
     * 伺服器與客戶端建立連線
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionCreated(session: IoSession?) {
        super.sessionCreated(session)
        handler.post {
            connectCallback?.onConnected()
        }
        LogUtils.i("伺服器與客戶端建立連線")
    }

    /**
     * 伺服器與客戶端連線開啟
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionOpened(session: IoSession?) {
        super.sessionOpened(session)
        LogUtils.i("伺服器與客戶端連線開啟")
    }

    /**
     * 關閉與客戶端的連線時會呼叫此方法
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionClosed(session: IoSession?) {
        super.sessionClosed(session)
        handler.post {
            connectCallback?.onDisConnected()
        }
        LogUtils.i("關閉與客戶端的連線時會呼叫此方法")
    }

    /**
     * 伺服器進入空閒狀態
     * @param session
     * @param status
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
        super.sessionIdle(session, status)
        LogUtils.i("伺服器進入空閒狀態")
    }

    /**
     * 異常
     * @param session
     * @param cause
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun exceptionCaught(session: IoSession?, cause: Throwable) {
        super.exceptionCaught(session, cause)
        handler.post {
            connectCallback?.onError(cause)
        }
        LogUtils.i("伺服器異常$cause")
    }

    private var connectCallback:ConnectCallback? = null
    fun setConnectCallback(callback:ConnectCallback){
        this.connectCallback = callback
    }
    interface ConnectCallback{
        fun onSendSuccess()
        fun onGetMessage(message: Any?)
        fun onOpened()
        fun onConnected()
        fun onDisConnected()
        fun onError(cause: Throwable)
    }

}
複製程式碼

在服務端中的activity中使用:

 			var mServer = MinaServer()
            mServer
                    .connect(2333)
                    .setConnectCallback(object : MinaServer.ConnectCallback {
                        override fun onSendSuccess() {
                            //傳送訊息成功
                        }

                        override fun onGetMessage(message: Any?) {
                            //接收訊息成功
                            val msg = message.toString()
                        }

                        override fun onOpened() {
                            
                        }
                        override fun onConnected() {

                        }

                        override fun onDisConnected() {

                        }

                        override fun onError(cause: Throwable) {
                            Toast.makeText(applicationContext, "伺服器異常" + cause.toString(), Toast.LENGTH_SHORT).show()
                        }

                    })
複製程式碼

再看客戶端程式碼:

lass MinaClient : IoHandlerAdapter(){
    private val connector: NioSocketConnector
    private var session: IoSession? = null

    var isConnected = false
    private var handler:Handler by Delegates.notNull()
    init {
        connector = NioSocketConnector()
        // 設定連結超時時間
        connector.connectTimeoutMillis = 15000
        // 新增過濾器
        connector.filterChain.addLast("codec",
                ProtocolCodecFilter(TextLineCodecFactory()))
        handler = Handler()
    }

    fun connect(ip: String, port: Int): MinaClient {
        if (isConnected)
            return this
        thread {
            connector.handler = this
            connector.setDefaultRemoteAddress(InetSocketAddress(ip, port))
            //開始連線
            try {
                val future = connector.connect()
                future.awaitUninterruptibly()// 等待連線建立完成
                session = future.session// 獲得session
                isConnected = session != null && session!!.isConnected
            } catch (e: Exception) {
                e.printStackTrace()
                handler.post {
                    connectCallback?.onError(e)
                }
                println("客戶端連結異常...")
            }
        }
        return this
    }
    fun disConnect(){
        if (isConnected){
            session?.closeOnFlush()
            connector.dispose()
        }else{
            connectCallback?.onDisConnected()
        }
    }

    fun sendText(message: String){
        var ioBuffer = IoBuffer.allocate(message.toByteArray().size)
        ioBuffer.put(message.toByteArray())
        ioBuffer.flip()
        session?.write(ioBuffer)
    }
    /**
     * 向服務端端傳送訊息後會呼叫此方法
     * @param session
     * @param message
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun messageSent(session: IoSession?, message: Any?) {
        super.messageSent(session, message)
        LogUtils.i("客戶端傳送訊息成功")
        handler.post {
            connectCallback?.onSendSuccess()
        }
    }

    /**
     * 從埠接受訊息,會響應此方法來對訊息進行處理
     * @param session
     * @param message
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun messageReceived(session: IoSession?, message: Any?) {
        super.messageReceived(session, message)
        LogUtils.i("客戶端接收訊息成功:")
        handler.post {
            connectCallback?.onGetMessage(message)
        }
    }

    /**
     * 伺服器與客戶端建立連線
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionCreated(session: IoSession?) {
        super.sessionCreated(session)
        LogUtils.i("伺服器與客戶端建立連線")
        handler.post {
            connectCallback?.onConnected()
        }
    }

    /**
     * 伺服器與客戶端連線開啟
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionOpened(session: IoSession?) {
        super.sessionOpened(session)
        LogUtils.i("伺服器與客戶端連線開啟")
    }

    /**
     * 關閉與客戶端的連線時會呼叫此方法
     * @param session
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionClosed(session: IoSession?) {
        super.sessionClosed(session)
        LogUtils.i("關閉與客戶端的連線時會呼叫此方法")
        isConnected = false
        handler.post {
            connectCallback?.onDisConnected()
        }
    }

    /**
     * 客戶端進入空閒狀態
     * @param session
     * @param status
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun sessionIdle(session: IoSession?, status: IdleStatus?) {
        super.sessionIdle(session, status)
        LogUtils.i("客戶端進入空閒狀態")
    }

    /**
     * 異常
     * @param session
     * @param cause
     * @throws Exception
     */
    @Throws(Exception::class)
    override fun exceptionCaught(session: IoSession?, cause: Throwable) {
        super.exceptionCaught(session, cause)
        LogUtils.i("客戶端異常$cause")
        handler.post {
            connectCallback?.onError(cause)
        }
    }
    private var connectCallback:ConnectCallback? = null
    fun setConnectCallback(callback:ConnectCallback){
        this.connectCallback = callback
    }
    interface ConnectCallback{
        fun onSendSuccess()
        fun onGetMessage(message: Any?)
        fun onConnected()
        fun onDisConnected()
        fun onError(cause: Throwable)
    }
}
複製程式碼

客戶端的activity中使用:

		var mClient = MinaClient()
        mClient
                .connect("192.168.0.108", 2333)
                .setConnectCallback(object : MinaClient.ConnectCallback {
                    override fun onGetMessage(message: Any?) {
                        val msg = message.toString()
                    }
                    override fun onConnected() {
                        
                    }

                    override fun onDisConnected() {
                        Toast.makeText(applicationContext, "斷開連線成功", Toast.LENGTH_SHORT).show()
                    }
                    override fun onError(cause: Throwable) {
                        Toast.makeText(applicationContext, "伺服器異常" + cause.toString(), Toast.LENGTH_SHORT).show()
                    }

                    override fun onSendSuccess() {

                    }

                })
複製程式碼

介面佈局比較簡單,就是一個recyclerview+幾個button,如果覺得我講得不夠清楚T^T,可以到github上檢視原始碼:minaSimple

相關文章