俗話說「不要重複造輪子」,但是我覺得通過研究大神造的輪子,然後自己去嘗試造一個簡陋版的,對於提升自己的軟件構思是很有幫助的。

迴歸正題,最近在做一個作業,和計算機網絡相關的,筆者選擇了用Java開發一個簡陋版的HTTP客戶端,於是筆者去拜讀了 Square 公司開源的 OkHttp ,參照了Okhttp的設計思想,開發了 Yohttp

這裏給出 Github 地址: YoHttp ,歡迎大家一起學習探討。

軟件架構

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

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

  1. 調度中心

    調度中心在使用異步請求的時候用到,調度中心維護着一個請求隊列和一個線程池,請求隊列裏面存儲的是處理鏈 Chain 。線程池負責執行隊列中的處理鏈。

    筆者認爲這裏使用線程池能提高隊列的處理效率,畢竟現在PC都是多核心的,充分利用CPU提高效率還是不錯的。

  2. 連接池

    每個請求都是去連接池獲取 Socket 連接,如果連接池中存在 IPPORT 相同的連接則直接返回,否則創建一個 Socket 連接存儲到連接池然後返回,而連接池中的連接 閒置時間超過最大允許閒置的時間後就會被關閉

    筆者認爲通過使用連接池能減少連接創建銷燬的開銷,在請求較多、請求頻率較高的場景下能提高效率。

介紹完了架構,我們看看怎麼使用我們的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。

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 ,歡迎大家一起學習探討。

相關文章