個人更喜歡著眼於執行個體,從最簡單的開始,一步步進行測試。
理論什麼的先放一邊,把程式跑起來再說。只有跑起來了,才會有動力去繼續往下學,參透整個代碼的運行機制。
本次的執行個體目標是——
類比一個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通訊的實現與歸納:
http://blog.csdn.net/shenpibaipao/article/details/70237697
Java輸入輸出資料流詳解:
http://blog.csdn.net/zsw12013/article/details/6534619
Socket 通訊原理(Android用戶端和伺服器以TCP&&UDP方式互連):
http://blog.csdn.net/mad1989/article/details/9147661