Volley源码完全解析之HttpStack

HttpURLConnection和HttpClient

  • 第一篇系列文章中我们说过,volley的底层是使用HttpURLConnection和HttpClient来实现的,所以在对HttpStack进行分析时,我们需要对HttpURLConnection和HttpClient进行必要的了解。

HttpURLConnection

  • HttpURLConnection是Java的标准类,它继承自URLConnection,可用于向指定网站发送GET请求、POST请求。需要注意的是HttpURLConnection是一个抽象类,所以不能直接创建它的实例,所以一般我们通过URL类的openConnection()方法获得它的实例。
  • 其用法可归纳如下:
    • 获取HttpURLConnection的实例
    URL url = new URL("http://www.baidu.com");
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    
    • 设置HTTP请求所使用的方法:GET和POST。GET标识希望从服务器得到数据,而POST表示希望提交数据到服务器
    connection.setRequestMethod("GET");
    connection.setRequestMethod("POST");
    
    • 设置连接超时,读取超时的毫秒数等
    connection.setConnectTimeout(8000);
    connection.setReadTimeout(8000);
    
    • 获取服务器返回的输入流(若HTTP请求为GET)
    InputStream in = connection.getInputStream();
    
    • 向服务器提交数据(若HTTP请求为POST)
    DataOutputStream out = new DataOutputStream(connection.getOutputStresm());
    out.writeBytes("username=admin&password=123456");
    
    • 将HTTP连接关闭
    connection.disconnect();
    
  • 注意事项
    • HttpURLConnection的connect()函数,实际上只是建立了一个与服务器的tcp连接,并没有实际发送http请求。无论是post还是get,http请求实际上直到HttpURLConnection的getInputStream()这个函数里面才正式发送出去。
    • 在用POST方式发送URL请求时,URL请求参数的设定顺序是重中之重,对connection对象的一切配置(那一堆set函数),都必须要在connect()函数执行之前完成。而对outputStream的写操作,又必须要在inputStream的读操作之前。 这些顺序实际上是由http请求的格式决定的。 如果inputStream读操作在outputStream的写操作之前,会抛出异常: java.net.ProtocolException: Cannot write output after reading input……
    • http请求实际上由两部分组成,一个是http头,所有关于此次http请求的配置都在http头里面定义,一个是正文content。 connect()函数会根据HttpURLConnection对象的配置值生成http头部信息,因此在调用connect函数之前,就必须把所有的配置准备好。
    • 在http头后面紧跟着的是http请求的正文,正文的内容是通过outputStream流写入的, 实际上outputStream不是一个网络流,充其量是个字符串流,往里面写入的东西不会立即发送到网络,而是存在于内存缓冲区中,待outputStream流关闭时,根据输入的内容生成http正文。 至此,http请求的东西已经全部准备就绪。在getInputStream()函数调用的时候,就会把准备好的http请求 正式发送到服务器了,然后返回一个输入流,用于读取服务器对于此次http请求的返回信息。由于http请求在getInputStream的时候已经发送出去了(包括http头和正文),因此在getInputStream()函数 之后对connection对象进行设置(对http头的信息进行修改)或者写入outputStream(对正文进行修改) 都是没有意义的了,执行这些操作会导致异常的发生。

HttpClient

  • 它是一个简单的HTTP客户端(并不是浏览器),可以用于发送HTTP请求,接收HTTP响应。但不会缓存服务器的响应,不能执行HTML页面中嵌入的Javascript代码;也不会对页面内容进行任何解析、处理。
  • 简单来说,HttpClient就是一个增强版的HttpURLConnection,HttpURLConnection可以做的事情HttpClient全部可以做;HttpURLConnection没有提供的有些功能,HttpClient也提供了,但它只是关注于如何发送请求、接收响应,以及管理HTTP连接。
  • 其用法可总结如下:
    • 创建一个DefaultHttpClient实例
    HttpClient httpClient = new DefaultHttpClient();
    
    • 如果是进行GET请求
    HttpGet httpGet = new HttpGet("http://www.baidu.com");
    HttpResponse httpResponse = httpClient.execute(httpGet);
    
    • 如果是进行POST请求
      创建一个HttpPost对象
    HttpPost httpPost = new HttpPost("http://www.baidu.com");
    
    通过一个NameValuePair集合来存放待提交的参数,并将这个参数集合传入到一个UrlEncodedFormEntity中,然后调用HttpPost的setEntity()方法将构建好的UrlEncodedEntity传入,之后调用httpClient的execute()方法,并将httpPost对象传入,并接受服务器返回的httpResponse对象。
    List<NameValuePair> params = new ArrayList<NameValuePair>();
    params.add(new BasicNameValuePair("username", "admin"));
    params.add(new BasicNameValuePair("password", "123456"));
    UrlEncodedFormEntity entity = new   UrlEncodedFormEntity(params, "utf-8");
    httpPost.setEntity(entity);
    HttpResponse httpResponse = httpClient.execute(httpPost);
    
    • 取出服务器返回的状态码,如果等于200就说明请求和响应都成功了
    if(httpResponse.getStatusLine().getStatusCode() == 200) {
      HttpEntity entity = httpResponse.getEntity();
      String response = EntityUtils.toString(entity, "utf-8");
    }
    

HttpURLConnection和HttpClient的选择

  • 在Android 2.2版本之前,HttpClient拥有较少的bug,因此使用它是最好的选择。
  • 而在Android 2.3版本及以后,HttpURLConnection则是最佳的选择。它的API简单,体积较小,因而非常适用于Android项目。压缩和缓存机制可以有效地减少网络访问的流量,在提升速度和省电方面也起到了较大的作用。对于新的应用程序应该更加偏向于使用HttpURLConnection,因为在以后的工作当中我们也会将更多的时间放在优化HttpURLConnection上面。
  • 在前面的Volley解析中我们也提到,它遵从上面的规律使用的。

HttpStack

  • 在对HttpURLConnection和HttpClient有了初步的了解后,我们就可以进行HttpStack的解析了。
  • HttpStack是一个接口,它有一个抽象方法performRequest,也就是进行请求的方法。
HttpResponse performRequest(Request<?> var1, Map<String, String> var2) throws IOException, AuthFailureError;
  • 该接口分别有HurlStack和HttpClientStack两实现类,也就是分别对应HttpURLConnection和HttpClient,两种不同的HTTP请求方式,接下来我们分别介绍这两个不同的类。

HurlStack

  • 首先看到HurlStack的成员变量
    private static final String HEADER_CONTENT_TYPE = "Content-Type";
    private final HurlStack.UrlRewriter mUrlRewriter;
    private final SSLSocketFactory mSslSocketFactory;

HurlStack的成员变量只有三个,一个是表示“Content-Type”字符串的静态常量,一个URL转换接口,还有一个是生成SSLSocket的工厂类。
至于SSLSocket,它是扩展Socket并提供使用SSL或TLS协议的安全套接字。这种套接字是正常的流套接字,但是它们在基础网络传输协议(如TCP)上添加了安全保护层。

  • 既然HurlStack实现了HttpStack接口的,那么performRequest方法的实现肯定是重中之重。
    这里先直接贴出performRequest的实现,再对其进行分析。
 public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError {
        String url = request.getUrl();
        HashMap<String, String> map = new HashMap();
        map.putAll(request.getHeaders());
        map.putAll(additionalHeaders);
        if(this.mUrlRewriter != null) {
            String rewritten = this.mUrlRewriter.rewriteUrl(url);
            if(rewritten == null) {
                throw new IOException("URL blocked by rewriter: " + url);
            }

            url = rewritten;
        }

        URL parsedUrl = new URL(url);
        HttpURLConnection connection = this.openConnection(parsedUrl, request);
        Iterator var8 = map.keySet().iterator();

        while(var8.hasNext()) {
            String headerName = (String)var8.next();
            connection.addRequestProperty(headerName, (String)map.get(headerName));
        }

        setConnectionParametersForRequest(connection, request);
        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if(responseCode == -1) {
            throw new IOException("Could not retrieve response code from HttpUrlConnection.");
        } else {
            StatusLine responseStatus = new BasicStatusLine(protocolVersion, connection.getResponseCode(), connection.getResponseMessage());
            BasicHttpResponse response = new BasicHttpResponse(responseStatus);
            response.setEntity(entityFromConnection(connection));
            Iterator var12 = connection.getHeaderFields().entrySet().iterator();

            while(var12.hasNext()) {
                Entry<String, List<String>> header = (Entry)var12.next();
                if(header.getKey() != null) {
                    Header h = new BasicHeader((String)header.getKey(), (String)((List)header.getValue()).get(0));
                    response.addHeader(h);
                }
            }

            return response;
        }
    }

首先这个方法将请求的参数信息全部整合放在一个map里,然后通过判断mUrlRewriter是否为空来决定是否转换URL,我们可以通过实现mUrlRewriter类去扩展Volley从而实现过滤非法字符之类的操作。
接着,这个方法通过openConnection方法打开了一个TCP连接,并配置连接的相关信息,如超时时间,是否采用缓存等,并且如果是采用HTTP协议,则会为其设置默认的SSLSocketFactory(当然是在SSLSocketFactory不为空的情况下),需要注意的是,现在只是打开了一个TCP连接,并没有实际发送http请求。
openConnection:

    private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
        HttpURLConnection connection = this.createConnection(url);
        int timeoutMs = request.getTimeoutMs();
        connection.setConnectTimeout(timeoutMs);
        connection.setReadTimeout(timeoutMs);
        connection.setUseCaches(false);
        connection.setDoInput(true);
        if("https".equals(url.getProtocol()) && this.mSslSocketFactory != null) {
            ((HttpsURLConnection)connection).setSSLSocketFactory(this.mSslSocketFactory);
        }

        return connection;
    }

再接下来,就是配置http的请求头信息了,通过遍历存放着所有参数信息的map,再调用addRequestProperty将信息加入http的请求头。
而setConnectionParametersForRequest方法的作用则是根据request的请求类型来设置connection的请求类型,如果是POST或者PUT的话还需要将发送的内容输出到并且设置相应的content-type。
setConnectionParametersForRequest:

static void setConnectionParametersForRequest(HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
        switch(request.getMethod()) {
        case -1:
            byte[] postBody = request.getPostBody();
            if(postBody != null) {
                connection.setDoOutput(true);
                connection.setRequestMethod("POST");
                connection.addRequestProperty("Content-Type", request.getPostBodyContentType());
                DataOutputStream out = new DataOutputStream(connection.getOutputStream());
                out.write(postBody);
                out.close();
            }
            break;
        case 0:
            connection.setRequestMethod("GET");
            break;
          .....

再接着就是设置协议的类型以及版本,然后构造一个BasicHttpResponse封装本次的响应的信息。其中包含了响应的头部信息和内容,返回给调用该方法的上级进行进一步的解析。(其真正解析成我们需要的数据是在Request的子类中,如StringRequest类中)。

HttpClientStack

  • HttpClientStack类与HurlStack类的作用类似,结构也差不多,只不过HttpClientStack的底层是使用HttpClient实现的。
  • 看到HttpClientStack的成员变量:
    protected final HttpClient mClient;
    private static final String HEADER_CONTENT_TYPE = "Content-Type";

很简单,只有一个HttpClient对象和一个字符串常量。其中HttpClient对象在初始化时传入,也就是在VOLLEY类的newRequestQueue方法中通过AndroidHttpClient.newInstance(userAgent)方法取得。而AndroidHttpClient则是实现了HttpClient接口的类,主要是帮我们做了一些缺省的配置,如连接超时和socket超时都是设置为20秒,连接管理器设置为ThreadSafeClientConnManager。

  • 在看到它的performRequest()方法:
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError {
        HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders);
        addHeaders(httpRequest, additionalHeaders);
        addHeaders(httpRequest, request.getHeaders());
        this.onPrepareRequest(httpRequest);
        HttpParams httpParams = httpRequest.getParams();
        int timeoutMs = request.getTimeoutMs();
        HttpConnectionParams.setConnectionTimeout(httpParams, 5000);
        HttpConnectionParams.setSoTimeout(httpParams, timeoutMs);
        return this.mClient.execute(httpRequest);
    }

首先通过createHttpRequest()方法,根据请求的类型产生一个网络请求,这里跟HurlStack里的处理类似。接着配置头部信息,加入请求的参数,设置超时时长,最后再通过mClient执行请求。

  • 本质上HttpClientStack和HurlStack都是进行同样的封装行为,只不过HttpClientStack的底层是采用HttpClient实现的。而HttpClientStack和HurlStack最大的不同应该是HttpClientStack还拥有一个onPrepareRequest方法,我们可以继承HttpClientStack类重写该方法做一些执行前的工作,类似相当于AsyncTask的onPreExecute()。

总结

  • 到这里,和HttpStack有关的就已经解析完了,其实还是挺容易看懂的,但这是在对HttpURLConnection和HttpClient有一定了解的前提下。
  • 下一篇计划分析VOLLEY的缓存机制。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • Volley介绍 Volley 是 Google 在 2013 Google I/O 大会上发布推出的 Andro...
    cgzysan阅读 609评论 0 4
  • 我们再来看看volley是怎么工作的。首先还是要带着重点去看源码,我们要关注的地方除了最核心的工作流程之外,还有一...
    反复横跳的龙套阅读 489评论 0 1
  • 一、WebView view=(WebView) findViewById(R.id.webView1); vie...
    在你左右2018阅读 514评论 0 0
  • 注:本文转自http://codekk.com/open-source-project-analysis/deta...
    Ten_Minutes阅读 1,290评论 1 16
  • 国庆做为一个有比较长假期的节日,大部分人自然是会有大动作,那就是出行。除了加班的和没有计划的应该都会在路上。 你今...
    一只某某7阅读 98评论 2 0