Android網路:由手機與手機WIFI互傳照片談Android TCP Socket開發要點

來源:互聯網
上載者:User

本文背景:區域網路內手機與手機利用wifi建立tcp串連,通過socket互傳照片。即一個手機當作伺服器,另一個手機是用戶端,用戶端可以看到伺服器指定檔案夾內的圖片縮圖,並選擇下載到本機。另外,用戶端會顯示本地某個檔案夾內的圖片縮圖,並選擇上傳到伺服器。總而言之本例中圖片的發送和接收都是雙向的。除發送圖片外,還含有字串(圖片的名字)、檔案夾內圖片的個數(重新整理適配器)的發送。

眾所周知,android常用的網路開發無外乎http和socket,其中http是應用程式層的協議,tcp是傳輸層。所以,http也是用socket封裝的,用起來更方便。由於是封裝過的,它提供了更強大的功能。socket又分為TCP和UDP,區域網路內TCP速度就很快了,鑒於區域網路內傳送東西不需要考慮流量,所以此種場合多用socket。首先看下本例的運行效果:

下面是用戶端初始介面:


點擊下載後進入下載介面,伺服器就開始給用戶端傳縮圖和圖片的名字了,如下:


選擇需要下載的圖片:


點擊確定按鈕後,伺服器就開始給用戶端傳大圖了,就是原始圖片,傳輸完畢後,彈出提示框:


點擊上面的確定後,就自動結束當前activity,返回到初始介面,就不附圖了。

下面是用戶端的上傳介面,將圖片上傳到伺服器上,這個沒啥難的,就是本地圖片產生縮圖填充到listview上,不涉及到網路部分。其傳輸,跟伺服器往用戶端傳東西是一樣的。:


最後來看下伺服器,由於伺服器沒任何UI上的要求,所以就是些簡單的log列印:


本以為很簡單的一個功能,但網上大多數是手機是用戶端,PC是服務端。雖然都是流傳輸,但伺服器在PC和手機上解析成圖片還是不大一樣的,中間走了很多彎路才搞定,另外,就是網上大多是簡單的收發,本例中既要傳輸圖片個數、圖片名字等,用戶端牽涉到切換到activity,要多個socket連結,還是有些麻煩的。下面把開發中遇到的問題、開發要點和步驟記錄下來。

1、許可權問題

這裡包括WIFI許可權,Internet許可權和檔案讀寫權限。我第一次的時候許可權沒添加完,來來回回折騰了好幾次。伺服器和用戶端的許可權是一樣的.

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />    <uses-permission android:name="android.permission.INTERNET" />    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

2、伺服器再開啟服務前,首先要 判斷WIFI是否串連,如果沒有WIFI串連應該彈出個提示框。如果有WIFI串連上了,則進一步提取出IP地址。我將其封裝到NetworkUtil類裡弄成靜態方法,如下:

package org.yanzi.util;import android.content.Context;import android.net.ConnectivityManager;import android.net.NetworkInfo;import android.net.wifi.WifiInfo;import android.net.wifi.WifiManager;public class NetworkUtil {/** *判斷wifi是否串連  * @param context * @return  */public static boolean isWiFiConnected(Context context){ConnectivityManager connectManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo networkInfo = connectManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);if(networkInfo.isConnected()){return true;}else{return false;}}/** * 得到wifi串連的IP地址 * @param context * @return */public static String getWifiIP(Context context){WifiManager wifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);WifiInfo wifiInfo = wifiManager.getConnectionInfo();int ipAddr = wifiInfo.getIpAddress();String ipStr =  int2string(ipAddr);return ipStr;}/** * 輸入int 得到String類型的ip地址 * @param i * @return */private static String int2string(int i){return (i & 0xFF)+ "." + ((i >> 8 ) & 0xFF) + "." + ((i >> 16 ) & 0xFF) +"."+((i >> 24 ) & 0xFF );}}

在wifi地址這塊,當時還參考了網上一個,但是轉出來的string類型的IP地址是錯誤的,上面貼的代碼是正確的。如果wifi沒有串連,則彈出提示框報錯:

public void showAlterDialog(){Dialog alterDialog = new AlertDialog.Builder(this).setTitle("警告").setMessage("當前WiFi沒有正常串連,請串連後再操作.").setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton("確定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {// TODO Auto-generated method stubdialog.dismiss();}}).create();alterDialog.show();}

3、伺服器點擊開啟服務後,如果WIFI串連好了。service沒有運行就要開啟Service了。當前Activity名字為ServerActivity,服務是TCPService。

if(!isServiceRun){startService(new Intent(ServerActivity.this,TCPService.class));noticeTextView.append("\n" + "服務已開啟," + "正在服務..."+"\n");manageButton.setText("銷毀共用服務");isServiceRun = true; }else{stopService(new Intent(ServerActivity.this, TCPService.class));noticeTextView.append("\n" + "服務已銷毀"+"\n");isServiceRun = false;manageButton.setText("開啟共用服務");}


4、伺服器端因為用了service,所以牽涉到Activity和Service的通訊問題,最常見的莫過於用廣播了。在ServerActivity裡定義一個繼承BroadcastReceiver的類。

/** * @author Administrator *Activity接收Service訊息,更新UI */public class MsgReceiver extends BroadcastReceiver{@Overridepublic void onReceive(Context context, Intent intent) {// TODO Auto-generated method stubString s = intent.getStringExtra("INFO");noticeTextView.append(s);}}

在onCreate()裡執行個體化並註冊:

msgReceiver = new MsgReceiver();IntentFilter intentFilter = new IntentFilter();intentFilter.addAction("org.yanzi.shareserver.receiver");registerReceiver(msgReceiver, intentFilter);
其中的addAction是為了區別哪個廣播。這個標籤和Service裡保持同步。如果是有多個Service的話,Activity利用它來區分是哪個Service發來的廣播。

然後再Service裡利用下面代碼給Activity發廣播:

private Intent mIntent = new Intent("org.yanzi.shareserver.receiver");

/** * 通知Activity更新UI * @param s * @param len */public void sendBroadcastString(String s, int len){mIntent.putExtra("INFO", s +"長度  = "+ len+ "\n");sendBroadcast(mIntent);mIntent.removeExtra("INFO");}
mIntent發送完訊息後就remove一下。


5、接著就輪到Service上場了。想對於一般的Socket DEMO伺服器功能都比較單一,本例的Service任務和邏輯如下:

a、首先有個全域的socket,負責監聽用戶端的請求,是 下載 還是上傳;

b、如果監聽到是下載,就把這個socket關閉。然後再開個socket和用戶端串連,此時對應的用戶端頁面也進行了切換,用戶端也開了一個socket負責接收圖片的。在傳輸圖片時一定要先傳遞圖片的個數,否則的話用戶端的listview沒法執行個體化,不知道view有多少個。

備忘:Socket的對等性

Socket的對等性一定要時刻清晰,socket就像一條線的兩個端,串連伺服器和用戶端。伺服器的socket對等於用戶端的socket,是一一對應的。其實用對應還不大準確,就如同手心和手背一樣,一體兩面。因此伺服器和用戶端,兩端的socket如果串連好了,任何一端關閉socket都會造成對方的socket關閉。不僅如此,socket、輸入資料流、輸出資料流,三者有任何一個關閉都會造成整個socket串連的關閉。但輸出資料流的flush()函數不會造成這個問題,在發送東西時flush也是必須的。因此,大量網上給出的socket樣本,說發送完畢後都要把輸出資料流close()掉其實是不對的。如果有多個圖片,一定要等圖片發送完畢後,再去close。

6、伺服器的Service裡可以弄個死迴圈一直監聽,也可以弄個線程一直監聽。這裡是弄的線程ServiceThread,如果要考慮多用戶端,這塊要弄成線程池,即來一個用戶端串連上,就專門開個線程來處理。

在Service的onCreate函數裡new一個ServertSocket,將監聽的線程執行個體化:

/** * 初始化ServerSocket,buffer分配記憶體 */public void initTCPService(){try {mServerSocket = new ServerSocket(PORT);Log.i(tag, "Service啟動監聽...");} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}if(mServiceThread == null){mServiceThread = new ServiceThread();}}
此後mServerSocket就會一直監聽PORT連接埠。下面是我的ServiceThread的完整實現:

class ServiceThread extends Thread{@Overridepublic void run() {// TODO Auto-generated method stubwhile(allowRun){try {client = mServerSocket.accept();Log.i(tag, "Service監聽到訊息...");InputStream inputStream = client.getInputStream();int temp = 0;while((temp = inputStream.read(mByteBuffer)) != -1){if(temp < 10){String s = new String(mByteBuffer, 0, temp);inputStream.close();client.close();if(s.trim().equals(new String("DOWNLOAD"))){sendBroadcastString("用戶端下載開始:" + s, temp);sendThumnail(SRC_PAH);}else if(s.trim().equals(new String("UPLOAD"))){sendBroadcastString("用戶端上傳開始:" + s, temp);getUploadImages();}}else{String s = new String(mByteBuffer, 0, temp);Log.i(tag, "temp = " + temp+" = " + s);inputStream.close();if(client.isConnected()){client.close();}sendBroadcastString("伺服器:收到用戶端的下載列表 = " + s,temp);sendImage(s.trim());}}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}
這個Thread一旦運行就會進入死迴圈,如果服務端監聽到有用戶端串連,client = mServerSocket.accept(); client的類型是Socket。之後程式會阻塞在InputStream inputStream = client.getInputStream();這一句,而不是上面的accept()這裡。

簡單介紹下InputStream和OutputStream,當socket是接收方時,就用Socket.getInputStream(),得到的是輸入資料流。如果是socket想發送東西,就用Socket.getOutputStream。無論是輸入資料流還是輸出資料流,都是為了和基礎資料型別 (Elementary Data Type)進行轉換。何為基礎資料類型?比如byte[]這些。其實InputStream是利用read函數,將輸入資料流的東西read到基礎資料類型裡。OutputStream是利用write函數,將基礎資料型別 (Elementary Data Type)的東西write到流裡面。
上面的代碼中,首先判斷用戶端發來的是字串有多長,如果小於10,則認為他是命令級的,即要麼是上傳或下載。根據上傳或下載再跳到具體的函數裡開一個新的socket去發送和接收圖片。如果string的長度大於10,那就認為是用戶端勾選的下載列表的名字。其實這麼搞不大精確,可以用戶端和服務端約定好,流的第一個位元組寫進入一個int,用來區分判斷是下載和上傳,及其他功能。

7、手機與手機TCP Socket發送字串:

最簡單的就是writeUTF然後接收方readUTF就ok來了。我這裡弄的還是有點複雜了,見用戶端裡的

sendMsgToServer

函數。

8、手機與手機TCP Socket發送圖片,這裡是以服務端上的發送圖片和接收圖片來樣本的:

發送圖片個數:

Socket socket = null;Bitmap bitmap = null;try {socket = mServerSocket.accept();} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}//第一次傳送檔案夾內有多少個圖片int num = listFile.length;try {DataOutputStream dos1 = new DataOutputStream(socket.getOutputStream());dos1.writeInt(num);dos1.flush();Log.i(tag, "服務端:檔案夾片數 = " + num);} catch (IOException e1) {// TODO Auto-generated catch blocke1.printStackTrace();}
先往流裡寫入有多少個圖片,將來用戶端readInt讀出來傳給適配器。

下面是發送圖片:

FileInputStream fileStream = new FileInputStream(listFile[i]);bitmap = BitmapFactory.decodeStream(fileStream);Bitmap thumBitmap = ThumbnailUtils.extractThumbnail(bitmap, THUM_WIDTH, THUM_HEIGHT);ImageUtil.saveJpeg(thumBitmap);//發送給用戶端DataOutputStream dos = new DataOutputStream(socket.getOutputStream());ByteArrayOutputStream baos = new ByteArrayOutputStream();thumBitmap.compress(Bitmap.CompressFormat.JPEG, 85, baos);Log.i(tag, "baos.size() " + baos.size());dos.writeInt(baos.size());byte[] bytes = baos.toByteArray();dos.write(bytes);dos.writeUTF(name);//寫進去名字dos.flush();

在已有Bitmap的情況, Bitmap---compress到ByteArrayOutPutStream baos-----baos轉到byte[] bytes裡,然後將基礎資料byte[] bytes write到dos裡取,dos的類型如下:
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

接收圖片,還是以伺服器為例,我就把接收用戶端傳來的照片函數getUploadImages()完整貼出來:

public void getUploadImages(){Socket socket = null;int numNeedDownload = 0;try {socket = mServerSocket.accept();DataInputStream is = new DataInputStream(socket.getInputStream());numNeedDownload = is.readInt(); //獲得需要下載的個數sendBroadcastString("伺服器:用戶端將要上傳檔案個數 = " +numNeedDownload, 000);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}for(int i=0; i<numNeedDownload; i++){try {DataInputStream is = new DataInputStream(socket.getInputStream());int size = is.readInt(); //得到byte的長度byte[] buffer = new byte[size];int len = 0;while(len < size){len += is.read(buffer, len, size-len);}Bitmap bitmap = BitmapFactory.decodeByteArray(buffer, 0, buffer.length);if(bitmap == null){Log.i(tag, "伺服器:bitmap為空白");}String name = is.readUTF();ImageUtil.saveJpeg(bitmap, name);sendBroadcastString("伺服器:"+name+"接收成功", 000);} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}//接收完畢後,關閉sockettry {socket.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}

首先獲得需要下載的個數,然後進入一個迴圈。將得到的輸入資料流讀到byte裡,這裡通過

int size = is.readInt(); //得到byte的長度
byte[] buffer = new byte[size];
int len = 0;
while(len < size){
len += is.read(buffer, len, size-len);
}

先獲得輸入資料流裡存的照片的byte長度,然後進入while迴圈,讀到buffer裡。之後就是BitmapFactory解析了。再從流裡讀出來圖片的名字。

用戶端部分:

1、用戶端跟伺服器端差不多,唯一不同的是伺服器是ServertSocket負責監聽,監聽到串連後調用accept()函數得到socket。而用戶端是直接new一個socket,串連伺服器。需要注意用戶端的點擊下載和上傳的執行要寫在單獨的線程裡,自從android4.0後,關於網路的操作都不能放在主線程裡。下面的代碼是用戶端向伺服器發送選擇的下載列表名字:

/** * @author Administrator *發送給伺服器,需要下載的圖片的名字 */class SendDownName implements Runnable{@Overridepublic void run() {// TODO Auto-generated method stubString sendName = getNeedDownNames();try {Socket socket = new Socket(Ip, Port);sendMsgToServer(socket, sendName);socket.close();mainHanlder.sendEmptyMessage(START_DOWNLOAD_ASYNCTASK);} catch (UnknownHostException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public void sendMsgToServer(Socket socket, String s){try {OutputStreamWriter outputStrWriter = new OutputStreamWriter(socket.getOutputStream());BufferedWriter buffWriter = new BufferedWriter(outputStrWriter);PrintWriter printWriter = new PrintWriter(buffWriter);printWriter.println(s);printWriter.flush();printWriter.close();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}

當發送完畢後,給mainandler傳一個訊息,開一個非同步線程類來接收圖片。在下載縮圖過程中,每次接受到一個圖片都應該通知listview的適配器更新一下,mainHanlder.sendEmptyMessage(UPDATE_LIST_ADAPTER);也是通過mainHandler來做的。

case UPDATE_LIST_ADAPTER:listViewAdapter.notifyDataSetChanged();break;

2、用戶端裡用了AsyncTask,博主singwhatiwanna關於它的用法講的十分透徹,參見:http://blog.csdn.net/singwhatiwanna/article/details/17596225  http://blog.csdn.net/singwhatiwanna/article/details/9272195  補充一點,在AsyncTask的onPreExecute() 、onPostExecute()都只能做主線程UI的事情,而不能操作網路這些主線程不允許做的事情。 AsyncTask<Void, Integer, Void>的第一個參數輸入、第二個參數是中間值、第三個參數是輸出。本例為了能接收到一個圖片,就更新下progressbar,需要以下語句:

在doInBackground()裡publishProgress(mCnt); 然後是下面的:

@Overrideprotected void onProgressUpdate(Integer... values) {// TODO Auto-generated method stubsuper.onProgressUpdate(values);Log.i(tag, "進度條 = " + values[0]);progressBar.setProgress(values[0]);}

本例遺留的問題:

現在是用戶端接收到一個圖片然後進度條走一下,但是我們用手機藍芽傳照片時,是一個圖片在傳輸過程中就能顯示進度的變化。這就需要在傳輸時,分好幾份來傳,然後合并。等完善了進度條的UI再貢獻源碼下載。

------------------------本文系原創,轉載請註明作者yanzi1225627 

歡迎Android愛好者加群,群號:192413115 備忘: yanzi





聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.