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
- <?xmlversion="1.0"encoding="utf-8"?>
- <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <TextView
- android:id="@+id/netinfo"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:text="network information"
- />
- </LinearLayout>
(2)、Activity所在.java檔案NetworkExplorer.java
- package com.android.sample.NetworkExplorer;
- import android.app.Activity;
- import android.content.Context;
- import android.net.ConnectivityManager;
- import android.net.NetworkInfo;
- import android.os.Bundle;
- import android.widget.TextView;
- publicclass NetworkExplorer extends Activity {
- ConnectivityManager cgr;
- NetworkInfo netinfo;
- TextView netinfo_tv;
- /** Called when the activity is first created. */
- @Override
- publicvoid onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- netinfo_tv = (TextView)findViewById(R.id.netinfo);
- cgr = (ConnectivityManager)this.getSystemService(Context.CONNECTIVITY_SERVICE);
- }
- @Override
- protectedvoid onStart() {
- super.onStart();
- netinfo = cgr.getActiveNetworkInfo();
- netinfo_tv.setText(netinfo.toString());
- }
- }
最後,我的捉蟲之旅
啟動emulator,並執行上面的程式碼,結果螢幕顯示一個大大的Sorry,看著就來氣。看看logcat給出下面一大段Error資訊:
- ERROR/AndroidRuntime(1985): FATAL EXCEPTION: main
- ERROR/AndroidRuntime(1985): java.lang.RuntimeException: Unable to start activity ComponentInfo{com.android.sample.
- NetworkExplorer/com.android.sample.NetworkExplorer.NetworkExplorer}: java.lang.SecurityException:
- ConnectivityService: Neither user 10042 nor current process has
- android.permission.ACCESS_NETWORK_STATE.
- ERROR/AndroidRuntime(1985): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1622)
- ERROR/AndroidRuntime(1985): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1638)
- ERROR/AndroidRuntime(1985): at android.app.ActivityThread.access$1500(ActivityThread.java:117)
- ERROR/AndroidRuntime(1985): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:928)
- ERROR/AndroidRuntime(1985): at android.os.Handler.dispatchMessage(Handler.java:99)
- ERROR/AndroidRuntime(1985): at android.os.Looper.loop(Looper.java:123)
- ERROR/AndroidRuntime(1985): at android.app.ActivityThread.main(ActivityThread.java:3647)
- ERROR/AndroidRuntime(1985): at java.lang.reflect.Method.invokeNative(Native Method)
- ERROR/AndroidRuntime(1985): at java.lang.reflect.Method.invoke(Method.java:507)
- ERROR/AndroidRuntime(1985): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
- ERROR/AndroidRuntime(1985): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
- ERROR/AndroidRuntime(1985): at dalvik.system.NativeStart.main(Native Method)
- ERROR/AndroidRuntime(1985): Caused by: java.lang.SecurityException: ConnectivityService:
- Neither user 10042 nor current process has android.permission.ACCESS_NETWORK_STATE.
- ERROR/AndroidRuntime(1985): at android.os.Parcel.readException(Parcel.java:1322)
- ERROR/AndroidRuntime(1985): at android.os.Parcel.readException(Parcel.java:1276)
- ERROR/AndroidRuntime(1985): at android.net.IConnectivityManager$Stub$Proxy.getActiveNetworkInfo(IConnectivityManager.
- java:345)
- ERROR/AndroidRuntime(1985): at android.net.ConnectivityManager.getActiveNetworkInfo(ConnectivityManager.java:242)
- ERROR/AndroidRuntime(1985): at com.android.sample.NetworkExplorer.NetworkExplorer.onStart(NetworkExplorer.java:29)
- ERROR/AndroidRuntime(1985): at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1129)
- ERROR/AndroidRuntime(1985): at android.app.Activity.performStart(Activity.java:3791)
- ERROR/AndroidRuntime(1985): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1595)
- ERROR/AndroidRuntime(1985): ... 11 more
- ......(略)
- <applicationandroid:icon="@drawable/icon"
- ......(略)
- </application>
- <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
- package com.android.sample.SocketServerDemo;
- import android.app.Activity;
- import android.content.Intent;
- import android.os.Bundle;
- publicclass SocketServerDemo extends Activity{
- @Override
- protectedvoid onCreate(Bundle savedInstanceState) {
- // TODO Auto-generated method stub
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- System.out.println("begin start service");
- this.startService(new Intent(this, SocketService.class));
- }
- @Override
- protectedvoid onDestroy() {
- // TODO Auto-generated method stub
- super.onDestroy();
- this.stopService(new Intent(this, SocketService.class));
- }
- }
- package com.android.sample.SocketServerDemo;
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStreamWriter;
- import java.io.PrintWriter;
- import java.net.ServerSocket;
- import java.net.Socket;
- import android.app.Service;
- import android.content.Intent;
- import android.os.IBinder;
- publicclass SocketService extends Service{
- Thread mServiceThread;
- Socket client;
- @Override
- public IBinder onBind(Intent intent) {
- // TODO Auto-generated method stub
- returnnull;
- }
- @Override
- publicvoid onCreate() {
- // TODO Auto-generated method stub
- super.onCreate();
- mServiceThread = new Thread(new SocketServerThread());
- }
- @Override
- publicvoid onStart(Intent intent, int startId) {
- // TODO Auto-generated method stub
- super.onStart(intent, startId);
- mServiceThread.start();
- }
- @Override
- publicvoid onDestroy() {
- // TODO Auto-generated method stub
- super.onDestroy();
- }
- publicclass SocketServerThread extends Thread {
- privatestaticfinalint PORT = 54321;
- private SocketServerThread(){
- }
- @Override
- publicvoid run() {
- try {
- ServerSocket server = new ServerSocket(PORT);
- while(true){
- System.out.println("begin client connected");
- client = server.accept();
- System.out.println("client connected");
- BufferedReader reader = new BufferedReader(new InputStreamReader(client.getInputStream()));
- System.out.println("read from client:");
- String textLine = reader.readLine();
- if(textLine.equalsIgnoreCase("EXIT")){
- System.out.println("EXIT invoked, closing client");
- break;
- }
- System.out.println(textLine);
- PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())));
- writer.println("ECHO from server: " + textLine);
- writer.flush();
- writer.close();
- reader.close();
- }
- } catch (IOException e) {
- // TODO Auto-generated catch block
- System.err.println(e);
- }
- }
- }
- }
- <?xmlversion="1.0"encoding="utf-8"?>
- <manifestxmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.sample.SocketServerDemo"
- android:versionCode="1"
- android:versionName="1.0">
- <uses-sdkandroid:minSdkVersion="9"/>
- <applicationandroid:icon="@drawable/icon"android:label="@string/app_name">
- <activityandroid:name=".ScreenCastServer"
- android:label="@string/app_name">
- <intent-filter>
- <actionandroid:name="android.intent.action.MAIN"/>
- <categoryandroid:name="android.intent.category.LAUNCHER"/>
- </intent-filter>
- </activity>
- <serviceandroid:name="com.android.sample.SocketServerDemo.SocketService">
- </service>
- </application>
- <uses-permissionandroid:name="android.permission.INTERNET"/>
- </manifest>
1)、佈局檔案main.xml
- <?xmlversion="1.0"encoding="utf-8"?>
- <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <TextView
- android:id="@+id/receive_msg"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- />
- <EditText
- android:id="@+id/edit_msg"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- />
- <Button
- android:id="@+id/send_msg"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="send"
- />
- </LinearLayout>
- package com.android.sample.SocketClientDemo;
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.io.OutputStreamWriter;
- import java.io.PrintWriter;
- import java.net.Socket;
- import java.net.UnknownHostException;
- import android.app.Activity;
- import android.os.Bundle;
- import android.view.View;
- import android.view.View.OnClickListener;
- import android.widget.Button;
- import android.widget.EditText;
- import android.widget.TextView;
- publicclass SocketClientDemo extends Activity {
- privatestaticfinal String SERVERIP = "192.168.1.68";
- privatestaticfinalint SERVERPORT = 54321;
- TextView mMsgRev;
- EditText mMsgEdit;
- Button mMsgSendBtn;
- String mSendMsg;
- String mReceivedMsg;
- /** Called when the activity is first created. */
- @Override
- publicvoid onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- mMsgRev = (TextView)findViewById(R.id.receive_msg);
- mMsgEdit = (EditText)findViewById(R.id.edit_msg);
- mMsgSendBtn = (Button)findViewById(R.id.send_msg);
- mMsgSendBtn.setOnClickListener(new OnClickListener(){
- @Override
- publicvoid onClick(View v) {
- Socket socket = null;
- mSendMsg = mMsgEdit.getText().toString();
- try {
- socket = new Socket(SERVERIP, SERVERPORT);
- PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
- writer.println(mSendMsg);
- writer.flush();
- BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- mReceivedMsg = reader.readLine();
- if(mReceivedMsg != null){
- mMsgRev.setText(mReceivedMsg);
- }else{
- mMsgRev.setText("receive data error");
- }
- writer.close();
- reader.close();
- socket.close();
- } catch (UnknownHostException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- }
- });
- }
- }
5、執行環境搭建
(三)網路狀態檢測
前面寫過一篇關於網路狀態檢測的博文章,看連線點選開啟連結。那片文章中,只是檢測當前處於活動狀態的網路。而且,還有一個不確定的問題:當裝置中有多個可用的活動網路時,也只能顯示其中之一。在本文中,給出列舉當前裝置中所有網路及其狀態的方法。
實現的方法很簡單,修改連線文章中,sample程式中的類NetworkExplorer.java的程式碼如下:
- package com.android.sample.NetworkExplorer;
- import android.app.Activity;
- import android.content.Context;
- import android.net.ConnectivityManager;
- import android.net.NetworkInfo;
- import android.os.Bundle;
- import android.widget.TextView;
- publicclass NetworkExplorer extends Activity {
- ConnectivityManager cgr;
- NetworkInfo netinfo_arry[];
- TextView netinfo_tv;
- int i;
- /** Called when the activity is first created. */
- @Override
- publicvoid onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- netinfo_tv = (TextView)findViewById(R.id.netinfo);
- netinfo_tv.setEnabled(false);
- cgr = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
- }
- @Override
- protectedvoid onStart() {
- super.onStart();
- netinfo_arry = cgr.getAllNetworkInfo();
- for(i = 0; i < netinfo_arry.length; i++){
- netinfo_tv.append("Net " + (i + 1) + ": " + netinfo_arry[i].toString() + "\n\n");
- }
- }
- }
1、模擬器上:
2、開發板上: