okhttp3源码解析(4)-连接与流 I

okhttp3源码解析(4)-连接与流 I

前言

上两篇文章我们讲解了okhttp的拦截器,虽然不是很深入,但是主要流程与内容大致时有个了解了,这篇文章来看下okhttp连接与流相关的内容。

okhttp3源码解析(3)-拦截器 II

概述

经过前面三篇文章,我认为啊,大概就剩下三个功能类了:StreamAllocation、HttpCodec、RealConnection。

StreamAllocation是连接、流、请求三者关联的工具,HttpCodec是对socket流的一个封装,RealConnection是连接的管理工具。下面我们来详细学习下!

StreamAllocation

根据前面对拦截器的分析,我们知道StreamAllocation是这三个类中第一个创建的,它创建在RetryAndFollowUpInterceptor中,但是在ConnectInterceptor中才用到,创建了httpCodec和connection两个对象。下面着重讲下它的核心方法。

newStream

newStream是用来创建HttpCodec的,它会获得Connection,路由选择,发起socket连接,生成httpCodec(流对象:提供的I/O操作完成网络通信)。

第一步,通过 findHealthyConnection() 获取可用的Connection
    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);
第二步,不停通过findConnection拿到RealConnection,并进行检查,直到可用
  /**
   * 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;
    }
  }
第三步,获取Connection,对旧的Connection验证,或从connectionPool里面获取新的
  /**
   * 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.
        // (1)已有旧的connection
        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) {
        // Attempt to get a connection from the pool.
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          // (2)当前连接池里有connection
          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;
    }

英文注释写得非常清楚,不多解释了。这里注意Internal.instance.get方法传入了connectionPool、address以及当前StreamAllocation(this)。

Internal的实例是OkHttpClient,里面会从connectionPool取出connection,最终调用this的acquire方法修改this的connection和route,也就是说这里并不是通过返回的形式拿到返回值,而是传入了this,在里面修改this的成员属性,有利有弊吧。

第四步,根据路由选择(指代理发送数据路径的选择: DNS返回多个IP)情况复用或者创建connection
    // If we need a route selection, make one. This is a blocking operation.
    boolean newRouteSelection = false;
    // 注意这个条件,没有selectedRoute且没有routeSelection(未初始化)、没有selectedRoute且当前routeSelection没有Route了
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      // 切换routeSelection,routeSelector.next()会获得合适的路由集合(Selection)用于连接目标服务器
      routeSelection = routeSelector.next();
    }

    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      // routeSelection换了,在其所有路线(Route)的connectionPool里找connection
      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 coalescing.
        List<Route> routes = routeSelection.getAll();
        for (int i = 0, size = routes.size(); i < size; i++) {
          Route route = routes.get(i);
          
          // Internal的实例是OkHttpClient,里面会从connectionPool取出connection
          // 最终调用this的acquire方法修改this的connection和route
          Internal.instance.get(connectionPool, address, this, route);
          if (connection != null) {
            foundPooledConnection = true;
            // (3)不同路由选择路线里面的connection
            result = connection;
            this.route = route;
            break;
          }
        }
      }// 结束newRouteSelection块

      // 旧的routeSelection没拿到、新的routeSelection(新建了的话)也没有拿到可复用的connection,就需要新创建了
      if (!foundPooledConnection) {
      
        // 没有选中Route,在当前routeSelection里面取一个,注意这个next和上面routeSelector的不一样
        if (selectedRoute == null) {
          selectedRoute = routeSelection.next();
        }

        // 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.
        route = selectedRoute;
        refusedStreamCount = 0;
        
        // (4)没找到可以复用的connection,新创建RealConnection并保存到connectionPool去
        result = new RealConnection(connectionPool, selectedRoute);
        // 让当前实例持有connection
        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;
    }

这里又看得懵逼了,得先理解下RouteSelector、RouteSelector.Selection、Route几者的情况,这里我也说不太清楚,可以看下下面文章:
Okhttp之RouteSelector简单解析
Android | 彻底理解 OkHttp 代理与路由

我的理解就是访问一个url,其域名经过DNS解析后会有多个路由集合(Selection),使用RouteSelector的next方法获取。每个Selection内部有一个路由(Route)列表和下一个路由的索引,调用next会获得这个索引的路由。

在上一步result没有从上面的连接池拿到,仅仅说明当前Selection的当前Route没有该url的connection,可能其他Selection中还有connection缓存,实在不行再创建。

第五步,使用connection发起socket连接,保存到connectionPool,如果连接地址相同进行复用
    // Do TCP + TLS handshakes. This is a blocking operation.
    // 调用RealConnection的connect发起连接
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
    routeDatabase().connected(result.route());

    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.
      // 防止异步创建了两个同地址的connection,关闭当前connection,取出socket并关闭其流
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    eventListener.connectionAcquired(call, result);
    return result;
  }

上一步经过四个地方终于获取到了result(即connection),这里就是发起了下连接,并将connection存到connectionPool,前面只是用acquire让当前实例持有,最后还验证了下异步是否创建了两个connection。

第六步,根据获得的Connection(已连接server)创建Codec
      HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }

到这里newStream就讲解完了,这也是StreamAllocation最重要的方法,其他方法就没这么复杂了。

codec方法

  public HttpCodec codec() {
    synchronized (connectionPool) {
      return codec;
    }
  }

前面讲到了newStream会返回RealConnection对象,这里codec方法也会返回HttpCodec对象。这里的方法只是返回了当前对象的codec属性,这个值就是上面第六步的resultCodec。

noNewStreams方法

  /** Forbid new streams from being created on the connection that hosts this allocation. */
  public void noNewStreams() {
    Socket socket;
    Connection releasedConnection;
    synchronized (connectionPool) {
      releasedConnection = connection;
      socket = deallocate(true, false, false);
      if (connection != null) releasedConnection = null;
    }
    closeQuietly(socket);
    if (releasedConnection != null) {
      eventListener.connectionReleased(call, releasedConnection);
    }
  }

noNewStreams方法在前面分析中好像出现了很多次,不过没怎么讲,现在来看下。看下方法注释,禁止在此connection创建streams,再看下此类开头相关描述:

prevents the connection from being used for new streams in the future.
Use this after a Connection: close header, or when the connection may be inconsistent.

大致意思就是防止创建新的streams,一是可能连接不一致了,二是已经读取完header了,header都读没人创建新的streams(HttpCodec)并没什么用了吧。

release方法

  public void release() {
    Socket socket;
    Connection releasedConnection;
    synchronized (connectionPool) {
      releasedConnection = connection;
      socket = deallocate(false, true, false);
      if (connection != null) releasedConnection = null;
    }
    closeQuietly(socket);
    if (releasedConnection != null) {
      Internal.instance.timeoutExit(call, null);
      eventListener.connectionReleased(call, releasedConnection);
      eventListener.callEnd(call);
    }
  }

这里和上面差不多,大致看下该类相关描述:

removes the call’s hold on the connection. Note that this won’t immediately free the connection if there is a stream still lingering.
That happens when a call is complete but its response body has yet to be fully consumed.

HttpCodec

上面讲完了StreamAllocation的相关内容,这里我们先讲下HttpCodec,估计后面得再开一篇文章来讲RealConnection与ConnectionPool。

HttpCodec是一个接口,分Http1Codec和Http2Codec,还有点不一样,下面先来看Http1Codec。HttpCodec是在CallServerInterceptor使用到的,主要有四个地方,对请求、回复的header及body进行流的读写。

writeRequestHeaders

    @Override 
    public void writeRequestHeaders(Request request) throws IOException {
        // write第一步,读取请求头
        String requestLine = RequestLine.get(
                request, streamAllocation.connection().route().proxy().type());

        // write第二步,写入header到sink(输入流,在RealConnection中创建并传到HttpCodec中)
        writeRequest(request.headers(), requestLine);
    }

这里很简单,就是用RequestLine获取了第一行的请求信息(GET xxx.com HTTP/1.1),第二步将请求行和header全部写入到sink(socket输出流)里面,使用Utf8格式。

    /** Returns bytes of a request header for sending on an HTTP transport. */
    public void writeRequest(Headers headers, String requestLine) throws IOException {
        if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
        sink.writeUtf8(requestLine).writeUtf8("\r\n");
        for (int i = 0, size = headers.size(); i < size; i++) {
            sink.writeUtf8(headers.name(i))
                    .writeUtf8(": ")
                    .writeUtf8(headers.value(i))
                    .writeUtf8("\r\n");
        }
        sink.writeUtf8("\r\n");
        state = STATE_OPEN_REQUEST_BODY;
    }

这里还维护了一个state,用来控制流程的准确性。

createRequestBody

上面写了request的header部分,这里就是request的body部分了。

    @Override public Sink createRequestBody(Request request, long contentLength) {
        // 根据request创建一个输出流
        if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
            // Stream a request body of unknown length.
            return newChunkedSink();
        }

        if (contentLength != -1) {
            // Stream a request body of a known length.
            return newFixedLengthSink(contentLength);
        }

        throw new IllegalStateException(
                "Cannot stream a request body without chunked encoding or a known content length!");
    }

该方法就是创建了一个Sink对象,根据分段标准创建了不同的对象,newFixedLengthSink多了一个bytesRemaining来控制分段。其实这里的Sink的对象,就是对流的一个封装,更好控制罢了,唯一目的就是向流写入数据。

        @Override public void write(Buffer source, long byteCount) throws IOException {
            if (closed) throw new IllegalStateException("closed");
            if (byteCount == 0) return;

            sink.writeHexadecimalUnsignedLong(byteCount);
            sink.writeUtf8("\r\n");
            sink.write(source, byteCount);
            sink.writeUtf8("\r\n");
        }

        @Override public synchronized void flush() throws IOException {
            if (closed) return; // Don't throw; this stream might have been closed on the caller's behalf.
            sink.flush();
        }

        @Override public synchronized void close() throws IOException {
            if (closed) return;
            closed = true;
            sink.writeUtf8("0\r\n\r\n");
            detachTimeout(timeout);
            state = STATE_READ_RESPONSE_HEADERS;
        }

flushRequest

    @Override public void flushRequest() throws IOException {
        sink.flush();
    }

还记得前面说发送request吗?就是这里了,对sink(socket输出流)刷新就把数据发送出去了,接下来只要等着读取socket的返回流就行了。

readResponseHeaders

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
        if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
            throw new IllegalStateException("state: " + state);
        }

        try {
            // read第一步,从source流中读取header的字符串,并处理成statusLine
            StatusLine statusLine = StatusLine.parse(readHeaderLine());

            // read第二步,根据statusLine创建responseBuilder
            Response.Builder responseBuilder = new Response.Builder()
                    .protocol(statusLine.protocol)
                    .code(statusLine.code)
                    .message(statusLine.message)
                    .headers(readHeaders());

            // read第三步,根据CallServerInterceptor里面三个调用,得到不同结果
            if (expectContinue && statusLine.code == HTTP_CONTINUE) {
                return null;
            } else if (statusLine.code == HTTP_CONTINUE) {
                state = STATE_READ_RESPONSE_HEADERS;
                return responseBuilder;
            }

            state = STATE_OPEN_RESPONSE_BODY;
            return responseBuilder;
        } catch (EOFException e) {
            // Provide more context if the server ends the stream before sending a response.
            IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
            exception.initCause(e);
            throw exception;
        }
    }

这里其实和request类似,不过麻烦了一点,看下注释,我这里分了三点。需要注意下第三点,我们在CallServerInterceptor中传入了一个expectContinue,并且有三次对readResponseHeaders的调用。

第一个是client发送HTTP_CONTINUE的情况,传入expectContinue为true,得到一个null的Response.Builder,这样就等于读了一次只有header没有body的response,且把Response.Builder置空,就好像没发生这次读取一样,下面和正常逻辑一样执行。

第二个是正常读取response,第三个是client收到HTTP_CONTINUE的情况,这时候要多读取一次回复,以第二条为准,也就是说服务器再HTTP_CONTINUE情况下会连发两条回复。

openResponseBody

最后我们来看response的body是如何封装的:

@Override public ResponseBody openResponseBody(Response response) throws IOException {
        streamAllocation.eventListener.responseBodyStart(streamAllocation.call);
        String contentType = response.header("Content-Type");

        // 这里只是根据response的header创建不同的source(输入流),并创建不同的ResponseBody,
        // response包含响应头和响应体,这里的响应体是对原始httpCodec的source封装
        if (!HttpHeaders.hasBody(response)) {

            // !!注意AbstractSource是内部类,持有httpCodec的source,并从中读取内容到ResponseBody
            Source source = newFixedLengthSource(0);
            return new RealResponseBody(contentType, 0, Okio.buffer(source));
        }

        if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
            Source source = newChunkedSource(response.request().url());
            return new RealResponseBody(contentType, -1L, Okio.buffer(source));
        }

        long contentLength = HttpHeaders.contentLength(response);
        if (contentLength != -1) {
            Source source = newFixedLengthSource(contentLength);
            return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
        }

        return new RealResponseBody(contentType, -1L, Okio.buffer(newUnknownLengthSource()));
    }

这里先根据情况创建不同AbstractSource实现类,实际就是对长度的一个控制,FixedLengthSource是固定长度或无长度,ChunkedSource是对分段的处理,内部有一个bytesRemainingInChunk来控制剩余读取数据长度。

AbstractSource实现类中最重要的就是read了,最终会调用super.read方法,里面对对source(socket输入流)读取,进入到传入的sink,这里涉及到Okio,后面看看吧。

        @Override public long read(Buffer sink, long byteCount) throws IOException {
            try {
                long read = source.read(sink, byteCount);
                if (read > 0) {
                    bytesRead += read;
                }
                return read;
            } catch (IOException e) {
                endOfInput(false, e);
                throw e;
            }
        }

经过RealResponseBody的封装后就成了RealResponseBody的BufferedSource,这里如果我很要对ResponseBody读取还是要按流的形式读取的,并且得关闭流。

        long contentLength = HttpHeaders.contentLength(response);
        if (contentLength != -1) {
            Source source = newFixedLengthSource(contentLength);
            return new RealResponseBody(contentType, contentLength, Okio.buffer(source));
        }
public final class RealResponseBody extends ResponseBody {
  private final @Nullable String contentTypeString;
  private final long contentLength;
  private final BufferedSource source;

  public RealResponseBody(
      @Nullable String contentTypeString, long contentLength, BufferedSource source) {
    this.contentTypeString = contentTypeString;
    this.contentLength = contentLength;
    this.source = source;
  }

这里小结下,大致意思就是根据header信息(长度、分段)去读取socket输入流中得body,并封装到ResponseBody中得过程。

Http2Codec

Http2Codec和Http1Codec类似,我们注意上面五个方法就行,看了下好像还更简单些,不过多了个Http2Stream,有兴趣得读者可以看下。

结语

这里就写了下StreamAllocation和HttpCodec,没有特别深入但是也算大致功能都有了解了,下面一篇再来研究RealConnection和ConnectionPool。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Java中使用OkHttp3客户端连接到Flask-SocketIO服务器,您需要遵循以下步骤: 1. 在Java应用程序中,使用Maven或Gradle等构建工具将OkHttp3客户端库添加到您的项目中。例如,可以使用以下Maven依赖项: ```xml <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.9.1</version> </dependency> ``` 2. 在Java应用程序中,使用以下代码创建OkHttp3客户端实例并建立与Flask-SocketIO服务器的连接: ```java import okhttp3.*; import org.jetbrains.annotations.NotNull; import java.io.IOException; public class SocketIOClient { private final OkHttpClient client; private final Request request; public SocketIOClient() { this.client = new OkHttpClient.Builder().build(); this.request = new Request.Builder().url("http://localhost:5000/socket.io/").build(); } public void connect() { WebSocketListener listener = new WebSocketListener() { @Override public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) { System.out.println("Connected to server: " + response.message()); } @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { System.out.println("Received message: " + text); } @Override public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) { System.out.println("Disconnected from server: " + reason); } @Override public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, Response response) { System.err.println("Error: " + t.getMessage()); } }; WebSocket ws = client.newWebSocket(request, listener); } } ``` 在上面的代码中,您需要将“http://localhost:5000”替换为您的Flask-SocketIO服务器的URL和端口。 3. 现在,您可以使用WebSocket实例发送和接收消息。例如,以下代码将向服务器发送“hello”消息并处理服务器发送的回复消息: ```java ws.send("hello"); ws.send("hello", new WebSocketListener() { @Override public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { System.out.println("Received reply: " + text); } }); ``` 在上面的代码中,服务器将使用“message”事件发送回复消息。您可以使用“onMessage”方法侦听此事件并处理消息。 希望这可以帮助您使用OkHttp3客户端连接到Flask-SocketIO服务器!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值