1. 程式人生 > >Android TCP通訊的簡單例項以及常見問題[超時/主執行緒阻塞]

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方式互通):