问题
最近项目需要一个下载的功能,所以在安卓端实现了一个普遍的下载功能模块。但Protal admin端发来信息说,怎么安卓端cal了l两次下载接口,当时以为是代码写错了,改了几次之后发现问题还在。经过网上搜索以及源码分析之后,才知道这是安卓或者说Java封装API的原因。
安卓端,使用Thread实现一个http get请求普遍方法:
<pre name="code" class="java">@Override
public void run(){
HttpURLConnection connection = null;
BufferedInputStream bis = null ;
RandomAccessFile accessFile = null ;
try{
URL url = new URL(url_str);
connection = (HttpURLConnection)url.openConnection();
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
fileSize = connection.getContentLength();
//package name
String con_dis = connection.getHeaderField("Content-Disposition");
fileName = con_dis.substring(con_dis.indexOf('"')+1,con_dis.lastIndexOf('"'));
Log.e("Name", fileName);
//以 路径+文件名 创建文件
File saveFile = new File(path+"/"+fileName);
//操作文件,设置长度
accessFile = new RandomAccessFile(saveFile,"rwd");
accessFile.setLength(fileSize);
//buffer空间
byte[] buffer = new byte[BUFF_SIZE];
bis = new BufferedInputStream(connection.getInputStream(),BUFF_SIZE);
int len ;
//写入buffer空间
while((len = bis.read(buffer,0,BUFF_SIZE))!= -1){
accessFile.write(buffer,0,len); //写入文件
downSize = downSize + len;
downPercent = (downSize*100)/fileSize;
}
/**
* 自动安装
*/
Message msg = new Message();
msg.what = 1 ; //下载完成
Bundle bundle = new Bundle();
bundle.putString("filePath", saveFile.getAbsolutePath());
msg.setData(bundle);
handler.sendMessage(msg);
}catch (Exception e){
e.printStackTrace();
}finally {
//close
if(connection!=null){
connection.disconnect();
}
try{
if(bis!=null){
bis.close();
}
if(accessFile!=null){
accessFile.close();
}
}catch (Exception e){
e.printStackTrace();
}
}
}
通过Protal admin端日志发现上述代码会call两次URL。先说结论:
1.第一次call发生在url.openConnection();
2.第二次call发生在connection.getInputStream();
openConnection()
如果创建了一个含有正确url的URL对象,并使用该对象的openConnection()方法时,该方法会先尝试进行请求以返回正确的URLConnection对象,请求不成功或协议不匹配则抛出异常。这也解释了为什么在执行完openConnection()之后,可以通过connection.getContentLength()、connection.getHeaderField()等方法获得关于这次请求的响应信息,然后通过这些信息来提前为正式的请求作一些准备(判断响应内容、创建文件、设置文件大小等)。
该方法将返回一个关于这次请求的HttpURLconnection对象,但在我想避免call两接口而想自己创建一个HttpURLConnection对象时,Gradle给我一个这样的回应:
connection = new HttpURLConnection() {
@Override
public void disconnect() {
}
@Override
public boolean usingProxy() {
return false;
}
@Override
public void connect() throws IOException {
}
};
原来HttpURLCoonection是一个抽象的类,那么URL.openConnection()返回的是什么?想要解答疑问,还是要看回源码。
/**
* Returns a new connection to the resource referred to by this URL.
*
* @throws IOException if an error occurs while opening the connection.
*/
public URLConnection openConnection() throws IOException {
<span style="white-space:pre"> </span> return streamHandler.openConnection(this);
}
返回的是URLConnection对象,由于HttpURLConnection继承了URLConnection,所以可以进行类型转换。继续分析streamHandler,它在URLConnection的setupHandlerStream中被赋值。
/**
* Sets the receiver's stream handler to one which is appropriate for its
* protocol.
*
* <p>Note that this will overwrite any existing stream handler with the new
* one. Senders must check if the streamHandler is null before calling the
* method if they do not want this behavior (a speed optimization).
*
* @throws MalformedURLException if no reasonable handler is available.
*/
void setupStreamHandler() {
// Check for a cached (previously looked up) handler for
// the requested protocol.
streamHandler = streamHandlers.get(protocol);
if (streamHandler != null) {
return;
}
// If there is a stream handler factory, then attempt to
// use it to create the handler.
if (streamHandlerFactory != null) {
streamHandler = streamHandlerFactory.createURLStreamHandler(protocol);
if (streamHandler != null) {
streamHandlers.put(protocol, streamHandler);
return;
}
}
// Check if there is a list of packages which can provide handlers.
// If so, then walk this list looking for an applicable one.
String packageList = System.getProperty("java.protocol.handler.pkgs");
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (packageList != null && contextClassLoader != null) {
for (String packageName : packageList.split("\\|")) {
String className = packageName + "." + protocol + ".Handler";
try {
Class<?> c = contextClassLoader.loadClass(className);
streamHandler = (URLStreamHandler) c.newInstance();
if (streamHandler != null) {
streamHandlers.put(protocol, streamHandler);
}
return;
} catch (IllegalAccessException ignored) {
} catch (InstantiationException ignored) {
} catch (ClassNotFoundException ignored) {
}
}
}
// Fall back to a built-in stream handler if the user didn't supply one
if (protocol.equals("file")) {
streamHandler = new FileHandler();
} else if (protocol.equals("ftp")) {
streamHandler = new FtpHandler();
} else if (protocol.equals("http")) {
// 判断一下如果是HTTP协议,就会创建HtppHandler。看到这里明白了,原来使用的是okhttp.
try {
String name = "com.android.okhttp.HttpHandler";
streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
} catch (Exception e) {
throw new AssertionError(e);
}
} else if (protocol.equals("https")) {
try {
String name = "com.android.okhttp.HttpsHandler";
streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
} catch (Exception e) {
throw new AssertionError(e);
}
} else if (protocol.equals("jar")) {
streamHandler = new JarHandler();
}
if (streamHandler != null) {
streamHandlers.put(protocol, streamHandler);
}
}
注意代码中间部分,streamHandler被赋值了一个在”com.android.okhttp”包里的HttpHandler对象,继续看HttpHandler里面的openConnection()方法。
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 com.squareup.okhttp;
import java.io.IOException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
public final class HttpHandler extends URLStreamHandler {
@Override protected URLConnection openConnection(URL url) throws IOException {
// 调用了OKHttpClient()的方法
return new OkHttpClient().open(url);/*************************************/
}
@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
return new OkHttpClient().setProxy(proxy).open(url);/****************************************/
}
@Override protected int getDefaultPort() {
return 80;
}
}
接着是OkHttpClient.open()方法。
public HttpURLConnection open(URL url) {
return open(url, proxy);
}
HttpURLConnection open(URL url, Proxy proxy) {
String protocol = url.getProtocol();
// 将该对象clone后设置一些其他的属性返回,里面会设置一个默认的连接池。
OkHttpClient copy = copyWithDefaults();
copy.proxy = proxy;
// 返回了HttpURLConnectionImpl,并且把clone后的OKHttpClient对象传递进去。
if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
最后调用终结,返回的是一个HttpURLConnectionImpl对象,继续翻查HttpConnectionImpl类源码,发现它是一个继承了
HttpConnection的类。
public class HttpURLConnectionImpl extends HttpURLConnection {
/*.......................*/
}
getInputStream()
调用getInputStream()方法会隐式地进行连接,并且得到请求的响应。
上面分析到openConnection()返回的是HttpURLConnectionImpl对象,接着看HttpURLConnectionImpl的getInputStream()方法源码。
@Override
public final InputStream getInputStream() throws IOException {
if (!doInput) {
throw new ProtocolException("This protocol does not support input");
}
HttpEngine response = getResponse();
// if the requested file does not exist, throw an exception formerly the
// Error page from the server was returned if the requested file was
// text/html this has changed to return FileNotFoundException for all
// file types
if (getResponseCode() >= HTTP_BAD_REQUEST) {
throw new FileNotFoundException(url.toString());
}
InputStream result = response.getResponseBody();
if (result == null) {
throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
}
return result;
}
接着看getResponse()。
/**
* Aggressively tries to get the final HTTP response, potentially making
* many HTTP requests in the process in order to cope with redirects and
* authentication.
*/
private HttpEngine getResponse() throws IOException {
initHttpEngine();
if (httpEngine.hasResponse()) {
return httpEngine;
}
while (true) {
if (!execute(true)) {
continue;
}
Retry retry = processResponseHeaders();
if (retry == Retry.NONE) {
httpEngine.automaticallyReleaseConnectionToPool();
return httpEngine;
}
// The first request was insufficient. Prepare for another...
String retryMethod = method;
OutputStream requestBody = httpEngine.getRequestBody();
// Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
// redirect should keep the same method, Chrome, Firefox and the
// RI all issue GETs when following any redirect.
int responseCode = getResponseCode();
if (responseCode == HTTP_MULT_CHOICE
|| responseCode == HTTP_MOVED_PERM
|| responseCode == HTTP_MOVED_TEMP
|| responseCode == HTTP_SEE_OTHER) {
retryMethod = "GET";
requestBody = null;
}
if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
throw new HttpRetryException("Cannot retry streamed HTTP body",
httpEngine.getResponseCode());
}
if (retry == Retry.DIFFERENT_CONNECTION) {
httpEngine.automaticallyReleaseConnectionToPool();
}
httpEngine.release(false);
httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
(RetryableOutputStream) requestBody);
}
}
注释讲的很清楚了,就是尝试得到最终的响应,所以在调用getInputStream()之前,请确保你的请求信息已完整。
getOutputStream()
其实不只getInputStream(),调用getOutputStream()方法也会隐式地进行连接,所以最后在添加请求体后,connect()可以不写。
上面分析到openConnection()返回的是HttpURLConnectionImpl对象,接着看HttpURLConnectionImpl的getOutputStream()方法源码。
@Override
public final OutputStream getOutputStream() throws IOException {
connect();
OutputStream out = httpEngine.getRequestBody();
if (out == null) {
throw new ProtocolException("method does not support a request body: " + method);
} else if (httpEngine.hasResponse()) {
throw new ProtocolException("cannot write request body after response has been read");
}
if (faultRecoveringRequestBody == null) {
faultRecoveringRequestBody = new FaultRecoveringOutputStream(MAX_REPLAY_BUFFER_LENGTH, out) {
@Override protected OutputStream replacementStream(IOException e) throws IOException {
if (httpEngine.getRequestBody() instanceof AbstractOutputStream
&& ((AbstractOutputStream) httpEngine.getRequestBody()).isClosed()) {
return null; // Don't recover once the underlying stream has been closed.
}
if (handleFailure(e)) {
return httpEngine.getRequestBody();
}
return null; // This is a permanent failure.
}
};
}
return faultRecoveringRequestBody;
}
该方法会调用connect()方法,接着看connect源码。
@Override
public final void connect() throws IOException {
initHttpEngine();
boolean success;
do {
success = execute(false);
} while (!success);
}
接着initHttpEngine()和excute()
private void initHttpEngine() throws IOException {
if (httpEngineFailure != null) {
throw httpEngineFailure;
} else if (httpEngine != null) {
return;
}
connected = true;
try {
if (doOutput) {
if (method.equals("GET")) {
// they are requesting a stream to write to. This implies a POST method
method = "POST";
} else if (!method.equals("POST") && !method.equals("PUT")) {
// If the request method is neither POST nor PUT, then you're not writing
throw new ProtocolException(method + " does not support writing");
}
}
httpEngine = new HttpEngine(method, rawRequestHeaders, null, null);
} catch (IOException e) {
httpEngineFailure = e;
throw e;
}
}
/**
* Sends a request and optionally reads a response. Returns true if the
* request was successfully executed, and false if the request can be
* retried. Throws an exception if the request failed permanently.
*/
private boolean execute(boolean readResponse) throws IOException {
try {
httpEngine.sendRequest();
if (readResponse) {
httpEngine.readResponse();
}
return true;
} catch (IOException e) {
if (handleFailure(e)) {
return false;
} else {
throw e;
}
}
}
这里将调用sendRequest()方法,也就是发送请求了。
/**
* Figures out what the response source will be, and opens a socket to that
* source if necessary. Prepares the request headers and gets ready to start
* writing the request body if it exists.
*/
public final void sendRequest() throws IOException {
if (responseSource != null) {
return;
}
prepareRawRequestHeaders();
initResponseSource();
if (policy.responseCache != null) {
policy.responseCache.trackResponse(responseSource);
}
// The raw response source may require the network, but the request
// headers may forbid network use. In that case, dispose of the network
// response and use a GATEWAY_TIMEOUT response instead, as specified
// by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) {
if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
Util.closeQuietly(cachedResponseBody);
}
this.responseSource = ResponseSource.CACHE;
this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE;
RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true);
setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody());
}
if (responseSource.requiresConnection()) {
sendSocketRequest();
} else if (connection != null) {
policy.connectionPool.recycle(connection);
connection = null;
}
}
从上面可以看到,Java内部为普通开发者封装好了各种复杂协议的API,使得开发者可以通过简单的调用就可以完成一个本应庞大且复杂的网络请求。但是弊端也是可以遇见的,就是当发现一个问题或者疑惑时,需要把API层层剥开并分析每一个函数的作用。
相关
参考:http://blog.csdn.net/charon_chui/article/details/46895773
HttpURLConnectionImpl.java源码:传送门