okhttp3源码解析(5)-RealConnection、Http2Connection

OkHttp-ConnectInterceptor源码解析 ConnectInterceptor源码解析 本文基于okhttp3.10.0 1. 概述 ConnectInterceptor主要是用于建立连接,并再连接成功后将流封装成对象传递给下一个拦截器CallServerInterceptor与远端进行读写操作。 这个过程中会涉及比较多类我们简述下每个类的作用 StreamAllocation:类似一个工厂用来创建连接RealConnection... 阅读详情

okhttp3源码解析(5)-RealConnection、Http2Connection

前言

上一篇文章我们讲了StreamAllocation和HttpCodec的内容,本来想一篇文章讲完连接与流的,但是篇幅有点长了,多分几篇吧。我这看了下RealConnection,感觉没看得多懂,内容也有点多,HTTPS以及HTTP2相关的东西我不是很明白,学得也迷迷糊糊,下面只算是我的理解了,如有问题可以评论区指出,大家一起进步!

这篇文章还是主要讲RealConnection,然后由RealConnection到Http2Connection,再讲部分Http2Stream,不算深入,也就是有个大致了解吧。

okhttp3源码解析(4)-StreamAllocation、HttpCodec

RealConnection

RealConnection在ConnectInterceptor中由StreamAllocation创建,在上一篇文章中StreamAllocation的newStream部分,我们讲到了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.
        route = selectedRoute;
        refusedStreamCount = 0;
        
        // (4)没找到可以复用的connection,新创建RealConnection并保存到connectionPool去
        result = new RealConnection(connectionPool, selectedRoute);
        // 让当前实例持有connection
        acquire(result, false);

创建的时候传入了两个参数:connectionPool以及selectedRoute,newStream部分的逻辑比较复杂,这里不在详细说,这里知道它的创建地方就可以了。

RealConnection使用的地方也在StreamAllocation中,这里调用了它的connect方法,其他地方就没用到了。

    // Do TCP + TLS handshakes. This is a blocking operation.
    // 调用RealConnection的connect发起连接
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);

第一步,验证route参数

        RouteException routeException = null;
        List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
        ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

        // sslSocketFactory: null 的时候不是 HTTPS address
        if (route.address().sslSocketFactory() == null) {
            // CLEARTEXT: 不加密、不认证的http url
            if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
                throw new RouteException(new UnknownServiceException(
                        "CLEARTEXT communication not enabled for client"));
            }
            String host = route.address().url().host();
            // 是否允许CLEARTEXT
            if (!Platform.get().isCleartextTrafficPermitted(host)) {
                throw new RouteException(new UnknownServiceException(
                        "CLEARTEXT communication to " + host + " not permitted by network security policy"));
            }
        } else {
            // HTTPS链接:CLEARTEXT需要H2_PRIOR_KNOWLEDGE,但HTTPS不需要这个
            if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
                throw new RouteException(new UnknownServiceException(
                        "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
            }
        }

第二步,在循环中进行连接

        while (true) {
            try {
                // 是否需要隧道: Returns true if this route tunnels HTTPS through an HTTP proxy
                // 如果此路由通过 HTTP 代理隧道传输 HTTPS,则返回 true
                if (route.requiresTunnel()) {
                    // 第三步,对于HTTPS连接创建隧道
                    connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
                    if (rawSocket == null) {
                        // We were unable to connect the tunnel but properly closed down our resources.
                        break;
                    }
                } else {
                    // 第四步,根据route信息创建socket连接,获得输入输出流
                    connectSocket(connectTimeout, readTimeout, call, eventListener);
                }
                // 第五步,根据协议进行连接,HTTP直接返回,HTTPS使用http2Connection去处理,完全HTTPS还有Tls
                establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
                eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
                break;
            } catch (IOException e) {
                // 其他代码...
            }
            
            // 其他代码...

第三步,对于HTTPS连接创建隧道

private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
                               EventListener eventListener) throws IOException {
        // 创建一个request
        Request tunnelRequest = createTunnelRequest();
        HttpUrl url = tunnelRequest.url();
        // 是做MAX_TUNNEL_ATTEMPTS次尝试创建Tunnel吗?
        for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
            // 根据route信息创建socket连接,获得输入输出流
            connectSocket(connectTimeout, readTimeout, call, eventListener);
            // 通过HTTP代理创建HTTPS连接,发送一次请求,正常情况返回null
            tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

            // 正常情况(HTTP_OK)返回null
            if (tunnelRequest == null) break; // Tunnel successfully created.

            // The proxy decided to close the connection after an auth challenge. We need to create a new
            // connection, but this time with the auth credentials.
            closeQuietly(rawSocket);
            rawSocket = null;
            sink = null;
            source = null;
            eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
        }
    }

下面是创建tunnelRequest,会为了为了支持先发的认证(preemptive authentication),对proxyConnectRequest进行一层包装。

private Request createTunnelRequest() throws IOException {
        // 对代理服务器的request(TLS tunnel),request是未加密的,不要发敏感数据(cookies)
        Request proxyConnectRequest = new Request.Builder()
                .url(route.address().url())
                .method("CONNECT", null)
                .header("Host", Util.hostHeader(route.address().url(), true))
                .header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
                .header("User-Agent", Version.userAgent())
                .build();

        // 为了支持先发的认证(preemptive authentication),创建一个假的“Auth Failed” response给authenticator
        Response fakeAuthChallengeResponse = new Response.Builder()
                .request(proxyConnectRequest)
                .protocol(Protocol.HTTP_1_1)
                .code(HttpURLConnection.HTTP_PROXY_AUTH)
                .message("Preemptive Authenticate")
                .body(Util.EMPTY_RESPONSE)
                .sentRequestAtMillis(-1L)
                .receivedResponseAtMillis(-1L)
                .header("Proxy-Authenticate", "OkHttp-Preemptive")
                .build();

        // authenticatedRequest=null 则不需要先发的认证
        Request authenticatedRequest = route.address().proxyAuthenticator()
                .authenticate(route, fakeAuthChallengeResponse);

        return authenticatedRequest != null
                ? authenticatedRequest
                : proxyConnectRequest;
    }

connectSocket部分见下一步,下面看createTunnel做了什么,这里就是对TLS做验证吧,会发送header之类的,正常的话返回位null。

private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
                                 HttpUrl url) throws IOException {
        // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
        String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
        while (true) {
            // Http1Codec用来操作socket输入输出流,createTunnel前面已经connectSocket了
            // 关键是Http1Codec吗?所以用的是Http,SSL放在Header内吗?
            Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
            source.timeout().timeout(readTimeout, MILLISECONDS);
            sink.timeout().timeout(writeTimeout, MILLISECONDS);

            // 向输出流写入header,finishRequest发送
            tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
            tunnelConnection.finishRequest();

            // 读取返回流的header
            Response response = tunnelConnection.readResponseHeaders(false)
                    .request(tunnelRequest)
                    .build();

            // The response body from a CONNECT should be empty, but if it is not then we should consume
            // it before proceeding.
            long contentLength = HttpHeaders.contentLength(response);
            if (contentLength == -1L) {
                contentLength = 0L;
            }
            // 这里要求response body为empty,如果不是就跳过内容(到达exhausted),或者deadline到达
            Source body = tunnelConnection.newFixedLengthSource(contentLength);
            Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
            body.close();

            switch (response.code()) {
                case HTTP_OK:
                    // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
                    // that happens, then we will have buffered bytes that are needed by the SSLSocket!
                    // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
                    // that it will almost certainly fail because the proxy has sent unexpected data.
                    if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
                        throw new IOException("TLS tunnel buffered too many bytes!");
                    }
                    return null;

                case HTTP_PROXY_AUTH:
                    // 代理需要认证认证,修改tunnelRequest进入下一个循环
                    tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
                    if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");

                    // response告知连接结束,直接返回tunnelRequest,上一步中会做关闭流等操作
                    if ("close".equalsIgnoreCase(response.header("Connection"))) {
                        return tunnelRequest;
                    }
                    break;

                default:
                    throw new IOException(
                            "Unexpected response code for CONNECT: " + response.code());
            }
        }
    }

到这里,就结束了connectTunnel方法,这里会先创建socket连接,然后做了一些数据传输(createTunnel)。

第四步,根据route信息创建socket连接,获得输入输出流

private void connectSocket(int connectTimeout, int readTimeout, Call call,
                               EventListener eventListener) throws IOException {
        Proxy proxy = route.proxy();
        Address address = route.address();

        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
                ? address.socketFactory().createSocket()
                : new Socket(proxy);

        eventListener.connectStart(call, route.socketAddress(), proxy);
        rawSocket.setSoTimeout(readTimeout);
        try {
            // 调用socket的connect
            Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
        } catch (ConnectException e) {
            ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
            ce.initCause(e);
            throw ce;
        }

        // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
        // More details:
        // https://github.com/square/okhttp/issues/3245
        // https://android-review.googlesource.com/#/c/271775/
        try {
            // 获得原始socket的输入输出流
            source = Okio.buffer(Okio.source(rawSocket));
            sink = Okio.buffer(Okio.sink(rawSocket));
        } catch (NullPointerException npe) {
            if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
                throw new IOException(npe);
            }
        }
    }

这里实际就是socket的实际连接了,到这里客户端和服务端应该就已经建立连接了,下面就是HTTP协议的内容了。

第五步,根据协议进行连接,HTTP直接返回,HTTPS使用http2Connection去处理

private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
                                   int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
        // 为null的时候为HTTP连接
        if (route.address().sslSocketFactory() == null) {
            // 通过HTTP运行的HTTPS
            if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
                socket = rawSocket;
                protocol = Protocol.H2_PRIOR_KNOWLEDGE;
                // 上面设置好socket、协议,用http2Connection去处理
                startHttp2(pingIntervalMillis);
                return;
            }

            // HTTP协议使用rawSocket就可以了,并且到这就连接结束了
            socket = rawSocket;
            protocol = Protocol.HTTP_1_1;
            return;
        }

        eventListener.secureConnectStart(call);
        // 进行TLS连接,sslSocket会代替原始socket,输入输出流会更新
        // HTTPS是运行在SSL\TLS协议之上的,HTTP2使用了SSL\TLS协议,实际就是HTTPS
        connectTls(connectionSpecSelector);
        eventListener.secureConnectEnd(call, handshake);

        if (protocol == Protocol.HTTP_2) {
            // 使用Http2Connection进行处理
            startHttp2(pingIntervalMillis);
        }
    }

这里有三种情况,一个是运行在HTTP上的HTTPS,一个正常的HTTP,一个正常的HTTPS。到这HTTP协议就结束了,可以使用原始socket进行输入输出流读写数据,使用Http1Codec处理。

剩下的HTTPS就到了startHttp2这个方法中:

private void startHttp2(int pingIntervalMillis) throws IOException {
        // HTTP/2 连接超时是按流设置的,指后面再设置吗?
        socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
        http2Connection = new Http2Connection.Builder(true)
                .socket(socket, route.address().url().host(), source, sink)
                // onStream和onSetting两个方法
                .listener(this)
                .pingIntervalMillis(pingIntervalMillis)
                .build();
        http2Connection.start();
    }

下面新开一个一个章节来学习Http2Connection,这个也超级复杂。。我也还挺懵的。

Http2Connection

这里看源码之前最好学习下HTTP2相关的内容,HTTP2就是HTTPS,HTTPS就是HTTP+TLS,至少要知道HTTP2是分帧的、多路复用、流的概念等。下面是我找的的一篇不错的文章:

半小时搞懂 HTTP、HTTPS和HTTP2

读完上面这篇文章,估计就能对HTTP2有个大致了解了,去读源码没那么懵逼了,下面是我理解的源码:

Http2Connection构建方法

Http2Connection(Builder builder) {
        pushObserver = builder.pushObserver;
        client = builder.client;
        listener = builder.listener;
        // http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-5.1.1

        // 这里做了说明,client用单数StreamId,server用偶数StreamId,并且1作为Upgrade帧?
        nextStreamId = builder.client ? 1 : 2;
        if (builder.client) {
            nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade.
        }

        // Flow control was designed more for servers, or proxies than edge clients.
        // If we are a client, set the flow control window to 16MiB.  This avoids
        // thrashing window updates every 64KiB, yet small enough to avoid blowing
        // up the heap.
        if (builder.client) {
            okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, OKHTTP_CLIENT_WINDOW_SIZE);
        }

        hostname = builder.hostname;

        writerExecutor = new ScheduledThreadPoolExecutor(1,
                Util.threadFactory(Util.format("OkHttp %s Writer", hostname), false));
        if (builder.pingIntervalMillis != 0) {
            writerExecutor.scheduleAtFixedRate(new Http2Connection1.PingRunnable(false, 0, 0),
                    builder.pingIntervalMillis, builder.pingIntervalMillis, MILLISECONDS);
        }

        // Like newSingleThreadExecutor, except lazy creates the thread.
        pushExecutor = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(),
                Util.threadFactory(Util.format("OkHttp %s Push Observer", hostname), true));
        peerSettings.set(Settings.INITIAL_WINDOW_SIZE, DEFAULT_INITIAL_WINDOW_SIZE);
        peerSettings.set(Settings.MAX_FRAME_SIZE, Http2.INITIAL_MAX_FRAME_SIZE);
        bytesLeftInWriteWindow = peerSettings.getInitialWindowSize();
        socket = builder.socket;
        writer = new Http2Writer(builder.sink, client);

        // 这里,Http2Writer和Http2Reader传入了socket的输入输出流
        readerRunnable = new Http2Connection1.ReaderRunnable(new Http2Reader(builder.source, client));
    }

上面的startHttp2方法通过Builder创建了Http2Connection,这个构造方法还是要瞧一下的,关键的地方我注释了。

client应该是一个标识位,也就是说当前TCP连接中,一端既可以作为peer也可以作为client,不同的角色选择不同奇偶性的nextStreamId。

start方法

    public void start() throws IOException {
        start(true);
    }
    
    void start(boolean sendConnectionPreface) throws IOException {
        if (sendConnectionPreface) {
            // 向输出流写入一些配置
            writer.connectionPreface();
            writer.settings(okHttpSettings);
            // stream的窗口值
            int windowSize = okHttpSettings.getInitialWindowSize();
            if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
                writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
            }
        }
        new Thread(readerRunnable).start(); // Not a daemon thread.
    }

RealConnection中调用了startHttp2方法,这里做了一些处理后,居然进入到了一个new Thread。。。okhttp也和我这个野生程序员一样写吗?

这个readerRunnable是在上面的构造方法里创建的,也就是说后面的功能都在readerRunnable处理了,下面我们看下这个runnable:

    class ReaderRunnable extends NamedRunnable implements Http2Reader.Handler {
        final Http2Reader reader;

        ReaderRunnable(Http2Reader reader) {
            super("OkHttp %s", hostname);
            this.reader = reader;
        }

        @Override protected void execute() {
            ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
            ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
            try {
                // server读取connectionPreface进行比较,client读取下一帧
                reader.readConnectionPreface(this);

                // 进入Http2Reader中,循环读取帧,通过回调在当前runnable中处理
                // HTTP/2 是基于帧的协议。采用分帧是为了将重要信息封装起来,让协议的解析方可以轻松阅读、解析并还原信息。
                // 有多种帧类型,每次处理不同的帧,回调到当前runnable中处理 //see: https://zhuanlan.zhihu.com/p/141458270
                while (reader.nextFrame(false, this)) {
                }
                // 循环体结束,连接结束
                connectionErrorCode = ErrorCode.NO_ERROR;
                streamErrorCode = ErrorCode.CANCEL;
            } catch (IOException e) {
                connectionErrorCode = ErrorCode.PROTOCOL_ERROR;
                streamErrorCode = ErrorCode.PROTOCOL_ERROR;
            } finally {
                try {
                    close(connectionErrorCode, streamErrorCode);
                } catch (IOException ignored) {
                }
                Util.closeQuietly(reader);
            }
        }
        
        // 其他代码。。。

这个NamedRunnable前面见过了,大致就是在run方法里调用execute方法,所以着重看execute方法。这里就是用Http2Reader去读取数据了,readConnectionPreface好像是HTTP/2连接前奏的连接前奏:

https://quafoo.gitbooks.io/http2-rfc7540-zh-cn-en/content/chapter3/section3.5.html

然后,就是最重要的部分了: nextFrame,点进去看下:

public boolean nextFrame(boolean requireSettings, Handler handler) throws IOException {
    try {
      source.require(9); // Frame header size
    } catch (IOException e) {
      return false; // This might be a normal socket close.
    }

    //  0                   1                   2                   3
    //  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    // |                 Length (24)                   |
    // +---------------+---------------+---------------+
    // |   Type (8)    |   Flags (8)   |
    // +-+-+-----------+---------------+-------------------------------+
    // |R|                 Stream Identifier (31)                      |
    // +=+=============================================================+
    // |                   Frame Payload (0...)                      ...
    // +---------------------------------------------------------------+
    int length = readMedium(source);
    if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
      throw ioException("FRAME_SIZE_ERROR: %s", length);
    }
    byte type = (byte) (source.readByte() & 0xff);
    if (requireSettings && type != TYPE_SETTINGS) {
      throw ioException("Expected a SETTINGS frame but was %s", type);
    }
    byte flags = (byte) (source.readByte() & 0xff);
    int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
    if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));

    switch (type) {
      case TYPE_DATA:
        readData(handler, length, flags, streamId);
        break;

      // 省略了一些情况。。。
      
      default:
        // Implementations MUST discard frames that have unknown or unsupported types.
        source.skip(length);
    }
    return true;
  }

这里的注释写得很漂亮,这里就是对HTTP2的帧进行读取,我们取TYPE_DATA看下调用链:

private void readData(Handler handler, int length, byte flags, int streamId)
      throws IOException {
    if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_DATA streamId == 0");

    // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED
    boolean inFinished = (flags & FLAG_END_STREAM) != 0;
    boolean gzipped = (flags & FLAG_COMPRESSED) != 0;
    if (gzipped) {
      throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA");
    }

    short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0;
    length = lengthWithoutPadding(length, flags, padding);

    handler.data(inFinished, streamId, source, length);
    source.skip(padding);
  }

标志位的读取我们忽略,最终到了handler.data方法,这个handler实际就是我们nextFrame(false, this)闯进去的Http2Connection,那就回到了Http2Connection的data方法:

    @Override public void data(boolean inFinished, int streamId, BufferedSource source, int length)
                throws IOException {
            // (推测)由于多路复用、乱序发送,所以提前读取的帧延时稍后处理?还是别的流的帧?
            // 好像是pushedStream是服务端,所以作为服务端要晚一点发送数据?等启动后台任务后发送吗?
            if (pushedStream(streamId)) {
                pushDataLater(streamId, source, length, inFinished);
                return;
            }
            // 根据streamId拿到对应的dataStream,每个HTTPS请求会在TCP连接上创建一个流
            Http2Stream dataStream = getStream(streamId);
            if (dataStream == null) {
                writeSynResetLater(streamId, ErrorCode.PROTOCOL_ERROR);
                updateConnectionFlowControl(length);
                source.skip(length);
                return;
            }
            // 将拿到的帧数据写到对应dataStream
            dataStream.receiveData(source, length);
            if (inFinished) {
                dataStream.receiveFin();
            }
        }

还记得上面的ReaderRunnable吗?他这里实现了Http2Reader.Handler这个接口,data等一系列的方法就是从这里面继承过来的,目的就是处理不同的HTTP2的帧。

class ReaderRunnable extends NamedRunnable implements Http2Reader.Handler {

这里有个pushedStream和pushDataLater我不是很懂,一开始以为是帧多路复用、乱序发送要排序呢,又看了下下面方法的注释,好像是说作为服务端的时候,先收到了数据,要延后处理,等启动后台任务再处理(我是这么理解的)

    boolean pushedStream(int streamId) {
        // 这就用到上面说的streamId奇偶性吧,这里(streamId & 1)验证是不是奇数,返回true:非0且为偶数,偶数是服务端
        return streamId != 0 && (streamId & 1) == 0;
    }
    
    /**
     * Eagerly reads {@code byteCount} bytes from the source before launching a background task to
     * process the data.  This avoids corrupting the stream.
     */
    // 在启动后台任务以处理数据之前,急切地从源读取 byteCount 字节。这避免了破坏流。
    void pushDataLater(final int streamId, final BufferedSource source, final int byteCount,
                       final boolean inFinished) throws IOException {
        final Buffer buffer = new Buffer();
        // 在触发客户端线程之前急切地阅读框架。
        source.require(byteCount); // Eagerly read the frame before firing client thread.
        source.read(buffer, byteCount);()
        if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount);
        pushExecutorExecute(new NamedRunnable("OkHttp %s Push Data[%s]", hostname, streamId) {
            @Override public void execute() {
                try {
                    // 该方法注释: 与推送请求对应的响应数据块。必须读取或跳过此数据
                    // pushObserver在构造方法中通过builder传入,默认是PushObserver.CANCEL,即跳过
                    boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished);
                    if (cancel) writer.rstStream(streamId, ErrorCode.CANCEL);
                    if (cancel || inFinished) {
                        synchronized (Http2Connection.this) {
                            currentPushRequests.remove(streamId);
                        }
                    }
                } catch (IOException ignored) {
                }
            }
        });
    }

接下来的代码就到了拿去对应streamId的Http2Stream,去接收数据,这里Http2Stream我就不追进去了,大致看了下,就是把这里的数据写入到里面的source里面,这个source是Http2Stream构造的时候根据connection.okHttpSettings创建的。

其他读取帧的操作应该类似,这里就不管了。

newStream方法

前面讲解HttpCodec的时候,我只讲了Http1Codec,然后说Http2Codec差不多,好吧,我错了,这里讲到Http2Stream之后,Http2Codec就是通过Http2Stream来实现流的操作的,也还是HttpCodec那一套,这里就看下下面这个方法:

  @Override public void writeRequestHeaders(Request request) throws IOException {
    if (stream != null) return;

    boolean hasRequestBody = request.body() != null;
    List<Header> requestHeaders = http2HeadersList(request);
    stream = connection.newStream(requestHeaders, hasRequestBody);
    stream.readTimeout().timeout(chain.readTimeoutMillis(), TimeUnit.MILLISECONDS);
    stream.writeTimeout().timeout(chain.writeTimeoutMillis(), TimeUnit.MILLISECONDS);
  }

这里通过connection(Http2Connection)创建了stream,下面就来分析下:

    public Http2Stream newStream(List<Header> requestHeaders, boolean out) throws IOException {
        // Http2Codec的writeRequestHeaders会调用这里,创建一个Http2Stream,输入输出都是在一个流上
        return newStream(0, requestHeaders, out);
    }
    
    private Http2Stream newStream(
            int associatedStreamId, List<Header> requestHeaders, boolean out) throws IOException {
        boolean outFinished = !out;
        boolean inFinished = false;
        boolean flushHeaders;
        Http2Stream stream;
        int streamId;

        synchronized (writer) {
            synchronized (this) {
                if (nextStreamId > Integer.MAX_VALUE / 2) {
                    shutdown(REFUSED_STREAM);
                }
                if (shutdown) {
                    throw new ConnectionShutdownException();
                }
                // 输入输出流,所以加2,奇偶用来区分是服务端还是客户端(奇)
                streamId = nextStreamId;
                nextStreamId += 2;
                // 创建的Http2Stream会用来接收Http2Reader里面的帧数据,写入的时候会通过Http2Reader写入
                stream = new Http2Stream(streamId, this, outFinished, inFinished, null);
                flushHeaders = !out || bytesLeftInWriteWindow == 0L || stream.bytesLeftInWriteWindow == 0L;
                if (stream.isOpen()) {
                    // 保存起来,一个连接可以有多个HTTPS请求,即多个Http2Stream
                    streams.put(streamId, stream);
                }
            }
            if (associatedStreamId == 0) {
                // 发送请求头,associatedStreamId==0就是刚开始吧
                writer.synStream(outFinished, streamId, associatedStreamId, requestHeaders);
            } else if (client) {
                throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
            } else { // HTTP/2 has a PUSH_PROMISE frame.
                writer.pushPromise(associatedStreamId, streamId, requestHeaders);
            }
        }

        if (flushHeaders) {
            writer.flush();
        }

        return stream;
    }

上面一节写了很长的ReaderRunnable,这个是对帧的读取,那发送数据在哪里呢?没错,就是这里了,还记得前面Http2Connection构造函数里的writer吗?

writer = new Http2Writer(builder.sink, client);

发送数据以writer的synStream为例追踪,最终还是回到了sink里面:

  public synchronized void synStream(boolean outFinished, int streamId,
      int associatedStreamId, List<Header> headerBlock) throws IOException {
    if (closed) throw new IOException("closed");
    headers(outFinished, streamId, headerBlock);
  }
  
  void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {
    if (closed) throw new IOException("closed");
    hpackWriter.writeHeaders(headerBlock);

    long byteCount = hpackBuffer.size();
    int length = (int) Math.min(maxFrameSize, byteCount);
    byte type = TYPE_HEADERS;
    byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
    if (outFinished) flags |= FLAG_END_STREAM;
    frameHeader(streamId, length, type, flags);
    sink.write(hpackBuffer, length);

    if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
  }

上面也就是Http2Codec的writeRequestHeaders最终执行的地方,而Http2Codec的createRequestBody直接就把stream的Sink返回了,要写入body直接就往Sink写就行了。

  @Override public Sink createRequestBody(Request request, long contentLength) {
    return stream.getSink();
  }

小结

关于RealConnection的内容差不多就是这样了,整篇文章还是以我自己的理解为主吧,可能有不对的地方,可以评论区指出,一起进步!

YOLOv8轻量化模型瘦身术:深度解析与实战LAMP剪枝 YOLOv8(2021) YOLOv8模型LAMP剪枝技术解析与实战 本文深入探讨了LAMP剪枝技术在YOLOv8目标检测模型中的应用。针对深度学习模型部署面临的资源限制问题,LAMP剪枝通过最小化L2失真和层自适应稀疏度分配,实现了高效模型压缩。相比传统幅度剪枝的"一刀切"方法,LAMP创新性地将剪枝建模为优化问题,定义LAMP分数来衡量权重重要性,自动确定各层最佳剪枝率。文章从理论基础出发,详细解析了LAMP的核心数学公式和算法流程,并提供了完整的YOLOv8移植实战指南,包括代码结构、剪枝步骤和微调策略。通 阅读详情

相关推荐

【学习系列】SAP RAP 2:创建第一个RAP应用

本文介绍了从零开始创建基于SAP RAP(ABAP RESTful Application Programming Model)的Fiori应用的完整流程。首先说明了学习RAP需要结合官方文档和教程,并提供了相关资源链接。文章详细讲解了开发环境的准备要求,包括系统版本、开发工具和基本知识。重点演示了如何通过8个步骤创建支持CRUD操作和草稿功能的RAP应用:1)创建数据库表;2)定义CDS视图实体;3)创建投影视图;4)定义行为;5)创建行为投影;6)定义服务;7)绑定服务;8)添加UI注解。

DeveloperMrMeng的博客 1356

OkHttp源码解析(上)

导语 学过Android开发的同学都知道,我们使用网络请求获取服务器的数据时,通常使用的是封装好的Retrofit框架,这个框架很好的帮助我们对网络的发起,以及返回的数据进行操作,我们使用起来十分方便,对于Retrofit来说,我们仅仅看到了它的表面,如何正确使用等,其内部还是要借助OkHtttp来请求网络的,Retrofit只是对OkHttp进行了再次的封装,而且Retrofit不具备网络请求功能,只是在OkHtttp的外表又套了一层,对返回的数据支持RxJava转换和Gson解析。真正起到网络请求的还是

伟子时代的博客 2711

DataX Web分布式数据同步工具 v2.1.2.zip

DataX Web分布式数据同步工具 v2.1.2.zip

k8s okhttp ConnectionShutdownException

k8s client api连接k8s1.18集群的时候报错ConnectionShutdownException,连接k8s 1.15集群没报错。最后发现问题是,jdk版本的问题,用的jdk版本1.8.0_251用问题,修改为1.8.0_144问题解决。

hello 533

Android OKHttp 分发器和拦截器源码解析

整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。重试拦截器重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器桥接拦截器桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据缓存拦截器。

风的轨迹 1263

Android,CLEARTEXT communication not enabled for client

使用的http协议,执行http请求时报错: W/WebSocketConnection: onFailure() java.net.UnknownServiceException: CLEARTEXT communication not enabled for client at okhttp3.internal.connection.RealConnection.connect(RealConnection.java:143) at okhttp3.interna

yzpbright的博客 2815

OkHttp源码分析

1.背景 okHttp作为Android开发最火的开源网络请求框架在Android网络请求中有重要位置 2.OkHttp的基本使用 File externalCacheDir = context.getExternalCacheDir(); int cacheSize = 10 * 1024 * 1024; OkHttpClient.Builder builder ...

自古深情留不住,总是套路得人心 280

基于OkHttpHttp监控

Http请求过程 指标数据 1.入队到请求结束耗时 2.dns查询耗时 3.socket connect耗时 4.tls连接的耗时 5.请求发送耗时 6.响应传输耗时 7.首包耗时 8.响应解析耗时 9.Http错误,区分业务错误和请求错误 采集到以上指标,结合数据可视化的工具,可以对Http个阶段的耗时和错误分布有直观的感受,同时对优化业务Http请求提供数据支持。 如何获取指标 获取指标数据...

jhyshenyu的专栏 2335

OkHttp源码解析

文章目录一、请求流程二、同步、异步网络请求2.1 同步网络请求2.2 异步网络请求三、分发器Dispatcher四、责任链执行过程4.1 重试和重定向RetryAndFollowUpInterceptor4.2 桥拦截器BridgeInterceptor4.3 缓存拦截器CacheInterceptor4.4 连接拦截器ConnectInterceptor4.5 服务器请求拦截器CallServe...

墨玉浮白的博客 618

OkHttp连接拦截器篇

文章目录缓存篇流程图`ConnectInterceptor`详解ExchangeFinder`ExchangeFinder#findConnection`源码分析RealConnection连接池ConnectionPoolTransmitter代理和DNSJava定义的代理类型三种`DIRECT`、`HTTP`、`SOCKS`DNS解析 缓存篇 https://blog.csdn.net/followYouself/article/details/121086502 流程图 ConnectInter

智障儿童欢乐多 1924

OkHttp3源码解析05-连接池

我们都知道HTTP协议采用请求-应答模式,为了解决TCP握手和挥手效率的问题,HTTP有一个keepalive模式。当使用普通模式(非KeppAlive模式)时,每个请求-应答都要新建一个连接,完成后立即断开连接(HTTP是无连接的协议) 当使用KeepAlive模式(又称持久连接、连接重用)时,KeepAlive功能使客户端到服务器连接持续有效,避免了频繁地重新建立连接。 http 1.

朱豪凯的博客 1584

OkHttp源码深度剖析:Android Open Framework Analysis带你解密网络请求核心机制

OkHttp作为Android平台主流的网络请求框架,以其高效的连接管理、强大的缓存机制和简洁的API设计成为开发者的首选。本文将通过Android Open Framework Analysis项目的视角,带你全面理解OkHttp的核心工作原理,掌握其设计精髓与实战应用技巧。 ## 一、OkHttp架构全景:分层设计的艺术 OkHttp采用分层架构设计,将复杂的网络请求流程拆解为清晰的功能模

gitblog_00044的博客 418

彻底掌握网络通信(二十)走进OkHttp3的世界(五)拦截器深入分析二

彻底掌握网络通信(一)Http协议基础知识 彻底掌握网络通信(二)Apache的HttpClient基础知识 彻底掌握网络通信(三)Android源码HttpClient的在不同版本的使用 彻底掌握网络通信(四)Android源码HttpClient的发送框架解析 彻底掌握网络通信(五)DefaultRequestDirector解析 彻底掌握网络通信(六)HttpRequestRetryHa...

yi_master的博客 1215

Android面试】OkHttp & Retrofit 专题

OkHttp & Retrofit 专题

studyForMokey的博客 510

okhttp3实例源码浅析(5-连接池管理

okhttp封装了一套连接池管理,通过复用连接,减少了频繁网络请求连接时的握手开销。 在ConnectInterceptor这一层拦截器中获取缓存/新建socket连接: @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (Real...

chidh的博客 1221

OkhttpRealConnection建立链接简单分析

RealConnect 建立TCP链接简单说明

菜鸟博客 5745

Android 当使用http2okhttp2 v3.4版本网络连接超时分析

最近遇到了一个奇葩的问题,有个别用户反馈App上的菊花一直在转消失不掉,当时产生了几个猜想: 1、dns解析出问题了 2、服务端有问题 3、哪里超时了 4、哪里死循环了 一开始以为是偶然,结果短短一天内,有好几个用户反馈有这个问题,所以这绝对不是偶然,一定是有一个条件触发了这个bug。 由于我们自己调度了dns,所以一开始我们以为是httpdns的问题,但是通过简单的日志分析,发现其实并...

VIPqiangqiang的专栏 3432

Okhttp3框架中的那些坑点记录

Okhttp3框架中的那些坑点记录

风小猪的博客 3138

okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR

BUG:okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR原因是协议错误导致的,在实例化 okhttpclient 的时候用以下方法:okHttpClient = httpBuilder .protocols(Collections.singletonList(Pro

Chittyo 1万+

OkHttp(二)—— 拦截器

本篇文章我们将介绍 OkHttp 的拦截器,拦截器是 OkHttp 的核心,正是因为 OkHttp 内置的五大拦截器各司其职,才使得使用者可以轻松的发送网络请求并收到响应。这五大拦截器分别是:这些拦截器是通过责任链串联起来的。所以我们先来看看 OkHttp 是如何应用责任链的。在上一篇讲分发器时我们能看到,RealCall 不论是被同步执行,还是被异步执行,都调用到了一个使用责任链的方法 getResponseWithInterceptorChain(),该方法会将所有的拦截器都添加到 intercepto

tmacfrank 2307

OKHttp五大拦截器

文章目录【1】五大拦截器总体概述一、责任链设计模式①定义:②为什么要使用责任链模式【2】拦截器的工作流程一、默认的5大拦截器有哪些?【3】RetryAndFollowUpInterceptor拦截器一、重试二、重定向【4】BridgeInterceptor 桥接拦截器【5】CacheInterceptor缓存拦截器【6】ConnectInterceptor连接拦截器【7】CallServerInterceptor请求服务连接器【8】总结 【1】五大拦截器总体概述 OkHttp最核心的工作是在 getResp

zjcoming 7195
上一篇: 对 FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP 的实践
下一篇: okhttp3源码解析(6)-ConnectionPool、StreamAllocation补充
捡一晌贪欢
博客等级 码龄9年 70粉丝 156原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值