OkHttp 源码简要分析 (Request 详解)

我们知道,http 请求分为三个部分, 请求行、请求头和请求体;对应的消息也分为三个部分:响应行、响应头和响应体。以前使用 HttpURLConnection 时,我们很容易设置消息头及参数,它内部是封装了 Socket 供我们使用。补充一点,我们知道网络运输层是由 TCP 和 UDP 构成的,TCP 建立连接,安全可靠,以流传输数据,没有大小限制,速度慢;UDP 是不建立连接,每次传递数据限制在64k内,数据容易丢失,但是速度快。TCP 和 UDP 都是依据Socket来生效的,而 http 则是建立在 TCP 基础上产生的。 举个 HttpURLConnection 的 get 和 pos 请求的例子

    private void get(){
        try {
            //子线程中执行请求
            String age = "20", address = "SH";
            URL url = new URL("http://baidu.com" + "?age=" + age + "&address=" + address);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("GET");
            connection.setConnectTimeout(5000);
            if (connection.getResponseCode() == 200) {
                InputStream inputStream = connection.getInputStream();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private void post() {
        try {
            //子线程中执行请求
            String age = "20", address = "SH";
            URL url = new URL("http://baidu.com");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);
            String content = "age=" + URLEncoder.encode(age) + "&address=" + URLEncoder.encode(address);//数据编解码
            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");//设置请求头
            connection.setRequestProperty("Content-Length", content.length() + "");
            connection.setDoOutput(true);
            OutputStream outputStream = connection.getOutputStream();
            outputStream.write(content.getBytes());
            if (connection.getResponseCode() == 200) {
                InputStream inputStream = connection.getInputStream();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


通过对比,明显可以看出get和post请求的设置方式不一样,由于 HttpURLConnection 封装的比较好,我们直接设置就行了,接下来看看 OkHttp,OkHttp 是个网络请求框架,支持异步和同步请求,也支持 get 、post 及压缩上传等,举个栗子

    private void okhttp() {
        OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                .build();

        Request request = new Request.Builder()
                .url("http://publicobject.com/helloworld.txt")
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback()
        {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String string = response.body().string();
                Log.e("onResponse", string);
            }
        });
    }

    private void okhttpPost() {
        OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                .build();

        RequestBody body = new FormBody.Builder()
                .add("useName", "老老")
                .add("usePwd", "321")
                .build();

        Request request = new Request.Builder()
                .url("https://kp.dftoutiao.com/announcement")
                .header("User-Agent", "OkHttp Example")
                .addHeader("Accept", "application/json; q=0.5")
                .post(body)
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback()
        {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    if(response.body() != null){
                        String responseBody = response.body().string();
                        Log.i("onResponse"," onResponse    "  +  responseBody);

                    }
                }
            }
        });

    }

    private void okhttpPostGizp(String content) {
        OkHttpClient mOkHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Request request =  chain.request().newBuilder()
                                .header("Content-Encoding","gzip")
                                .build();
                        return chain.proceed(request);
                    }
                })
                .build();

        RequestBody requestBody = new RequestBody() {

            @Override
            public MediaType contentType() {
                return  MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8");
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
                gzipSink.writeUtf8(content);
                gzipSink.flush();
                gzipSink.close();
            }
        };

        Request request = new Request.Builder()
                .url("https://test.upstring.cn")
                .post(requestBody)
                .build();
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new Callback()
        {

            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

            }
        });

    }

今天着重看看请求相关的代码,先看看 Request 这个类

public final class Request {
  private final HttpUrl url;
  private final String method;
  private final Headers headers;
  private final RequestBody body;
  private final Object tag;

  private volatile CacheControl cacheControl; // Lazily initialized.

    ...
}

用到了 Builder 模式,这个模式适合有大量参数需要设置的bean,提高写作效率及观赏性。 HttpUrl 对应的是请求行,即所谓的url;method 对应的是请求格式,比如是 get 还是 post;headers 是消息头,比如 User-Agent 等;body 是请求体,get方式没有,它是post的,里面包含一些请求参数,get方式的请求参数是拼接在url后面;tag 是用来做标识的,根据标识来找到请求的 request,可以取消请求等;cacheControl 是告诉服务端缓存模式,no-cache表示不使用缓存。

先看看 HttpUrl 这个类,它说白了就是对 url 做了详细的拆分的工具类,对外提供各种细节,具体操作都是java代码,我直接举个例子

    private static void testHttpUrl() {
        HttpUrl parsed = HttpUrl.parse("https://translate.google.cn/?view=home&op=translate&sl=auto&tl=zh-CN&text=老大");
        System.out.println("scheme:  " +  parsed.scheme() + "\n"  +
                        "query:   " +  parsed.query() + "\n"  +
                        "encodedQuery:  " +  parsed.encodedQuery() + "\n"  +
                        "host:    " +  parsed.host() + "\n"  +
                        "port:    " +  parsed.port() + "\n"
                );
    }


打印结果是

scheme:  https
query:   view=home&op=translate&sl=auto&tl=zh-CN&text=老大
encodedQuery:  view=home&op=translate&sl=auto&tl=zh-CN&text=%E8%80%81%E5%A4%A7
host:    translate.google.cn
port:    443

这里基本就是我们需要的各种值了。如果我们想在外部添加参数,可以使用 Builder 模式中的 addQueryParameter() 、addEncodedQueryParameter() 方法,区别就是传入的参数是否已经转码,默认会用 URLEncoder.encode(value, "utf-8" ) 来转码,防范中文出错。

请求方式 method 默认是 "GET",如果有传入 RequestBody 则变为 "POST",也可以通过暴露的方法设置它的值;

Headers 这个也比较简单,里面是个字符串数组对象,用来存储头部信息的 key 和 value,它只能是字符串,不能是汉字,如果需要则先 URLEncoder 转码。


最后看看 RequestBody 这个类,它是个抽象类,里面有抽象方法和静态方法

    public abstract class RequestBody {
        public abstract MediaType contentType();
        public long contentLength() throws IOException {
            return -1;
        }
        public abstract void writeTo(BufferedSink sink) throws IOException;
        ...
    }

这里面有两个比较关键的类,一个是 MediaType, 一个是 Okio 中的 Sink,Okio 前面几章讲过了,这里就不多说了,不动Okio的话,OkHttp 基本就很难弄懂了;看看 MediaType 这个类

public final class MediaType {
      private final String mediaType;
      private final String type;
      private final String subtype;
      private final String charset;
    
      private MediaType(String mediaType, String type, String subtype, String charset) {
        this.mediaType = mediaType;
        this.type = type;
        this.subtype = subtype;
        this.charset = charset;
      }

      public static MediaType parse(String string) {
            Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
            if (!typeSubtype.lookingAt()) return null;
            String type = typeSubtype.group(1).toLowerCase(Locale.US);
            String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
        
            String charset = null;
            Matcher parameter = PARAMETER.matcher(string);
            for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
              parameter.region(s, string.length());
              if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
        
              String name = parameter.group(1);
              if (name == null || !name.equalsIgnoreCase("charset")) continue;
              String charsetParameter = parameter.group(2) != null
                  ? parameter.group(2)  // Value is a token.
                  : parameter.group(3); // Value is a quoted string.
              if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
                throw new IllegalArgumentException("Multiple different charsets: " + string);
              }
              charset = charsetParameter;
            }
        
            return new MediaType(string, type, subtype, charset);
      }
    
      public static MediaType parse(String string) {
            Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
            if (!typeSubtype.lookingAt()) return null;
            String type = typeSubtype.group(1).toLowerCase(Locale.US);
            String subtype = typeSubtype.group(2).toLowerCase(Locale.US);
        
            String charset = null;
            Matcher parameter = PARAMETER.matcher(string);
            for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
              parameter.region(s, string.length());
              if (!parameter.lookingAt()) return null; // This is not a well-formed media type.
        
              String name = parameter.group(1);
              if (name == null || !name.equalsIgnoreCase("charset")) continue;
              String charsetParameter = parameter.group(2) != null
                  ? parameter.group(2)  // Value is a token.
                  : parameter.group(3); // Value is a quoted string.
              if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
                throw new IllegalArgumentException("Multiple different charsets: " + string);
              }
              charset = charsetParameter;
            }
        
            return new MediaType(string, type, subtype, charset);
      }
  
      ...
}

这个类中有几个属性,比较核心的就是 parse() 方法,这里会把传进去的值按照正则去切分,分别赋值给几个属性,例如 
 MediaType type = MediaType.parse("application/x-www-form-urlencoded;charset=utf-8"); 其中,它  type : application;   subtype : x-www-form-urlencoded;    charset : UTF-8;    mediaType : application/x-www-form-urlencoded;charset=utf-8。

重新回到 RequestBody 中,发现最终会执行

  public static RequestBody create(final MediaType contentType, final byte[] content,
      final int offset, final int byteCount) {
    if (content == null) throw new NullPointerException("content == null");
    Util.checkOffsetAndCount(content.length, offset, byteCount);
    return new RequestBody() {
      @Override public MediaType contentType() {
        return contentType;
      }

      @Override public long contentLength() {
        return byteCount;
      }

      @Override public void writeTo(BufferedSink sink) throws IOException {
        sink.write(content, offset, byteCount);
      }
    };
  }


方法,这个方法中对应也是需要 MediaType 和 Okio 才能理解,我们看看上文中的例子 FormBody ,看看它有什么特别的。FormBody 中有个内部类 Builder,对外提供的添加参数的方法,也是两个,一个是直接添加,另一个添加已经编码过的值,然后通过 Builer 模式创建 FormBody 对象,看看 FormBody 中的三个抽象方法:

  private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");

  @Override public MediaType contentType() {
    return CONTENT_TYPE;
  }

  @Override public long contentLength() {
    return writeOrCountBytes(null, true);
  }

  @Override public void writeTo(BufferedSink sink) throws IOException {
    writeOrCountBytes(sink, false);
  }

contentType() 方法中返回的是静态对象 CONTENT_TYPE,这个是固定的;另外两个方法都调用了同一个方法,区别就是参数不一样

  private long writeOrCountBytes(BufferedSink sink, boolean countBytes) {
    long byteCount = 0L;

    Buffer buffer;
    if (countBytes) {
      buffer = new Buffer();
    } else {
      buffer = sink.buffer();
    }

    for (int i = 0, size = encodedNames.size(); i < size; i++) {
      if (i > 0) buffer.writeByte('&');
      buffer.writeUtf8(encodedNames.get(i));
      buffer.writeByte('=');
      buffer.writeUtf8(encodedValues.get(i));
    }

    if (countBytes) {
      byteCount = buffer.size();
      buffer.clear();
    }

    return byteCount;
  }

这里不得不感慨,OkHttp 的作者真是把 Okio 用到了极致,writeOrCountBytes(null, true) 时,此时创建了 Buffer 对象,然后把参数都添加了进去,重点是它通过 buffer.size() 算出参数的长度,此时是以字节作为个数的,然后把 Buffer 清空; writeOrCountBytes(sink, false) 中是传入一个 sink 对象,获取它内部的 Buffer 对象,把参数添加到 Buffer 中。这里真的是很巧妙。RequestBody 还有个子类,是 MultipartBody,这个暂不分析。


Cache-Control: no-cache 这个意思是不用缓存,每次都用最新的。

发布了176 篇原创文章 · 获赞 11 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/Deaht_Huimie/article/details/102826550
今日推荐