從0開始造一個輪子(方的)

前言

俗話說「不要重複造輪子」,可是我以爲經過研究大神造的輪子,而後本身去嘗試造一個簡陋版的,對於提高本身的軟件構思是頗有幫助的。
迴歸正題,最近在作一個做業,和計算機網絡相關的,筆者選擇了用Java開發一個簡陋版的HTTP客戶端,因而筆者去拜讀了Square公司開源的OkHttp,參照了Okhttp的設計思想,開發了Yohttpjava

這裏給出Github地址:YoHttp,歡迎你們一塊兒學習探討。git

軟件架構

筆者將軟件大概設計成五大模塊:github

  1. 請求信息
    這部分即對應上圖的Request,用於用戶構建請求信息,如URLmethod、請求頭等。這部分是用戶能夠操做的。
  2. Yohttp客戶端
    用戶建立一個YoHttp,而後將請求信息注入到Yohttp便可以開始使用請求功能,請求包括同步請求和異步請求,其中一個YoHttp包含一個調度中心、一個鏈接池,因此對於一個項目來講,維護着一個YoHttp客戶端就足以。
  3. 處理鏈
    這裏是請求的具體實現操做,筆者將一個一個操做封裝成一個攔截器,如把獲取Socket鏈接的操做封裝成鏈接攔截器、把Socket流的讀寫封裝成收發攔截器,而後咱們請求須要用到哪些操做,便可把這些攔截器一個一個拼接起來組合成一個處理鏈(Chain),一個處理鏈對應着一個請求。執行處理鏈中的一個個攔截器,直到執行完全部的攔截器,也對應着一個請求的完成。這也是爲何咱們須要將收發攔截器放在最後,由於一個請求的最後一個操做確定是進行Socket流的寫和讀。
    筆者認爲這樣將一個一個操做封裝成攔截器,而後組合攔截器拼湊成處理鏈,最後執行處理鏈便可達到執行操做,極大的解耦了請求過程,同時也提升了擴展性。

  1. 調度中心
    調度中心在使用異步請求的時候用到,調度中心維護着一個請求隊列和一個線程池,請求隊列裏面存儲的是處理鏈Chain。線程池負責執行隊列中的處理鏈。
    筆者認爲這裏使用線程池能提升隊列的處理效率,畢竟如今PC都是多核心的,充分利用CPU提升效率仍是不錯的。緩存

  2. 鏈接池
    每一個請求都是去鏈接池獲取Socket鏈接,若是鏈接池中存在IPPORT相同的鏈接則直接返回,不然建立一個Socket鏈接存儲到鏈接池而後返回,而鏈接池中的鏈接閒置時間超過最大容許閒置的時間後就會被關閉
    筆者認爲經過使用鏈接池能減小鏈接建立銷燬的開銷,在請求較多、請求頻率較高的場景下能提升效率。bash

介紹完了架構,咱們看看怎麼使用咱們的HTTP客戶端:網絡

  1. 同步請求
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客戶端,選擇同步請求並將請求信息注入,執行請求。架構

  1. 異步請求
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

YoHttpClient

在YoHttp客戶端中有一個調度中心和一個鏈接池,調度中心是使用異步請求的時候用上的,鏈接池則是在請求獲取Socket鏈接的時候使用。

  1. 構造方法
    筆者設置了兩個構造方法:
public YoHttpClient() {
    this(5, TimeUnit.MINUTES);
}

public YoHttpClient(int keepAliveTime, TimeUnit timeUnit) {
    this.dispatcher = new Dispatcher();
    this.connectionPool = new ConnectionPool(keepAliveTime, timeUnit);
}
複製代碼

一個是無參構造方法,一個是指定鏈接池中鏈接最大閒置時間的構造方法,若是用戶使用了無參構造方法,默認設置鏈接池中的鏈接最大閒置時間是5分鐘。

  1. 同步請求方法SynchCall
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。

  1. 異步請求AsyncCall
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;
}
複製代碼

第一步是獲取請求信息中的IPPORT(筆者將這二者封裝成了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。

  1. 獲取鏈接
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,目的是防止多個線程同時建立同一個鏈接,鎖住以後再次判斷鏈接池是否存在鏈接了,沒有則進行建立而後返回。

  1. 清理超過閒置時間的鏈接
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。

總結

這個項目僅是學習使用,請勿用於生產環境
目前僅實現了GETPOSTDELETEPUT方法,但願後面會完善更多功能還有把IO改爲NIO提升性能。
但願各位前輩看完以後能給點意見或者留下個贊~
最後再附上Github地址:YoHttp,歡迎你們一塊兒學習探討。

原文地址:ddnd.cn/2019/04/12/…

相關文章
相關標籤/搜索