zoukankan      html  css  js  c++  java
  • 解决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.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里组织数据,并作相应的处理(压缩或者不压缩),并发送。

    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个字节来压缩呢?

      综上,虽然这个协议适合大小数据动态切换的数据传输,但是合理的选择大数据和小数据的分割点(定义多少大的数据要压缩,定义多少以下的数据不需要压缩)是需要好好权衡的。

  • 相关阅读:
    面向对象的继承关系体现在数据结构上时,如何表示
    codeforces 584C Marina and Vasya
    codeforces 602A Two Bases
    LA 4329 PingPong
    codeforces 584B Kolya and Tanya
    codeforces 584A Olesya and Rodion
    codeforces 583B Robot's Task
    codeforces 583A Asphalting Roads
    codeforces 581C Developing Skills
    codeforces 581A Vasya the Hipster
  • 原文地址:https://www.cnblogs.com/yishujun/p/3638630.html
Copyright © 2011-2022 走看看