1. 程式人生 > >Android網路程式設計實踐之旅

Android網路程式設計實踐之旅

(一):網路狀態檢測
  

一直以來本人都在做Android Multi-Media Framework下的Lib支援庫的開發和修改,終於最近告一段落,但根據專案要求,需要寫一個和網路相關的service,用java來實現。其實,在Framework及其之上的應用層用java開發,本人並不陌生,此前也做過一段時間,包括定製View,實現介面特效以及多媒體播放器和音樂編輯器,都做過。唯一遺憾的是,自進入嵌入式領域以來,從來沒有做過網路相關的程式設計,此次,欣然答應,就是想借機來填補下個人職業生涯中的一項空白,呵呵,於私於公都是有利的。    開始進入正題。    

        網路狀態檢測的目的是檢測當前裝置是否已經連線到網路,屬於何種型別,是否可用等資訊,這些是進行正常網路通訊的前提。這裡需要說明,這裡提供的sample程式,如無說明,預設都是在emulator上執行的,OS是2.3版本的。

        首先,介紹網路狀態檢測的兩個核心class

        1)、ConnectivityManager($SOURCE/frameworks/base/core/java/android/net/ConnectivityManager.java):給出網路連線狀態,並在網路連線改變時(如由Wi-Fi連線變為Bluetooth連線)通知應用程式,主要職責有以下幾個:

               (1)、監視網路連線(Wi-Fi,GPRS,UMTS,BT等等);

               (2)、在網路連線發生變化時,嚮應用傳送broadcast  Intent;

               (3)、在網路連線失敗時,嘗試進行“失敗轉接”到其它可用網路

               (4)、提供API,允許應用程式查詢可用網路的粗粒度和細粒度狀態

      2)、NetworkInfo($SOURCE/frameworks/base/core/java/android/net/NetworkInfo.java):描述給定型別的網路介面的狀態,截止到2.3.4(3.0以上的原始碼尚未開放),網路連線僅支援Mobile和Wi-Fi。順便說一下,該class是個網路狀態資訊儲存體,實現了Parcelable class中的部分介面,其餘的介面都是進行網路狀態的設定和查詢。值得注意的是:NetworkInfo原始碼中提到兩個概念:coarse-grained state(粗粒度狀態)和fine-grained state(細粒度狀態),這和1)的第四點職責相對應。通過分析原始碼,coarse-grained state包括:CONNECTING(正在連線), CONNECTED(已連線), SUSPENDED(掛起), DISCONNECTING(正在斷開連線),DISCONNECTED(連線斷開),UNKNOWN(未知狀態)。對於fine-grained state這裡也不做太多說明,根據原始碼註釋來看,應用程式基本上用的都是coarse-grained state,極少使用fine-grained state。當然,分析原始碼我們可以看出,實際的網路連線是按fine-grained state進行狀態遷移的,只是Android已經進行了fine-grained state到coarse-grained state的對映,是通過一個狀態對映表來完成的,舉例來說:如將fine-grained state的(IDLE+SCANNING+CONNECTING+AUTHENTICATING+OBTAINING_IPADDR)都對映為coarse-grained state的DISCONNECTED。

        其次,我的sample程式:

(1)、main.xml

  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3.     android:orientation="vertical"
  4.     android:layout_width="fill_parent"
  5.     android:layout_height="fill_parent"
  6.     >
  7.     <TextView
  8.         android:id="@+id/netinfo"
  9.         android:layout_width="fill_parent"
  10.         android:layout_height="wrap_content"
  11.         android:text="network information"
  12.         />
  13. </LinearLayout>

(2)、Activity所在.java檔案NetworkExplorer.java

  1. package com.android.sample.NetworkExplorer;  
  2. import android.app.Activity;  
  3. import android.content.Context;  
  4. import android.net.ConnectivityManager;  
  5. import android.net.NetworkInfo;  
  6. import android.os.Bundle;  
  7. import android.widget.TextView;  
  8. publicclass NetworkExplorer extends Activity {  
  9.     ConnectivityManager cgr;  
  10.     NetworkInfo netinfo;  
  11.     TextView netinfo_tv;  
  12.     /** Called when the activity is first created. */
  13.     @Override
  14.     publicvoid onCreate(Bundle savedInstanceState) {  
  15.         super.onCreate(savedInstanceState);  
  16.         setContentView(R.layout.main);  
  17.         netinfo_tv = (TextView)findViewById(R.id.netinfo);  
  18.         cgr = (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);  
  19.     }  
  20.     @Override
  21.     protectedvoid onStart() {  
  22.         super.onStart();  
  23.         netinfo = cgr.getActiveNetworkInfo();  
  24.         netinfo_tv.setText(netinfo.toString());  
  25.     }  
  26. }  

        最後,我的捉蟲之旅

        啟動emulator,並執行上面的程式碼,結果螢幕顯示一個大大的Sorry,看著就來氣。看看logcat給出下面一大段Error資訊:

  1. ERROR/AndroidRuntime(1985): FATAL EXCEPTION: main  
  2. ERROR/AndroidRuntime(1985): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.android.sample.  
  3.                             NetworkExplorer/com.android.sample.NetworkExplorer.NetworkExplorer}: java.lang.SecurityException:   
  4.                             ConnectivityService: Neither user 10042 nor current process has   
  5.                             android.permission.ACCESS_NETWORK_STATE.  
  6. ERROR/AndroidRuntime(1985):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1622)  
  7. ERROR/AndroidRuntime(1985):     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1638)  
  8. ERROR/AndroidRuntime(1985):     at android.app.ActivityThread.access$1500(ActivityThread.java:117)  
  9. ERROR/AndroidRuntime(1985):     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:928)  
  10. ERROR/AndroidRuntime(1985):     at android.os.Handler.dispatchMessage(Handler.java:99)  
  11. ERROR/AndroidRuntime(1985):     at android.os.Looper.loop(Looper.java:123)  
  12. ERROR/AndroidRuntime(1985):     at android.app.ActivityThread.main(ActivityThread.java:3647)  
  13. ERROR/AndroidRuntime(1985):     at java.lang.reflect.Method.invokeNative(Native Method)  
  14. ERROR/AndroidRuntime(1985):     at java.lang.reflect.Method.invoke(Method.java:507)  
  15. ERROR/AndroidRuntime(1985):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)  
  16. ERROR/AndroidRuntime(1985):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)  
  17. ERROR/AndroidRuntime(1985):     at dalvik.system.NativeStart.main(Native Method)  
  18. ERROR/AndroidRuntime(1985): Caused by: java.lang.SecurityException: ConnectivityService:   
  19.                             Neither user 10042 nor current process has android.permission.ACCESS_NETWORK_STATE.  
  20. ERROR/AndroidRuntime(1985):     at android.os.Parcel.readException(Parcel.java:1322)  
  21. ERROR/AndroidRuntime(1985):     at android.os.Parcel.readException(Parcel.java:1276)  
  22. ERROR/AndroidRuntime(1985):     at android.net.IConnectivityManager$Stub$Proxy.getActiveNetworkInfo(IConnectivityManager.  
  23.                                 java:345)  
  24. ERROR/AndroidRuntime(1985):     at android.net.ConnectivityManager.getActiveNetworkInfo(ConnectivityManager.java:242)  
  25. ERROR/AndroidRuntime(1985):     at com.android.sample.NetworkExplorer.NetworkExplorer.onStart(NetworkExplorer.java:29)  
  26. ERROR/AndroidRuntime(1985):     at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1129)  
  27. ERROR/AndroidRuntime(1985):     at android.app.Activity.performStart(Activity.java:3791)  
  28. ERROR/AndroidRuntime(1985):     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1595)  
  29. ERROR/AndroidRuntime(1985):     ... 11 more  
從下而上來分析上面這段錯誤資訊,可以看出NetworkExplorer activity的onStart()函式執行NetworkExplorer.java的29行程式碼時引發的crash,對比原始碼看出,29行正好是ConnectivityManager的getActiveNetworkInfo( ),與錯誤資訊的下一步提示完全一致。按照同樣的思路往下推,最後到上面發現應用程式缺少網路訪問許可權:android.permission.ACCESS_NETWORK_STATE。於是在AndroidManifest.xml中為該sample程式新增網路訪問許可權,如下:
  1.    ......(略)  
  2. <applicationandroid:icon="@drawable/icon"
  3.    ......(略)  
  4. </application>
  5. <uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/>
再次執行程式,成功,輸出如下資訊:

分析emulator的網路連線如下:

1)、網路連線型別(type):主型別是mobile,子型別是UTMS(University Mobile Telecommunications System,3G);

2)、連線狀態(state):CONNECTED/CONNECTED(已連線);

3)、採用該連線的原因(reason):simLoaded(已載入sim卡),目前本人尚未留意android上網路連線的選擇原則,如:同時有Mobile和Wi-Fi,則選擇何種連線方式;

4)、附加資訊(extra):internet(可以使用IP network);

5)、是否漫遊(roaming):false,不解釋

6)、是否失敗轉接(failover):false,見前面ConnectivityManager職責說明中的第三點

7)、是否可用(isAvailable):true,不解釋

(二)Socket通訊機制

Socket(套接字)是一種通訊機制,可以實現單機或跨網路進行通訊,其建立需要明確的區分C(客戶端)/S(伺服器端),支援多個客戶端連線到同一個伺服器。有兩種傳輸模式:

1)、面向連線的傳輸:基於TCP協議,可靠性高,但效率低;

2)、面向無連線的傳輸:基於UDP協議,可靠性低,但效率高;

        Android中,直接採用Socket通訊應該是我們遇到的最低階的網路運用。儘管已經作了很大程度的抽象,但是純粹的Socket通訊,仍然給開發者留下很多細節需要處理,尤其在伺服器端,開發者需要處理多執行緒以及資料緩衝等的設計問題。相對而言,處於更高抽象層的HTTP等,已經對Socket通訊中需要處理的技術細節進行了很好的封裝,開發者無須關心,因此,HTTP在網路開發中通常具有決定性的優勢。

        Android在其核心庫的java包中,提供了用於客戶端的Socket class和用於伺服器端的ServerSocket class,分別位於$SOURCE/libcore/luni/src/main/java/java/net/Socket.java和$SOURCE/libcore/luni/src/main/java/java/net/ServerSocket.java檔案中。分析兩個class的原始碼,可以看出封裝考慮的很全面,只構造方法一向每個class都考慮了很多種使用情況。由於本人只是初學者,很多理解的不深入,這裡只拋磚引玉的對兩個class的構造方法分別介紹一種,就是我下面的程式中用到的:

Socket(String dstName, int dstPort):建立一個以流的方式(基於TCP協議)連線到目標機(這裡可以理解為伺服器)的客戶端Socket;dstName是目標機的IP地址,dstPort是要連線的目標機的端  口號。這裡要注意對埠的理解,它不能理解為物理上的一個介面,而是對計算機中一塊特殊記憶體區域的形象表述。

ServerSocket(int aport):建立一個繫結到本機指定埠的服務端Socket;aport就是指定的本機埠。與上述客戶端Socket對應,通過TCP連線時,ServerSocket建立後需要在aport埠上進行監聽,等待客戶端的連線。

        上面所寫都是些背景知識,下面對本人的程式設計實踐進行詳細說明。

1、功能描述

     1)、簡單的基於Socket的資料通訊;

     2)、採用TCP方式連線;

     3)、採用C/S結構,但服務端只支援一個連線;

     4)、客戶端能夠向服務端傳送資料,並顯示服務端的返回資訊;

     5)、服務端能夠接收客戶端的資料,並將收到的資料以特定的方式返回給客戶端;

2、程式實現思路

    1)、服務端:設計為在後臺執行的service,用一個獨立的執行緒來處理客戶端的連線請求、資料接收和返回。為了啟動該service,編寫個簡單的Activity。

    2)、客戶端:設計為一個Activity,介面由三部分組成:顯示服務端返回資訊的文字區域(一個文字框);進行資料輸入的編輯區域(一個編輯框);以及觸發連線請求並執行資料傳送的觸發區域(一個按鈕)。

3、服務端源程式

    1)、Activity檔案SocketServerDemo.java

  1. package com.android.sample.SocketServerDemo;  
  2. import android.app.Activity;  
  3. import android.content.Intent;  
  4. import android.os.Bundle;  
  5. publicclass SocketServerDemo extends Activity{  
  6.     @Override
  7.     protectedvoid onCreate(Bundle savedInstanceState) {  
  8.         // TODO Auto-generated method stub
  9.         super.onCreate(savedInstanceState);  
  10.         setContentView(R.layout.main);  
  11.         System.out.println("begin start service");   
  12.         this.startService(new Intent(this, SocketService.class));  
  13.     }  
  14.     @Override
  15.     protectedvoid onDestroy() {  
  16.         // TODO Auto-generated method stub
  17.         super.onDestroy();  
  18.         this.stopService(new Intent(this, SocketService.class));  
  19.     }  
  20. }  
    2)、service檔案SocketService.java
  1. package com.android.sample.SocketServerDemo;  
  2. import java.io.BufferedReader;  
  3. import java.io.BufferedWriter;  
  4. import java.io.IOException;  
  5. import java.io.InputStreamReader;  
  6. import java.io.OutputStreamWriter;  
  7. import java.io.PrintWriter;  
  8. import java.net.ServerSocket;  
  9. import java.net.Socket;  
  10. import android.app.Service;  
  11. import android.content.Intent;  
  12. import android.os.IBinder;  
  13. publicclass SocketService extends Service{  
  14.     Thread mServiceThread;  
  15.     Socket client;  
  16.     @Override
  17.     public IBinder onBind(Intent intent) {  
  18.         // TODO Auto-generated method stub
  19.         returnnull;  
  20.     }  
  21.     @Override
  22.     publicvoid onCreate() {  
  23.         // TODO Auto-generated method stub
  24.         super.onCreate();  
  25.         mServiceThread = new Thread(new SocketServerThread());  
  26.     }  
  27.     @Override
  28.     publicvoid onStart(Intent intent, int startId) {  
  29.         // TODO Auto-generated method stub
  30.         super.onStart(intent, startId);  
  31.         mServiceThread.start();  
  32.     }  
  33.     @Override
  34.     publicvoid onDestroy() {  
  35.         // TODO Auto-generated method stub
  36.         super.onDestroy();  
  37.     }  
  38.     publicclass SocketServerThread extends Thread {  
  39.         privatestaticfinalint PORT = 54321;  
  40.         private SocketServerThread(){  
  41.         }  
  42.         @Override
  43.         publicvoid run() {  
  44.             try {  
  45.                 ServerSocket server = new ServerSocket(PORT);  
  46.                 while(true){  
  47.                     System.out.println("begin client connected");  
  48.                     client = server.accept();  
  49.                     System.out.println("client connected");  
  50.                     BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));  
  51.                     System.out.println("read from client:");  
  52.                     String textLine = reader.readLine();  
  53.                     if(textLine.equalsIgnoreCase("EXIT")){  
  54.                         System.out.println("EXIT invoked, closing client");  
  55.                         break;  
  56.                     }  
  57.                     System.out.println(textLine);  
  58.                     PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())));  
  59.                     writer.println("ECHO from server: " + textLine);  
  60.                     writer.flush();  
  61.                     writer.close();  
  62.                     reader.close();  
  63.                 }                             
  64.             } catch (IOException e) {  
  65.                 // TODO Auto-generated catch block
  66.                 System.err.println(e);  
  67.             }     
  68.         }  
  69.     }  
  70. }  
    3)、AndroidManifest.xml檔案,因為需要在其中新增service和網路訪問許可權,這裡一併貼出
  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  3.       package="com.android.sample.SocketServerDemo"
  4.       android:versionCode="1"
  5.       android:versionName="1.0">
  6.     <uses-sdkandroid:minSdkVersion="9"/>
  7.     <applicationandroid:icon="@drawable/icon"android:label="@string/app_name">
  8.         <activityandroid:name=".ScreenCastServer"
  9.                   android:label="@string/app_name">
  10.             <intent-filter>
  11.                 <actionandroid:name="android.intent.action.MAIN"/>
  12.                 <categoryandroid:name="android.intent.category.LAUNCHER"/>
  13.             </intent-filter>
  14.         </activity>
  15.         <serviceandroid:name="com.android.sample.SocketServerDemo.SocketService">
  16.         </service>
  17.     </application>
  18.     <uses-permissionandroid:name="android.permission.INTERNET"/>
  19. </manifest>
4、客戶端程式

    1)、佈局檔案main.xml

  1. <?xmlversion="1.0"encoding="utf-8"?>
  2. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  3.     android:orientation="vertical"
  4.     android:layout_width="fill_parent"
  5.     android:layout_height="fill_parent"
  6.     >
  7.     <TextView
  8.         android:id="@+id/receive_msg"
  9.         android:layout_width="fill_parent"
  10.         android:layout_height="wrap_content"
  11.         />
  12.     <EditText
  13.         android:id="@+id/edit_msg"
  14.         android:layout_width="fill_parent"
  15.         android:layout_height="wrap_content"
  16.         />
  17.     <Button
  18.         android:id="@+id/send_msg"
  19.         android:layout_width="wrap_content"
  20.         android:layout_height="wrap_content"
  21.         android:text="send"
  22.         />
  23. </LinearLayout>
    2)、Activity檔案SocketClientDemo.java
  1. package com.android.sample.SocketClientDemo;  
  2. import java.io.BufferedReader;  
  3. import java.io.BufferedWriter;  
  4. import java.io.IOException;  
  5. import java.io.InputStreamReader;  
  6. import java.io.OutputStreamWriter;  
  7. import java.io.PrintWriter;  
  8. import java.net.Socket;  
  9. import java.net.UnknownHostException;  
  10. import android.app.Activity;  
  11. import android.os.Bundle;  
  12. import android.view.View;  
  13. import android.view.View.OnClickListener;  
  14. import android.widget.Button;  
  15. import android.widget.EditText;  
  16. import android.widget.TextView;  
  17. publicclass SocketClientDemo extends Activity {  
  18.     privatestaticfinal String SERVERIP = "192.168.1.68";  
  19.     privatestaticfinalint SERVERPORT = 54321;  
  20.     TextView mMsgRev;  
  21.     EditText mMsgEdit;  
  22.     Button   mMsgSendBtn;  
  23.     String mSendMsg;  
  24.     String mReceivedMsg;  
  25.     /** Called when the activity is first created. */
  26.     @Override
  27.     publicvoid onCreate(Bundle savedInstanceState) {  
  28.         super.onCreate(savedInstanceState);  
  29.         setContentView(R.layout.main);  
  30.         mMsgRev = (TextView)findViewById(R.id.receive_msg);  
  31.         mMsgEdit = (EditText)findViewById(R.id.edit_msg);  
  32.         mMsgSendBtn = (Button)findViewById(R.id.send_msg);  
  33.         mMsgSendBtn.setOnClickListener(new OnClickListener(){  
  34.             @Override
  35.             publicvoid onClick(View v) {  
  36.                 Socket socket = null;  
  37.                 mSendMsg = mMsgEdit.getText().toString();  
  38.                 try {  
  39.                     socket = new Socket(SERVERIP, SERVERPORT);  
  40.                     PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));  
  41.                     writer.println(mSendMsg);  
  42.                     writer.flush();  
  43.                     BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));   
  44.                     mReceivedMsg = reader.readLine();  
  45.                     if(mReceivedMsg != null){  
  46.                         mMsgRev.setText(mReceivedMsg);  
  47.                     }else{  
  48.                         mMsgRev.setText("receive data error");  
  49.                     }  
  50.                     writer.close();  
  51.                     reader.close();  
  52.                     socket.close();  
  53.                 } catch (UnknownHostException e) {  
  54.                     // TODO Auto-generated catch block
  55.                     e.printStackTrace();  
  56.                 } catch (IOException e) {  
  57.                     // TODO Auto-generated catch block
  58.                     e.printStackTrace();  
  59.                 }  
  60.             }             
  61.         });  
  62.     }  
  63. }  
    3)、在AndroidManifest.xml中向伺服器端的該檔案一樣,新增網路訪問許可權:<uses-permission android:name="android.permission.INTERNET"/>


5、執行環境搭建

        服務端程式在開發板上執行,客戶端程式在模擬器上執行,實現了基於Socket的資料收發。但是我這裡只是進行了區域網內的通訊,至於跨網路能不能行,目前條件不夠,沒法進行驗證。

(三)網路狀態檢測

前面寫過一篇關於網路狀態檢測的博文章,看連線點選開啟連結。那片文章中,只是檢測當前處於活動狀態的網路。而且,還有一個不確定的問題:當裝置中有多個可用的活動網路時,也只能顯示其中之一。在本文中,給出列舉當前裝置中所有網路及其狀態的方法。

        實現的方法很簡單,修改連線文章中,sample程式中的類NetworkExplorer.java的程式碼如下:

  1. package com.android.sample.NetworkExplorer;  
  2. import android.app.Activity;  
  3. import android.content.Context;  
  4. import android.net.ConnectivityManager;  
  5. import android.net.NetworkInfo;  
  6. import android.os.Bundle;  
  7. import android.widget.TextView;  
  8. publicclass NetworkExplorer extends Activity {  
  9.     ConnectivityManager cgr;  
  10.     NetworkInfo netinfo_arry[];  
  11.     TextView netinfo_tv;  
  12.     int i;  
  13.     /** Called when the activity is first created. */
  14.     @Override
  15.     publicvoid onCreate(Bundle savedInstanceState) {  
  16.         super.onCreate(savedInstanceState);  
  17.         setContentView(R.layout.main);  
  18.         netinfo_tv = (TextView)findViewById(R.id.netinfo);  
  19.         netinfo_tv.setEnabled(false);  
  20.         cgr = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);  
  21.     }  
  22.     @Override
  23.     protectedvoid onStart() {  
  24.         super.onStart();  
  25.         netinfo_arry = cgr.getAllNetworkInfo();  
  26.         for(i = 0; i < netinfo_arry.length; i++){              
  27.             netinfo_tv.append("Net " + (i + 1) + ": " + netinfo_arry[i].toString() + "\n\n");  
  28.         }  
  29.     }  
  30. }  
其餘程式碼不需要作任何改動,再次執行程式結果如下:

1、模擬器上:


2、開發板上:


        結合前面對網路狀態資訊的分析說明,可以很明顯的看出當前裝置上可用的各種網路的狀態,包括有線的ETH和無線的mobile。