欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

OkHttp原理分析小結

 更新時間:2024年01月17日 14:32:00   作者:Young丶  
OkHttp 是 Square 公司開源的一款網絡框架,封裝了一個高性能的 http 請求庫,本文對OkHttp原理給大家詳細講解,感興趣的朋友跟隨小編一起看看吧

Okhttp 介紹

OkHttp 是 Square 公司開源的一款網絡框架,封裝了一個高性能的 http 請求庫。

https://github.com/square/okhttp

特點

  • 支持 spdy、http2.0、websocket 等協(xié)議
  • 支持同步、異步請求
  • 封裝了線程池,封裝了數據轉換,提高性能。
  • 在 Android 6.0 中自帶的網絡請求 API 的底層就是使用了 okhttp 來進行的
  • 使用 okhttp 比較接近真正的 HTTP 協(xié)議的框架

這個類主要是用來配置 okhttp 這個框架的,通俗一點講就是這個類是管理這個框架的各種設置的。

Call 類的工廠,通過 OkHttpClient 才能得到 Call 對象。

Okhttp 中幾個重要類的介紹

OkHttpClient

這個類主要是用來配置 okhttp 這個框架的,通俗一點講就是這個類是管理這個框架的各種設置的。

Call 類的工廠,通過 OkHttpClient 才能得到 Call 對象。

OkHttpClient使用注意

OkHttpClient 應該被共享,使用 okhttp 這個框架的時候,最好要將 OkHttpClient 設置成單例模式,所有的 HTTP 在進行請求的時候都要使用這一個 Client 。因為每個 OkHttpClient 都對應了自己的連接池和線程池。減少使用連接池和線程池可以減少延遲和內存的使用。相反的如果每個請求都創(chuàng)建一個 OkHttpClient 的話會很浪費內存資源。

OkHttpClient的創(chuàng)建

OkHttpClient 有三個創(chuàng)建方法

第一個方法:直接使用 new OkHttpClient() 來創(chuàng)建一個實例對象就可以了,這個實例對象有默認的配置。默認請求連接超時時間 10 s ,讀寫超時時間 10 s,連接不成功會自動再次連接。

第二個方法:就是通過 Builder的方式來自己定義一個 OkHttpclient 。當然如果你直接 build 沒有自己配置參數的話,效果和第一個方法是一樣的。

public final OkHttpClient = new OkHttpClient.Builder()
  .addInterceptor(new HttpLoggingInterceptor())
  .cache(new Cache(cacheDir,cacheSize))
  .等等配置
  .build();

第三個方法:就是通過已有的 OkHttpClient 對象來復制一份共享線程池和其他資源的 OkHttpClient 對象。

OkHttpClient agerClient = client.newBuilder()
  .readTimeout(500,TimeUnit.MILLSECONS)
  .build();

這種方法的好處就是,當我們有一個特殊的請求,有的配置有點不一樣,比如要求連接超過 1 s 就算超時,這個時候我們就可以使用這個方法來生成一個新的實例對象,不過他們共用很多其他的資源,不會對資源造成浪費。

關于 OkHttpClient 的配置改變都在 Builder 中進行

不需要了可以關閉

其實持有的線程池和連接池將會被自定釋放如果他們保持閑置的話。

你也可以自動釋放,釋放后將來再調用 call 的時候會被拒接。

client.dispatcher().excurorService().shutdown()

清除連接池,注意清除后,連接池的守護線程可能會立刻退出。

client.connectionPool().evictAll()

如果 Client 有緩存,可以關閉。注意:再次調用一個被關閉的 cache 會發(fā)生錯誤。也會造成 crash。

client.cache().close();

OkHttp 在 HTTP/2 連接的時候也會使用守護線程。他們閑置的時候將自動退出。

知道有這么一回事就行,一般不會主動調用。

Call 類

Call 這個類就是用來發(fā)送 HTTP 請求和讀取 HTTP 響應的一個類

image-20221108142020997

這個類的方法很少,從上到下依次是:放棄請求、異步執(zhí)行請求、同步執(zhí)行請求。

Request 類

這個類就是相當于 http 請求中的請求報文,是用來表達請求報文的,所以這里可以設置請求的 url、請求頭、請求體等等和請求報文有關的內容。

主要方法羅列:

// 獲取請求 url
public HttpUrl url();
// 獲取請求方法類型
public String method();
// 獲取請求頭
public Headers headers();
//獲取請求體
public RequestBody body();
// 獲取 tag
public Object tag();
// 返回緩存控制指令,永遠不會是 null ,即使響應不包含 Cache-Control 響應頭
public CacheControl cacheControl();
// 是否是 https 請求
public boolean isHttps();
// Resquest{method=" ",url=" ",tag = " "}
public String toString();

image-20221108142440493

這是它的 Builder 中提供的方法,只設置 .url() 的時候默認是 post 請求。

RequestBody

介紹完請求報文就要介紹請求體了,這都是和 http協(xié)議緊密聯系的。

RequestBody 就是用來設置請求體的,它的主要方法就是下面這個幾個靜態(tài)方法,用來生成對應的請求體:

在這里插入圖片描述

就是通過這幾個方法來產生對應的不同的請求體。MediaType 是用來描述請求體或者響應體類型的。比如請求體類型是 json 串格式的,那對應的 MediaType 就是MediaType.parse("application/json; charset=utf-8"); ,如果上傳的是文件那么對應的就是 application/octet-stream,還有幾個常用的類型 text/plain imge/png text/x-markdown 等等。

它還有兩個子類:

在這里插入圖片描述

FormBody 這個請求體是我們平時最常用的,就是我們平時使用 post 請求的時候,參數是鍵值對的形式。就是使用這個請求體最簡單了。

說深一點,對應的請求報文是:

POST /test HTTP/1.1   請求行
Host: 32.106.24.148:8080  下面都是請求頭
Content-Type: application/x-www-form-urlencoded 用于指明請求體的類型。
User-Agent: PostmanRuntime/7.15.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 954bda0d-dbc2-4193-addf-a7631cab2cfa,5ba2ebed-90b4-4f35-bcf5-80c4777de471
Host: 39.106.24.148:8080
accept-encoding: gzip, deflate
content-length: 133
Connection: keep-alive
cache-control: no-cache

key0=value0&key1=value1  請求體(也是我們的參數)

這是發(fā)送的原始的報文格式,用代碼實現的話就是

// 創(chuàng)建客戶端
OkHttpClient client = new OkHttpclient();
// 建立請求體 
FormBody formBody = new FormBody.Builder()
                    .add("key0", "value0")
                    .add("key1","value1")
                    .build();
// 建立請求報文
Request request = new Request.Builder
                                .post(formBody)
                                .url("請求url")
                                .addHeader("Content-Type", "application/x-www-form-urlencoded")
  .addHeader("User-Agent", "PostmanRuntime/7.15.0")
  .addHeader("Accept", "*/*")
  .addHeader("Cache-Control", "no-cache")
  .addHeader("Postman-Token", "954bda0d-dbc2-4193-addf-a7631cab2cfa,af7c027c-a7ba-4560-98ae-3a2a473ab88a")
  .addHeader("Host", "39.106.24.148:8080")
  .addHeader("accept-encoding", "gzip, deflate")
  .addHeader("content-length", "133")
  .addHeader("Connection", "keep-alive")
  .addHeader("cache-control", "no-cache")
  .build();
// 發(fā)起請求
client.newCall(request).excute();

上面是使用了 FormBody 的形式,如果使用 RequestBody 的話就要更麻煩一些。

OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
RequestBody body = RequestBody.create(mediaType, "key0=value0&key1=value1");
Request request = new Request.Builder()
  .url("http://39.106.24.148:8080/test")
  .post(body)
  .addHeader("Content-Type", "application/x-www-form-urlencoded")
  .addHeader("User-Agent", "PostmanRuntime/7.15.0")
  .addHeader("Accept", "*/*")
  .addHeader("Cache-Control", "no-cache")
  .addHeader("Postman-Token", "954bda0d-dbc2-4193-addf-a7631cab2cfa,af7c027c-a7ba-4560-98ae-3a2a473ab88a")
  .addHeader("Host", "39.106.24.148:8080")
  .addHeader("accept-encoding", "gzip, deflate")
  .addHeader("content-length", "133")
  .addHeader("Connection", "keep-alive")
  .addHeader("cache-control", "no-cache")
  .build();
Response response = client.newCall(request).execute();

當然平時我們使用的時候,不用拼上這么多的請求頭,我這樣寫的目的就是為了更加還原請求報文。

還有一個子類 MultipartBody這個可以用來構建比較復雜的請求體。

1995 年 Content-Type 的類型擴充了 multipart/form-data 用來支持向服務器發(fā)送二進制數據。如果一次提交多種類型的數據,比如:一張圖片和一個文字,這個時候引入了 boundary ,boundary使得 POST 可以滿足這種提交多種不同的數據類型。通過 boundary 可以實現多個不同類型的數據同時存在在一個 Request 中。兩個 boundary之間就是一個類型的數據,并且可以重新設置 Content-Type

與 HTML 文件上傳形式兼容。每塊請求體都是一個請求體,可以定義自己的請求頭。這些請求頭可以用來描述這塊請求。例如,他們的 Content-Disposition。如果 Content-Length 和 Content-Type 可用的話,他們會被自動添加到請求頭中。

來看一下這種類型的請求報文是什么樣的:

POST /web/UploadServlet HTTP/1.1
Content-Type: multipart/form-data; boundary=e1b05ca4-fc4e-4944-837d-cc32c43c853a
Content-Length: 66089
Host: localhost.tt.com:8080
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/3.5.0

–e1b05ca4-fc4e-4944-837d-cc32c43c853a
Content-Disposition: form-data; name=”file”; filename=”**.png”
Content-Type: image/png
Content-Length: 65744

fdPNG
IHDR?0B7M?iM?M?CCPIM?CC ProfileH??……………………IEND?B`?
–e1b05ca4-fc4e-4944-837d-cc32c43c853a
Content-Disposition: form-data; name=”comment”
Content-Length: 30

上傳一個圖
–e1b05ca4-fc4e-4944-837d-cc32c43c853a–

第一個數據是一張 png 的圖,重新設置了 Content-Type:image/png 中間的亂碼就是圖片的數據。這一堆數據前有一個空行,表示上下分別是請求頭、請求體。

第二個數據,就是一個文本數據。

這樣它們一起構成了請求體。

講起來可能比較復雜,就記住,當既需要上傳參數,又需要上傳文件的時候用這種請求體。

MediaType mediaType = MediaType.parse("image/png");
        RequestBody requestBody = new MultipartBody.Builder()
                    // 需要設置成表單形式否則無法上傳鍵值對參數
                .setType(MultipartBody.FORM)
                .addPart(Headers.of("Content-Disposition", "form-data;name=\"title\""),
                        RequestBody.create(null, "Square Logo"))
                .addPart(
                        Headers.of("Content-Disposition", "form-data;name=\"imge\""),
                        RequestBody.create(mediaType, new File("路徑/logo.png"))
                ).
                        build();
        Request request = new Request.Builder()
                .post(requestBody)
                .url("https://api.imgur.com/3/image")
                .build();
        try {
            mOkHttpClient.newCall(request).execute();
        } catch (IOException e) {
            e.printStackTrace();
        }

簡化寫法:

MediaType mediaType = MediaType.parse("image/png");
        RequestBody requestBody = new MultipartBody.Builder()
                 .setType(MultipartBody.FORM)
               .addFormDataPart("title","logo")
                .addFormDataPart("img","logo.png",RequestBody.create(mediaType,new File("路徑/logo.png")))
                .build();

Content-Disposition 可以用在消息體的子部分中,用來給出其對應字段的相關信息。作為 multipart body 中的消息頭,第一個參數總是固定不變的 form-data; 附加的參數不區(qū)分大小寫,并且擁有參數值,參數名與參數值用等號連接,參數之間用分號分隔。參數值用雙引號括起來

// 比如這樣,就是這種固定的格式
"Content-Disposition","form-data;name=\"mFile\";filename=\"xxx.mp4\""

到這里關于請求的幾個重要的類就講完了。

總結一下

只要掌握 http 請求的原理,使用起 okhttp 來也就不是什么問題了。

首先 OkHttpClient 是用來設置關于請求工具的一些參數的,比如超時時間、是否緩存等等。

Call 對象是發(fā)起 Http 請求的對象,通過 Call 對象來發(fā)起請求。

發(fā)起請求的時候,需要有請求報文,Request 對象就是對應的請求報文,可以添加對應的請求行、請求頭、請求體。

說起請求體就是對應了 RequestBody 了。然后這個網絡請求過程就完成了!

OKHTTP架構圖

OKHTTP架構圖

OKHttp發(fā)送主體流程

image-20221108145253547

在使用OkHttp發(fā)起一次請求時,對于使用者最少存在OkHttpClient、Request與Call三個角色。其中OkHttpClient和Request的創(chuàng)建可以使用它為我們提供的Builder(建造者模式)。而Call則是把Request交給OkHttpClient之后返回的一個已準備好執(zhí)行的請求。

同時OkHttp在設計時采用的門面模式,將整個系統(tǒng)的復雜性給隱藏起來,將子系統(tǒng)接口通過一個客戶端OkHttpClient統(tǒng)一暴露出來。

OkHttpClient中全是一些配置,比如代理的配置、ssl證書的配置等。而Call本身是一個接口,我們獲得的實現為:RealCall

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
}

Callexecute代表了同步請求,而enqueue則代表異步請求。兩者唯一區(qū)別在于一個會直接發(fā)起網絡請求,而另一個使用OkHttp內置的線程池來進行。這就涉及到OkHttp的任務分發(fā)器。

  • Call: 每一個請求的實例,比如登錄login 對應一個Call、獲取用戶信息 對應一個Call。Call本身就是一個接口,用戶的每一個Http請求就是一個Call實例,而且每一個Call都對應一個線程。
  • Call包含了 request()、execute()、enqueue() 方法。
  • RealCall: 具體的Call接口實現類,代表每一個HTTP請求。每一個RealCall內部有一個AsyncCall final類。
  • AsyncCall: RealCall類的內部final類,實現了NamedRunnable類的execute()。繼承于NamedRunnable類,NamedRunnable類實現了Runnable接口,并且有一個execute()抽象方法,這個抽象方法在Runnable的run()里執(zhí)行。
  • Dispatcher:
    • OkHttp的任務隊列,其內部維護了一個線程池,進行線程分發(fā),實現非阻塞,高可用,高并發(fā)。
    • 當有接收到一個Call時,Dispatcher負責在線程池中找到空閑的線程并執(zhí)行其execute方法。
    • Okhttp采用Deque作為緩存隊列,按照入隊的順序先進先出。
    • OkHttp最出彩的地方就是在try/finally中調用了finished函數,可以主動控制等待隊列的移動,而不是采用 鎖或者wait/notify,極大減少了編碼復雜性。

分發(fā)器

Dispatcher,分發(fā)器就是來調配請求任務的,內部會包含一個線程池??梢栽趧?chuàng)建OkHttpClient時,傳遞我們自己定義的線程池來創(chuàng)建分發(fā)器。

這個Dispatcher中的成員有:

//異步請求同時存在的最大請求
private int maxRequests = 64;
//異步請求同一域名同時存在的最大請求
private int maxRequestsPerHost = 5;
//閑置任務(沒有請求時可執(zhí)行一些任務,由使用者設置)
private @Nullable Runnable idleCallback;
//異步請求使用的線程池
private @Nullable ExecutorService executorService;
//異步請求等待執(zhí)行隊列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//異步請求正在執(zhí)行隊列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//同步請求正在執(zhí)行隊列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

同步請求

synchronized void executed(RealCall call) {
	runningSyncCalls.add(call);
}

因為同步請求不需要線程池,也不存在任何限制。所以分發(fā)器僅做一下記錄。

異步請求

synchronized void enqueue(AsyncCall call) {
	if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) 	  {
		runningAsyncCalls.add(call);
		executorService().execute(call);
	} else {
		readyAsyncCalls.add(call);
	}
}

當正在執(zhí)行的任務未超過最大限制64,同時runningCallsForHost(call) < maxRequestsPerHost同一Host的請求不超過5個,則會添加到正在執(zhí)行隊列,同時提交給線程池。否則先加入等待隊列。

加入線程池直接執(zhí)行沒啥好說的,但是如果加入等待隊列后,就需要等待有空閑名額才開始執(zhí)行。因此每次執(zhí)行完一個請求后,都會調用分發(fā)器的finished方法

//異步請求調用
void finished(AsyncCall call) {
	finished(runningAsyncCalls, call, true);
}
//同步請求調用
void finished(RealCall call) {
	finished(runningSyncCalls, call, false);
}
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
	int runningCallsCount;
	Runnable idleCallback;
	synchronized (this) {
        //不管異步還是同步,執(zhí)行完后都要從隊列移除(runningSyncCalls/runningAsyncCalls)
		if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
		if (promoteCalls) promoteCalls();
        //異步任務和同步任務正在執(zhí)行的和
		runningCallsCount = runningCallsCount();
		idleCallback = this.idleCallback;
	}
	// 沒有任務執(zhí)行執(zhí)行閑置任務
	if (runningCallsCount == 0 && idleCallback != null) {
		idleCallback.run();
	}
}

需要注意的是 只有異步任務才會存在限制與等待,所以在執(zhí)行完了移除正在執(zhí)行隊列中的元素后,異步任務結束會執(zhí)行promoteCalls()。很顯然這個方法肯定會重新調配請求。

private void promoteCalls() {
    //如果任務滿了直接返回
	if (runningAsyncCalls.size() >= maxRequests) return; 
    //沒有等待執(zhí)行的任務,返回
	if (readyAsyncCalls.isEmpty()) return; 
    //遍歷等待執(zhí)行隊列
	for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
		AsyncCall call = i.next();
        //等待任務想要執(zhí)行,還需要滿足:這個等待任務請求的Host不能已經存在5個了
		if (runningCallsForHost(call) < maxRequestsPerHost) {
			i.remove();
			runningAsyncCalls.add(call);
			executorService().execute(call);
		}
		if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
	}
}

請求流程

用戶是不需要直接操作任務分發(fā)器的,獲得的RealCall中就分別提供了executeenqueue來開始同步請求或異步請求。

@Override public Response execute() throws IOException {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    try {
      //調用分發(fā)器
      client.dispatcher().executed(this);
      //執(zhí)行請求
      Response result = getResponseWithInterceptorChain();
      if (result == null) throw new IOException("Canceled");
      return result;
    } catch (IOException e) {
      eventListener.callFailed(this, e);
      throw e;
    } finally {
      //請求完成
      client.dispatcher().finished(this);
    }
}

異步請求的后續(xù)同時是調用getResponseWithInterceptorChain()來執(zhí)行請求

@Override
public void enqueue(Callback responseCallback) {
	synchronized (this) {
		if (executed) throw new IllegalStateException("Already Executed");
		executed = true;
	}
	captureCallStackTrace();
	eventListener.callStart(this);
    //調用分發(fā)器
	client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

如果該RealCall已經執(zhí)行過了,再次執(zhí)行是不允許的。異步請求會把一個AsyncCall提交給分發(fā)器。

AsyncCall實際上是一個Runnable的子類,使用線程啟動一個Runnable時會執(zhí)行run方法,在AsyncCall中被重定向到execute方法:

final class AsyncCall extends NamedRunnable {
	private final Callback responseCallback;
	AsyncCall(Callback responseCallback) {
		super("OkHttp %s", redactedUrl());
		this.responseCallback = responseCallback;
	}
    //線程池執(zhí)行
	@Override
	protected void execute() {
	 boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
       //.......
      } catch (IOException e) {
       //......
      } finally {
        //請求完成
        client.dispatcher().finished(this);
      }
    }
}
public abstract class NamedRunnable implements Runnable {
    protected final String name;
    public NamedRunnable(String format, Object... args) {
        this.name = Util.format(format, args);
    }
    @Override
    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(name);
        try {
            execute();
        } finally {
            Thread.currentThread().setName(oldName);
        }
    }
    protected abstract void execute();
}

同時AsyncCall也是RealCall的普通內部類,這意味著它是持有外部類RealCall的引用,可以獲得直接調用外部類的方法。

可以看到無論是同步還是異步請求實際上真正執(zhí)行請求的工作都在getResponseWithInterceptorChain()中。這個方法就是整個OkHttp的核心:攔截器責任鏈。但是在介紹責任鏈之前,我們再來回顧一下線程池的基礎知識。

分發(fā)器線程池

前面我們提過,分發(fā)器就是來調配請求任務的,內部會包含一個線程池。當異步請求時,會將請求任務交給線程池來執(zhí)行。那分發(fā)器中默認的線程池是如何定義的呢?為什么要這么定義?

public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(
          					0,   				//核心線程
                            Integer.MAX_VALUE,  //最大線程
                            60,					//空閑線程閑置時間
                            TimeUnit.SECONDS,	//閑置時間單位
                            new SynchronousQueue<Runnable>(), //線程等待隊列
                            Util.threadFactory("OkHttp Dispatcher", false) //線程創(chuàng)建工廠
      );
    }
    return executorService;
}

為什么選擇使用OKHttp

1.可擴展性高。類似于緩存,Dns,請求/連接/響應超時時間等等都可以通過配置傳入,甚至線程池都可以根據自己的需求來配置。
2.OKHttp使用了連接池緩存,提高通信效率。
3.責任鏈五層攔截器模式,每層功能清晰明了,并且提供了兩層可擴展的攔截器方便進行所需要的改造。
4.層次結構清晰,方便進行問題的排查。
5.觀察者模式的充分使用,查看請求狀態(tài)和監(jiān)控請求狀態(tài)變得十分簡單。
6.使用了OKIO框架進行數據的處理,效率和安全性上更高。

到此這篇關于OkHttp原理分析總結的文章就介紹到這了,更多相關OkHttp原理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論