【问题标题】:How to avoid parsing when the server returns the same response using Retrofit?当服务器使用 Retrofit 返回相同的响应时,如何避免解析?
【发布时间】:2016-01-04 16:00:05
【问题描述】:

我曾经通过计算响应的哈希避免解析服务器响应,如果它没有没有改变:: p>

public class HttpClient {

    protected OkHttpClient mClient = new OkHttpClient();

    public String get(final URL url, final String[] responseHash)
        throws IOException {
        HttpURLConnection connection = new OkUrlFactory(mClient).open(url);
        InputStream inputStream = null;
        MessageDigest messageDigest = null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        assert messageDigest != null;
        try {
            // Read the response.
            inputStream = connection.getInputStream();
            byte[] response = readFully(inputStream);
            final byte[] digest = messageDigest.digest(response);
            responseHash[0] = Base64.encodeToString(digest, Base64.DEFAULT);
            return new String(response, Util.UTF_8);
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }

    private byte[] readFully(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        for (int count; (count = in.read(buffer)) != -1; ) {
            out.write(buffer, 0, count);
        }
        return out.toByteArray();
    }

}

这是响应头:

HTTP/1.1 200 OK
Server: Apache/2.4.10 (Linux/SUSE)
X-Powered-By: PHP/5.4.20
X-UA-Compatible: IE=edge
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Expires: Thu, 08 Oct 2015 16:15:09 +0000
X-Frame-Options: SAMEORIGIN
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Date: Wed, 07 Oct 2015 16:15:09 GMT
X-Varnish: 505284843
Age: 0
Via: 1.1 varnish
Connection: keep-alive

现在我切换到Retrofit 我想知道避免解析相同响应的优雅方法是什么? 拦截器是要走的路吗?我不负责服务器后端,也不能修改它。

【问题讨论】:

  • If-Modified-Since ?当然你需要逻辑后端
  • E-tag 标头,当然,如果您的后端已实现此功能。 w3.org/Protocols/rfc2616/rfc2616-sec14.html14.9
  • @JJD 您是否愿意重新下载数据然后检查内容,而不是在下载前检查数据是否已更改?那不是效率低吗?
  • @axierjhtjz 不。如果我可以避免使用给定的后端多次下载数据,我宁愿不这样做。但是,当这不可能时,我想避免解析。

标签: http-headers response retrofit okhttp http-caching


【解决方案1】:

更新

您可以使用 Expires 标头进行缓存控制,以便避免不必要的下载。我认为这不是一个好方法,但在这种情况下,由于您无法控制服务器端,这是我现在能想到的唯一方法。

实体的过期时间可以由源服务器指定 使用 Expires 标头(参见第 14.21 节)。或者,它可能是 在响应中使用 max-age 指令指定。当最大年龄 缓存控制指令存在于缓存的响应中,响应 如果它的当前年龄大于给定的年龄值,则它是陈旧的(在 秒)在对该资源的新请求时。最大年龄 响应上的指令意味着响应是可缓存的(即, "public") 除非还有其他一些限制性更强的缓存指令 现在。

如果响应同时包含 Expires 标头和 max-age 指令, max-age 指令会覆盖 Expires 标头,即使 Expires 标头更具限制性。此规则允许源服务器 为给定的响应提供更长的到期时间 HTTP/1.1(或更高版本)缓存而不是 HTTP/1.0 缓存。这可能是 如果某些 HTTP/1.0 缓存不正确地计算年龄或 过期时间,可能是由于时钟不同步。

许多 HTTP/1.0 缓存实现将处理 Expires 值 小于或等于响应日期值作为等效 到 Cache-Control 响应指令“no-cache”。如果一个 HTTP/1.1 缓存接收到这样的响应,并且响应中不包含 Cache-Control 头域,它应该认为响应是 不可缓存,以保持与 HTTP/1.0 服务器的兼容性。

注意:源服务器可能希望使用相对较新的 HTTP 缓存 网络上的控制功能,例如“私人”指令 包括不理解该功能的旧缓存。起源 服务器将需要将新功能与 Expires 字段相结合 其值小于或等于 Date 值。这将防止 旧缓存不正确缓存响应。


有不同的方法。我用这个:

  • 在服务器响应中,我们获得 Etag 标头并将其保存在 SharedPreferences 中。
  • 每个服务器调用都带有带有 Etag 值的“If-None-Match”标头。
  • 服务器比较 Etag 值并返回 304 - 未修改或请求本身的结果(如果发生更改且内容需要更新)。

您可以使用RequestInterceptor 来执行您指出的操作:

public class HeaderRequestInterceptor implements RequestInterceptor {

    private final static String TAG = 
        HeaderRequestInterceptor.class.getSimpleName();

    private SharedPreferences mPreferences;

    public HeaderRequestInterceptor() {
        mPreferences = PreferenceManager.getDefaultSharedPreferences(
            DaoApplication.getAppContext());
    }

    @Override
    public void intercept(RequestFacade request) {
        String etagValue = mPreferences.getString(EtagConfig.MY_ETAG_VALUE, "");
        request.addHeader("If-None-Match", etagValue);
    }
}

样本输出:

Retrofit  D  ---> HTTP GET https://url.irontec.com/rest/schedule
    D  If-None-Match:
    D  Authorization: MyToken M2JiOGQwZGNjNWJiNWNiOTA1Yjc3YTA0YTAyMzEwYWY6OjIwMTUtMTAtMDhUMTM6MDc6MDMrMDA6MDA=
    D  Connection: close

Retrofit  D  <--- HTTP 200 https://url.irontec.com/rest/schedule (559ms)
    D  : HTTP/1.1 200 OK
    D  Access-Control-Allow-Credentials: true
    D  Access-Control-Allow-Headers: Authorization, Origin, Content-Type, X-CSRF-Token
    D  Access-Control-Allow-Methods: GET, PUT, POST, OPTIONS, DELETE
    D  Access-Control-Allow-Origin: *
    D  Connection: close
    D  Content-Type: application/json; charset=UTF-8;
    D  Date: Thu, 08 Oct 2015 13:07:07 GMT
    D  Etag: a3145c3f85f2dca1c78f87107331c766
    D  Server: Apache
    D  Transfer-Encoding: chunked
    D  X-Android-Received-Millis: 1444309624169
    D  X-Android-Response-Source: NETWORK 200
    D  X-Android-Sent-Millis: 1444309623870
    D  X-Content-Type-Options: nosniff
    D  X-Frame-Options: sameorigin

现在刷新内容时:

Retrofit  D  ---> HTTP GET https://url.irontec.com/rest/schedule
    D  If-None-Match: a3145c3f85f2dca1c78f87107331c766
    D  Authorization: MyToken MGQ1OWM4YjViYTMxZWM3OGRmMDBlYTZjNmFjNDY3MmI6OjIwMTUtMTAtMDhUMTM6MTA6MDkrMDA6MDA=
    D  Connection: close
    D  ---> END HTTP (no body)

Retrofit  D  <--- HTTP 304 https://url.irontec.com/rest/schedule (299ms)
    D  : HTTP/1.1 304 Not Modified
    D  Connection: close
    D  Date: Thu, 08 Oct 2015 13:10:12 GMT
    D  Server: Apache
    D  X-Android-Received-Millis: 1444309809335
    D  X-Android-Response-Source: NETWORK 304
    D  X-Android-Sent-Millis: 1444309809163
    D  <--- END HTTP (0-byte body)

【讨论】:

  • 感谢您的回答,但“我不负责服务器后端,也不能修改它”。从服务器响应中可以看出,没有 Etag 也没有 If-Modified-Since 标头。
  • 您能否在回答中明确说明您的方法是避免不必要的下载还是避免解析相等的响应
  • @JJD 我的答案是避免不必要的下载,我会更新我的答案以使其明确。
【解决方案2】:

是的,您可以使用拦截器。请记住,它将根据您的所有请求运行,因此需要考虑到这一点。这是一个示例拦截器,以避免在数据与给定哈希相同时进行解析。首先,它使用标头来传达预期和计算的哈希值。由于标头有字符限制,因此我使用十六进制编码而不是 base64 作为哈希值。如果预期的哈希为空,它让请求正常处理并且不进行哈希检查。这是为了解决您可能不想散列的请求。如果预期的哈希值不为空且不等于计算的哈希值,则 Retrofit 解析会正常进行,除了我们在响应中添加一个标头,以便调用者可以存储返回的哈希值。如果预期和计算出的哈希值相等,则响应将转换为没有正文的204 (No content),这将阻止解析。

public class HashingInterceptor implements Interceptor {
    public static final String HASH_HEADER = "content-hash";

    final protected static char[] hexArray = "0123456789abcdef".toCharArray();

    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = hexArray[v >>> 4];
            hexChars[j * 2 + 1] = hexArray[v & 0x0F];
        }
        return new String(hexChars);
    }

    @Override
    public com.squareup.okhttp.Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        String expectedHash = request.header(HASH_HEADER);
        if (expectedHash != null) {
            com.squareup.okhttp.Response response = chain.proceed(request);
            byte[] bytes = response.body().bytes();
            try {
                MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                final byte[] digest = messageDigest.digest(bytes);
                String responseHash = bytesToHex(digest);
                if (responseHash.equals(expectedHash)) {
                    return response.newBuilder()
                            .code(204).build();
                } else {
                    return response.newBuilder()
                        .body(ResponseBody.create(
                            response.body().contentType(), bytes))
                        .addHeader(HASH_HEADER, responseHash)
                        .build();
                }
            } catch (NoSuchAlgorithmException e) {
                throw new IOException(e);
            }

        } else {
            // Header was not set, just proceed as usual
            return chain.proceed(request);
        }
    }
}

要使用,请创建一个接口。注意:我假设您在这里使用的是 Retrofit 2。

public interface GitHubService {

    @GET("/users/{user}")
    Call<User> users(
        @Path("user") String user, 
        @Header(HashingInterceptor.HASH_HEADER) String hash);

}

创建和使用 --

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.interceptors()
        .add(new HashingInterceptor());
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .client(okHttpClient)
        .build();
final GitHubService gitHubService = retrofit.create(GitHubService.class);
// Note: make sure hashValue is non-null on the first request to 
// make sure the hash is computed
Call<User> users = gitHubService.users("octocat", hashValue);
users.enqueue(new Callback<User>() {
    @Override
    public void onResponse(Response<User> response, Retrofit retrofit) {
        // 200 = updated date, 204 = same data, not parsed
        Log.d("response", "code = " + response.code());  
        Log.d("response", "returned hash = " +  
            response.headers().get(HashingInterceptor.HASH_HEADER));
    }

    @Override
    public void onFailure(Throwable t) {
        t.printStackTrace();
    }
});

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-01-30
    相关资源
    最近更新 更多