馬昭征
摘 要:文章主要實現了基于HTTP的安卓與服務器交互的幾種實用方法。簡析安卓上傳數據和下載數據的傳輸過程以及服務器接受收據并響應的過程。通過比較各參數,以簡單直接的方法實現上述目的。文章也附上實際案例,由安卓端的上傳和下載文字及圖片,演示了客戶端對于數據請求的發(fā)送、服務器對該請求的處理、服務器向安卓發(fā)送數據以及安卓處理服務器相應的整個流程。驗證了文中所介紹方法的實用性。
關鍵詞:安卓;HttpClient;通信
2014年7月,市場分析機構Strategy Analytics公布了2014年第二季度智能手機操作系統(tǒng)全球分布情況。報告顯示,目前安卓操作系統(tǒng)的全球市場份額已達85%(有史以來最高比重),而蘋果的iOS、微軟的WindowsPhone等系統(tǒng)占比均有所下滑[1]。近年來,在谷歌努力的研發(fā)和推廣下,安卓迅速興起和發(fā)展,碎片化問題也隨著Android 4.4的發(fā)布得到了改觀。開發(fā)者們對安卓開發(fā)的熱情也隨之空前的高漲。然而除了諸如計算機、錄音機、手電筒等完全本地化的應用,目前安卓手機上大部分應用幾乎都要和網絡打交道。作為一個開發(fā)者,掌握安卓的通訊機制是十分必要的。目前而言,安卓與服務器的通訊最為普遍。這種通訊經常采用超文本傳送協(xié)議,即HTTP(Hypertext transfer protocol)。另外也有FTP(File Transfer Protocol)、Telnet、SMTP(Simple Mail Transfer Protocol)等幾種不同的協(xié)議,但考慮到使用的廣泛性,文章僅介紹最常用的基于HTTP的通交互方式。在這當中較為常用的訪問網絡方法有如下幾種:直接使用統(tǒng)一資源定位符,即URL(Uniform Resource Locator)的HttpURLConnection、Apache的HttpClient、還有就是利用WebView等等。這幾種方法各有利弊。WebView主要是用來顯示網頁的,而就數據的傳輸而言,前兩者使用廣泛。java.net包提供了通過HTTP訪問資源的基本功能,即HttpURLConnection。它是URLConnection的一個子類,是一個輕量級的類,它的實例可以共享連接到HTTP服務器的基礎網絡。HttpURLConnection因為較為輕便,因此理論上傳輸速度較快,另外其可擴展性也較強。但是由于協(xié)議應用本身的復雜性,使得在大量實際項目單純使用Java語言的軟件開發(fā)工具包進行HTTP編程仍然相對比較困難。而且在Android2.2之前有一個未修復的錯誤。針對這種情況,開源軟件組織Apach推出了HttpClient開源組件,并且提供穩(wěn)定持續(xù)的升級版本。它提供高效的、最新的、功能豐富的支持HTTP協(xié)議的客戶端編程工具包,并且它支持HTTP 協(xié)議最新的版本和建議。它封裝了很多實用的方法共開發(fā)者調用,開發(fā)者無需再寫大量代碼。因此在實際項目中采用HttpClient組件進行HTTP協(xié)議編程是一種高效經濟的解決方案[2]。因此,文章介紹的與服務器交互的方法是基于HttpClient的。
1 安卓端
1.1 安卓端的上傳
首先是請求方式的選擇。眾所周知,HTTP的請求方法有GET、POST、HEAD、PUT、DELETE、TRACE和OPTIONS幾種,其中用的較為廣泛的有GET和POST兩種。相對而言,我更加推薦使用POST方法。原因是GET方法的請求中只包含request-line部分,它將數據直接添加在URI中,這樣的話一些重要且私密的數據就會暴露在地址欄里。而POST則可以通過添加request-body,將數據放進body里再進行傳送,這樣不會出現數據暴露的安全隱患。因此相較于GET方法,POST的傳輸數據量更大,安全性也更高。
要注意的是,使用POST方式看不到傳送的數據不是因為POST方式自身的處理,而是因為瀏覽器做了相應的處理和限制,因此使數據不會被明顯暴露在瀏覽器界面上。但是只要利用一些工具還是可以查找到數據的。所以從這個角度講,GET和POST都是不安全的。但是相對而言,GET是把數據直接顯示在地址欄里,而POST則要隱秘許多。因此POST相對GET確實比較安全。
在安卓客戶端要使用HttpClient首先要創(chuàng)建一個它的實例。由于安卓自帶有HttpClient,所以可以直接調用系統(tǒng)應用程序編程接口來創(chuàng)建。此后需要創(chuàng)建請求方式的實例。
HttpPost post=new HttpPost(URL);
注意,這里的URL是請求的地址,務必要填寫,不然的話在執(zhí)行POST方法時會報一個NullPointer的空指針錯誤。由于文章中的服務器采用的是Struts2+Hibernate框架,因此URL的基本格式是:
http://服務器ip地址/項目名/action名稱.action
如果需要傳送一些數據,在這里可以用剛才說到的向request-body添加數據的方式來傳送。HttpClient的結構如圖1所示。
由圖1可以看到,我們關注的各種HTTP方法都被定義成一個個獨立的類,而他們都繼承自HttpRequestBase。其中比較特殊一點的是HttpPut和HttpPost,可以看出只有他們都是繼承自HttpEntityEnclosingRequestBase這個抽象類。這是因為它們需要設置request-body,即請求實體。而HttpEntityEnclosingRequestBase里有HttpEntity的成員變量。HttpEntity是一個接口,程序員可以根據具體項目中需要傳遞的數據類型選擇ByteArrayEntity、StringEntity、InputreamEntity、FileEntity等等類。他們均實現了HttpEntity這個接口。除此之外HttpClient還提供了UrlEncodedFormEntity類和MultipartEntity類來滿足更多的需求。
在此,筆者介紹一種通過模擬超文本標記語言HTML(HyperText Mark-up Language)的表單來傳送POST請求里參數的方法。而用到的工具則是UrlEncodedFormEntity這個類。
首先創(chuàng)建一個List的實例:
List
這里的NameValuePair是用于關聯(lián)某一名稱與某一值的專門類。
第二步就可以向實例params里添加需要傳送的數據了。使用的是params的add方法:add(param1, String1)。
這里有兩個參數。實際上就相當于Map里的鍵值對的概念。而這里的第二個參數必須是字符串格式。
再將添加完數據后的List實例params加入POST的body里。如果數據中有漢字,必須設置POST的編碼格式:
post.setEntity(new UrlEncodedFormEntity(params,"UTF-8"));
之后可以執(zhí)行請求并且讀取Response:
HttpResponse response = client.execute(post);
最后可用releaseConnection釋放連接。
如果需要上傳圖片、音頻等文件,也可以使用這種模擬表單的方法??梢允褂脛倓偺岬降脑贖ttpClient程序包中另一個類MultipartEntity。它同樣實現了HttpEntity接口。但由于該類使用起來并不是最方便,并且已經介紹過表單模擬的相關方法了。是這里介紹一種更為簡單的方法,即使用FileEntity類來實現。只需要實例化FileEntity
FileEntity entity = new FileEntity(file, "binary/octet-stream");
再同樣用setEntity方法后就可以執(zhí)行了。
1.2 安卓端的接受
上一節(jié)講到接受服務器的響應可以實例HttpResponse來實現:
HttpResponse response = client.execute(post);
然后可以用if語句來判斷Response的情況:
if(re.getStatusLine().getStatusCode()= =HttpStatus.SC_OK)
這里的HttpStatus.SC_OK即200,代表整個傳送順利進行。
接下來對服務器傳遞過來的數據進行解析。
首先要獲取承載數據的實體:
HttpEntity entity = response.getEntity();
判斷entity是否為空,如果非空則從里面獲取其實際的數據。
這里需要用到EntityUtils這個類。它是一個工具類,是為HttpEntity對象提供的靜態(tài)幫助類。利用它可以快速獲取服務器傳遞的數據:
String out = EntityUtils.toString(n, "UTF-8");
如果是圖片、音頻等轉換而成的byte數組,則可以用如下方法直接得到byte數組:
byte[] by = EntityUtils.toByteArray(en);
另外,如果服務器的數據是通過JSON (JavaScript Object Notation)包裝過的,則還需將字符串out轉換為JSON對象:
JSONObject json = new JSONObject(out);
最后就可以從json實例里直接得到服務器反饋的數據了,如:
int result1 = jsonObject1.getInt("result1");
Sring result2=jsonObject1.getString("result2");
在此對JSON進行簡單介紹。
JSON是一種輕量級的數據交換格式。易于人閱讀和編寫。同時也易于機器解析和生成。它基于JavaScript Programming Language,Standard ECMA-262 3rd Edition- December 1999的一個子集。JSON采用完全獨立于語言的文本格式,但是也使用了類似于C語言家族的習慣(包括C,C++,C#,Java,JavaScript,Perl,Python等)。這些特性使JSON成為理想的數據交換語言。由于JSON的數據格式非常簡單,我們可以用JSON傳輸一個簡單的String、Integer、Boolean、也可以傳輸一個數組或者一個復雜的Object對象[3]。
2 服務器端
2.1 服務器的接受
服務器用的是Struts2+Hibernate框架。其中Struts框架是整個系統(tǒng)應用框架的基礎,它實現了各個模塊的低耦合,使用Hibernate框架只考慮持久層應用。所謂的持久層就是由DAO(Data Access Object)組件構成,簡單說就是屏蔽了與數據庫打交道的細節(jié),只需調用DAO接口中的方法就可以對后臺數據操作[4]。用戶通過表示層向服務器發(fā)送應用請求,Struts框架的主要功能就是攔截用戶的操作請求,解析用戶請求的對象,并把請求轉發(fā)到相應的Action類處理,在Action類中,調用相應的持久層再把操作結果返回前端表示層顯示。在持久層,Hibernate主要責任就是負責實體對象與數據庫之間的交互映射,使得我們只需通過操作DAO層的實體對象就可以操作數據庫,獲得我們想要的數據,再經過業(yè)務邏輯層、表示層最終返回給表示層,展示給用戶使用。因此它接受客戶端傳來的數據十分簡單。按照Struts的規(guī)則,在Action類里面寫對應的方法即可[5]。
這里需要注意的有4點。
(1)在struts.xml里配置action時,package里的extends一定要寫成extends="json-default"。同時result里的type要寫成type="json"。這樣就可以通過JSON來傳遞數據了。
(2)而Action類需要實現ServletRequestAware和ServletResponseAware這兩個接口,這樣就可以得到Request和Response的實例了。
(3)所有的Action類都要拋出IOException。
(4)Action類的成員變量要有getter()和setter()方法[6]。
接下來只需要根據傳遞數據的格式來取數據:
request.getParameter("name");
request.getInputStream;
這里的"name"是NameValuePair里Value所對應的Name。而InputStream則適用于圖片等文件的讀取。
2.2 服務器的響應
與接收數據一樣,也是在Action類里寫對應的方法,再在struts.xml里配置。
要通過JSON傳遞數據先要創(chuàng)建JSONObject的 實例:
JSONObject jsonObject = new JSONObject();
如果是傳遞多組數據,在此可以使用在JSONArray里面添加JSONObject的方法:
JSONArray jsonArray = new JSONArray();
之后向jsonObject里添加需要反饋給客戶端的數據:
jsonObject.put("result", result);
這里也要注意如果數據包含中文字符,則需要設置Response的編碼格式: response.setCharacterEncoding("utf-8"); response.setContentType("text/html;charset=utf-8");
最后將jsonObject轉換為String,利用Response的Writer把數據發(fā)送出去:
response.getWriter().write(jsonObject.toString());
對于發(fā)送圖片,可以將服務器上待發(fā)送的文件通過文件輸出流和字節(jié)數組流轉換為字節(jié)數組。之后獲取Response的OutputStream的實例,就可以由該實例的write方法將字節(jié)數組發(fā)送出去[7]?;玖鞒炭蓞⒖家幌麓a片段:
FileInputStream fis=new FileInputStream(file);
ByteArrayOutputStream bao =
new ByteArrayOutputStream();
int data = -1;
while ((data = fis.read()) != -1) {
bao.write(data);
}
byte[] b = bao.toByteArray();
OutputStream os = response.getOutputStream();
os.write(b);
os.flush();
3 實際案例
3.1 開發(fā)環(huán)境
3.1.1 安卓端
操作系統(tǒng):MOKEE - Android 4.4.4
手機型號:Sony L36H
開發(fā)環(huán)境:Eclipse Ver 4.2.0
3.1.2 PC
操作系統(tǒng):Windows 7 旗艦版 64位
3.1.3 服務器:apache-tomcat-7.0.57
集成環(huán)境:MyEclipse Ver 4.3.0
3.1.4 工程名:SSHTest
3.2 具體實施
該案例是對文章所講述的方法做一次實際的操作來進行驗證。因此分別實現文字的上傳與下載和圖片的上傳與下載四個功能。界面布局如圖2所示。
需要上傳的圖片和文字打開應用時就直接顯示在界面上。
3.2.1 安卓端的上傳和服務器的接受
安卓端:
在進入正題之前,有兩點需要注意。首先要在AndroidManifest.xml文件里添加INTERNET權限。不添加這個用戶權限,該應用無法進行網絡訪問。但這往往是最容易忘記的一步。
第二點,在Android 4.0以上的版本,只能由主線程更改界面,而訪問網絡只能在子線程進行。所以這里可以用異步類AsyncTask和Handler機制。
接下來直接進入正題。
首先找到圖片文件的路徑,如果文件是儲存在SD卡里的話,還需要在AndroidManifest.xml里添加WRITE_EXTERNAL_STORAGE權限。隨后將文件添加到FileEntity里。之后創(chuàng)建HttpClient和HttpPost的實例并且執(zhí)行。
代碼:
//這里的 arg0[0]是AsyncTask傳遞進來的第一//個URL。對應服務器里相應的Action
HttpPost post = new HttpPost(arg0[0]);
HttpClient client = new DefaultHttpClient();
//添加圖片
File file = new File(path);
FileEntity entity = new FileEntity(file, "binary/octet-stream");
entity.setContentEncoding("binary/octet-stream");
post.setEntity(entity);
//或者添加文字
String wordsToUpload = words.getText().toString().trim();
List
params.add(new BasicNameValuePair ("mystring", wordsToUpload));
post.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
//執(zhí)行請求
HttpResponse re = client.execute(post);
服務器:
在相應的Action方法里只需要一步就可以獲取安卓端發(fā)過來的數據了:
//獲取圖片流
InputStream is = request.getInputStream();
//獲取模擬表單數據
String string=request.getParameter("mystring");
注意,這里的"mystring"和安卓端添加數據時
params.add(new BasicNameValuePair ("mystring", wordsToUpload));
這一句里的"mystring"是相對應的。該值由程序員自定義。
在接受完數據后,將圖片儲存在電腦的硬盤里,我選擇的路徑是D盤的FromAndroid文件夾。再將字符串打印到控制臺顯示出來。
如果程序順利執(zhí)行,安卓端收到HttpResponse的StatusCode將會等于200。此時執(zhí)行一個Toast提示用戶上傳成功,如圖3所示。反之,如果StatusCode不等于200,則說明執(zhí)行過程存在錯誤。那么也執(zhí)行Toast提醒上傳失敗,請重新上傳!此后可以查看Eclipse的LogCat和MyEclipse的Console來確認哪一步出錯。
3.2.2 安卓端的下載和服務器的響應
安卓端的請求:
由于下載過程中安卓端無需向服務器發(fā)送數據,因此只需要簡單地請求服務器中響應的Action就可以了。不用再添加request-body。
服務器的響應:
當服務器接受到安卓發(fā)出的請求后,與URL相對應的Action就開始工作,如圖4所示。
代碼:
//發(fā)送圖片。圖片位于D盤內
File file = new File("D:\\FromServer.png");
FileInputStream fis= new FileInputStream(file);
ByteArrayOutputStream bos=new ByteArrayOutputStream();
int data = -1;
while ((data = fis.read()) != -1) {
bops.write(data);
}
byte[] b = bos.toByteArray(); //最終將圖片轉換為字節(jié)數組
//得到response的輸出流并寫出
OutputStream os = response.getOutputStream();
os.write(b);
os.flush();
fis.close();
bos.close();
//發(fā)送字符組
//創(chuàng)建JSON格式的列表用戶添加多組數據
JSONArray jsonArray = new JSONArray();
JSONObject jsonObject1 = new JSONObject();
jsonObject1.put("id", 1);
jsonObject1.put("place", "陽明山");
//創(chuàng)建JSONObject對象,將數據一一填入
JSONObject jsonObject2 = new JSONObject();
jsonObject2.put("id", 2);
jsonObject2.put("place", "日月潭");
//向列表里添加各個JSONObject。它們將順序//排列
jsonArray.add(0, jsonObject1);
jsonArray.add(1, jsonObject2);
//從這里開始發(fā)送。
//設置字符編碼格式和數據/格式
response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=utf-8");
response.getWriter().write(jsonArray.toString());
安卓端的接受主要就是從HttpResponse里獲取響應實體:
HttpEntity entity = re.getEntity();
第二部分里提到過用于接受服務器數據的幫助類EntityUtils。在這里就要使用它。
按照服務器發(fā)送的數據格式我們可以調用EntityUtils里不同方法。
對于字符串可以使用EntityUtils. toString()方法。對于字節(jié)數組可以使用EntityUtils.toByteArray()方法。
byte[] by = EntityUtils.toByteArray(entity);
String out = EntityUtils.toString(entity, "UTF-8");
隨后只需把獲取的數據裝入Message中,并用Handler發(fā)送給主線程,就可以顯示在界面上了,如圖5所示。
4 結語
文章對安卓與服務器之間交互的具體方法做了相應的介紹。采用的通訊方式是HTTP。它采用了請求/響應模型。安卓端向服務器發(fā)送的請求包含了:請求的方法、URL、協(xié)議版本和客戶信息等等。而服務器勢必要有一個反饋。如果安卓端的請求內容中包括對服務器內客戶資料、服務器文件或其他數據的請求,那么服務器在響應的同時還需要將這些數據一并傳送到安卓端。因此文章就上傳和下載這兩個生活中最常見的動作為主,分別從安卓端和服務器端介紹了它們之間通訊的具體過程。
而在具體方法的選擇中,文章是本著簡單實用多樣的原則進行介紹的。在傳輸方式上選擇了更為安全和更具傳輸量的POST方法。傳送字符串數據用的是模擬HTML表單的NameValuePair列表方式。類似鍵值對的輸入過程一目了然。需要關心的只有字符編碼的統(tǒng)一設置,規(guī)避亂碼的問題。而對于圖片的傳送則用到了繼承HttpEntity接口的FileEntity。它是專門用于傳送文件的類,只需在創(chuàng)建其實例時將文件放入,再設置文件的格式就可以方便地傳送該文件到服務器;而對于下載字符串數據,則用了輕量級的數據交換格式JSON。下載圖片則是將文件轉換為字節(jié)數組再進行傳輸。
最后的實例分析也驗證了以上方法切實可行。希望能讓讀者對于安卓與服務器之間的通訊交互的細節(jié)有更多的實際認識。