http協議相關
一個http請求的過程
- 輸入url和引數
- ->DNS解析(訪問域名伺服器53號埠,根據域名拿到ip,可能會拿到好幾個ip)
- ->根據ip和埠號連線socket(TCP三次握手已封裝在socket api內部,對開發者透明)
- -> socket連線成功後,往socket輸出流中寫入http報文
一個響應的接收過程
- 伺服器接受請求並處理後發出響應
- 客戶端從socket輸入流中讀取http報文
注意: 請求行/狀態行和報文頭部是字元,而請求體和響應體可以是字元,也可以是二進位制流
http請求報文的格式
注: 下圖第一行名字標錯了,是叫請求行.狀態行是響應報文中的叫法.
特殊一點的: 檔案上傳的http報文格式:
注:下圖請求引數為:
鍵值對:
"uploadFile555","1474363536041.jpg"
"api_secret777","898767hjk"
檔案:
"uploadFile","/storage/emulated/0/qxinli.apk"
http響應報文的格式
常見的json response:
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協議詳解(真的很經典)