5.okhttp

簡單使用

OkHttpClient client = new OkHttpClient.Builder()
                .build();

Request request = new Request.Builder()
                .url(ENDPOINT)
                .build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        System.out.println("onFailure");
    }
    @Override
    public void onResponse(Call call, Response response) throws IOException {
        System.out.println("onResponse");
        ResponseBody body = response.body();
        System.out.println(body.source().readString(Charset.forName("utf-8")));
    }
});

源碼分析

先是創建HttpClient,通過建造者模式來創建,我們來看build方法

OkHttpClient.Builder.build()

public OkHttpClient build() {
            return new OkHttpClient(this);
}

它調用了OkHttpClient的含有build參數的構造方法

OkHttpClient(Builder builder) {
    //調度者,用於調度線程
    this.dispatcher = builder.dispatcher;
    //代理服務器
    this.proxy = builder.proxy;
    //支持協議
    this.protocols = builder.protocols;
    //連接規格
    this.connectionSpecs = builder.connectionSpecs;
    //請求攔截器,在框架網路發生前調用和框架處理完響應後的時間段處理
    this.interceptors = Util.immutableList(builder.interceptors);
    //網絡攔截器,在框架進行網絡握手後和發送實際請求信息前這段時間處理
    this.networkInterceptors = Util.immutableList(builder.networkInterceptors);
    //事件監聽者工廠
    this.eventListenerFactory = builder.eventListenerFactory;
    //代理選擇器
    this.proxySelector = builder.proxySelector;
    //Cookie存儲器
    this.cookieJar = builder.cookieJar;
    //緩存
    this.cache = builder.cache;
    //內部緩存
    this.internalCache = builder.internalCache;
    //客戶端套接字工廠
    this.socketFactory = builder.socketFactory;
    boolean isTLS = false;
    for (ConnectionSpec spec : connectionSpecs) {
        isTLS = isTLS || spec.isTls();
    }
    if (builder.sslSocketFactory != null || !isTLS) {
        //SSL套接字工廠
        this.sslSocketFactory = builder.sslSocketFactory;
        //證書鏈整理者
        this.certificateChainCleaner = builder.certificateChainCleaner;
    } else {
        X509TrustManager trustManager = Util.platformTrustManager();
        this.sslSocketFactory = newSslSocketFactory(trustManager);
        this.certificateChainCleaner = CertificateChainCleaner.get(trustManager);
    }
    if (sslSocketFactory != null) {
        Platform.get().configureSslSocketFactory(sslSocketFactory);
    }
    // 主機名驗證器,給https使用,驗證對方的host是不是要訪問的host
    this.hostnameVerifier = builder.hostnameVerifier;
    //證書固定器,用於做自簽名
    this.certificatePinner = builder.certificatePinner.withCertificateChainCleaner(
            certificateChainCleaner);
    //代理授權認證器
    this.proxyAuthenticator = builder.proxyAuthenticator;
    //授權認證器
    this.authenticator = builder.authenticator;
    //連接池
    this.connectionPool = builder.connectionPool;
    //dns
    this.dns = builder.dns;
    //HTTP與https切換的重定向是否允許
    this.followSslRedirects = builder.followSslRedirects;
    //是否允許重定向
    this.followRedirects = builder.followRedirects;
    //是否請求連接失敗重試
    this.retryOnConnectionFailure = builder.retryOnConnectionFailure;
    //連接超時
    this.connectTimeout = builder.connectTimeout;
    //下載緩存超時
    this.readTimeout = builder.readTimeout;
    //寫入緩存超時
    this.writeTimeout = builder.writeTimeout;
    //針對WebSocket,定義發送心跳的間隔
    this.pingInterval = builder.pingInterval;
    if (interceptors.contains(null)) {
        throw new IllegalStateException("Null interceptor: " + interceptors);
    }
    if (networkInterceptors.contains(null)) {
        throw new IllegalStateException("Null network interceptor: " + networkInterceptors);
    }
}

上面對OkHttp全局屬性做了配置。

接下來是生成一個請求對象

Request.Builder.build()

public Request build() {
    if (url == null) throw new IllegalStateException("url == null");
    return new Request(this);
}

它也是調用了request用build做參數的構造法

Request(Builder builder) {
    //請求url
    this.url = builder.url;
    //請求方式
    this.method = builder.method;
    //請求頭
    this.headers = builder.headers.build();
    //請求體
    this.body = builder.body;
    this.tags = Util.immutableMap(builder.tags);
}

這裏對http的這次請求信息進行設置

然後用client和這個request對象生成call,call對象是這次通信的抽象,可以進行同/異步請求,取消等

client.newCall(request)

@Override
public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
}

調用RealCall的靜態方法newRealCall方法

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    //安全地將Call實例發佈到EventListener。
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
}

接下來纔是真正調用RealCall的構造方法,傳入client,request,和forwebSocket參數

private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    this.client = client;
    this.originalRequest = originalRequest;
    this.forWebSocket = forWebSocket;
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}

client就是之前我們生成的Client對象,也是從這個對象調用方法鏈接過來的

request就是上面定義的這次請求的相關信息

forwebSocket是標記是否是長連接

這樣我們就獲得了call對象,拿到這個對象我們就可以進行同/異步請求了,我們先看異步請求

call.enqueue(callback)

@Override
public void enqueue(Callback responseCallback) {
    synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    //這裏使用了調度者進行調用
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

最後一行使用了client的dispatcher的enqueue方法將新建的AsynCall對象調度

AsyncCall繼承了NamedRunnable類,NamedRunnable類實現了Runnable方法,並且定義了當run方法執行時,執行其execute方法,在NamedRunnable裏execute方法,是一個抽象方法,AsyncCall實現了其方法,並進行網絡請求。怎麼請求後面再說,我們先看dispatcher的enqueue方法

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        //如果正在運行的的調用數小於最大請求書且當前主機的調用數小於每個主機的最大請求數
        //加入到正在運行的調用者隊列
        runningAsyncCalls.add(call);
        //executorService執行調用
        executorService().execute(call);
    } else {
        //超過了最大數
        //添加到準備運行的隊列中
        readyAsyncCalls.add(call);
    }
}

dispatcher的enqueue方法裏如果沒有超過運行數量限制的話,將用線程池執行asynCall對象,執行時會運行AsyncCall的run方法,然後 是execute方法,跟上面對應上了。

而如果超過數量限制的話則加入到準備運行隊列中。等待執行

準備隊列執行的任務會在promoteCalls方法執行後進行檢查,將readyAsyncCalls裏的call提升到runningAsyncCalls隊列執行

/**
 * 提升調用
 */
private void promoteCalls() {
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall call = i.next();
        if (runningCallsForHost(call) < maxRequestsPerHost) {
            i.remove();
            runningAsyncCalls.add(call);
            executorService().execute(call);
        }
        if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
}

而promoteCalls的調用時機有dispatcher.setMaxRequests方法dispatcher.setMaxRequestsPerHost以及dispatcher.finished方法

而dispatcher.finished會在AsyncCall的execute方法執行的finally代碼塊裏,也就是說每執行完一個AsyncCall,就會通知dispatcher自己結束執行了,然後就會去執行promoteCalls方法將一定數量的readyAsyncCalls隊列的call提升執行

接下來看AsyncCall的execute方法

AsyncCall.execute()

protected void execute() {
    boolean signalledCallback = false;
    try {
        //用攔截器鏈獲取到response
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
            signalledCallback = true;
            responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
            signalledCallback = true;
            responseCallback.onResponse(RealCall.this, response);
        }
    } catch (IOException e) {
        if (signalledCallback) {
            // Do not signal the callback twice!不要兩次回調通知!
            Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
            eventListener.callFailed(RealCall.this, e);
            responseCallback.onFailure(RealCall.this, e);
        }
    } finally {
        client.dispatcher().finished(this);
    }
}

通過getResponseWithInterceptorChain方法獲取到了這次請求的response對象

然後將處理結果通過callBack回調到應用層的代碼 我們的一次請求即完成

getResponseWithInterceptorChain()

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    //應用層攔截器
    interceptors.addAll(client.interceptors());
    //出錯重試,重定向重新請求的攔截器
    interceptors.add(retryAndFollowUpInterceptor);
    //橋接攔截器,根據用戶請求構建網絡請求,它根據網絡響應構建用戶響應。
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //緩存攔截器,在緩存有效時,直接從緩存取數據,不再進行網絡請求
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //連接攔截器,做了Tcp連接和Tls連接
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
        //網絡請求攔截器針對完整原始數據進行處理
        interceptors.addAll(client.networkInterceptors());
    }
    //網絡呼叫攔截器
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //生成真正的攔截器鏈
    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
            originalRequest, this, eventListener, client.connectTimeoutMillis(),
            client.readTimeoutMillis(), client.writeTimeoutMillis());
    //執行鏈
    return chain.proceed(originalRequest);
}

該方法將所有的攔截器、請求、超時時間的組合生成一個RealInterceptorChain鏈,然後所有的操作就獎給了這個攔截器鏈進行處理

然後是chain.proceed(originalRequest)方法

chain.proceed(originalRequest)

@Override
public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
}

然後是proceed(request, streamAllocation, httpCodec, connection)方法

/**
 * 負責生成下一個RealInterceptorChain,並取出當前的攔截器進行攔截
 *
 * @param request          請求
 * @param streamAllocation 流分配
 * @param httpCodec        http編解碼器
 * @param connection       連接
 * @return 被後面攔截器處理好的response
 */
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();
    calls++;
    // If we already have a stream, confirm that the incoming request will use it.
    //如果我們已經有了一個流,請確認傳入的請求將使用它
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
        throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
                + " must retain the same host and port");
    }
    // If we already have a stream, confirm that this is the only call to chain.proceed().
    //如果我們已經有了一個流,請確認這是對chain.proceed()的唯一調用
    if (this.httpCodec != null && calls > 1) {
        throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
                + " must call proceed() exactly once");
    }
    // Call the next interceptor in the chain.
    //構建下一個RealInterceptorChain,指明這個chain會使用第幾個攔截器
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
            connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
            writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);
    // Confirm that the next interceptor made its required call to chain.proceed().
    //確認下一個攔截器已對chain.proceed()進行了必要的調用
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
        throw new IllegalStateException("network interceptor " + interceptor
                + " must call proceed() exactly once");
    }
    // Confirm that the intercepted response isn't null.
    //確認截獲的響應不爲空
    if (response == null) {
        throw new NullPointerException("interceptor " + interceptor + " returned null");
    }
    if (response.body() == null) {
        throw new IllegalStateException(
                "interceptor " + interceptor + " returned a response with no body");
    }
    
    return response;
}

proceed方法會取出當前chain指明的攔截器,並且用剩下的攔截器的信息生成下一個RealInterceptorChain,並調用當前chain指明的攔截器的intercept方法獲取到intercept(next)方法獲取到response並返回回去,實際的工作是由intercept來完成的

接下來根據getResponseWithInterceptorChain方法裏的interceptors添加順序一個個來分析源碼

interceptors源碼分析

RetryAndFollowUpInterceptor

這個攔截器負責兩部分功能,retry和followup。

  • retry即重試,當一個請求進來,它會首先扔給後面的環節處理,當它下游的環節都搞不定,請求失敗並拋出異常,這時候由這一環負責決定是否再來一次,判斷是否重試的條件主要在RetryAndFollowUpInterceptor類的recover方法
  • followup功能在RetryAndFollowUpInterceptor的 followup()方法中,主要是檢查response的返回碼,根據返回碼採取相應措施,比如代理驗證、重定向、請求超時等。
@Override
public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();
    //創建流分配
    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
            createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;
    int followUpCount = 0;
    //預先的Response
    Response priorResponse = null;
    while (true) {
        if (canceled) {
            streamAllocation.release();
            throw new IOException("Canceled");
        }
        Response response;
        boolean releaseConnection = true;
        try {
            response = realChain.proceed(request, streamAllocation, null, null);
            releaseConnection = false;
        } catch (RouteException e) {
            //路由異常,例如ip錯了
            // The attempt to connect via a route failed. The request will not have been sent.
            //嘗試通過路由連接失敗。該請求將不會被髮送
            if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                throw e.getFirstConnectException();
            }
            releaseConnection = false;
            continue;
        } catch (IOException e) {
            // An attempt to communicate with a server failed. The request may have been sent.
            //嘗試與服務器通信失敗。該請求可能已發送。
            boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
            if (!recover(e, streamAllocation, requestSendStarted, request)) {
                throw e;
            }
            releaseConnection = false;
            continue;
        } finally {
            // We're throwing an unchecked exception. Release any resources.
            //我們拋出了一個未經檢查的異常。釋放所有資源。
            if (releaseConnection) {
                streamAllocation.streamFailed(null);
                streamAllocation.release();
            }
        }
        // Attach the prior response if it exists. Such responses never have a body.
        //附加先前的responses(如果存在)。這樣的responses從來沒有body。
        if (priorResponse != null) {
            response = response.newBuilder()
                    .priorResponse(priorResponse.newBuilder()
                            .body(null)
                            .build())
                    .build();
        }
        //後續請求
        Request followUp;
        try {
            //嘗試獲取後續請求
            followUp = followUpRequest(response, streamAllocation.route());
        } catch (IOException e) {
            streamAllocation.release();
            throw e;
        }
        if (followUp == null) {
            if (!forWebSocket) {
                streamAllocation.release();
            }
            //沒有後續請求,返回這次執行的響應
            return response;
        }
        closeQuietly(response.body());
        if (++followUpCount > MAX_FOLLOW_UPS) {
            //後續請求過多
            streamAllocation.release();
            throw new ProtocolException("Too many follow-up requests: " + followUpCount);
        }
        if (followUp.body() instanceof UnrepeatableRequestBody) {
            //無法重試流式HTTP正文
            streamAllocation.release();
            throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
        }
        if (!sameConnection(response, followUp.url())) {
            //如果原來的連接不能重用,流分配釋放掉,並新建一個流分配
            streamAllocation.release();
            streamAllocation = new StreamAllocation(client.connectionPool(),
                    createAddress(followUp.url()), call, eventListener, callStackTrace);
            this.streamAllocation = streamAllocation;
        } else if (streamAllocation.codec() != null) {
            throw new IllegalStateException("Closing the body of " + response
                    + " didn't close its backing stream. Bad interceptor?");
        }
        //請後續請求賦值給請求,響應賦值給原來的響應
        //while循環會使用新的請求重新走一遍上面邏輯
        request = followUp;
        priorResponse = response;
    }
}

followUpRequest方法用於判斷是否要重新請求

/**
 * 主要針對http 狀態碼爲3xx、4xx、5xx的出錯信息進行處理,看下是否可以重新請求
 * Figures out the HTTP request to make in response to receiving {@code userResponse}. This will
 * either add authentication headers, follow redirects or handle a client request timeout. If a
 * follow-up is either unnecessary or not applicable, this returns null.
 * 找出response收到{@code userResponse}而發出的HTTP請求。這將添加身份驗證標頭,遵循重定向或處理客戶端請求超時。如果後續跟蹤是不必要的或不適用的,則返回null。
 *
 * @param userResponse 之前的響應
 */
private Request followUpRequest(Response userResponse, Route route) throws IOException {
    if (userResponse == null) {
        throw new IllegalStateException();
    }
    int responseCode = userResponse.code();
    final String method = userResponse.request().method();
    switch (responseCode) {
        case HTTP_PROXY_AUTH:
            //407,需要代理身份認證,請求要求代理的身份認證
            Proxy selectedProxy = route != null
                    ? route.proxy()
                    : client.proxy();
            if (selectedProxy.type() != Proxy.Type.HTTP) {
                throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
            }
            return client.proxyAuthenticator().authenticate(route, userResponse);
        case HTTP_UNAUTHORIZED:
            //401,認證失敗,請求要求用戶的身份認證
            return client.authenticator().authenticate(route, userResponse);
        case HTTP_PERM_REDIRECT:
        case HTTP_TEMP_REDIRECT:
            //308:永久轉移,307:臨時重定向
            // "If the 307 or 308 status code is received in response to a request other than GET
            // or HEAD, the user agent MUST NOT automatically redirect the request"
            //“如果響應除GET或HEAD以外的請求而接收到307或308狀態代碼,則用戶代理務必不得自動重定向該請求”
            if (!method.equals("GET") && !method.equals("HEAD")) {
                return null;
            }
            // fall-through
        case HTTP_MULT_CHOICE:
        case HTTP_MOVED_PERM:
        case HTTP_MOVED_TEMP:
        case HTTP_SEE_OTHER:
            //300:多種選擇,301:永久移動,302:臨時移動,303:查看其他位置
            // Does the client allow redirects?
            if (!client.followRedirects()) return null;
            String location = userResponse.header("Location");
            if (location == null) return null;
            HttpUrl url = userResponse.request().url().resolve(location);
            // Don't follow redirects to unsupported protocols.
            //不能跟隨重定向到不受支持的協議
            if (url == null) return null;
            // If configured, don't follow redirects between SSL and non-SSL.
            //如果已配置,請不要遵循SSL和非SSL之間的重定向。
            //如果協議不同(例如一個是HTTP一個是https)並且配置了不允許http和https只見重定向,則返回null
            boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
            if (!sameScheme && !client.followSslRedirects()) return null;
            // Most redirects don't include a request body.
            //大多數重定向不包含請求正文。
            Request.Builder requestBuilder = userResponse.request().newBuilder();
            if (HttpMethod.permitsRequestBody(method)) {
                final boolean maintainBody = HttpMethod.redirectsWithBody(method);
                if (HttpMethod.redirectsToGet(method)) {
                    requestBuilder.method("GET", null);
                } else {
                    RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
                    requestBuilder.method(method, requestBody);
                }
                if (!maintainBody) {
                    requestBuilder.removeHeader("Transfer-Encoding");
                    requestBuilder.removeHeader("Content-Length");
                    requestBuilder.removeHeader("Content-Type");
                }
            }
            // When redirecting across hosts, drop all authentication headers. This
            // is potentially annoying to the application layer since they have no
            // way to retain them.
            //跨主機重定向時,請刪除所有身份驗證標頭。這可能會困擾應用程序層,因爲它們無法保留它們。
            if (!sameConnection(userResponse, url)) {
                requestBuilder.removeHeader("Authorization");
            }
            return requestBuilder.url(url).build();
        case HTTP_CLIENT_TIMEOUT:
            //408:請求超時,服務器等待客戶端發送的請求時間過長,超時
            // 408's are rare in practice, but some servers like HAProxy use this response code. The
            // spec says that we may repeat the request without modifications. Modern browsers also
            // repeat the request (even non-idempotent ones.)
            //408在實踐中很少見,但某些服務器(例如HAProxy)使用此響應代碼。規範說,我們可以重複請求而不做任何修改。現代瀏覽器還會重複請求(甚至是非冪等的請求)。
            if (!client.retryOnConnectionFailure()) {
                //應用層配置了不允許連接失敗重試
                // The application layer has directed us not to retry the request.
                return null;
            }
            if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
                return null;
            }
            if (userResponse.priorResponse() != null
                    && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
                // We attempted to retry and got another timeout. Give up.
                //我們嘗試重試並再次超時。放棄。
                return null;
            }
            if (retryAfter(userResponse, 0) > 0) {
                return null;
            }
            return userResponse.request();
        case HTTP_UNAVAILABLE:
            //503:暫停服務,由於超載或系統維護,服務器暫時的無法處理客戶端的請求,延時的長度可包含在服務器的Retry-After頭信息中
            if (userResponse.priorResponse() != null
                    && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
                // We attempted to retry and got another timeout. Give up.
                //我們嘗試重試並再次超時。放棄
                return null;
            }
            if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
                // specifically received an instruction to retry without delay
                //特別收到立即重試的指令
                return userResponse.request();
            }
            return null;
        default:
            return null;
    }
}

recover用於判斷異常請求是否可以恢復

/**
 * Report and attempt to recover from a failure to communicate with a server. Returns true if
 * {@code e} is recoverable, or false if the failure is permanent. Requests with a body can only
 * be recovered if the body is buffered or if the failure occurred before the request has been
 * sent.
 * 報告並嘗試從與服務器通信的故障中恢復。如果{@code e}是可恢復的,則返回true;如果失敗是永久的,則返回false。僅在緩衝正文或在發送請求之前發生故障時,才能恢復帶有正文的請求。
 */
private boolean recover(IOException e, StreamAllocation streamAllocation,
                        boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);
    // The application layer has forbidden retries.
    //客戶端配置連接失敗不允許失敗重試,不再恢復
    if (!client.retryOnConnectionFailure()) return false;
    // We can't send the request body again.
    //我們無法再次發送請求正文
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;
    // This exception is fatal.
    //此異常是致命的
    if (!isRecoverable(e, requestSendStarted)) return false;
    // No more routes to attempt.
    //沒有其他路由地址了,恢復失敗
    if (!streamAllocation.hasMoreRoutes()) return false;
    // For failure recovery, use the same route selector with a new connection.
    return true;
}

接下來是BridgeInterceptor

BridgeInterceptor

橋接攔截器

  • 主要功能是把便於用戶識別和設置的請求信息轉換成網絡標準信息,網絡返回結果後,再把網絡返回的信息轉變成用戶便於識別的應用信息。
@Override
public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    //請求重新構造化
    Request.Builder requestBuilder = userRequest.newBuilder();
    RequestBody body = userRequest.body();
    if (body != null) {
        MediaType contentType = body.contentType();
        if (contentType != null) {
            requestBuilder.header("Content-Type", contentType.toString());
        }
        long contentLength = body.contentLength();
        //contentLength有值則加入"Content-Length"的Header,移除"Transfer-Encoding"
        //"Transfer-Encoding:chunked"用於當響應發起時,內容長度還沒能確定的情況下,和 Content-Length 不同時使用。
        if (contentLength != -1) {
            requestBuilder.header("Content-Length", Long.toString(contentLength));
            requestBuilder.removeHeader("Transfer-Encoding");
        } else {
            requestBuilder.header("Transfer-Encoding", "chunked");
            requestBuilder.removeHeader("Content-Length");
        }
    }
    if (userRequest.header("Host") == null) {
        requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }
    if (userRequest.header("Connection") == null) {
        //默認保持長連接
        requestBuilder.header("Connection", "Keep-Alive");
    }
    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing the transfer stream.
    // 如果我們添加“ Accept-Encoding:gzip”標頭字段,我們還將負責解壓縮傳輸流。
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
        transparentGzip = true;
        requestBuilder.header("Accept-Encoding", "gzip");
    }
    List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
        requestBuilder.header("Cookie", cookieHeader(cookies));
    }
    if (userRequest.header("User-Agent") == null) {
        requestBuilder.header("User-Agent", Version.userAgent());
    }
    //當前攔截器處理請求完畢交個下一個攔截器處理
    // 而此次處理會找到下一個攔截器,並用生成下一個Chain,這個chain構建會指名此次處理使用那個攔截器,並調用下一個攔截器扥intercept方法,依次循環
    Response networkResponse = chain.proceed(requestBuilder.build());
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
    Response.Builder responseBuilder = networkResponse.newBuilder()
            .request(userRequest);
    if (transparentGzip
            && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
            && HttpHeaders.hasBody(networkResponse)) {
        GzipSource responseBody = new GzipSource(networkResponse.body().source());
        Headers strippedHeaders = networkResponse.headers().newBuilder()
                .removeAll("Content-Encoding")
                .removeAll("Content-Length")
                .build();
        responseBuilder.headers(strippedHeaders);
        String contentType = networkResponse.header("Content-Type");
        responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
    return responseBuilder.build();
}

接下來是CacheInterceptor

CacheInterceptor

服務來自緩存的請求並將響應寫入緩存

@Override
public Response intercept(Chain chain) throws IOException {
    //緩存候選
    Response cacheCandidate = cache != null
            ? cache.get(chain.request())
            : null;
    long now = System.currentTimeMillis();
    //根據時間、請求信息、緩存的響應生成一個緩存策略
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    if (cache != null) {
        cache.trackResponse(strategy);
    }
    //策略篩選後沒有緩存響應
    if (cacheCandidate != null && cacheResponse == null) {
        //候選緩存不適用。關閉它。
        closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
    // If we're forbidden from using the network and the cache is insufficient, fail.
    //如果我們被禁止使用網絡並且緩存不足,請失敗。
    //策略篩選後既不能請求也沒有響應,返回請求失敗的響應
    if (networkRequest == null && cacheResponse == null) {
        return new Response.Builder()
                .request(chain.request())
                .protocol(Protocol.HTTP_1_1)
                .code(504)
                .message("Unsatisfiable Request (only-if-cached)")
                .body(Util.EMPTY_RESPONSE)
                .sentRequestAtMillis(-1L)
                .receivedResponseAtMillis(System.currentTimeMillis())
                .build();
    }
    // If we don't need the network, we're done.
    //如果我們不需要網絡,那就完成了。
    if (networkRequest == null) {
        return cacheResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .build();
    }
    Response networkResponse = null;
    try {
        networkResponse = chain.proceed(networkRequest);
    } finally {
        // If we're crashing on I/O or otherwise, don't leak the cache body.
        //如果我們在 I/O或其他方面崩潰,請不要泄漏高速緩存主體。
        if (networkResponse == null && cacheCandidate != null) {
            closeQuietly(cacheCandidate.body());
        }
    }
    // If we have a cache response too, then we're doing a conditional get.
    //如果我們也有一個緩存響應,那麼我們正在做一個有條件的獲取。
    if (cacheResponse != null) {
        if (networkResponse.code() == HTTP_NOT_MODIFIED) {
            Response response = cacheResponse.newBuilder()
                    .headers(combine(cacheResponse.headers(), networkResponse.headers()))
                    .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
                    .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
                    .cacheResponse(stripBody(cacheResponse))
                    .networkResponse(stripBody(networkResponse))
                    .build();
            networkResponse.body().close();
            // Update the cache after combining headers but before stripping the
            // Content-Encoding header (as performed by initContentStream()).
            //在合併標頭之後但在剝離Content-Encoding標頭之前(由initContentStream()執行),更新緩存。
            cache.trackConditionalCacheHit();
            cache.update(cacheResponse, response);
            return response;
        } else {
            closeQuietly(cacheResponse.body());
        }
    }
    //networkResponse不爲空cacheResponse爲空,返回這次網絡請求的response
    Response response = networkResponse.newBuilder()
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
    if (cache != null) {
        if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
            // Offer this request to the cache.
            //將此請求提供給緩存
            CacheRequest cacheRequest = cache.put(response);
            //生成RealResponseBody
            return cacheWritingResponse(cacheRequest, response);
        }
        if (HttpMethod.invalidatesCache(networkRequest.method())) {
            try {
                cache.remove(networkRequest);
            } catch (IOException ignored) {
                // The cache cannot be written.
                //無法寫入緩存
            }
        }
    }
    return response;
}

我們來看CacheStrategy.get()

/**
 * Returns a strategy to satisfy {@code request} using the a cached response {@code response}.
 * 使用緩存的響應{@code response}返回滿足{@code request}的策略。
 */
public CacheStrategy get() {
    CacheStrategy candidate = getCandidate();
    if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        //我們被禁止使用網絡,並且緩存不足。
        return new CacheStrategy(null, null);
    }
    return candidate;
/**
 * Returns a strategy to use assuming the request can use the network.
 * 假設請求可以使用網絡,則返回要使用的策略。
 */
private CacheStrategy getCandidate() {
    // No cached response.
    if (cacheResponse == null) {
        return new CacheStrategy(request, null);
    }
    // Drop the cached response if it's missing a required handshake.
    //如果缺少必需的握手,則刪除緩存的響應
    if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
    }
    // If this response shouldn't have been stored, it should never be used
    // as a response source. This check should be redundant as long as the
    // persistence store is well-behaved and the rules are constant.
    //如果不應存儲此響應,則永遠不應將其用作響應源。只要持久性存儲行爲良好且規則是恆定的,則此檢查應該是多餘的。
    if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
    }
    CacheControl requestCaching = request.cacheControl();
    if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
    }
    CacheControl responseCaching = cacheResponse.cacheControl();
    //響應緩存是一成不變的
    if (responseCaching.immutable()) {
        return new CacheStrategy(null, cacheResponse);
    }
    long ageMillis = cacheResponseAge();
    long freshMillis = computeFreshnessLifetime();
    if (requestCaching.maxAgeSeconds() != -1) {
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
    }
    long minFreshMillis = 0;
    if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
    }
    //最大過時的毫秒值
    long maxStaleMillis = 0;
    if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
    }
    if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
            builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
            builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
    }
    // Find a condition to add to the request. If the condition is satisfied, the response body
    // will not be transmitted.
    //查找要添加到請求中的條件。如果滿足條件,將不會發送響應主體。
    String conditionName;
    String conditionValue;
    if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
    } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
    } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
    } else {
        // No condition! Make a regular request.沒有條件!提出定期要求
        return new CacheStrategy(request, null);
    }
    Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
    Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
    Request conditionalRequest = request.newBuilder()
            .headers(conditionalRequestHeaders.build())
            .build();
    return new CacheStrategy(conditionalRequest, cacheResponse);
}

接下來是ConnectInterceptor

ConnectInterceptor

打開與目標服務器的連接,然後進入下一個攔截器。

@Override
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    //我們需要網絡來滿足此要求。可能用於驗證條件GET
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //http編碼解碼器
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

重要內容都在streamAllocation.newStream方法裏

public HttpCodec newStream(
        OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();
    try {
        //找到可用連接
        RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
                writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
        //創建http編解碼器
        HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
        synchronized (connectionPool) {
            codec = resultCodec;
            return resultCodec;
        }
    } catch (IOException e) {
        throw new RouteException(e);
    }

findHealthyConnection方法

/**
 * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
 * until a healthy connection is found.
 * 查找連接,如果連接狀況良好,則將其返回。如果不健康,請重複此過程,直到找到健康的連接爲止。
 */
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
                                             int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
                                             boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
        //獲取一個連接
        RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
                pingIntervalMillis, connectionRetryEnabled);
        // If this is a brand new connection, we can skip the extensive health checks.
        //如果這是一個全新的連接,我們可以跳過廣泛的運行狀況檢查。
        synchronized (connectionPool) {
            if (candidate.successCount == 0) {
                return candidate;
            }
        }
        // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
        // isn't, take it out of the pool and start again.
        //進行(可能很慢)檢查以確認池中連接仍然良好。如果不是,請將其從池中取出並重新開始。
        //檢查這個連接是不是不健康的,不健康的則禁止在承載此分配的連接上創建新的流並關閉
        if (!candidate.isHealthy(doExtensiveHealthChecks)) {
            noNewStreams();
            continue;
        }
        return candidate;
    }
}

findConnection方法

/**
 * Returns a connection to host a new stream. This prefers the existing connection if it exists,
 * then the pool, finally building a new connection.
 * 返回用於託管新流的連接。如果存在現有連接,則首選現有連接,然後是從池中選擇,最後建立一個新連接。
 */
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
                                      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean foundPooledConnection = false;
    RealConnection result = null;
    Route selectedRoute = null;
    Connection releasedConnection;
    Socket toClose;
    synchronized (connectionPool) {
        if (released) throw new IllegalStateException("released");
        if (codec != null) throw new IllegalStateException("codec != null");
        if (canceled) throw new IOException("Canceled");
        // Attempt to use an already-allocated connection. We need to be careful here because our
        // already-allocated connection may have been restricted from creating new streams.
        //嘗試使用已分配的連接。我們在這裏需要小心,因爲我們已經分配的連接可能受到限制,無法創建新的流。
        releasedConnection = this.connection;
        toClose = releaseIfNoNewStreams();
        if (this.connection != null) {
            // We had an already-allocated connection and it's good.
            //我們已經分配了一個連接,很好
            result = this.connection;
            releasedConnection = null;
        }
        if (!reportedAcquired) {
            // If the connection was never reported acquired, don't report it as released!
            //如果從未報告該連接已獲得,請不要將其報告爲已釋放!
            releasedConnection = null;
        }
        if (result == null) {
            //根據已知的address在connectionPool裏面找,如果有連接,則返回
            // Attempt to get a connection from the pool.
            //嘗試從池中獲取連接。
            Internal.instance.get(connectionPool, address, this, null);
            if (connection != null) {
                foundPooledConnection = true;
                result = connection;
            } else {
                selectedRoute = route;
            }
        }
    }
    closeQuietly(toClose);
    if (releasedConnection != null) {
        eventListener.connectionReleased(call, releasedConnection);
    }
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
    }
    if (result != null) {
        // If we found an already-allocated or pooled connection, we're done.
        //如果我們發現一個已經分配或連接的連接,就完成了。
        return result;
    }
    //更換路由,更換線路,在connectionPool裏面再次查找,如果有則返回。
    // If we need a route selection, make one. This is a blocking operation.
    //如果需要路線選擇,請選擇一個。這是一個阻塞操作。
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
        newRouteSelection = true;
        routeSelection = routeSelector.next();
    }
    synchronized (connectionPool) {
        if (canceled) throw new IOException("Canceled");
        if (newRouteSelection) {
            // Now that we have a set of IP addresses, make another attempt at getting a connection from the pool. This could match due to connection coalescin
            // 現在我們有了一組IP地址,請再次嘗試從池中獲取連接。由於連接合並,這可能匹配。
            List<Route> routes = routeSelection.getAll();
            for (int i = 0, size = routes.size(); i < size; i++) {
                Route route = routes.get(i);
                //嘗試從池中獲取連接。
                Internal.instance.get(connectionPool, address, this, route);
                if (connection != null) {
                    foundPooledConnection = true;
                    result = connection;
                    this.route = route;
                    break;
                }
            }
        }
        if (!foundPooledConnection) {
            if (selectedRoute == null) {
                selectedRoute = routeSelection.next();
            }
            //*如果以上條件都不滿足則直接new一個RealConnection出來
            // Create a connection and assign it to this allocation immediately. This makes it possible
            // for an asynchronous cancel() to interrupt the handshake we're about to do.
            //創建一個連接,並將其立即分配給該分配。這使得異步cancel()可以中斷我們將要進行的握手。
            route = selectedRoute;
            refusedStreamCount = 0;
            result = new RealConnection(connectionPool, selectedRoute);
            //*新建的RealConnection通過acquire關聯到connection.allocations上
            acquire(result, false);
        }
    }
    // If we found a pooled connection on the 2nd time around, we're done.
    //如果我們在第二次發現池化連接,就完成了。
    if (foundPooledConnection) {
        eventListener.connectionAcquired(call, result);
        return result;
    }
    // Do TCP + TLS handshakes. This is a blocking operation.
    //執行TCP + TLS握手。這是一個阻塞操作。
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
            connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());
    ////做去重判斷,如果有重複的socket則關閉
    Socket socket = null;
    synchronized (connectionPool) {
        reportedAcquired = true;
        // Pool the connection.
        //彙集連接。
        Internal.instance.put(connectionPool, result);
        // If another multiplexed connection to the same address was created concurrently, then
        // release this connection and acquire that one.
        //如果同時創建了另一個到同一地址的多路複用連接,則釋放該連接並獲取該連接
        if (result.isMultiplexed()) {
            socket = Internal.instance.deduplicate(connectionPool, address, this);
            result = connection;
        }
    }
    closeQuietly(socket);
    eventListener.connectionAcquired(call, result);
    return result;
}

最後是CallServerInterceptor

CallServerInterceptor

這是鏈中的最後一個攔截器。它對服務器進行網絡呼叫

  • 實現鏈接建立後讀寫請求和響應內容的功能
@Override
public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
    long sentRequestMillis = System.currentTimeMillis();
    realChain.eventListener().requestHeadersStart(realChain.call());
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);
    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
        // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
        // Continue" response before transmitting the request body. If we don't get that, return
        // what we did get (such as a 4xx response) without ever transmitting the request body.
        //如果請求上有一個“期望:100-繼續”標頭,則在發送請求正文之前,等待“ HTTP / 1.1 100繼續”響應。
        // 如果沒有得到,請返回我們得到的結果(例如4xx響應),而無需傳輸請求主體。
            if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            httpCodec.flushRequest();
            realChain.eventListener().responseHeadersStart(realChain.call());
            //讀取響應頭
            responseBuilder = httpCodec.readResponseHeaders(true);
        }
        if (responseBuilder == null) {
            // Write the request body if the "Expect: 100-continue" expectation was met.
            //如果滿足“Expect: 100-continue”的期望,請寫請求體。
            realChain.eventListener().requestBodyStart(realChain.call());
            long contentLength = request.body().contentLength();
            CountingSink requestBodyOut =
                    new CountingSink(httpCodec.createRequestBody(request, contentLength));
            BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
            request.body().writeTo(bufferedRequestBody);
            bufferedRequestBody.close();
            realChain.eventListener()
                    .requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
        } else if (!connection.isMultiplexed()) {
            // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
            // from being reused. Otherwise we're still obligated to transmit the request body to
            // leave the connection in a consistent state.
            //如果未達到“Expect: 100-continue”的期望,請防止HTTP / 1連接被重用。否則,我們仍然有義務傳輸請求主體,以使連接保持一致狀態
            streamAllocation.noNewStreams();
        }
    }
    httpCodec.finishRequest();
    if (responseBuilder == null) {
        realChain.eventListener().responseHeadersStart(realChain.call());
        //讀取響應頭
        responseBuilder = httpCodec.readResponseHeaders(false);
    }
    Response response = responseBuilder
            .request(request)
            .handshake(streamAllocation.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    int code = response.code();
    if (code == 100) {
        // server sent a 100-continue even though we did not request one.
        // try again to read the actual response
        //即使我們沒有請求,服務器仍發送了100-continu。再試一次以讀取實際響應
        responseBuilder = httpCodec.readResponseHeaders(false);
        response = responseBuilder
                .request(request)
                .handshake(streamAllocation.connection().handshake())
                .sentRequestAtMillis(sentRequestMillis)
                .receivedResponseAtMillis(System.currentTimeMillis())
                .build();
        code = response.code();
    }
    realChain.eventListener()
            .responseHeadersEnd(realChain.call(), response);
    if (forWebSocket && code == 101) {
        // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
        //連接正在升級,但是我們需要確保攔截器看到非空的響應主體
        response = response.newBuilder()
                .body(Util.EMPTY_RESPONSE)
                .build();
    } else {
        response = response.newBuilder()
                .body(httpCodec.openResponseBody(response))
                .build();
    }
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
            || "close".equalsIgnoreCase(response.header("Connection"))) {
        streamAllocation.noNewStreams();
    }
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
        throw new ProtocolException(
                "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
    return response;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章