android 流量 壓縮

引用: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個字節來壓縮呢?

綜上,雖然這個協議適合大小數據動態切換的數據傳輸,可是合理的選擇大數據和小數據的分割點(定義多少大的數據要壓縮,定義多少如下的數據不須要壓縮)是須要好好權衡的。

相關文章
相關標籤/搜索