Channel、Connection、Http2Stream、Steam的那些事(基于Netty)

Channel、Connection、Http2Stream、Stream的那些事(基于Netty)

看过了第一篇gRPC的网络模型,相信大家已经对gRPC的网络模型有了一定的了解,今天博主会结合大名鼎鼎的Netty,详细掰掰扯和数据交互密不可分的这些类,他们的区别和联系。

系列目录

Channel

Channel是JAVA针对NIO提出的一种类似于InputStream、OutputStream的概念。

   A channel represents an open connection to an entity such as a hardware
 * device, a file, a network socket, or a program component that is capable of
 * performing one or more distinct I/O operations, for example reading or
 * writing.

Channel 类型有:

  • FileChannel, 文件操作
  • DatagramChannel, UDP 操作
  • SocketChannel, TCP 操作
  • ServerSocketChannel, TCP 操作, 使用在服务器端 这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO。

Http2Connection

这里的Connection和连接池的连接是有区别的,Http2的连接默认使用的是DefaultHttp2Connection,这个类主要是对两个EndPoint进行连接管理(注意,这里的EndPoint可以视为两台通讯设备),本文中的另一主角Http2Stream就是被管理在这个类中的。 那么这个类在做些什么事情呢? 首先我们看看类里有什么:

        final IntObjectMap<Http2Stream> streamMap = new IntObjectHashMap<Http2Stream>();
        final ConnectionStream connectionStream = new ConnectionStream();
        final DefaultEndpoint<Http2LocalFlowController> localEndpoint;
        final DefaultEndpoint<Http2RemoteFlowController> remoteEndpoint;
        ...
        final List<Listener> listeners = new ArrayList<Listener>(4);
        final ActiveStreams activeStreams;
        Promise<Void> closePromise;

比较重要的应该就是上述的这些数据,其中ConnectionStream其实是初始化Http2Connection时为了区别其他Stream,添加的一个自身标识。 初次之外,我们看到,Connection管理Stream的应该就是connectionStream这个Map了。 那为什么还需要activeStreams呢,这是个很好的问题,博主追了下代码,从DefaultEndpoint中找到了答案,原来在EndPoint创建Stream的时候:

        @Override
        public DefaultStream createStream(int streamId, boolean halfClosed) throws Http2Exception {
            State state = activeState(streamId, IDLE, isLocal(), halfClosed);

            checkNewStreamAllowed(streamId, state);

            // Create and initialize the stream.
            DefaultStream stream = new DefaultStream(streamId, state);

            incrementExpectedStreamId(streamId);
            //放在streamMap中去
            addStream(stream);
            //放入activeStreams中去
            stream.activate();
            return stream;
        }

从这里就可以看出,StreamAdd和Stream的Active是两个不同的事件,除此之外呢?看来是会有Stream,是不属于activeStreams的行列的。什么Stream呢?发Push promise使用的Stream

        @Override
        public DefaultStream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception {
            ...
            DefaultStream stream = new DefaultStream(streamId, state);

            incrementExpectedStreamId(streamId);

            // Register the stream.
            addStream(stream);
            return stream;
        }

Http2Stream

终于,我们要到Stream了。 在一个HTTP/2的连接中, 流是服务器与客户端之间用于帧交换的一个独立双向序列. 流有几个重要的特点:

  • 一个HTTP/2连接可以包含多个并发的流, 各个端点从多个流中交换frame
  • 流可以被客户端或服务器单方面建立, 使用或共享
  • 流也可以被任意一方关闭
  • frames在一个流上的发送顺序很重要. 接收方将按照他们的接收顺序处理这些frame. 特别是HEADERSDATA frame的顺序, 在协议的语义上显得尤为重要.
  • 流用一个整数(流标识符)标记. 端点初始化流的时候就为其分配了标识符. 拷贝RFC中HTT2中关于流的状态图如下:
                               +--------+
                       send PP |        | recv PP
                      ,--------|  idle  |--------.
                     /         |        |         \
                    v          +--------+          v
             +----------+          |           +----------+
             |          |          | send H /  |          |
      ,------| reserved |          | recv H    | reserved |------.
      |      | (local)  |          |           | (remote) |      |
      |      +----------+          v           +----------+      |
      |          |             +--------+             |          |
      |          |     recv ES |        | send ES     |          |
      |   send H |     ,-------|  open  |-------.     | recv H   |
      |          |    /        |        |        \    |          |
      |          v   v         +--------+         v   v          |
      |      +----------+          |           +----------+      |
      |      |   half   |          |           |   half   |      |
      |      |  closed  |          | send R /  |  closed  |      |
      |      | (remote) |          | recv R    | (local)  |      |
      |      +----------+          |           +----------+      |
      |           |                |                 |           |
      |           | send ES /      |       recv ES / |           |
      |           | send R /       v        send R / |           |
      |           | recv R     +--------+   recv R   |           |
      | send R /  `----------->|        |<-----------'  send R / |
      | recv R                 | closed |               recv R   |
      `----------------------->|        |<----------------------'
                               +--------+
    
         send:   endpoint sends this frame
         recv:   endpoint receives this frame
    
         H:  HEADERS frame (with implied CONTINUATIONs)
         PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
         ES: END_STREAM flag
         R:  RST_STREAM frame
    

    该图只展示了流的状态转换以及frame和标记如何对转换产生影响. 这方面。CONTINUATIONframes不会导致状态的转换, 他们只是跟在HEADERSPUSH_PROMISE frame后面的有效组成部分。 状态转换的用途, 对于设置了END_STREAM标记的frame来说,END_STREAM被当做一个分开的事件处理. 设置了END_STREAM标记的HEADERS frame会导致两次状态转换。 在传输过程中, 每个端点对流状态的主观认识可能不同。这些终端不会协商流的创建, 都是由终端独立创建的. 端点的流状态不同会带来负面影响: 在发送了RST_STREAM之后流处于关闭状态,而frame可能在流关闭之后才到达。 流有如下状态:

  • idle 所有流最初状态都是idle。 下面描述了流从idle状态到其它状态的几种可能转换:
    • 发送或接收到一个HEADERSframe会使流状态变换open。 流标识符的选择参上图里的描述. 收到相同的HEADERSframe会导致流立即变为half-close状态。
    • (Sending a PUSH_PROMISE frame on another stream reserves the idle stream that is identified for later use.)在另一个流上发送一个PUSH_PROMISEframe 被标识为以后使用。预留流的状态对应转换到reserved (local)
    • (Receiving a PUSH_PROMISE frame on another stream reserves an idle stream that is identified for later use.)在另一个流上接收一个PUSH_PROMISEframe 被标识为以后使用。预留流的状态对应转换到reserved (remote)
    • 注意PUSH_PROMISEframe并不在idle流上发送,只是promised流的ID字段引用了新的reserved流。 在idle状态接收到任何非HEADERSPUSH_PROMISEframe必须视为连接错误, 错误类型为PROTOCOL_ERROR
  • reserved (local) 处于这种状态的流表示它已经发送了一个PUSH_PROMISEframe并成为promised流。PUSH_PROMISEframe通过关联一个由远程对等点初始化的流来转换idle流到reserved流。 处于这个状态的流, 只有下面的几种可能状态转换:
    • 端点发送一个HEADERSframe, 流进入half-closed (remote)状态。
    • 任何一个端点发送一个RST_STREAMframe, 流变成closed状态. 这将释放一个流保留的资源. 端点不准发送除HEADERS, RST_STREAMPRIORITY之外任何类型的frame。 这一状态可能收到PRIORITYWINDOW_UPDATEframe。 除了RST_STREAMPRIORITY以及WINDOW_UPDATEframe之外,收到其他类型的frame必须视为PROTOCOL_EROR类型的连接错误。
  • reserved (remote) 如果一个流已被远程对等点保留, 状态就会变成reserved(remote)。 可能的转换如下:
    • 收到一个HEADERS frame导致状态变为half-close(local)
    • 任何端点发送一个RST_STREAMframe会导致状态变成closed, 并释放流保留的资源。 端点可以发送一个PRIORITY frame以重新确定reserved流的优先级次序. 不允许发送除RST_STREAM, WINDOW_UPDATEPRIORITY之外的frame. 在一个流上拿到非HEADERS ,RST_STREAMPRIORITY的frame必须视为PROTOCOL_EROR类型的连接错误。
  • open 任何一对等方可以使用open状态的流发送任意类型的frame. 这一状态下, 发送方会监视给出的流级别和流控范围. 在任意一方发送设置了END_STREAM标记的frame后, 流状态会变为half-closed的其中一个状态: 如果一方发送了该frame, 其流变为half-closed(local);如果一方收到该frame, 流变为half-closed(remote)。 在这个状态发送RST_STREAM frame可以使状态立即变成closed

  • half-closed (local) 处于这个状态的流不能发送除WINDOW_UPDATEPRIORITY以及RST_STREAM之外的frame。 收到一个标记了END_STREAM的frame或者发送一个RST_STREAM frame, 都会使状态变成closed。 端点允许接收任意类型的frame。 便于后续接收用于流控的frame, 使用WINDOW_UPDATE frame提供流控credit很有必要. 接收方可以选择忽略WINDWO_UPDATE frame, (which might arrive for a short period after a frame bearing the END_STREAM flag is sent.) 收到的PRIORITY frame用于重定流的优先级次序(依据流的标记而定)。

  • half-closed (remote) 处于这个状态的流,对端不再用来发送frame了。 并且端点也无需继续维护接收方流控窗口。 如果端点收到额外的frame,并且不是WINDOW_UPDATEPRIORITYRST_STREAM,那么必须响应一个类型为STREAM_CLOSED的流错误。 这一状态下的流可以发送任意类型的frame. 端点仍会继续监视已知的流级别和流控范围. 发送一个END_STERAM标记的frame或任意一个对等方发送了RST_STREAM frame都会使流变为closed

  • closed closed标识终止状态。 在一个closed的流上不允许发送PRIORITY之外的其他frame. 端点在收到RST_STREAM frame后又收到非PRIORITY的frame的话, 一定被视为流错误对待(类型STREAM_CLOSED)。 同样, 收到END_STREAM标记后又收到非如下描述的frame, 会触发一个连接错误(类型STREAM_CLOSED): 发送了包含END_STREAM标记的DATAHEADERS frame后的一小段时间内,允许WINDOW_UPDATERST_STREAM frame被接收。 直到远程对等端收到并处理了RST_STERAM或包含END_STREAM标记的frame, 才可以发送这些类型的frame。 假如在发送了END_STREAM后已明显过了超时时间, 这时却再次收到frame, 尽管终端可以选择把这个frame当成PROTOCOL_ERROR类型的连接错误来处理, 但无论如何最终必须忽略这种情况下收到的WINDOW_UPDATERST_STREAM frame。 PRIORITY帧可从closed流上发到优先级更高的流(取决于closed流)。终端应该处理PRIORITY帧, 尽管他们可能因为流已经从依赖树中移除而被忽略。 如果是发送RST_STREAM帧的原因让状态转换到了closed,收到RST_STREAM的对等端这时可能已经发送了RST_STREAM或者入队等待发送中, 但是已经在流上传输的帧是不可以被撤销的. 这时, 终端必须忽略从closed的流上再取得的帧,如果这个closed流已经发送了RST_STREAM帧。终端也可以选择一个超时时间, 忽略在此之后到达的帧, 并一律视作错误。 在发送了RST_STREAM之后收到的流控帧(比如DATA帧)也会被用于计算当前连接的流控窗口。(are counted toward the connection flow-control window.) 尽管这些帧有可能被忽略掉,但是因为他们在发送方收到RST_STREAM之前被发送了, 所以发送方仍有可能会根据这些帧计算流控窗口大小. 终端发送了RST_STREAM帧之后可以再接收一个PUSH_PROMISE帧。PUSH_PROMISE帧会将流状态变为reserved即使相关的流已经被重置. 因此需要一个RST_STREAM帧去关闭不再需要的promised流。

本文档中没有给出更具体说明的地方, 对于收到的那些未在上述状态描述中明确认可的帧, 协议实现上应该视这种情况为一个类型为PROTOCOL_ERROR的连接错误,另外注意PRIORITY帧可以在流的任何一个状态被发送/接收。 忽略未知类型的帧。

Stream In gRPC

Stream,按照官方的说法,是:

A single stream of communication between two end-points within a transport.

什么个意思呢,就是两个end-points之间一次完整的通信中的一个最小单元(可能含多个),叫做Stream,其中可能会包含很多Frame,其中被gRPC用的最多的就是NettyClientStream(Client端维护的)和NettyServerStream(Server端维护的)。首先我们分析下这两个类的父类ServerStreamClientStream的接口约定。 Stream的接口定义: placeholder ServerStream的代码: placeholder CleitStream的代码: placeholder 看来,双方Stream都是可以写数据writeMessage()和请求数据request()的,并且根据参数,我们也能够清楚的判断,这个Stream就是和Protobuf(gRPC中的序列化组件)到Netty组件的连接点。 同时,我们也能够看到,只有ClientStreamstart(),这就意味着一个请求的生命周期是从Client端开始的,并且ClientStream只有halfClose(),看来,关闭Stream的事是交给了ServerStream(异常关闭使用的是cancel这个注意一下)。 那么StreamHttp2Stream是什么关系呢? 答,是一对一维护一个映射的关系,一个Stream只有一个TransportState,而TransportState中含有一个id,这个id是谁呢?大家看看下边这个NettyClientHandlercreateStream()方法。 placeholder 原来这个id就是Http2Streamid。 看来Google,为了更加方便的使用Http2Stream,就对其封装了一层。 顺便说句,Netty中的StreamID是int型的,最大为2147483647,当耗尽时还是抛出Stream IDs have been exhausted,这时还是需要去捕获异常进行Fail Over的(分布式环境不需要担心,因为还有其他机器可以处理,单点机器要做好防护)。

四者的关系

其实通过上述介绍,读者们应该将他们的关系猜的八九不离十了,简单的讲就是: Http2Connection管理Http2Stream,而Http2Stream只是一个状态管理,真正的数据传输还是要通过Channel,那ChannelHttp2Stream的连接点在哪里呢?对于Netty来说,连接点就是HttpConnectionHandler,因此,对于一个Channel,就对应一个Http2Connection,一个Http2Connection对应多个Http2Stream,而gRPC中Stream就是对Http2Stream的一次封装,他们之间的关联就是用StreamID关联起来的。

本文为作者原创,转载请注明出处 。邮箱:568718043@qq.com