Part 1 of this series focused on getting the Android application set up to make an HTTP request. In part 2, we’ll focus on implementing the use of data serialization and compression on the Android and PHP sides of the request.
本系列的第1部分重点介绍如何设置Android应用程序以发出HTTP请求。 在第2部分中,我们将重点介绍在请求的Android和PHP方面实现数据序列化和压缩的使用。
Before data can be transferred to the client, it must be serialized into a data stream that the client can later convert back into a useful data structure. In your PHP REST service, you’ll need to determine the most appropriate data serialization format to use. This involves parsing the Accept header sent by the client to determine what server-supported format it prefers.
在将数据传输到客户端之前,必须将其序列化为数据流,客户端以后可以将其转换回有用的数据结构。 在PHP REST服务中,您需要确定要使用的最合适的数据序列化格式。 这涉及解析客户端发送的Accept标头,以确定它偏爱哪种服务器支持的格式。
If you’re using a PHP framework to build your REST service, you should consult its documentation on how to do this, but here’s what the body of a method to determine the response format might look like:
如果您使用PHP框架构建REST服务,则应查阅其文档以了解如何执行此操作,但这是确定响应格式的方法的主体如下所示:
<?php // Define types supported by the server $supportedTypes = array(); if (extension_loaded("json")) { $supportedTypes["application/json"] = "json_encode"; } if (extension_loaded("msgpack")) { $supportedTypes["application/x-msgpack"] = "msgpack_pack"; } // Get client-supported media types ordered from most to least preferred $typeEntries = array_map("trim", explode(",", $_SERVER["HTTP_ACCEPT"])); $acceptedTypes = array(); foreach ($typeEntries as $entry) { $entry = preg_split('#;s*q=#', $entry); $mediaType = array_shift($entry); $qualityValue = count($entry) ? array_shift($entry) : 1; $acceptedTypes[$mediaType] = $qualityValue; } arsort($acceptedTypes); // Find the most preferred media type supported by client and server $supportedMediaTypes = array_keys($supportedTypes); foreach ($acceptedTypes as $mediaType => $qualityValue) { $pattern = "#" . str_replace("*", ".*", $mediaType) . "#"; if ($matches = preg_grep($pattern, $supportedMediaTypes)) { $supportedType = reset($matches); return array($supportedType, $supportedTypes[$supportedType]); } } return null;This code parses the value of the Accept header, sorts the MIME types it contains by their respective quality factors, and then loops through them in that order to determine which type is supported by the server and has the highest quality factor.
此代码解析Accept标头的值,按其各自的品质因数对它包含的MIME类型进行排序,然后按顺序循环遍历它们,以确定服务器支持哪种类型并具有最高品质因数。
You can make use of this value in your response as in the example below where $contentType is the return value of your method. If no mutually supported format can be found, an appropriate response code is returned per RFC 2616 Section 10.4.7.
您可以在响应中使用此值,如下面的示例,其中$contentType是方法的返回值。 如果找不到相互支持的格式,则根据RFC 2616第10.4.7节返回适当的响应代码。
<?php if ($contentType) { list ($name, $callback) = $contentType; header($_SERVER["SERVER_PROTOCOL"] . " 200 OK"); header("Content-Type: " . $name); $data = call_user_func($callback, $data); } else { header($_SERVER["SERVER_PROTOCOL"] . " 406 Not Acceptable"); }Once the data is serialized, it must be compressed to minimize the amount of bandwidth required to get the data from the server to the client. This process of determining the data compression format is similar to determining the data serialization format but instead parses the Accept-Encoding header, which does not use quality factors. This enables you to decide their precedence order if the client supports multiple compression formats.
数据序列化后,必须对其进行压缩以最大程度地减少从服务器到客户端获取数据所需的带宽量。 确定数据压缩格式的此过程类似于确定数据序列化格式,但解析不使用质量因子的Accept-Encoding标头。 如果客户端支持多种压缩格式,这使您能够确定其优先顺序。
Again, if you have a PHP framework of choice you should consult its documentation to see if there’s a more prevalent way to do this, but I’ve included a raw PHP sample below. Additional encodings can easily be added, but I’ve included the ones most commonly used in HTTP responses with bzip2 having the highest precedence because it has the best compression.
同样,如果您选择了一个PHP框架,则应查阅其文档,以查看是否有更普遍的方法可以这样做,但是我在下面提供了一个原始PHP示例。 可以轻松添加其他编码,但是我已经包括了HTTP响应中最常用的编码,其中bzip2具有最高的优先级,因为它具有最佳的压缩率。
<?php // Determine what encodings the client supports $clientEncodings = array_map("trim", explode(",", $_SERVER["HTTP_ACCEPT_ENCODING"])); // Determine what encodings the server supports $serverEncodings = array(); if (extension_loaded("bz2")) { $serverEncodings["bzip2"] = "bzcompress"; } if (extension_loaded("zlib")) { $serverEncodings["gzip"] = "gzdeflate"; $serverEncodings["deflate"] = "gzcompress"; } // Return an encoding supported by both if one is found foreach ($serverEncodings as $encoding => $callback) { if (in_array($encoding, $clientEncodings)) { return array($encoding, $callback); } } return array();In the example below, $encoding is the return value of this method. If a supported data compression format is found then its name is returned in the appropriate header per RFC 2616 Section 14.11.
在下面的示例中, $encoding是此方法的返回值。 如果找到了受支持的数据压缩格式,则根据RFC 2616第14.11节 ,在相应的标头中返回其名称。
<?php if ($encoding) { list ($name, $callback) = $encoding; header("Content-Encoding: " . $name); $data = call_user_func($callback, $data); }While some data serialization formats may appear more efficient prior to compression being applied, that doesn’t necessarily mean this will also hold true after compression is applied. For example, MessagePack is often more compact than JSON prior to compression, but less compact in most cases after compression. As such, you should test different serialization and compression combinations on real data to determine which is optimal for your application. As a general rule of thumb, use JSON and either gzip or (preferably) bzip2 as a baseline for comparison.
尽管某些数据序列化格式在应用压缩之前可能看起来更有效,但这并不一定意味着在应用压缩后也是如此。 例如,MessagePack在压缩之前通常比JSON更紧凑,但在大多数情况下在压缩之后更不那么紧凑。 这样,您应该在真实数据上测试不同的序列化和压缩组合,以确定哪种组合最适合您的应用程序。 作为一般经验法则,请使用JSON和gzip或(最好是bzip2)作为比较的基准。
By this point, you’ve sent the response code and data serialization and compression formats. One other useful header you can send back indicates the amount of data in the response body. Finally, you’ll send the optionally compressed serialized data stream back.
至此,您已经发送了响应代码以及数据序列化和压缩格式。 您可以发送回的另一个有用的标头指示响应正文中的数据量。 最后,您将发送可选的压缩序列化数据流。
<?php header("Content-Length: " . mb_strlen($data)); echo $data;When last we left DataModel.getData(), it was executing the HTTP request to get the data we need. Now that our PHP REST service has returned a response, we need to decompress and deserialize it before we can do something useful with it.
最后,当我们离开DataModel.getData() ,它正在执行HTTP请求以获取所需的数据。 现在,我们PHP REST服务已返回响应,我们需要对其进行解压缩和反序列化,然后才能对其进行有用的处理。
The exact code needed to decompress the response varies based on the compression scheme used. gzip and deflate are natively supported in Android. Surprisingly, bzip2 is both the more efficient of the three decompression formats and the more difficult to use because it’s not natively supported. The easiest way to add support for it is through the Apache Commons Compress library. Here’s what code to handle each of these compression schemes might look like:
解压缩响应所需的确切代码根据所使用的压缩方案而有所不同。 Android本身支持gzip和deflate。 出乎意料的是,bzip2既是三种解压缩格式中效率最高的一种,又由于其本身不受支持而更难使用。 添加支持的最简单方法是通过Apache Commons Compress库。 以下是处理每种压缩方案的代码:
if (httpResponse.getStatusLine().getStatusCode() != 200) { // An error occurred, throw an exception and handle it on the calling end } String encoding = httpResponse.getFirstHeader("Content-Encoding").getValue(); java.io.InputStream inputStream = null; if (encoding.equals("gzip")) { inputStream = AndroidHttpClient.getUngzippedContent(httpResponse.getEntity()); } else if (encoding.equals("deflate")) { inputStream = new java.util.zip.InflaterInputStream(httpResponse.getEntity().getContent(), new java.util.zip.Inflater(true)); } else if (encoding.equals("bzip2")) { inputStream = new org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream(httpResponse.getEntity().getContent()); } String content; try { content = new java.util.Scanner(inputStream).useDelimiter("\A").next(); } catch (java.util.NoSuchElementException e) { // An error occurred, throw an exception and handle it on the calling end }If you choose to use bzip2, you have two options in how you use the Apache Commons Compress library. The first is to add the library JAR file to your project. This is less tedious to do and makes upgrades as easy as replacing the JAR file and rebuilding the project, but it results in a larger Android APK file because it’s including the entire JAR contents. Here’s how to use this method:
如果选择使用bzip2,则在使用Apache Commons Compress库的方式上有两个选择。 第一种是将库JAR文件添加到您的项目中。 这样做不那么麻烦,并且使升级像替换JAR文件和重建项目一样容易,但是由于包含了整个JAR内容,因此它导致了更大的Android APK文件。 这是使用此方法的方法:
Download one of the Binaries archives for the library and extract it.
下载该库的二进制档案之一,并将其解压缩。
In Eclipse, click the Project menu and select the Properties option. 在Eclipse中,单击“项目”菜单,然后选择“属性”选项。 From the window that appears, select Java Build Path from the list on the left. 在出现的窗口中,从左侧列表中选择Java Build Path。 In the right pane, click the Libraries tab. 在右窗格中,单击“库”选项卡。 Click the Add External JARs button. 单击添加外部JAR按钮。Locate and select the commons-compress-#.jar file where # is the current version.
找到并选择commons-compress- # .jar文件,其中#是当前版本。
The second option is to get the individual source files you need for bzip2 compression and copy those into your project manually. This is a more tedious process, but your Android APK file size will be significantly smaller. Here’s how to use this method:
第二个选项是获取bzip2压缩所需的单个源文件,然后将其手动复制到项目中。 这是一个比较繁琐的过程,但是您的Android APK文件大小会大大减小。 这是使用此方法的方法:
Download the source tarball and extract it.
下载源tarball并将其解压缩。
In Eclipse, right-click on your project’s src directory, select New > Package, and enter the package name org.apache.commons.compress.compressors.
在Eclipse中,右键单击项目的src目录,选择New> Package,然后输入包名称org.apache.commons.compress.compressors 。
Repeat step 2, but this time use a package name of org.apache.commons.compress.compressors.bzip2.
重复步骤2,但是这次使用包名称org.apache.commons.compress.compressors.bzip2 。
Navigate to the src/main/java directory of the extracted source tarball.
导航到提取的源tarball的src/main/java目录。
Copy these files into your project within their associated packages: 将这些文件通过其关联的软件包复制到您的项目中:org/apache/commons/compress/compressors/CompressorInputStream.java
org/apache/commons/compress/compressors/CompressorInputStream.java
org/apache/commons/compress/compressors/bzip2/BZip2Constants.java
org/apache/commons/compress/compressors/bzip2/BZip2Constants.java
org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
org/apache/commons/compress/compressors/bzip2/BZip2CompressorInputStream.java
org/apache/commons/compress/compressors/bzip2/CRC.java
org/apache/commons/compress/compressors/bzip2/CRC.java
org/apache/commons/compress/compressors/bzip2/Rand.java
org/apache/commons/compress/compressors/bzip2/Rand.java
Once the response is decompressed, you’ll need to deserialize it into a usable data object. In the case of JSON, Android uses a Java implementation. The code to parse a JSON array of data might look like this:
将响应解压缩后,您需要将其反序列化为可用的数据对象。 对于JSON,Android 使用 Java实现。 解析JSON数据数组的代码可能如下所示:
import org.json.*; // ... // String content = ... JSONArray contacts = (JSONArray) new JSONTokener(content).nextValue(); JSONObject contact = contacts.getJSONObject(0); String email = contact.getString("email");The main focus of this article has been on minimizing the amount of data transferred during a request. In cases where the same resource may be requested multiple times, another way to make communication more efficient is to have the client (e.g. the Android application) cache responses and then indicate to the web service what version of the resource it last accessed.
本文的主要重点一直放在最小化请求期间传输的数据量。 在可能多次请求相同资源的情况下,使通信更有效的另一种方法是让客户端(例如Android应用程序) 缓存响应 ,然后向Web服务指示其上次访问资源的版本。
There are two ways to do this, both described in subsections of RFC 2616 Section 14. The first method is time-based where the server returns a Last-Modified header (subsection 29) in its response and the client can send that value in a If-Modified-Since header (subsection 25) in a subsequent request for the same resource. The second method is hash-based where the server sends a hash value in its response via the ETag header (subsection 19) and the client may send that value in a If-None-Match header (subsection 26) in a subsequent requests. In both cases, if the resource has not been updated, it will return a 304 (Not Modified) response status.
有两种方法可以做到,在RFC 2616第14节的小节中都有介绍。 第一种方法是基于时间的,其中服务器在其响应中返回一个Last-Modified头(第29小节),并且客户端可以在后续对同一资源的请求中,将该值发送到If-Modified-Since头(第25小节)中。 。 第二种方法是基于散列的,其中服务器通过ETag标头在子响应中发送一个散列值(第19小节),而客户端可以在后续请求中在If-None-Match标头中发送该值(第26小节)。 在这两种情况下,如果资源尚未更新,它将返回304(未修改)响应状态。
Using the first approach, you’d simply include a line like the one below on the PHP side where $timestamp is a UNIX timestamp representing the time that the requested resource was last modified. In many cases, a UNIX timestamp can be obtained from a formatted date string using the strtotime() function.
使用第一种方法,您只需在PHP端包含如下一行,其中$timestamp是UNIX时间戳,代表了所请求资源的最后修改时间。 在许多情况下,可以使用strtotime()函数从格式化的日期字符串中获取UNIX时间戳。
<?php header("Last-Modified: " . date("D, d M Y H:i:s", $timestamp) . " GMT");On the Android side, DataModel.getData() would be modified to look like this:
在Android方面, DataModel.getData()将被修改为如下所示:
// Fetch the Last-Modified response header value from a previous request from persistent storage if // available // String lastModified = ... // HttpGet httpRequest = ... httpRequest.addHeader("If-Modified-Since", lastModified); HttpResponse httpResponse = this.httpClient.execute(httpRequest); switch (httpResponse.getStatusLine().getStatusCode()) { case 304: // Use the cached version of the response break; case 200: // Handle the response normally lastModified = httpResponse.getFirstHeader( "Last-Modified").getValue(); // content = ... // Store both of the above variables in persistent storage break; }You now know how to implement content negotiation for serialization and compression purposes as well as response caching in both PHP and Android. I hope this article has proven useful to you and that your mobile applications are more efficient for it.
您现在知道了如何实现内容协商以进行序列化和压缩,以及如何在PHP和Android中进行响应缓存。 我希望本文已被证明对您有用,并且您的移动应用程序对其更有效。
Image via Fotolia
图片来自Fotolia
翻译自: https://www.sitepoint.com/lets-talk-2/