Android程序與服務器大容量數據交互問題打賞

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

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

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

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

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

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

package com.poorren.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里組織數據,并作相應的處理(壓縮或者不壓縮),并發送。

public ActionForward execute(ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) {
        JjjzForm jjjzForm = (JjjzForm) form;

        //基金凈值歷史走勢信息
        ArrayList 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請求后得到的字節數組數據做解析工作。

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

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

Android程序與服務器大容量數據交互問題
文章《Android程序與服務器大容量數據交互問題》二維碼
  • 微信打賞
  • 支付寶打賞

已有1條評論

  1. 珠寶網

    Android程序與服務器大容量數據交互問題,分析的很好

    2012-06-22 10:28 回復

(必填)

(必填)

(可選)

黑龙江22选5开奖