引用:http://my.eoe.cn/blue_rain/archive/340.htmlhtml
對於目前的情況來講,移動終端的網絡情況沒有PC網絡情況那麼理想。在一個Android應用中,若是須要接收來自服務器的大容量數據,那麼就不得不考慮客戶的流量問題。本文根據筆者的一個項目實戰經驗出發,解決大容量數據的交互問題,解決數據大小會根據實際狀況動態切換問題(服務器動態選擇是否要壓縮數據,客戶端動態解析數據是不是被壓縮的),還有數據交互的編碼問題。java
解決數據過大的問題,最直觀的方法就是壓縮數據。服務器將須要傳遞的數據先進行壓縮,再發送給Android客戶端,Android客戶端接收到壓縮的數據,對其解壓,獲得壓縮前的數據。算法
若是規定Android客戶端和服務器的交互數據必須是通過某種壓縮算法後的數據,那麼這種「規定」失去了視具體狀況而定的靈活性。筆者擬將Http協議進行封裝,將動態的選擇傳輸的數據是否要通過壓縮,客戶端也能動態的識別,整理並得到服務器想要發送的數據。Android客戶端向服務器請求某個方面的數據,這個數據也許是通過壓縮後傳遞比較合適,又也許是將原生數據傳遞比較合適。也就是說,筆者想要設計一種協議,這種協議適用於傳輸數據的數據量會動態的切換,也許它會是一個小數據,也許它又會是一個數據量龐大的大數據(大數據須要通過壓縮)。json
可能說的比較抽象,那麼我用實際狀況解釋一下。數組
我項目中的一個實際狀況是這樣的:這個項目是作一個Android基金客戶端,Android客戶端向服務器請求某一個基金的歷史走勢信息,因爲個人Android客戶端實現了本地緩存,這讓傳遞數據的大小浮動很是大。若是本地緩存的歷史走勢信息的最新日期是5月5日,服務器的歷史走勢信息的最新日期是5月7日,那麼服務器就像發送5月6日和5月7日這兩天的走勢信息,這個數據很小,不須要壓縮(我使用的壓縮算法,對於數據量太小的數據壓縮並不理想,數據量太小的數據壓縮後的數據會比壓縮前的數據大)。然而,Android客戶端也可能對於某個基金沒有任何的緩存信息,那麼服務器將發送的數據將是過去三四年間的歷史走勢信息,這個數據會有點大,就須要進行壓縮後傳遞。那麼客戶端對於同一個請求獲得的數據,如何判斷它是壓縮後的數據仍是不曾壓縮的數據呢?緩存
筆者使用的解決方案是把傳遞數據的第一個字節做爲標識字節,將標識這個數據是否被壓縮了。也能標識傳遞數據的編碼問題。Android對於接收到的數據(字節數組),先判斷第一個字節的數據,就能根據它所表明的數據格式和編碼信息進行相應的操做。說了那麼多,也許不如看實際的代碼理解的快。首先是壓縮算法,這裏筆者用到的是jdk自帶的zip壓縮算法。服務器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
package com.chenjun.utils.compress;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public class Compress {
private static final int BUFFER_LENGTH = 400;
//壓縮字節最小長度,小於這個長度的字節數組不適合壓縮,壓縮完會更大
public static final int BYTE_MIN_LENGTH = 50;
//字節數組是否壓縮標誌位
public static final byte FLAG_GBK_STRING_UNCOMPRESSED_BYTEARRAY = 0;
public static final byte FLAG_GBK_STRING_COMPRESSED_BYTEARRAY = 1;
public static final byte FLAG_UTF8_STRING_COMPRESSED_BYTEARRAY = 2;
public static final byte FLAG_NO_UPDATE_INFO = 3;
/**
* 數據壓縮
*
* @param is
* @param os
* @throws Exception
*/
public static void compress(InputStream is, OutputStream os)
throws Exception {
GZIPOutputStream gos = new GZIPOutputStream(os);
int count;
byte data[] = new byte[BUFFER_LENGTH];
while ((count = is.read(data, 0, BUFFER_LENGTH)) != -1) {
gos.write(data, 0, count);
}
gos.finish();
gos.flush();
gos.close();
}
/**
* 數據解壓縮
*
* @param is
* @param os
* @throws Exception
*/
public static void decompress(InputStream is, OutputStream os)
throws Exception {
GZIPInputStream gis = new GZIPInputStream(is);
int count;
byte data[] = new byte[BUFFER_LENGTH];
while ((count = gis.read(data, 0, BUFFER_LENGTH)) != -1) {
os.write(data, 0, count);
}
gis.close();
}
/**
* 數據壓縮
*
* @param data
* @return
* @throws Exception
*/
public static byte[] byteCompress(byte[] data) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 壓縮
compress(bais, baos);
byte[] output = baos.toByteArray();
baos.flush();
baos.close();
bais.close();
return output;
}
/**
* 數據解壓縮
*
* @param data
* @return
* @throws Exception
*/
public static byte[] byteDecompress(byte[] data) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 解壓縮
decompress(bais, baos);
data = baos.toByteArray();
baos.flush();
baos.close();
bais.close();
return data;
}
}
|
這裏供外部調用的方法是byteCompress()和byteDecompress(),都將接收一個byte數組,byteCompress是數據壓縮方法,將返回壓縮後的數組數據,byteDecompress是數據解壓方法,將返回解壓後的byte數組數據。FLAG_GBK_STRING_COMPRESSED_BYTEARRAY表示服務器傳遞的數據是GBK編碼的字符串通過壓縮後的字節數組。其它的常量也能根據其名字來理解。(這裏多說一句,最好將編碼方式和是否壓縮的標識位分開,好比將標識字節的前四個位定義成標識編碼方式的位,將後面四個位標識爲是否壓縮或者其它信息的標識位,經過位的與或者或方式來判斷標識位。筆者這裏偷懶了,直接就這麼寫了。)網絡
下面是處理傳遞數據的方法(判斷是否要壓縮)。我這裏用要的是Struts 1框架,在Action裏組織數據,並做相應的處理(壓縮或者不壓縮),併發送。併發
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
JjjzForm jjjzForm = (JjjzForm) form;
//基金淨值歷史走勢信息
ArrayList<Jjjz> jjjzs = null;
//獲得基金淨值歷史走勢的方法省略了
Gson gson = new Gson();
String jsonStr = gson.toJson(jjjzs, jjjzs.getClass());
byte[] resultOriginalByte = jsonStr.getBytes();
//組織最後返回數據的緩衝字節數組
ByteArrayOutputStream resultBuffer = new ByteArrayOutputStream();
OutputStream os = null;
try {
os = response.getOutputStream();
//若是要返回的結果字節數組小於50位,不將壓縮
if(resultOriginalByte.length < Compress.BYTE_MIN_LENGTH){
byte flagByte = Compress.FLAG_GBK_STRING_UNCOMPRESSED_BYTEARRAY;
resultBuffer.write(flagByte);
resultBuffer.write(resultOriginalByte);
}
else{
byte flagByte = Compress.FLAG_GBK_STRING_COMPRESSED_BYTEARRAY;
resultBuffer.write(flagByte);
resultBuffer.write(Compress.byteCompress(resultOriginalByte));
}
resultBuffer.flush();
resultBuffer.close();
//將最後組織後的字節數組發送給客戶端
os.write(resultBuffer.toByteArray());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally{
try {
os.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
|
這裏我預發送的數據是一個Json格式的字符串(GBK編碼),將判斷這個字符串的長度(判斷是否適合壓縮)。若是適合壓縮,就將緩衝字節數組(ByteArrayOutputStream resultBuffer)的第一個字節填充FLAG_GBK_STRING_COMPRESSED_BYTEARRAY,再將Json字符串的字節數組壓縮,並存入數據緩衝字節數組,最後向輸出流寫入緩衝字節數組,關閉流。若是不適合壓縮,將發送的數據的第一個字節填充爲FLAG_GBK_STRING_UNCOMPRESSED_BYTEARRAY,再將Json字符串的字節數組直接存入數據緩衝字節數組,寫入輸出流,關閉流。app
最後就是Android客戶端的解析了,將上述的Compress壓縮輔助類拷貝到Android項目中就行。下面是Http請求後獲得的字節數組數據作解析工做。(Android客戶端如何使用Http向服務器請求數據請參考我前面的一篇博客)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
byte[] receivedByte = EntityUtils.toByteArray(httpResponse.getEntity());
String result = null;
//判斷接收到的字節數組是不是壓縮過的
if (receivedByte[0] == Compress.FLAG_GBK_STRING_UNCOMPRESSED_BYTEARRAY) {
result = new String(receivedByte, 1, receivedByte.length - 1, EXCHANGE_ENCODING);
}
else if (receivedByte[0] == Compress.FLAG_GBK_STRING_COMPRESSED_BYTEARRAY) {
byte[] compressedByte = new byte[receivedByte.length - 1];
for (int i = 0; i < compressedByte.length; i++) {
compressedByte[i] = receivedByte[i + 1];
}
byte[] resultByte = Compress.byteDecompress(compressedByte);
result = new String(resultByte, EXCHANGE_ENCODING);
}
|
這裏最後獲得的result就是服務器實際要發送的內容。
缺陷反思:任何設計都是有缺陷的。我這樣作已經將Http協議作了進一層封裝。Http的數據部分的第一個字節並非實際數據,而是標識字節。這樣,下降了這個接口的可重用性。統一發送Json字符串的Action能被網頁(Ajax)或者其餘客戶端使用,通過封裝壓縮以後,只有能識別這個封裝(就是能進行解析)的客戶端能使用這個接口。網頁(Ajax)就不能解析,那麼這個Action就不能被Ajax使用。
具體開發過程當中要視具體狀況而定,若是數據量小的話我仍是建議使用標準的Http協議,也就是說直接發送字符串,不作任何的壓縮和封裝。若是數據量實在過於大的話,建議使用我上述的方法。
有博友問,對於Android應用來講,什麼樣的數據纔算是大數據。我想這個大數據的界限並非固定的,並非說10k以上,或者100k以上就算是大數據,這個界限是由許多方面的利弊來衡量的。首先我要說,我設計的這個協議是適用於大數據和小數據動態切換的狀況。對於大小數據界限的劃定,交給開發人員去衡量利弊。這個衡量標準我想應該包括如下幾部份內容:
第一,壓縮算法的有效臨界點。只有要壓縮的數據大於這個點,壓縮後的數據纔會更小,反之,壓縮後的數據會更加的大。我使用的zip算法這個點應該是50字節左右,所以,在我應用中,將大數據定義成50字節以上的數據。
第二:壓縮和解壓的開銷。服務器要壓縮數據,客戶端要解壓數據,這個都是須要CPU開銷的,特別是服務器,若是請求量大的話,須要爲每個響應數據進行壓縮,勢必下降服務器的性能。咱們能夠設想這樣的一種狀況,原生數據只有50字節,壓縮完會有40字節,那麼咱們就要思考是否有必要來消耗CPU來爲咱們這區區的10個字節來壓縮呢?
綜上,雖然這個協議適合大小數據動態切換的數據傳輸,可是合理的選擇大數據和小數據的分割點(定義多少大的數據要壓縮,定義多少如下的數據不須要壓縮)是須要好好權衡的。