俗話說「不要重複造輪子」,可是我以爲經過研究大神造的輪子,而後本身去嘗試造一個簡陋版的,對於提高本身的軟件構思是頗有幫助的。
迴歸正題,最近在作一個做業,和計算機網絡相關的,筆者選擇了用Java開發一個簡陋版的HTTP客戶端,因而筆者去拜讀了Square
公司開源的OkHttp
,參照了Okhttp的設計思想,開發了Yohttp
。java
這裏給出Github
地址:YoHttp,歡迎你們一塊兒學習探討。git
筆者將軟件大概設計成五大模塊:github
Request
,用於用戶構建請求信息,如URL
、method
、請求頭等。這部分是用戶能夠操做的。YoHttp
,而後將請求信息注入到Yohttp便可以開始使用請求功能,請求包括同步請求和異步請求,其中一個YoHttp
包含一個調度中心、一個鏈接池,因此對於一個項目來講,維護着一個YoHttp
客戶端就足以。Socket
鏈接的操做封裝成鏈接攔截器、把Socket
流的讀寫封裝成收發攔截器,而後咱們請求須要用到哪些操做,便可把這些攔截器一個一個拼接起來組合成一個處理鏈(Chain),一個處理鏈對應着一個請求。執行處理鏈中的一個個攔截器,直到執行完全部的攔截器,也對應着一個請求的完成。這也是爲何咱們須要將收發攔截器放在最後,由於一個請求的最後一個操做確定是進行Socket流的寫和讀。調度中心
調度中心在使用異步請求的時候用到,調度中心維護着一個請求隊列和一個線程池,請求隊列裏面存儲的是處理鏈Chain
。線程池負責執行隊列中的處理鏈。
筆者認爲這裏使用線程池能提升隊列的處理效率,畢竟如今PC都是多核心的,充分利用CPU提升效率仍是不錯的。緩存
鏈接池
每一個請求都是去鏈接池獲取Socket
鏈接,若是鏈接池中存在IP
、PORT
相同的鏈接則直接返回,不然建立一個Socket
鏈接存儲到鏈接池而後返回,而鏈接池中的鏈接閒置時間超過最大容許閒置的時間後就會被關閉。
筆者認爲經過使用鏈接池能減小鏈接建立銷燬的開銷,在請求較多、請求頻率較高的場景下能提升效率。bash
介紹完了架構,咱們看看怎麼使用咱們的HTTP客戶端:網絡
Request request = new Request.Builder()
.url("www.baidu.com")
.get()
.build();
YoHttpClient httpClient = new YoHttpClient();
Response response = httpClient.SyncCall(request).executor();
System.out.println(response.getBody());
複製代碼
第一步新建個請求信息Request
,填寫請求的URL
、請求方法、請求頭等信息。
第二步新建個YoHttp
客戶端,選擇同步請求並將請求信息注入,執行請求。架構
Request request = new Request.Builder()
.url("www.baidu.com")
.get()
.build();
YoHttpClient httpClient = new YoHttpClient();
httpClient.AsyncCall(request).executor(new CallBack() {
@Override
public void onResponse(Response response) {
System.out.println(response.getBody());
}
});
複製代碼
第一步新建個請求信息Request
,填寫請求的URL
、請求方法、請求頭等信息。
第二步新建個YoHttp
客戶端,選擇異步請求並將請求信息注入,執行請求,當請求有響應的時候,會經過回調異步請求的onResponse
方法來反饋響應內容。併發
說完了架構還有使用方法,接下來筆者介紹各個模塊的具體實現。異步
在實現Request
的時候,筆者使用的是Builder
模式,即構造者模式,在Request
中添加個靜態內部類Builder,用於構造Request。ide
在YoHttp客戶端中有一個調度中心和一個鏈接池,調度中心是使用異步請求的時候用上的,鏈接池則是在請求獲取Socket
鏈接的時候使用。
public YoHttpClient() {
this(5, TimeUnit.MINUTES);
}
public YoHttpClient(int keepAliveTime, TimeUnit timeUnit) {
this.dispatcher = new Dispatcher();
this.connectionPool = new ConnectionPool(keepAliveTime, timeUnit);
}
複製代碼
一個是無參構造方法,一個是指定鏈接池中鏈接最大閒置時間的構造方法,若是用戶使用了無參構造方法,默認設置鏈接池中的鏈接最大閒置時間是5
分鐘。
public SyncCall SyncCall(Request request) {
return new SyncCall(this, request);
}
// SyncCall.java
@Override
public Response executor() {
synchronized (this) {
if (this.executed)
throw new IllegalStateException("Call Already Executed");
this.executed = true;
}
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new ConnectionInterceptor(yoHttpClient, request));
interceptors.add(new CallServerInterceptor(request));
Chain chain = new Chain(interceptors, null);
Response response = chain.proceed();
chain = null;
return response;
}
//Chain.java
public Response proceed() {
Response response = new Response();
for (int i = 0; i < interceptors.size(); i++) {
response = interceptors.get(i).proceed(response);
}
return response;
}
複製代碼
建立一個SynchCall
同步請求,SynchCall裏面有個executor
方法,這個方法建立一個存儲攔截器Interceptor
的List,咱們把請求中須要用到的操做(攔截器)存入到List中,例如咱們用到了鏈接攔截器(ConnectionInterceptor)、收發攔截器(CallServerInterceptor),而後將List封裝成一個處理鏈(Chain),最後調用處理鏈的proceed
方法遍歷List中的攔截器並執行,這樣便可達到執行一個請求的全部操做,這裏是同步請求,因此阻塞處處理鏈執行完成返回response以後才return。
public AsyncCall AsyncCall(Request request) {
return new AsyncCall(this, request);
}
//AsyncCall.java
public void executor(CallBack callBack) {
synchronized (this) {
if (this.executed)
throw new IllegalStateException("Call Already Executed");
this.executed = true;
}
List<Interceptor> interceptors = new ArrayList<>();
interceptors.add(new ConnectionInterceptor(yoHttpClient, request));
interceptors.add(new CallServerInterceptor(request));
Chain chain = new Chain(interceptors, callBack);
yoHttpClient.getDispatcher().addChain(chain);
}
複製代碼
異步請求中,一樣是在executor
方法構造好所需的攔截器,將攔截器封裝成處理鏈,區別的地方在這裏並非立刻調用處理鏈的proceed
方法,而是將處理鏈添加到調度中心的請求隊列中,而後立刻返回了,調度中心的具體實如今後文介紹。
處理鏈在上文的YoHttpClient介紹的差很少了,這裏補充一下攔截器的設計。
全部的攔截器都實現Interceptor
這個接口,這個接口很簡單,只有一個方法proceed
,只須要將具體的操做寫到這個方法便可。例如鏈接攔截器ConnectionInterceptor
的實現以下。
@Override
public Response proceed(Response response) {
Address address = request.getAddress();
Connection connection = yoHttpClient.getConnectionPool().getConnection(address);
request.setConnection(connection);
return response;
}
複製代碼
第一步是獲取請求信息中的IP
、PORT
(筆者將這二者封裝成了Address)
第二步是使用這個address去鏈接池中獲取鏈接。
這個proceed方法是提供給處理鏈中執行的。
調度中心在異步請求中使用到,調度中心維護着一個請求隊列和一個線程池。筆者採用的是阻塞隊列(考慮到併發問題)和可緩存線程池,這個線程池的特色:核心線程數是0,線程數最大是Integer.MAX_VALUE
,線程閒置時間最大容許爲60秒。
調度中心有2個內部類,一個是CallRunnable
,這個內部類的做用是將處理鏈Chain封裝成Runnable
公線程執行。另外一個是ChainQueue
,這個內部類維護着一個阻塞隊列,控制着請求的入隊和出隊。
private void executor() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
while (chainQueue.size() > 0) {
executorService.submit(new CallRunnable(chainQueue.pollChain()));
}
}
}
});
thread.start();
}
//CallRunnable內部類
private final class CallRunnable implements Runnable {
private Chain chain;
CallRunnable(Chain chain) {
this.chain = chain;
}
@Override
public void run() {
Response response = chain.proceed();
chain.getCallBack().onResponse(response);
chain = null;
}
}
複製代碼
在調度中心開啓了一個線程,經過遍歷阻塞隊列,若是阻塞隊列中有請求,則交給線程池去處理,線程經過調用處理鏈的proceed方法來遍歷處理鏈中的攔截器,這個和同步請求中的同樣的,當執行完後才能經過回調將響應返回給客戶端。
筆者將Socket
鏈接封裝成一個Connection
,而鏈接池維護的則是一個存儲Connection
的HashMap。
public Connection getConnection(Address address) {
return tryAcquire(address);
}
private Connection tryAcquire(Address address) {
if (connections.containsKey(address)) {
connections.get(address).setTime(System.currentTimeMillis());
return connections.get(address);
}
synchronized (address) {
cleanUpConnection();
if (!connections.containsKey(address)) {
Connection connection = new Connection(address);
connection.setTime(System.currentTimeMillis());
connections.put(address, connection);
return connection;
} else {
connections.get(address).setTime(System.currentTimeMillis());
return connections.get(address);
}
}
}
複製代碼
經過調用getConnection
方法便可獲取到一個鏈接,而getConnection的實現是經過調用私有方法tryAcquire
,獲取的流程以下:
第一步先判斷鏈接池中是否存在address相同的鏈接,有則則更新線程的活躍時間而後直接返回,沒有則執行第二步。
第二步鎖住address,目的是防止多個線程同時建立同一個鏈接,鎖住以後再次判斷鏈接池是否存在鏈接了,沒有則進行建立而後返回。
private void cleanUpConnection() {
for (Map.Entry<Address, Connection> entry: connections.entrySet()) {
if (System.currentTimeMillis() - entry.getValue().getTime() <= keepAliveTime) {
try {
connections.get(entry.getKey()).getSocket().close();
} catch (IOException e) {
e.printStackTrace();
}
connections.remove(entry.getKey());
}
}
}
複製代碼
這個cleanUpConnection方法在每次獲取鏈接的時候都會執行一次,遍歷鏈接池中的鏈接,若是鏈接池中的鏈接超過容許的閒置時間則關閉這個鏈接而後將鏈接移除Map。
這個項目僅是學習使用,請勿用於生產環境。
目前僅實現了GET
、POST
、DELETE
、PUT
方法,但願後面會完善更多功能還有把IO改爲NIO提升性能。
但願各位前輩看完以後能給點意見或者留下個贊~
最後再附上Github
地址:YoHttp,歡迎你們一塊兒學習探討。
原文地址:ddnd.cn/2019/04/12/…