okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收

水牛發表於2017-03-13

http協議相關

一個http請求的過程

  • 輸入url和引數
  • ->DNS解析(訪問域名伺服器53號埠,根據域名拿到ip,可能會拿到好幾個ip)
  • ->根據ip和埠號連線socket(TCP三次握手已封裝在socket api內部,對開發者透明)
  • -> socket連線成功後,往socket輸出流中寫入http報文

一個響應的接收過程

  • 伺服器接受請求並處理後發出響應
  • 客戶端從socket輸入流中讀取http報文

注意: 請求行/狀態行和報文頭部是字元,而請求體和響應體可以是字元,也可以是二進位制流

http請求報文的格式

okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收
Paste_Image.png

注: 下圖第一行名字標錯了,是叫請求行.狀態行是響應報文中的叫法.

okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收
Paste_Image.png

特殊一點的: 檔案上傳的http報文格式:

注:下圖請求引數為:
鍵值對:
"uploadFile555","1474363536041.jpg"
"api_secret777","898767hjk"
檔案:
"uploadFile","/storage/emulated/0/qxinli.apk"

okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收
upload

http響應報文的格式

okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收
Paste_Image.png

okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收
Paste_Image.png

常見的json response:

okhttp 原始碼解析 - 網路協議的實現 - 請求流程: 請求的傳送與響應的接收
Paste_Image.png

okhttp中程式碼執行流程

直接看GetExample中的同步執行程式碼:核心為RealCall的execute()方法

OkHttpClient client = new OkHttpClient();

  String run(String url) throws IOException {
    Request request = new Request.Builder()
        .url(url)
        .build();

    try (Response response = client.newCall(request).execute()) {
          //即call.execute(),Call介面的具體實現為RealCall
      return response.body().string();
    }
  }複製程式碼

RealCall的execute():

@Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    try {
      client.dispatcher().executed(this);//只是將call物件新增到Deque<RealCall> runningSyncCalls 中,只是增加了個引用
      Response result = getResponseWithInterceptorChain();//真實執行的地方
      if (result == null) throw new IOException("Canceled");
      return result;
    } finally {
      client.dispatcher().finished(this);
    }
  }複製程式碼

getResponseWithInterceptorChain():
涉及到攔截器機制,參見:okhttp原始碼解析-coding skills-攔截器機制

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());//使用者設定的攔截器
    interceptors.add(retryAndFollowUpInterceptor);//重定向和重試的攔截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));//
    interceptors.add(new CacheInterceptor(client.internalCache()));//快取管理的攔截器
    interceptors.add(new ConnectInterceptor(client));//Dns查詢和獲取socket連線的地方
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//使用者設定的network攔截器
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//真實讀寫socket的地方

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }複製程式碼

從文章開頭,我們分析的請求流程:獲取socket連線->往連線的流裡讀寫資料 可以知道
請求流程的實現重點在兩個攔截器:ConnectInterceptor和CallServerInterceptor

ConnectInterceptor
具體的DNS解析和一個網路連線的獲取請參見 okhttp原始碼解析-網路框架業務處理-連線的建立與連線池

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);//httpCodec 封裝了輸入輸出流
    RealConnection connection = streamAllocation.connection();//RealConnection 封裝了"網路連線"物件

    return realChain.proceed(request, streamAllocation, httpCodec, connection);//將上面的物件封裝到chain中
  }複製程式碼

CallServerInterceptor: 真正讀寫socket流的地方

注: 這裡輸入輸出流用的不是java的stream,而是okio,其中Sink相當於OutputStream,用於寫請求,Source相當於InputStream,用於讀取響應

@Override public Response intercept(Chain chain) throws IOException {
    HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();//將輸入輸出流的封裝物件從chain中拿出
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
    Request request = chain.request();

    long sentRequestMillis = System.currentTimeMillis();
    httpCodec.writeRequestHeaders(request);//往流中寫入請求頭

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return what
      // we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        responseBuilder = httpCodec.readResponseHeaders(true);//從流中讀取響應頭
      }


      // Write the request body, unless an "Expect: 100-continue" expectation failed.
      if (responseBuilder == null) {
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);//往輸出流中寫入請求體
        bufferedRequestBody.close();
      }
    }

    httpCodec.finishRequest();

    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))//從流中讀取響應體
          .build();
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }

    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
  }複製程式碼

從上面看到幾個方法:

httpCodec.writeRequestHeaders(request);//往流中寫入請求頭
httpCodec.createRequestBody(request, request.body().contentLength())
httpCodec.readResponseHeaders(true);//從流中讀取響應頭
httpCodec.openResponseBody(response)複製程式碼

httpCodec 介面的實現類有Http1Codec和Http2Codec,我們看Http1Codec:

writeRequestHeaders:


@Override public void writeRequestHeaders(Request request) throws IOException {
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    writeRequest(request.headers(), requestLine);
  }


/** Returns bytes of a request header for sending on an HTTP transport. */
  public void writeRequest(Headers headers, String requestLine) throws IOException {
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    for (int i = 0, size = headers.size(); i < size; i++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
  }複製程式碼

讀響應頭:


@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
      throw new IllegalStateException("state: " + state);
    }

    try {
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      // Provide more context if the server ends the stream before sending a response.
      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
      exception.initCause(e);
      throw exception;
    }
  }

  /** Reads headers or trailers. */
  public Headers readHeaders() throws IOException {
    Headers.Builder headers = new Headers.Builder();
    // parse the result headers until the first blank line
    for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
      Internal.instance.addLenient(headers, line);
    }
    return headers.build();
  }複製程式碼

寫請求體:createRequestBody

@Override public Sink createRequestBody(Request request, long contentLength) {
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
      // Stream a request body of unknown length.
      return newChunkedSink();
    }

    if (contentLength != -1) {
      // Stream a request body of a known length.
      return newFixedLengthSink(contentLength);
    }

    throw new IllegalStateException(
        "Cannot stream a request body without chunked encoding or a known content length!");
  }

//讀寫: 在上方intercept中,傳入
 Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        request.body().writeTo(bufferedRequestBody);
        bufferedRequestBody.close();複製程式碼

讀響應體:

@Override public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }複製程式碼

請求體RequestBody和響應體ResponseBody:

內部封裝了輸入輸出流,具體的讀寫封裝在特定的子類中,

RequestBody的子類有

表單提交的FormBody,檔案上傳的MultipartBody

FormBody往流中寫資料的程式碼:

key和value要預先url編碼成encodedName,encodedValue,放入兩個list,然後根據index依次寫如輸出流

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;
  }複製程式碼

MultipartBody 寫資料的程式碼:

注意body也有content-type,這個與header中的content-type不一樣,前者是標識這一塊body是什麼類,而header中的是標識這個http請求是什麼型別.
而且,上傳body裡的content-type有特點的格式:MediaType.parse(type + "; boundary=" + boundary.utf8())


MultipartBody(ByteString boundary, MediaType type, List<Part> parts) {
    this.boundary = boundary;
    this.originalType = type;
    this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
    this.parts = Util.immutableList(parts);
  }

private static final byte[] COLONSPACE = {':', ' '};
  private static final byte[] CRLF = {'\r', '\n'};
  private static final byte[] DASHDASH = {'-', '-'};


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

    Buffer byteCountBuffer = null;
    if (countBytes) {
      sink = byteCountBuffer = new Buffer();
    }

    for (int p = 0, partCount = parts.size(); p < partCount; p++) {
      Part part = parts.get(p);
      Headers headers = part.headers;
      RequestBody body = part.body;

      sink.write(DASHDASH);
      sink.write(boundary);
      sink.write(CRLF);

      if (headers != null) {
        for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
          sink.writeUtf8(headers.name(h))
              .write(COLONSPACE)
              .writeUtf8(headers.value(h))
              .write(CRLF);
        }
      }

      MediaType contentType = body.contentType();
      if (contentType != null) {
        sink.writeUtf8("Content-Type: ")
            .writeUtf8(contentType.toString())
            .write(CRLF);
      }

      long contentLength = body.contentLength();
      if (contentLength != -1) {
        sink.writeUtf8("Content-Length: ")
            .writeDecimalLong(contentLength)
            .write(CRLF);
      } else if (countBytes) {
        // We can't measure the body's size without the sizes of its components.
        byteCountBuffer.clear();
        return -1L;
      }

      sink.write(CRLF);

      if (countBytes) {
        byteCount += contentLength;
      } else {
        body.writeTo(sink);
      }

      sink.write(CRLF);
    }

    sink.write(DASHDASH);
    sink.write(boundary);
    sink.write(DASHDASH);
    sink.write(CRLF);

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

    return byteCount;
  }複製程式碼

ResponseBody的子類有:

RealResponseBody和CacheResponseBody.前者程式碼少,主要實現在ResponseBody中:

提供有幾個常用方法:

  • bytes():返回byte陣列
  • string(): 返回字串
  • byteStream(): 返回java的inputStream,可用於檔案下載
public final byte[] bytes() throws IOException {
    long contentLength = contentLength();
    if (contentLength > Integer.MAX_VALUE) {
      throw new IOException("Cannot buffer entire body for content length: " + contentLength);
    }

    BufferedSource source = source();
    byte[] bytes;
    try {
      bytes = source.readByteArray();
    } finally {
      Util.closeQuietly(source);
    }
    if (contentLength != -1 && contentLength != bytes.length) {
      throw new IOException("Content-Length ("
          + contentLength
          + ") and stream length ("
          + bytes.length
          + ") disagree");
    }
    return bytes;
  }


public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
      Util.closeQuietly(source);
    }
  }

public final InputStream byteStream() {
    return source().inputStream();
  }複製程式碼

Response: 使用者拿到的最終返回物件

就是響應解析後變成記憶體物件,http報文各部分分別封裝好了.並提供相應的一些方法

public final class Response implements Closeable {
  final Request request;
  final Protocol protocol;
  final int code;
  final String message;
  final Handshake handshake;
  final Headers headers;
  final ResponseBody body;
  final Response networkResponse;
  final Response cacheResponse;
  final Response priorResponse;
  final long sentRequestAtMillis;
  final long receivedResponseAtMillis;

  private volatile CacheControl cacheControl; // Lazily initialized.
  ...
  public boolean isSuccessful() {
    return code >= 200 && code < 300;
  }
  public int code() {
    return code;
  }

  public ResponseBody body() {
    return body;
  }

  public List<String> headers(String name) {
    return headers.values(name);
  }

  public String header(String name) {
    return header(name, null);
  }

  public String header(String name, String defaultValue) {
    String result = headers.get(name);
    return result != null ? result : defaultValue;
  }

  public Headers headers() {
    return headers;
  }
  ...複製程式碼

總結:

  • 從上面的原始碼分析可知,okhttp的這一部分,本質上就是對http協議的一個實現,瞭解http協議,是讀懂原始碼的一個前提.
  • 直接使用okhttp時,涉及到兩個類: Request和Response
  • okhttp提供了兩種請求體的實現:表單提交時的FormBody,檔案上傳時的MultiPartBody
  • okhttp提供了響應體的幾個接收形式:
    以字串形式接收:string()方法
    以位元組陣列的形式接收: bytes()方法
    以流的形式接收: byteStream()方法

參考:
一個http請求的詳細過程
HTTP協議詳解(真的很經典)

封裝到極致的網路庫:

HttpUtilForAndroid

相關文章