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 Downloads — Mina x.x.xx,點選進入後如下圖。
點選標註的這項,進入下一個介面。
用它建議的這個連結下載Mina最新版,下載後是一個zip包。解壓它,如下圖。(我用的是Mina 2.0.15,都一樣)
好,到這裡Mina的準備工作算是完成了。
2、下面開始搭建Mina服務端,開啟Eclipse,建立Android工程(這裡將工程命名為MinaServer)。然後就是使用Mina了,就是在自己的工程中匯入Mina提供的第三方jar包。
在剛才解壓得到的Mina資料夾中找到 dist - mina-core-x.x.xx 和 lib - slf4j-api-x.x.xx這兩個Jar包,然後將他們拖到我們工程的libs資料夾中,選中它倆 右擊 - Build Path - Add to Build Path
(其實匯入包的方法不只這一種,還有別的方法,我也嘗試了,因為別的方法看上去比這一種顯得正規,但是在程式執行中會報錯: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。
大功告成啦!大家可以自己試一試,這裡不提供原始碼下載了,所有核心程式碼都在文中了。大家加油!