/* * Copyright (C) 2014 Neal * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package neal.http.impl; /** * Created by Neal on 2014/10/28. */ import android.os.SystemClock; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.impl.cookie.DateUtils; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.util.Date; import java.util.HashMap; import java.util.Map; import neal.http.base.Cache; import neal.http.base.HttpError; import neal.http.base.HttpStack; import neal.http.base.Network; import neal.http.base.NetworkResponse; import neal.http.base.Request; import neal.http.base.RetryPolicy; import neal.http.utils.ByteArrayPool; import neal.http.utils.HttpLog; import neal.http.utils.PoolingByteArrayOutputStream; /** * A network performing requests over an {@link neal.http.base.HttpStack}. */ public class BasicNetwork implements Network { protected static final boolean DEBUG = HttpLog.DEBUG; private static int SLOW_REQUEST_THRESHOLD_MS = 3000; private static int DEFAULT_POOL_SIZE = 4096; protected final HttpStack mHttpStack; protected final ByteArrayPool mPool; /** * @param httpStack HTTP stack to be used */ public BasicNetwork(HttpStack httpStack) { /** * If a pool isn't passed in, then build a small default pool that will give us a lot of *benefit and not use too much memory. */ this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); } /** * @param httpStack HTTP stack to be used * @param pool a buffer pool that improves GC performance in copy operations */ public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { mHttpStack = httpStack; mPool = pool; } @Override public NetworkResponse performRequest(Request<?> request) throws HttpError { long requestStart = SystemClock.elapsedRealtime(); /** * while(true) here is for retry */ while (true) { HttpResponse httpResponse = null; byte[] responseContents = null; Map<String, String> responseHeaders = new HashMap<String, String>(); try { /** * 加入缓存相关的报文头 */ Map<String, String> headers = new HashMap<String, String>(); addCacheHeaders(headers, request.getCacheEntry()); httpResponse = mHttpStack.performRequest(request, headers); StatusLine statusLine = httpResponse.getStatusLine(); int statusCode = statusLine.getStatusCode(); responseHeaders = convertHeaders(httpResponse.getAllHeaders()); /** * Handle cache validation. * 缓存有效,未过期 */ if (statusCode == HttpStatus.SC_NOT_MODIFIED) { return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, request.getCacheEntry() == null ? null : request.getCacheEntry().data, responseHeaders, true); } /** * Some responses such as 204s do not have content. We must check. */ if (httpResponse.getEntity() != null) { responseContents = entityToBytes(httpResponse.getEntity()); } else { /** * Add 0 byte response as a way of honestly representing a no-content request. */ responseContents = new byte[0]; } /** * if the request is slow, log it. */ long requestLifetime = SystemClock.elapsedRealtime() - requestStart; logSlowRequests(requestLifetime, request, responseContents, statusLine); if (statusCode < 200 || statusCode > 299) { throw new IOException(); } return new NetworkResponse(statusCode, responseContents, responseHeaders, false); } catch (SocketTimeoutException e) { attemptRetryOnException("socket", request, new HttpErrorCollection.TimeoutError()); } catch (ConnectTimeoutException e) { attemptRetryOnException("connection", request, new HttpErrorCollection.TimeoutError()); } catch (MalformedURLException e) { throw new RuntimeException("Bad URL " + request.getUrl(), e); } catch (IOException e) { int statusCode = 0; NetworkResponse networkResponse = null; if (httpResponse != null) { statusCode = httpResponse.getStatusLine().getStatusCode(); } else { throw new HttpErrorCollection.NoConnectionError(e); } HttpLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); if (responseContents != null) { networkResponse = new NetworkResponse(statusCode, responseContents, responseHeaders, false); if (statusCode == HttpStatus.SC_UNAUTHORIZED || statusCode == HttpStatus.SC_FORBIDDEN) { attemptRetryOnException("auth", request, new HttpErrorCollection.AuthFailureError(networkResponse)); } else { // TODO: Only throw ServerError for 5xx status codes. throw new HttpErrorCollection.ServerError(networkResponse); } } else { throw new HttpErrorCollection.NetworkError(networkResponse); } } } } /** * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. */ private void logSlowRequests(long requestLifetime, Request<?> request, byte[] responseContents, StatusLine statusLine) { if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { HttpLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + "[rc=%d], [retryCount=%s]", request, requestLifetime, responseContents != null ? responseContents.length : "null", statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount()); } } /** * Attempts to prepare the request for a retry. If there are no more attempts remaining in the * request's retry policy, a timeout exception is thrown. * @param request The request to use. */ private static void attemptRetryOnException(String logPrefix, Request<?> request, HttpError exception) throws HttpError { RetryPolicy retryPolicy = request.getRetryPolicy(); int oldTimeout = request.getTimeoutMs(); try { retryPolicy.retry(exception); } catch (HttpError e) { request.addMarker( String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); throw e; } request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); } private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) { // If there's no cache entry, we're done. if (entry == null) { return; } /** * 加入缓存报文头。 * 简单的说,Last-Modified 与If-Modified-Since 都是用于记录页面最后修改时间的 HTTP 头信息, * 只是 Last-Modified 是由服务器往客户端发送的 HTTP 头,而 If-Modified-Since 则是由客户端 * 往服务器发送的头,可 以看到,再次请求本地存在的 cache 页面时,客户端会通过 * If-Modified-Since 头将先前服务器端发过来的 Last-Modified 最后修改时间戳发送回去,这 * 是为了让服务器端进行验证,通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的, * 则返回新的内容,如果是最新的,则 返回 304 告诉客户端其本地 cache 的页面是最新的,于是客 * 户端就可以直接从本地加载页面了,这样在网络上传输的数据就会大大减少,同时也减轻了服务器的负担。 * ETags和If-None-Match是一种常用的判断资源是否改变的方法。类似于Last-Modified和HTTP-IF-MODIFIED-SINCE。 * 但是有所不同的是Last-Modified和HTTP-IF-MODIFIED-SINCE只判断资源的最后修改时间,而ETags和If-None-Match * 可以是资源任何的任何属性,不如资源的MD5等 ETags和If-None-Match的工作原理是在HTTP Response中添 * 加ETags信息。当客户端再次请求该资源时,将在HTTP Request中加入If-None-Match信息(ETags的值)。 * 如果服务器验证资源的ETags没有改变(该资源没有改变),将返回一个304状态;否则,服务器将返回200状态, * 并返回该资源和新的ETags。 */ if (entry.etag != null) { headers.put("If-None-Match", entry.etag); } if (entry.serverDate > 0) { Date refTime = new Date(entry.serverDate); headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); } } protected void logError(String what, String url, long start) { long now = SystemClock.elapsedRealtime(); HttpLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); } /** Reads the contents of HttpEntity into a byte[]. */ private byte[] entityToBytes(HttpEntity entity) throws IOException, HttpErrorCollection.ServerError { PoolingByteArrayOutputStream bytes = new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); byte[] buffer = null; try { InputStream in = entity.getContent(); if (in == null) { throw new HttpErrorCollection.ServerError(); } buffer = mPool.getBuf(1024); int count; while ((count = in.read(buffer)) != -1) { bytes.write(buffer, 0, count); } return bytes.toByteArray(); } finally { try { /**Close the InputStream and release the resources by "consuming the content".*/ entity.consumeContent(); } catch (IOException e) { /** * This can happen if there was an exception above that left the entity in * an invalid state. */ HttpLog.v("Error occured when calling consumingContent"); } mPool.returnBuf(buffer); bytes.close(); } } /** * Converts Headers[] to Map<String, String>. */ private static Map<String, String> convertHeaders(Header[] headers) { Map<String, String> result = new HashMap<String, String>(); for (int i = 0; i < headers.length; i++) { result.put(headers[i].getName(), headers[i].getValue()); } return result; } }