1. 程式人生 > >Socket跨平臺通訊——服務端Android、客戶端iOS

Socket跨平臺通訊——服務端Android、客戶端iOS

本文講述了:
1、如何在Android上搭建Mina服務端(使用Mina 2.0.15、程式設計環境Eclipse);
2、如何在iOS上建立Socket客戶端(使用CocoaAsyncSocket第三方通訊框架、Swift3.0);
3、Android iOS間的TCPSocket通訊測試。


一、在Android上搭建Mina服務端

1、下載Mina最新版
直接進官網:http://mina.apache.org/ 進入後左邊欄上方有一個 Latest DownloadsMina x.x.xx,點選進入後如下圖。
這裡寫圖片描述

點選標註的這項,進入下一個介面。
這裡寫圖片描述

用它建議的這個連結下載Mina最新版,下載後是一個zip包。解壓它,如下圖。(我用的是Mina 2.0.15,都一樣)
這裡寫圖片描述

好,到這裡Mina的準備工作算是完成了。

2、下面開始搭建Mina服務端,開啟Eclipse,建立Android工程(這裡將工程命名為MinaServer)。然後就是使用Mina了,就是在自己的工程中匯入Mina提供的第三方jar包。
在剛才解壓得到的Mina資料夾中找到 dist - mina-core-x.x.xxlib - slf4j-api-x.x.xx這兩個Jar包,然後將他們拖到我們工程的libs資料夾中,選中它倆 右擊 - Build Path - Add to Build Path

,下面圖片是匯入後的圖片,匯入後會多出一個Referenced Libraries 裡面有我們剛剛新增的那兩個第三方包。到這裡我們就將Mina匯入進了我們的專案。
(其實匯入包的方法不只這一種,還有別的方法,我也嘗試了,因為別的方法看上去比這一種顯得正規,但是在程式執行中會報錯:Could not find class ‘org.apache.mina.transport.socket.nio.NioSocketAcceptor’, referenced from method alex.example.minaserver.MainActivity.onCreate,這個問題的解決方案就是用這種看著不專業的jar包匯入方法。網上有不少人遇到此問題,可見此坑之深…)
這裡寫圖片描述

3、寫Mina服務端程式碼(這裡不做解釋,因為我也沒徹底玩透Mina框架,只是會簡單使用,也和大家一樣是小白)
開啟 MinaActivity.java

package alex.example.minaserver;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

import org.apache.mina.core.service.IoAcceptor;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.codec.textline.LineDelimiter;
import org.apache.mina.filter.codec.textline.TextLineCodecFactory;
import org.apache.mina.transport.socket.nio.NioSocketAcceptor;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;

public class MainActivity extends Activity {

    // Mina相關宣告-----------------------
    private IoAcceptor acceptor;
    private static int PORT = 4000; // 埠號,要求客戶端與伺服器端一致

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        try {
            Log.d("Mina", "開始啟動 MinaServer...");
            // 建立一個非阻塞的server端的Socket
            acceptor = new NioSocketAcceptor();
            // 設定過濾器(使用mina提供的文字換行符編解碼器)
            acceptor.getFilterChain().addLast("codec",
                    new ProtocolCodecFilter(new TextLineCodecFactory(Charset.forName("UTF-8"), LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue())));
            // 設定讀取資料的換從區大小
            // acceptor.getSessionConfig().setReadBufferSize(2048);
            // 讀寫通道10秒內無操作進入空閒狀態
            acceptor.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 10);
            // 為接收器設定管理服務
            acceptor.setHandler(new DemoServerHandler());
            ((NioSocketAcceptor) acceptor).setReuseAddress(true); // 埠重用,如果有客戶端在wait狀態,伺服器重啟但是埠釋放不掉
            // 繫結埠
            acceptor.bind(new InetSocketAddress(PORT));
            Log.d("Mina", "伺服器啟動成功,埠號為:" + PORT);
            Toast.makeText(MainActivity.this, "伺服器啟動成功", Toast.LENGTH_LONG).show();
        } catch (Exception e) {
            Log.d("Mina", "伺服器啟動異常..." + e);
            Toast.makeText(MainActivity.this, "伺服器啟動失敗", Toast.LENGTH_LONG).show();
            e.printStackTrace();
        }

    }

    // Mina --- IoHandlerAdapter處理Mina通訊中的各種事件 --------------------
    public class DemoServerHandler extends IoHandlerAdapter {

        // 從埠接受訊息,會響應此方法來對訊息進行處理
        public void messageReceived(IoSession session, Object message) throws Exception {
            super.messageReceived(session, message);
            String recvMsg = message.toString();
            Log.d("Mina", "*** Start *** 伺服器收到訊息:" + recvMsg);
            session.write("Received");
        }

        // 向客服端傳送訊息後會呼叫此方法
        public void messageSent(IoSession session, Object message) throws Exception {
            super.messageSent(session, message);
            // session.close(true); // 加上這句話實現短連線的效果,向客戶端成功傳送資料後斷開連線
            Log.d("Mina", "*** End *** 向客戶端傳送訊息:" + message.toString());
        }

        // 關閉與客戶端的連線時會呼叫此方法
        public void sessionClosed(IoSession session) throws Exception {
            super.sessionClosed(session);
            Log.d("Mina", "伺服器與客戶端斷開連線...");
        }

        // 伺服器與客戶端建立連線
        public void sessionCreated(IoSession session) throws Exception {
            super.sessionCreated(session);
            Log.d("Mina", "伺服器與客戶端建立連線...");
        }

        // 伺服器與客戶端連線開啟
        public void sessionOpened(IoSession session) throws Exception {
            Log.d("Mina", "伺服器與客戶端連線開啟...");
            super.sessionOpened(session);
        }

        // 伺服器進入空閒狀態IDLE會呼叫此方法
        public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
            super.sessionIdle(session, status);
            Log.d("Mina", "伺服器進入空閒狀態...");
        }

        // 伺服器捕捉到異常會呼叫此方法
        public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
            super.exceptionCaught(session, cause);
            Log.d("Mina", "伺服器出現異常:" + cause.toString());
        }

    }

}

這還不算完,因為這個App需要聯網,所以別忘了新增許可權。
開啟 AndroidManifest.xml(許可權有的用不到,帶著沒妨礙)

<!-- **************** 網 絡 ***************** -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

4、Mina服務端到此就完成了,下面執行下試試,我是用的真機除錯的,所以不方便圖片展示,下圖只展示Log中的內容。
這裡寫圖片描述


二、在iOS上建立Socket客戶端(Swift3.0)

1、下載和使用CocoaAsyncSocket。
https://github.com/robbiehanson/CocoaAsyncSocket
下載完成解壓得到如下圖資料夾。
這裡寫圖片描述

下面開始將它匯入到我們的App專案中,將上圖圈中的兩個檔案直接拖進xCode左邊欄的工程中,如下圖。
這裡寫圖片描述

將這兩個檔案拖進xCode後它會提示你是否建立一個Bridge-Header,就點選Create。
然後點選這個Bridge-Header檔案並輸入。

#import "GCDAsyncSocket.h"

這時候按下Command+B重新編譯一下。這樣CocoaAsyncSocket就匯入完畢了。

2、客戶端程式

介面展示
這裡寫圖片描述

編寫程式碼,進入ViewController.Swift

//
//  ViewController.swift
//  AsyncSocket_Exp
//
//  Created by 大老虎 on 2016/11/29.
//  Copyright © 2016年 Tiger. All rights reserved.
//

import UIKit

class ViewController: UIViewController, GCDAsyncSocketDelegate {

    @IBOutlet weak var serveripInput: UITextField!
    @IBOutlet weak var msgInput: UITextField!
    @IBOutlet weak var conBtn: UIButton!
    @IBOutlet weak var sendBtn: UIButton!
    @IBOutlet weak var msgView: UITextView!

    let serverPort: UInt16 = 4000 // 和上面的服務端一致

    var clientSocket:GCDAsyncSocket!

    override func viewDidLoad() {
        super.viewDidLoad()
        sendBtn.isEnabled = false // Socket未連線成功時傳送按鈕不能用
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // 連線伺服器按鈕事件
    @IBAction func conBtnClick(_ sender: AnyObject) {
        if serveripInput.text?.isEmpty == false { // 如果IP地址不為空則開始連線Socket
            clientSocket = GCDAsyncSocket()
            clientSocket.delegate = self
            clientSocket.delegateQueue = DispatchQueue.global()
            do {
                try clientSocket.connect(toHost: serveripInput.text!, onPort: serverPort)
            } catch {
                print("error")
                conBtn.backgroundColor = UIColor.red
            }
        } else {
            msgView.insertText("IP地址不能為空!\n")
        }
    }

    func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) -> Void {
        print("連線成功")
        sendBtn.isEnabled = true // 連線成功後傳送按鈕設為可用
        clientSocket.readData(withTimeout: -1, tag: 0)
    }

    func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) -> Void {
        print("與伺服器斷開連線")
    }

    func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) -> Void {
        // 1、獲取客戶端發來的資料,把 NSData 轉 NSString
        let readClientDataString: NSString? = NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)
        print("---Data Recv---")
        print(readClientDataString)

        // 2、主介面UI顯示資料
        DispatchQueue.main.async {
            let showStr: NSMutableString = NSMutableString()
            showStr.append(self.msgView.text)
            showStr.append(readClientDataString! as String)
            showStr.append("\n")
            self.msgView.text = showStr as String
        }

        // 3、處理請求,返回資料給客戶端OK
        // let serviceStr: NSMutableString = NSMutableString()
        // serviceStr.append("OK\r\n")
        // clientSocket.write(serviceStr.data(using: String.Encoding.utf8.rawValue)!, withTimeout: -1, tag: 0)

        // 4、每次讀完資料後,都要呼叫一次監聽資料的方法
        clientSocket.readData(withTimeout: -1, tag: 0)
    }

    // 傳送訊息按鈕事件
    @IBAction func sendBtnClick(_ sender: AnyObject) {
        if msgInput.text?.isEmpty == false { // 如果訊息不為空則傳送
            let serviceStr: NSMutableString = NSMutableString()
            serviceStr.append(self.msgInput.text!)
            serviceStr.append("\r\n")
            clientSocket.write(serviceStr.data(using: String.Encoding.utf8.rawValue)!, withTimeout: -1, tag: 0)
        }
    }

}

這段程式碼和上一篇博文99%是一樣的,只是在傳送的地方不同,每次傳送的結尾都要加上\r\n,只有這樣才能保證Android Mina服務端能收到我們傳送的資訊,否則,客戶端只能與服務端建立連線,而客戶端傳送過去的資訊服務端無法正常處理。


三、跨平臺通訊測試

1、開啟服務端和客戶端程式;
2、看服務端的Log中是否建立服務端成功;
3、在客戶端輸入服務端的ip地址,這個ip地址指的是區域網中的地址,外網ip地址我沒測試過,點選連線。雙方Log是否連線成功。
4、客戶端傳送資訊給服務端,服務端收到資訊反饋給客戶端,客戶端接受到反饋資訊,完畢。
5、關閉客戶端,服務端Log顯示斷開連線。
6、測試完畢。

下面是我測試的圖片,我的服務端使用的三星平板電腦,客戶端使用的模擬器iphone6s 10.0,客戶端也在真機上跑過,沒問題,真機使用的iphoneSE 10.1.1。
這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

大功告成啦!大家可以自己試一試,這裡不提供原始碼下載了,所有核心程式碼都在文中了。大家加油!