移動架構10-手寫Okhttp框架
移動架構10-手寫Okhttp框架
官方地址:ofollow,noindex">https://github.com/square/okhttp
為了學習Okhttp框架,手寫一個框架。用法和Okhttp框架一樣。
HttpClient client = new HttpClient.Builder().build(); Request request = new Request.Builder() .url("http://www.kuaidi100.com/query?type=yuantong&postid=11111111111") .build(); Call call = client.newCall(request); call.enqueue(new Callback() { @Override public void onFailure(Call call, Throwable throwable) { showReult(showResult, throwable.getMessage()); } @Override public void onResponse(Call call, Response response) { showReult(showResult, response.getBody()); } });
下面按照框架的使用步驟,來講解手寫Okhttp的思路。
一、建立HttpClient
HttpClient是框架的入口,使用建造者模式來建立。
主要作用是建立排程器、建立Http連線池、新增攔截器。
/** * 建立HttpClient */ public HttpClient build() { if (null == dispatcher) { dispatcher = new Dispatcher(); } if (null == httpConnectionPool) { httpConnectionPool = new HttpConnectionPool(); } return new HttpClient(this); }
1.Dispatcher
Dispatcher:排程器,管理所有的請求任務
Dispatcher執行請求任務需要先檢查最大請求數和相同的host的最大請求數,如果沒超過就加入執行佇列,使用ExecutorService來執行;如果超過了,就加入等待佇列。
/** * 執行請求任務 * * @param call */ public void enqueue(Call.AsyncCall call) { //不能超過最大請求數,同時執行相同的host請求不能超過最大host數 Log.e("Dispatcher", "同時有:" + runningAsyncCalls.size()); Log.e("Dispatcher", "host同時有:" + runningCallsForHost(call)); if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { Log.e("Dispatcher", "提交執行"); runningAsyncCalls.add(call); executorService().execute(call); } else { Log.e("Dispatcher", "等待執行"); readyAsyncCalls.add(call); } }
當一個請求完成時,就執行等待佇列中的任務。
/** * 執行等待佇列中的請求 */ private void promoteCalls() { //同時請求達到上限 if (runningAsyncCalls.size() >= maxRequests) { return; } //沒有等待執行請求 if (readyAsyncCalls.isEmpty()) { return; } for (Iterator<Call.AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { Call.AsyncCall call = i.next(); //同一host同時請求為達上限 if (runningCallsForHost(call) < maxRequestsPerHost) { i.remove(); runningAsyncCalls.add(call); executorService().execute(call); } //到達同時請求上限 if (runningAsyncCalls.size() >= maxRequests) { return; } } }
2.HttpConnectionPool
HttpConnectionPool:連線池,用來快取、複用Http連線。
每次新增Http連線時,都檢查清理無效的連線。
/** * 檢查需要移除的連線返回下次檢查時間 */ long cleanup(long now) { long longestIdleDuration = -1; synchronized (this) { for (Iterator<HttpConnection> i = connections.iterator(); i.hasNext(); ) { HttpConnection connection = i.next(); //獲得閒置時間 多長時間沒使用這個了 long idleDuration = now - connection.lastUsetime; //如果閒置時間超過允許 if (idleDuration > keepAliveDuration) { connection.closeQuietly(); i.remove(); Log.e("Pool", "移出連線池"); continue; } //獲得最大閒置時間 if (longestIdleDuration < idleDuration) { longestIdleDuration = idleDuration; } } //下次檢查時間 if (longestIdleDuration >= 0) { return keepAliveDuration - longestIdleDuration; } else { //連線池沒有連線 可以退出 cleanupRunning = false; return longestIdleDuration; } } }
二、建立Request
Request是請求資訊,主要作用是儲存請求頭、請求方法、請求地址、請求體。
/** * 請求資訊 */ public class Request { //請求頭 private Map<String, String> headers; //請求方法 private String method; //請求地址 private HttpUrl url; //請求體 private RequestBody body; ... }
HttpUrl:請求地址,用來儲存請求協議、主機名、請求路徑和埠號;
RequestBody:請求體,用來指定字元編碼,拼接請求引數。
//拼接請求引數 public String body() { StringBuffer sb = new StringBuffer(); for (Map.Entry<String, String> entry : encodedBodys.entrySet()) { sb.append(entry.getKey()) .append("=") .append(entry.getValue()) .append("&"); } if (sb.length() != 0) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); }
三、建立Call
Call:請求任務。用來儲存Request、HttpClient和請求狀態(是否執行過)。
public Call(HttpClient client, Request request) { this.client = client; this.request = request; }
四、呼叫Call
1.呼叫過程
Call.enqueue(),實際是排程器在呼叫AsyncCall。
//非同步呼叫 public Call enqueue(Callback callback) { //同一請求執行一次 synchronized (this) { if (executed) { throw new IllegalStateException("Already Executed"); } executed = true; } client.dispatcher().enqueue(new AsyncCall(callback)); return this; }
AsyncCall是一個Runnable,用來獲取響應,執行回撥函式。
@Override public void run() { //是否已經通知過callback boolean signalledCallback = false; try { //獲取響應 Response response = getResponse(); if (canceled) { signalledCallback = true; callback.onFailure(Call.this, new IOException("Canceled")); } else { signalledCallback = true; callback.onResponse(Call.this, response); } } catch (IOException e) { if (!signalledCallback) { callback.onFailure(Call.this, e); } } finally { client.dispatcher().finished(this); } }
getResponse():獲取響應,是使用責任鏈的模式層層調動的。可以理解為遞迴。
Response getResponse() throws IOException { ArrayList<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); interceptors.add(new InterceptorRetry()); interceptors.add(new InterceptorHeaders()); interceptors.add(new InterceptorConnection()); interceptors.add(new InterceptorCallService()); InterceptorChain interceptorChain = new InterceptorChain(interceptors, 0, this, null); return interceptorChain.proceed(); }
2.責任鏈
InterceptorChain:責任鏈,是框架的核心。責任鏈會呼叫攔截器,而每個攔截器都會呼叫下一個攔截器獲取Response,呼叫之前做請求初始化操作,呼叫之後做響應處理操作。
/** * 呼叫攔截器,獲取Response */ public Response proceed(HttpConnection connection) throws IOException { Interceptor interceptor = interceptors.get(index); InterceptorChain next = new InterceptorChain(interceptors, index + 1, call, connection); Response response = interceptor.intercept(next); return response; }
InterceptorRetry:請求失敗時,重新請求
public Response intercept(InterceptorChain chain) throws IOException { Log.e("interceprot", "重試攔截器...."); Call call = chain.call; IOException exception = null; for (int i = 0; i < chain.call.client().retrys(); i++) { if (call.isCanceled()) { throw new IOException("Canceled"); } try { //呼叫下一個攔截器 Response response = chain.proceed(); return response; } catch (IOException e) { exception = e; } } throw exception; }
InterceptorHeaders:新增請求頭資訊
public Response intercept(InterceptorChain chain) throws IOException { Log.e("interceprot","Http頭攔截器...."); Request request = chain.call.request(); Map<String, String> headers = request.headers(); headers.put(HttpCodec.HEAD_HOST, request.url().getHost()); headers.put(HttpCodec.HEAD_CONNECTION, HttpCodec.HEAD_VALUE_KEEP_ALIVE); if (null != request.body()) { String contentType = request.body().contentType(); if (contentType != null) { headers.put(HttpCodec.HEAD_CONTENT_TYPE, contentType); } long contentLength = request.body().contentLength(); if (contentLength != -1) { headers.put(HttpCodec.HEAD_CONTENT_LENGTH, Long.toString(contentLength)); } } //呼叫下一個攔截器 return chain.proceed(); }
InterceptorConnection:通過Socket建立Http連線,使用連線池來獲取Http連線
public Response intercept(InterceptorChain chain) throws IOException { Log.e("interceprot","連線攔截器...."); Request request = chain.call.request(); HttpClient client = chain.call.client(); HttpUrl url = request.url(); String host = url.getHost(); int port = url.getPort(); HttpConnection connection = client.connectionPool().get(host, port); if (null == connection) { connection = new HttpConnection(); } else { Log.e("call", "使用連線池......"); } connection.setRequest(request); try { //呼叫下一個攔截器 Response response = chain.proceed(connection); if (response.isKeepAlive()) { client.connectionPool().put(connection); } return response; } catch (IOException e) { throw e; } }
InterceptorCallService:傳送Http請求
public Response intercept(InterceptorChain chain) throws IOException { Log.e("interceprot", "通訊攔截器...."); HttpCodec httpCodec = chain.httpCodec; HttpConnection connection = chain.connection; InputStream is = connection.call(httpCodec); //HTTP/1.1 200 OK 空格隔開的響應狀態 String statusLine = httpCodec.readLine(is); //讀取響應頭 Map<String, String> headers = httpCodec.readHeaders(is); //是否保持連線 boolean isKeepAlive = false; if (headers.containsKey(HttpCodec.HEAD_CONNECTION)) { isKeepAlive = headers.get(HttpCodec.HEAD_CONNECTION).equalsIgnoreCase(HttpCodec .HEAD_VALUE_KEEP_ALIVE); } int contentLength = -1; if (headers.containsKey(HttpCodec.HEAD_CONTENT_LENGTH)) { contentLength = Integer.valueOf(headers.get(HttpCodec.HEAD_CONTENT_LENGTH)); } //分塊編碼資料 boolean isChunked = false; if (headers.containsKey(HttpCodec.HEAD_TRANSFER_ENCODING)) { isChunked = headers.get(HttpCodec.HEAD_TRANSFER_ENCODING).equalsIgnoreCase(HttpCodec .HEAD_VALUE_CHUNKED); } String body = null; if (contentLength > 0) { byte[] bytes = httpCodec.readBytes(is, contentLength); body = new String(bytes); } else if (isChunked) { body = httpCodec.readChunked(is); } String[] status = statusLine.split(" "); connection.updateLastUseTime(); //返回響應結果 return new Response(Integer.valueOf(status[1]), contentLength, headers, body, isKeepAlive); }
3.建立Http連線
HttpConnection:使用Socket建立http連線,支援Https協議
/** * 建立socket,支援Https協議 */ private void createSocket() throws IOException { if (null == socket || socket.isClosed()) { HttpUrl url = request.url(); //需要sslsocket if (url.getProtocol().equalsIgnoreCase(HTTPS)) { socket = SSLSocketFactory.getDefault().createSocket(); } else { socket = new Socket(); } socket.connect(new InetSocketAddress(url.getHost(), url.getPort())); os = socket.getOutputStream(); is = socket.getInputStream(); } }
HttpCodec:封裝http協議,讀寫http請求
/** * 封裝http協議,併發送http請求 */ public void writeRequest(OutputStream os, Request request) throws IOException { StringBuffer protocol = new StringBuffer(); //請求行 protocol.append(request.method()); protocol.append(SPACE); protocol.append(request.url().getFile()); protocol.append(SPACE); protocol.append(VERSION); protocol.append(CRLF); //http請求頭 Map<String, String> headers = request.headers(); for (Map.Entry<String, String> entry : headers.entrySet()) { protocol.append(entry.getKey()); protocol.append(COLON); protocol.append(SPACE); protocol.append(entry.getValue()); protocol.append(CRLF); } protocol.append(CRLF); //http請求體 如果存在 RequestBody body = request.body(); if (null != body) { protocol.append(body.body()); } //傳送http請求 os.write(protocol.toString().getBytes()); os.flush(); } /** * 讀取響應頭 */ public Map<String, String> readHeaders(InputStream is) throws IOException { HashMap<String, String> headers = new HashMap<>(); while (true) { String line = readLine(is); //讀取到空行 則下面的為body if (isEmptyLine(line)) { break; } int index = line.indexOf(":"); if (index > 0) { String name = line.substring(0, index); // ": "移動兩位到 總長度減去兩個("\r\n") String value = line.substring(index + 2, line.length() - 2); headers.put(name, value); } } return headers; } /** * 按行讀取http響應資料 */ public String readLine(InputStream is) throws IOException { try { byte b; boolean isMabeyEofLine = false; //標記 byteBuffer.clear(); byteBuffer.mark(); while ((b = (byte) is.read()) != -1) { byteBuffer.put(b); // 讀取到/r則記錄,判斷下一個位元組是否為/n if (b == CR) { isMabeyEofLine = true; } else if (isMabeyEofLine) { //上一個位元組是/r 並且本次讀取到/n if (b == LF) { //獲得目前讀取的所有位元組 byte[] lineBytes = new byte[byteBuffer.position()]; //返回標記位置 byteBuffer.reset(); byteBuffer.get(lineBytes); //清空所有index並重新標記 byteBuffer.clear(); byteBuffer.mark(); String line = new String(lineBytes); return line; } isMabeyEofLine = false; } } } catch (Exception e) { throw new IOException(e); } throw new IOException("Response Read Line."); } /** * 按位元組陣列讀取http響應資料 */ public byte[] readBytes(InputStream is, int len) throws IOException { byte[] bytes = new byte[len]; int readNum = 0; while (true) { readNum += is.read(bytes, readNum, len - readNum); //讀取完畢 if (readNum == len) { return bytes; } } } /** * 按塊讀取http響應資料 * 分塊傳輸編碼(Chunked transfer encoding)是超文字傳輸協議(HTTP)中的一種資料傳輸機制, * 允許HTTP由應用伺服器傳送給客戶端應用( 通常是網頁瀏覽器)的資料可以分成多個部分。 * 分塊傳輸編碼只在HTTP協議1.1版本(HTTP/1.1)中提供。 */ public String readChunked(InputStream is) throws IOException { int len = -1; boolean isEmptyData = false; StringBuffer chunked = new StringBuffer(); while (true) { //解析下一個chunk長度 if (len < 0) { String line = readLine(is); line = line.substring(0, line.length() - 2); len = Integer.valueOf(line, 16); //chunk編碼的資料最後一段為 0\r\n\r\n isEmptyData = len == 0; } else { //塊長度不包括\r\n所以+2將 \r\n 讀走 byte[] bytes = readBytes(is, len + 2); chunked.append(new String(bytes)); len = -1; if (isEmptyData) { return chunked.toString(); } } } }
最後
程式碼:https://gitee.com/yanhuo2008/Common/tree/master/ToolHttp
demo:https://gitee.com/yanhuo2008/Common/tree/master/DemoHttp