解決Android與服務器交互大容量數據問題

對於目前的狀況來說,移動終端的網絡狀況沒有PC網絡狀況那麼理想。在一個Android應用中,如果需要接收來自服務器的大容量數據,那麼就不得不考慮客戶的流量問題。本文根據筆者的一個項目實戰經驗出發,解決大容量數據的交互問題,解決數據大小會根據實際情況動態切換問題(服務器動態選擇是否要壓縮數據,客戶端動態解析數據是否是被壓縮的),還有數據交互的編碼問題。

 

  解決數據過大的問題,最直觀的方法就是壓縮數據。服務器將需要傳遞的數據先進行壓縮,再發送給Android客戶端,Android客戶端接收到壓縮的數據,對其解壓,得到壓縮前的數據。

 

  如果規定Android客戶端和服務器的交互數據必須是經過某種壓縮算法後的數據,那麼這種“規定”失去了視具體情況而定的靈活性。筆者擬將Http協議進行封裝,將動態的選擇傳輸的數據是否要經過壓縮,客戶端也能動態的識別,整理並獲得服務器想要發送的數據。Android客戶端向服務器請求某個方面的數據,這個數據也許是經過壓縮後傳遞比較合適,又也許是將原生數據傳遞比較合適。也就是說,筆者想要設計一種協議,這種協議適用於傳輸數據的數據量會動態的切換,也許它會是一個小數據,也許它又會是一個數據量龐大的大數據(大數據需要經過壓縮)。

 

  可能說的比較抽象,那麼我用實際情況解釋一下。

 

  我項目中的一個實際情況是這樣的:這個項目是做一個Android基金客戶端,Android客戶端向服務器請求某一個基金的歷史走勢信息,由於我的Android客戶端實現了本地緩存,這讓傳遞數據的大小浮動非常大。如果本地緩存的歷史走勢信息的最新日期是5月5日,服務器的歷史走勢信息的最新日期是5月7日,那麼服務器就像發送5月6日和5月7日這兩天的走勢信息,這個數據很小,不需要壓縮(我使用的壓縮算法,對於數據量過小的數據壓縮並不理想,數據量過小的數據壓縮後的數據會比壓縮前的數據大)。然而,Android客戶端也可能對於某個基金沒有任何的緩存信息,那麼服務器將發送的數據將是過去三四年間的歷史走勢信息,這個數據會有點大,就需要進行壓縮後傳遞。那麼客戶端對於同一個請求得到的數據,如何判斷它是壓縮後的數據還是未曾壓縮的數據呢?

 

  筆者使用的解決方案是把傳遞數據的第一個字節作爲標識字節,將標識這個數據是否被壓縮了。也能標識傳遞數據的編碼問題。Android對於接收到的數據(字節數組),先判斷第一個字節的數據,就能根據它所代表的數據格式和編碼信息進行相應的操作。說了那麼多,也許不如看實際的代碼理解的快。首先是壓縮算法,這裏筆者用到的是jdk自帶的zip壓縮算法。

 

  

複製代碼
  1 package com.chenjun.utils.compress;
  2 
  3 import java.io.ByteArrayInputStream;
  4 import java.io.ByteArrayOutputStream;
  5 import java.io.InputStream;
  6 import java.io.OutputStream;
  7 import java.util.zip.GZIPInputStream;
  8 import java.util.zip.GZIPOutputStream;
  9 
 10 public class Compress {
 11     private static final int BUFFER_LENGTH = 400;
 12     
 13     
 14     //壓縮字節最小長度,小於這個長度的字節數組不適合壓縮,壓縮完會更大
 15     public static final int BYTE_MIN_LENGTH = 50;
 16     
 17     
 18     //字節數組是否壓縮標誌位
 19     public static final byte FLAG_GBK_STRING_UNCOMPRESSED_BYTEARRAY = 0;
 20     public static final byte FLAG_GBK_STRING_COMPRESSED_BYTEARRAY = 1;
 21     public static final byte FLAG_UTF8_STRING_COMPRESSED_BYTEARRAY = 2;
 22     public static final byte FLAG_NO_UPDATE_INFO = 3;
 23     
 24     /**  
 25      * 數據壓縮  
 26      *   
 27      * @param is  
 28      * @param os  
 29      * @throws Exception  
 30      */  
 31     public static void compress(InputStream is, OutputStream os)   
 32             throws Exception {   
 33   
 34         GZIPOutputStream gos = new GZIPOutputStream(os);   
 35   
 36         int count;   
 37         byte data[] = new byte[BUFFER_LENGTH];   
 38         while ((count = is.read(data, 0, BUFFER_LENGTH)) != -1) {   
 39             gos.write(data, 0, count);   
 40         }   
 41   
 42         gos.finish();   
 43   
 44         gos.flush();   
 45         gos.close();   
 46     }   
 47     
 48     
 49     /**  
 50      * 數據解壓縮  
 51      *   
 52      * @param is  
 53      * @param os  
 54      * @throws Exception  
 55      */  
 56     public static void decompress(InputStream is, OutputStream os)   
 57             throws Exception {   
 58   
 59         GZIPInputStream gis = new GZIPInputStream(is);   
 60   
 61         int count;   
 62         byte data[] = new byte[BUFFER_LENGTH];   
 63         while ((count = gis.read(data, 0, BUFFER_LENGTH)) != -1) {   
 64             os.write(data, 0, count);   
 65         }   
 66   
 67         gis.close();   
 68     } 
 69     
 70     /** 
 71      * 數據壓縮 
 72      *  
 73      * @param data 
 74      * @return 
 75      * @throws Exception 
 76      */  
 77     public static byte[] byteCompress(byte[] data) throws Exception {  
 78         ByteArrayInputStream bais = new ByteArrayInputStream(data);  
 79         ByteArrayOutputStream baos = new ByteArrayOutputStream();  
 80   
 81         // 壓縮  
 82         compress(bais, baos);  
 83   
 84         byte[] output = baos.toByteArray();  
 85   
 86         baos.flush();  
 87         baos.close();  
 88   
 89         bais.close();  
 90   
 91         return output;  
 92     } 
 93     
 94     
 95     /** 
 96      * 數據解壓縮 
 97      *  
 98      * @param data 
 99      * @return 
100      * @throws Exception 
101      */  
102     public static byte[] byteDecompress(byte[] data) throws Exception {  
103         ByteArrayInputStream bais = new ByteArrayInputStream(data);  
104         ByteArrayOutputStream baos = new ByteArrayOutputStream();  
105   
106         // 解壓縮  
107   
108         decompress(bais, baos);  
109   
110         data = baos.toByteArray();  
111   
112         baos.flush();  
113         baos.close();  
114   
115         bais.close();  
116   
117         return data;  
118     }  
119 }
複製代碼

 

  這裏供外部調用的方法是byteCompress()和byteDecompress(),都將接收一個byte數組,byteCompress是數據壓縮方法,將返回壓縮後的數組數據,byteDecompress是數據解壓方法,將返回解壓後的byte數組數據。FLAG_GBK_STRING_COMPRESSED_BYTEARRAY表示服務器傳遞的數據是GBK編碼的字符串經過壓縮後的字節數組。其它的常量也能根據其名字來理解。(這裏多說一句,最好將編碼方式和是否壓縮的標識位分開,比如將標識字節的前四個位定義成標識編碼方式的位,將後面四個位標識爲是否壓縮或者其它信息的標識位,通過位的與或者或方式來判斷標識位。筆者這裏偷懶了,直接就這麼寫了。)

 

  下面是處理傳遞數據的方法(判斷是否要壓縮)。我這裏用要的是Struts 1框架,在Action裏組織數據,並作相應的處理(壓縮或者不壓縮),併發送。

 

 

複製代碼
     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字符串的字節數組直接存入數據緩衝字節數組,寫入輸出流,關閉流。

 

  最後就是Android客戶端的解析了,將上述的Compress壓縮輔助類拷貝到Android項目中就行。下面是Http請求後得到的字節數組數據做解析工作。(Android客戶端如何使用Http向服務器請求數據請參考我前面的一篇博客)。

 

 

複製代碼
        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個字節來壓縮呢?

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

轉正自:http://www.cnblogs.com/answer1991/archive/2012/05/07/2487052.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章