Android TCP通訊的簡單例項以及常見問題[超時/主執行緒阻塞]
個人更喜歡著眼於例項,從最簡單的開始,一步步進行測試。
理論什麼的先放一邊,把程式跑起來再說。只有跑起來了,才會有動力去繼續往下學,參透整個程式碼的執行機制。
本次的例項目標是——
模擬一個PC伺服器與android端的通訊,目標是儘量的做到精簡,使程式碼僅留下所需核心部分,降低筆記程式碼的閱讀難度。
--------------------------
>【例項】
PC上的伺服器的程式碼:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { //監聽埠12345 private static final int PORT = 12345; public static void main(String[] args) { try { System.out.println("等待客戶端"); ServerSocket serverSocket = new ServerSocket(PORT); Socket clientSocket = serverSocket.accept(); System.out.println("客戶端上線"); while (true) { //迴圈監聽客戶端請求 try { //獲取輸入流 BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); //獲取從客戶端發來的資訊 String msg = in.readLine(); System.out.println("客戶端訊息:"+msg); } catch (IOException e) { System.out.println("讀寫錯誤"); e.printStackTrace(); } finally { serverSocket.close(); clientSocket.close(); System.out.println("伺服器關閉"); break; } } } catch (Exception e) { System.out.println("埠被佔用"); e.printStackTrace(); } } }
從中可以看出伺服器的搭建主要有以下步驟:
1.建立伺服器的Socket,並設定一個監聽的埠PORT
ServerSocket serverSocket = new ServerSocket(PORT);
2.將伺服器的ServerSocket套接到客戶端的Socket上:(未套上時,會一直阻塞。諸位可以試試)
Socket clientSocket = serverSocket.accept();
由於需要進行迴圈監聽,因此獲取訊息的操作應放在一個while大迴圈中:
3.從客戶端發來的clientSocket上獲取輸入流的抽象類,然後例項化,並進行讀寫。
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String msg = in.readLine();
安卓上的客戶端程式碼:
從中,可以看到客戶端的搭建有以下步驟:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button b=(Button)findViewById(R.id.button); b.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(net).start();//新的子執行緒 } }); } Runnable net=new Runnable() { @Override public void run() { try { Socket socket; //socket=new Socket("192.168.1.102", 12345);//注意這裡 socket = new Socket(); SocketAddress socAddress = new InetSocketAddress("192.168.1.102", 12345); socket.connect(socAddress, 3000);//超時3秒 //傳送給服務端的訊息 String msg = "Good Night"; try { //獲取輸出流並例項化 BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); out.write(msg+"\n");//防止粘包 out.flush();//不加這個flush會怎樣? } catch (Exception e) { e.printStackTrace(); } finally { //關閉Socket socket.close(); System.out.println("客戶端關閉"); } } catch (Exception e) { System.out.println("連結錯誤"); e.printStackTrace(); } } };
1.建立客戶端本身的套接字Socket:
socket = new Socket();
2.建立一個連線(我知道這裡和程式碼不一樣,你可能會存疑,但請往下看)
socket = new Socket(ip,port);
3.傳送訊息:
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
out.write(msg);
out.flush();//不加這個flush會怎樣?
4.關閉客戶端Socket:socket.close();
現在,對於Android的TCP通訊功能還遠沒有完成,不過快了,先別急,繼續往下看:
觀察以上android客戶端程式碼。再想想,如何在Activity中使用相關的客戶端程式碼呢?為什麼我專門寫了一個執行緒來進行網路操作呢?
答:從4.0開始,安卓就已經不允許在主執行緒中進行網路相關的操作。這樣設計的原因是,由於網路的延遲、不確定性等因素,加之Socket本身在未套接上時是處於阻塞狀態的,如果在主執行緒中進行網路相關操作,就會導致整個app被嚴重阻塞。
因此,從4.0起,任何嘗試在主執行緒中進行網路操作的動作,都會導致丟擲“android.os.NetworkOnMainThreadException”的異常。
那我們該如何解決呢?
建立一個子執行緒,所以,你會在我給出的程式碼裡看到以下內容:
Runnable net=new Runnable() {
@Override
public void run() {
//網路操作
}
}
使用時,直接讓這個Runnable啟動就行了:new Thread(net).start();
此時你可能會注意到,我在客戶端對Socket的例項化,並沒有使用大多數人常寫的:
socket=new Socket("192.168.1.102", 12345);
而是這樣寫:
socket = new Socket();
SocketAddress socAddress = new InetSocketAddress("192.168.1.102", 12345);
socket.connect(socAddress, 3000);//超時3秒
為什麼呢?我們知道,socket在未連線上時,會一直處於阻塞狀態,而此時由於socket本身並未例項化,導致你無法對socket的超時時間進行設定。這往往會導致執行緒卡主很長一段時間,最後丟擲error110(TIME_OUT)。
我這麼寫,可以人為的設定超時時間,也便於進行多次的超時重播。
另外,android對許可權也卡得很死,有些機型或者系統版本根本不允許你的app使用網路。那麼我們需要在許可權中新增以下內容:
<!--允許應用程式改變網路狀態-->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--允許應用程式改變WIFI連線狀態-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<!--允許應用程式訪問有關的網路資訊-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--允許應用程式訪問WIFI網絡卡的網路資訊-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允許應用程式完全使用網路-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
現在可以測試這個例項了。如果你仍然無法成功連線你的PC伺服器,接著往下看。
------------------------
>【排錯】
從排錯的角度來看,我們需要做的,是從伺服器和客戶端兩個方面進行排錯。
首先,我們應當確保伺服器和埠是沒問題的。
在eclipse裡開啟伺服器,然後進入cmd,輸入talent localhost 12345。
如果成功,那麼eclipse的控制檯會輸出“客戶端已上線”。
然後,我們再測試android客戶端連線時輸入的PC伺服器的ip是沒問題的:(ip和port值請自行確定)
重啟伺服器,進入cmd輸入talent 192.168.1.102 12345。
如果這一步成功,但你的android客戶端依舊無法連線上你的PC伺服器,那麼,我想請你檢查以下你的windows防火牆的設定。
控制面板\系統和安全\Windows 防火牆 -> 高階設定 ->入站規則 ,看看以下這幾項是不是被防火牆禁止了:
現在再去測試一遍你的客戶端,看看是否能連上伺服器。大部分的樣例程式碼的超時問題(error:110)在此都可以被解決了。
如果還不行,我們接下來檢測安卓客戶端。
首先,確保你的手機給了你的app許可權。
接著,請檢查一遍你設定的埠port值,是不是處於1024以下,或者使用了常用軟體及敏感的port的值?如果是,請改成12345再進行測試。
基本上,到了這一步,只要編譯沒有報錯,操作正確,在安卓版本沒有發生大的變化的情況下,已經可以連上伺服器了。
----------------------
>【理論】
例項結合基礎,這裡找到了大手子的幾篇理論性的文章,可以參考我以上給出這個小例項,再進行深入學習:
Android UDP通訊的實現與歸納:
Java輸入輸出流詳解:
Socket 通訊原理(Android客戶端和伺服器以TCP&&UDP方式互通):