vislee / leevis.com

Blog
87 stars 13 forks source link

http3.0 协议 #193

Open vislee opened 1 year ago

vislee commented 1 year ago

概述

http3并没有改变http1的语义:

  1. 请求只能由客户端发起,而服务器针对每个请求返回一个相应。
  2. 请求与响应都由Header、Body(可选)组成,其中请求必须包含有URL和方法,而响应必须含有相应码。
  3. Header中各Name对应的含义保持不变。

只是改变了编码格式和传输:

  1. 采用二进、静态表、动态表、Huffman算法 对 Http Header编码,不只提供了高压缩率,还加快了发送端编码、接收端解码的速度。
  2. 基于UDP在应用层实现无序连接,并在此基础上通过有序的QUIC Stream提供了多路复用,连接迁移。 image

(图片来自:https://blog.cloudflare.com/http3-the-past-present-and-future/)

image

在UDP报文头部与HTTP消息之间,共有3层头部 image

这3层Header实现的功能各不相同:

Packet Header实现了可靠的连接。当UDP报文丢失后,通过Packet Header中的Packet Number实现报文重传。连接也是通过其中的Connection ID字段定义的; QUIC Frame Header在无序的Packet报文中,基于QUIC Stream概念实现了有序的字节流,这允许HTTP消息可以像在TCP连接上一样传输; HTTP3 Frame Header定义了HTTP Header、Body的格式,以及服务器推送、QPACK编解码流等功能。

Package 报文

QUIC报文基本通信数据单元是Packet,由 header 和 data 两部分组成。 header 是明文的,包含 4 个字段:Flags、Connection ID、QUIC Version、Packet Number; Data 是加密的,并由Frame组成,可以包含 1 个或多个 frame,每个 frame 又分为 type 和 payload,其中 payload 就是应用数据。 QUIC 协议有长报文头和短报文头两种,在建立连接过程中使用的长报文头。Package Number 被分配为:初始空间、握手空间和应用空间三个部分。每次都从对应空间的 0 号开始,而后以此递增。 双方只需要固定住Connection ID,就可以在客户端IP地址、端口变化后,绕过UDP四元组,实现连接迁移功能。 Packet Number是每个报文独一无二的序号,基于它可以实现丢失报文的精准重发。Packet Header又可以细分为两种: Long Packet Header用于首次建立连接; image Short Packet Header用于日常传输数据。 image

一个UDP包可能包含多个QUIC Packet,一个QUIC Packet报文中可以存放多个QUIC Frame,当然所有Frame的长度之和不能大于PMTUD(Path Maximum Transmission Unit Discovery,这是大于1200字节的值)。一个UDP报文必须是包含同一个连接的多个QUIC Package。 image

协商连接ID

QUIC 的数据包(packets)长报头(long header)包含两个连接ID:目标连接ID(Destination Connection ID)由数据包的接收者选择并用于提供一致的路由,源连接ID(Source Connection ID)用于对端(peer)响应时使用的目标连接ID(Destination Connection ID)。

在握手过程中,带有长报头的包用于建立两端(both endpoints)使用的连接ID。在处理第一个初始数据包(Initial packet)之后,每个端点使用其接收到的源连接ID(Source Connection ID)字段的值设置为后续数据包中的目标连接ID(Destination Connection ID)字段。

当客户端发送了一个初始包(Initial packet),而该客户端之前没有从服务端接收过初始数据包(Initial packet)或重试包(Retry packet),则客户端将用一个不可预测的值(长度至少为8字节)填充到目标连接ID字段。在从服务端接收到数据包之前,客户端必须对该连接中的所有数据包使用相同的目标连接ID值。

当第一次从服务端接收到初始(Initial)或重试(Retry)数据包时,客户端使用服务端提供的源连接ID作为后续数据包(包括任何 0-RTT 数据包)的目标连接ID。这意味着在建立连接的过程中,客户端可能需要两次更改它的目标连接ID字段:一次用于响应重试(Retry),一次用于响应来自服务端的初始数据包(Initial)。一旦客户端从服务端接收到有效的初始数据包,客户端必须丢弃它后续接收到的具有不同源连接ID的数据包。

服务端必须根据第一个接收到的初始数据包(Initial packet)的源连接ID,设置为用于发送数据包的目标连接ID。后续只有当接收到 NEW_CONNECTION_ID 帧时,才允许对目标连接ID进行更改。如果后续初始数据包包含不同的源连接ID,则必须将其丢弃。这样可以避免由于无状态(stateless)处理具有不同源连接ID的多个初始数据包而导致的不可预测结果。

端点可以在连接的生命周期内更改发送的目标连接ID,特别是在响应连接迁移(connection migration)时。

Long Header Packet {
  Header Form (1) = 1, // 对于长标头,字节0(第一个字节)的最高有效位(0x80)设置为1。
  Fixed Bit (1) = 1,  // 字节0的下一个位(0x40)设置为1。对此位包含零值的数据包在该版本中不是有效数据包,必须将其丢弃。
  Long Packet Type (2), // 字节0的后两位(掩码为0x30的那些位)包含一个数据包类型。0x0:Initial;0x1:0-RTT;0x2:Handshake;0x3:Retry
  Type-Specific Bits (4), // 字节0的低四位(掩码为0x0f的那四位)是特定于类型的。
  Version (32), // QUIC版本是第一个字节之后的32位字段。 该字段指示正在使用哪个版本的QUIC,并确定如何解释其余协议字段
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
}

// 初始数据包使用类型值为0x0的长标头。 它承载由客户端和服务器发送的第一个CRYPTO帧以执行密钥交换,并在任一方向上承载ACK。
Initial Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 0,
  Reserved Bits (2),
  Packet Number Length (2),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Token Length (i), // 一个可变长度的整数,以字节为单位指定令牌字段的长度。 如果没有令牌,则该值为零。 服务器发送的初始数据包必须将令牌长度字段设置为零; 收到带有非零令牌长度字段的初始数据包的客户必须丢弃该数据包或产生类型为PROTOCOL_VIOLATION的连接错误。
  Token (..), // 先前在“重试”数据包或NEW_TOKEN帧中提供的令牌的值。
  Length (i),
  Packet Number (8..32),
  Packet Payload (..),
}
// 0-RTT 数据包使用类型值为 0x1 的长标头
0-RTT Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 1,
  Reserved Bits (2),
  Packet Number Length (2),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Length (i),
  Packet Number (8..32),
  Packet Payload (..),
}
// 握手数据包使用类型值为 0x2 的长标头,后跟长度和数据包编号字段。第一个字节包含保留位和数据包编号长度位。它用于携带来自服务器和客户端的确认和加密握手消息。
Handshake Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 2,
  Reserved Bits (2),
  Packet Number Length (2),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Length (i),
  Packet Number (8..32),
  Packet Payload (..),
}
// 重试数据包使用类型值为 0x3 的长数据包标头。它带有服务器创建的地址验证令牌。它被希望执行重试的服务器使用
Retry Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2) = 3,
  Unused (4),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Retry Token (..),
  Retry Integrity Tag (128),
}

QUIC 帧

每一个QUIC Frame都有明确的类型 image

0x08-0x0f 是8种STREAM类型的Frame: image Stream ID标识了一个有序字节流。当HTTP Body非常大,需要跨越多个Packet时,只要在每个Stream Frame中含有同样的Stream ID,就可以传输任意长度的消息。多个并发传输的HTTP消息,通过不同的Stream ID加以区别; 消息序列化后的“有序”特性,是通过Offset字段完成的,它类似于TCP协议中的Sequence序号,用于实现Stream内多个Frame间的累计确认功能; Length指明了Frame数据的长度。

因为3个二进制位组成所以有8种类型的STREAM:第1位表示是否含有Offset,当它为0时,表示这是Stream中的起始Frame,这也是上图中Offset是可选字段的原因;第2位表示是否含有Length字段;第3位Fin,表示这是Stream中最后1个Frame。

STREAM Frame { Type (i) = 0x08..0x0f, // Stream ID (i), // 流ID 一个可变长度的整数,表示流的流ID [Offset (i)], // 一个可变长度整数,指定此 STREAM 帧中数据在流中的字节偏移量。当 OFF 位设置为 1 时,该字段存在。当 Offset 字段不存在时,偏移量为 0 [Length (i)], // 一个可变长度整数,指定此 STREAM 帧中流数据字段的长度。当 LEN 位设置为 1 时,该字段存在。当 LEN 位设置为 0 时,流数据字段消耗数据包中的所有剩余字节 Stream Data (..), }

HTTP3 帧

QUIC Stream数据中并不会直接存放HTTP消息,因为HTTP3还需要实现服务器推送、权重优先级设定、流量控制等功能,所以Stream Data中首先存放了HTTP3 Frame image Type: 0x00:DATA帧,用于传输HTTP Body包体; 0x01:HEADERS帧,通过QPACK 编码,传输HTTP Header头部; 0x03:CANCEL_PUSH控制帧,用于取消1次服务器推送消息,通常客户端在收到PUSH_PROMISE帧后,通过它告知服务器不需要这次推送; 0x04:SETTINGS控制帧,设置各类通讯参数; 0x05:PUSH_PROMISE帧,用于服务器推送HTTP Body前,先将HTTP Header头部发给客户端,流程与HTTP2相似; 0x07:GOAWAY控制帧,用于关闭连接(注意,不是关闭Stream); 0x0d:MAX_PUSH_ID,客户端用来限制服务器推送消息数量的控制帧。 Length指明了HTTP消息的长度

握手

image

nginx源码

介绍: https://nginx.org/en/docs/quic.html

listen 8443 quic reuseport; 通过示例可看出listen 增加了quic的选项。 代码ngx_http_core_listen函数:

        if (ngx_strcmp(value[n].data, "quic") == 0) {
#if (NGX_HTTP_V3)
            lsopt.quic = 1;
            lsopt.type = SOCK_DGRAM;
            continue;
#else
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "the \"quic\" parameter requires "
                               "ngx_http_v3_module");
            return NGX_CONF_ERROR;
#endif
        }

启动nginx初始化完配置后在ngx_worker_process_init函数调用了所有模块的init_process回调函数,会调用到“event_core”的ngx_event_process_init函数,在该函数添加listen回调函数并添加到epoll中。

#if (NGX_QUIC)
        } else if (ls[i].quic) {
            rev->handler = ngx_quic_recvmsg;
#endif

ngx_quic_recvmsg函数就是nginx处理quic的入口函数了。

// ngx_event_quic_transport.h

#define NGX_QUIC_PKT_LONG       0x80  /* header form */
#define NGX_QUIC_PKT_FIXED_BIT  0x40
#define NGX_QUIC_PKT_TYPE        0x30  /* in long packet */
// 判断是否为Long Packet Header
#define ngx_quic_long_pkt(flags)  ((flags) & NGX_QUIC_PKT_LONG)
#define ngx_quic_short_pkt(flags)  (((flags) & NGX_QUIC_PKT_LONG) == 0)

#define NGX_QUIC_PKT_INITIAL    0x00
#define ngx_quic_pkt_in(flags)                                                \
    (((flags) & NGX_QUIC_PKT_TYPE) == NGX_QUIC_PKT_INITIAL)

// ngx_event_quic_transport.c

// 从QUIC报文获取目标连接ID(Destination Connection ID)
ngx_int_t
ngx_quic_get_packet_dcid(ngx_log_t *log, u_char *data, size_t n,
    ngx_str_t *dcid)
{
    size_t  len, offset;

    if (n == 0) {
        goto failed;
    }

    if (ngx_quic_long_pkt(*data)) {
        if (n < NGX_QUIC_LONG_DCID_LEN_OFFSET + 1) {
            goto failed;
        }

        len = data[NGX_QUIC_LONG_DCID_LEN_OFFSET];
        offset = NGX_QUIC_LONG_DCID_OFFSET;

    } else {
        len = NGX_QUIC_SERVER_CID_LEN;    // Short Packet Header Destination Connection ID 是服务端点设置的
        offset = NGX_QUIC_SHORT_DCID_OFFSET;
    }

    if (n < len + offset) {
        goto failed;
    }

    dcid->len = len;
    dcid->data = &data[offset];

    return NGX_OK;

failed:

    ngx_log_debug0(NGX_LOG_DEBUG_EVENT, log, 0, "quic malformed packet");

    return NGX_ERROR;
}

ngx_quic_recvmsg函数中调用recvmsg接收消息,调用ngx_get_connection分配连接结构体,回调ngx_http_init_connection函数,如果是quic协议则调用ngx_http_v3_init_stream初始化流。

// ngx_http_v3_table.h
typedef struct {
    ngx_http_v3_field_t         **elts;
    ngx_uint_t                    nelts;
    ngx_uint_t                    base;
    size_t                        size;
    size_t                        capacity;
    uint64_t                      insert_count;
    uint64_t                      ack_insert_count;
    ngx_event_t                   send_insert_count;
} ngx_http_v3_dynamic_table_t;

// 
struct ngx_http_v3_session_s {
    ngx_http_v3_dynamic_table_t   table;

    ngx_event_t                   keepalive;
    ngx_uint_t                    nrequests;

    ngx_queue_t                   blocked;
    ngx_uint_t                    nblocked;

    uint64_t                      next_request_id;

    off_t                         total_bytes;
    off_t                         payload_bytes;

    unsigned                      goaway:1;
    unsigned                      hq:1;

    ngx_connection_t             *known_streams[NGX_HTTP_V3_MAX_KNOWN_STREAM];
};
// 解析quic package
ngx_int_t
ngx_quic_parse_packet(ngx_quic_header_t *pkt)
{
    if (!ngx_quic_long_pkt(pkt->flags)) {
        pkt->level = ssl_encryption_application;

        // 短标头 目标连接ID长度固定为NGX_QUIC_SERVER_CID_LEN==20
        if (ngx_quic_parse_short_header(pkt, NGX_QUIC_SERVER_CID_LEN) != NGX_OK)
        {
            return NGX_ERROR;
        }

        return NGX_OK;
    }

    if (ngx_quic_parse_long_header(pkt) != NGX_OK) {
        return NGX_ERROR;
    }

    if (!ngx_quic_supported_version(pkt->version)) {
        return NGX_ABORT;
    }

    if (ngx_quic_parse_long_header_v1(pkt) != NGX_OK) {
        return NGX_ERROR;
    }

    return NGX_OK;
}

// 解析长标头包
// https://datatracker.ietf.org/doc/id/draft-ietf-quic-transport-29.html#name-long-header-packets
static ngx_int_t 
ngx_quic_parse_long_header(ngx_quic_header_t *pkt)
{
    u_char   *p, *end;
    uint8_t   idlen;

    // ngx_quic_handle_datagram函数中对pkt.raw->pos++; pos是char*型+1就是加1字节,也就是8bit
    // Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2), Type-Specific Bits (4),
    p = pkt->raw->pos;
    end = pkt->data + pkt->len;

    // Version (32)
    p = ngx_quic_read_uint32(p, end, &pkt->version);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet is too small to read version");
        return NGX_ERROR;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
                   "quic packet rx long flags:%xd version:%xD",
                   pkt->flags, pkt->version);

    //   Fixed Bit (1) = 1,
    if (!(pkt->flags & NGX_QUIC_PKT_FIXED_BIT)) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic fixed bit is not set");
        return NGX_ERROR;
    }

    //   Destination Connection ID Length (8),
    p = ngx_quic_read_uint8(p, end, &idlen);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet is too small to read dcid len");
        return NGX_ERROR;
    }

    if (idlen > NGX_QUIC_CID_LEN_MAX) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet dcid is too long");
        return NGX_ERROR;
    }

    pkt->dcid.len = idlen;

    p = ngx_quic_read_bytes(p, end, idlen, &pkt->dcid.data);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet is too small to read dcid");
        return NGX_ERROR;
    }

    //   Source Connection ID Length (8),
    p = ngx_quic_read_uint8(p, end, &idlen);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet is too small to read scid len");
        return NGX_ERROR;
    }

    if (idlen > NGX_QUIC_CID_LEN_MAX) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet scid is too long");
        return NGX_ERROR;
    }

    pkt->scid.len = idlen;

    p = ngx_quic_read_bytes(p, end, idlen, &pkt->scid.data);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic packet is too small to read scid");
        return NGX_ERROR;
    }

    pkt->raw->pos = p;

    return NGX_OK;
}

// https://datatracker.ietf.org/doc/id/draft-ietf-quic-transport-29.html#name-initial-packet
static ngx_int_t
ngx_quic_parse_long_header_v1(ngx_quic_header_t *pkt)
{
    u_char    *p, *end;
    uint64_t   varint;

    p = pkt->raw->pos;
    end = pkt->raw->last;

    pkt->log->action = "parsing quic long header";

    if (ngx_quic_pkt_in(pkt->flags)) {

        if (pkt->len < NGX_QUIC_MIN_INITIAL_SIZE) {
            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                          "quic UDP datagram is too small for initial packet");
            return NGX_DECLINED;
        }

        //   Token Length (i),
        p = ngx_quic_parse_int(p, end, &varint);
        if (p == NULL) {
            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                          "quic failed to parse token length");
            return NGX_ERROR;
        }

        pkt->token.len = varint;
        //   Token (..),
        p = ngx_quic_read_bytes(p, end, pkt->token.len, &pkt->token.data);
        if (p == NULL) {
            ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                          "quic packet too small to read token data");
            return NGX_ERROR;
        }

        pkt->level = ssl_encryption_initial;

    } else if (ngx_quic_pkt_zrtt(pkt->flags)) {
        pkt->level = ssl_encryption_early_data;

    } else if (ngx_quic_pkt_hs(pkt->flags)) {
        pkt->level = ssl_encryption_handshake;

    } else {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic bad packet type");
        return NGX_DECLINED;
    }
    //   Length (i),
    p = ngx_quic_parse_int(p, end, &varint);
    if (p == NULL) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic bad packet length");
        return NGX_ERROR;
    }

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, pkt->log, 0,
                   "quic packet rx %s len:%uL",
                   ngx_quic_level_name(pkt->level), varint);

    if (varint > (uint64_t) ((pkt->data + pkt->len) - p)) {
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0, "quic truncated %s packet",
                      ngx_quic_level_name(pkt->level));
        return NGX_ERROR;
    }

    pkt->raw->pos = p;
    pkt->len = p + varint - pkt->data;

    return NGX_OK;
}

代码里随处可见的,获取ngx_quic_connection_t #define ngx_quic_get_connection(c) \ (((c)->udp) ? (((ngx_quic_socket_t *)((c)->udp))->quic) : NULL) ngx_quic_open_sockets函数中,先调用了ngx_quic_create_socket创建quic socket,结构如下,在ngx_quic_listen函数对quic socket结构体赋值,最后再把qsock的udp赋给ngx_connection_t结构体的udp c->udp = &qsock->udp; 因此,udp必须是ngx_quic_socket_s结构体的第一个字段,udp的地址和ngx_quic_socket_s的内存地址是一样,所以可以这样(ngx_quic_socket_t *)((c)->udp)强制转换后取quic,该quic也就是ngx_quic_listen函数中qsock->quic = qc;的值。

struct ngx_quic_socket_s {
    ngx_udp_connection_t              udp;
    ngx_quic_connection_t            *quic;
    ngx_queue_t                       queue;
    ngx_quic_server_id_t              sid;
    ngx_sockaddr_t                    sockaddr;
    socklen_t                         socklen;
    ngx_uint_t                        used; /* unsigned  used:1; */
};

// ngx_event_quic_socket.c

ngx_int_t
ngx_quic_listen(ngx_connection_t *c, ngx_quic_connection_t *qc,
    ngx_quic_socket_t *qsock)
{
    ngx_str_t              id;
    ngx_quic_server_id_t  *sid;

    sid = &qsock->sid;

    id.data = sid->id;
    id.len = sid->len;

    qsock->udp.connection = c;
    qsock->udp.node.key = ngx_crc32_long(id.data, id.len);
    qsock->udp.key = id;

    ngx_rbtree_insert(&c->listening->rbtree, &qsock->udp.node);

    ngx_queue_insert_tail(&qc->sockets, &qsock->queue);

    qc->nsockets++;
    qsock->quic = qc;

    ngx_log_debug3(NGX_LOG_DEBUG_EVENT, c->log, 0,
                   "quic socket seq:%L listening at sid:%xV nsock:%ui",
                   (int64_t) sid->seqnum, &id, qc->nsockets);

    return NGX_OK;
}

// ngx_event_quic_transport.c // 解析quic帧,只保留了STREAM帧的处理。

ssize_t
ngx_quic_parse_frame(ngx_quic_header_t *pkt, u_char *start, u_char *end,
    ngx_quic_frame_t *f)
{
    u_char      *p;
    uint64_t     varint;
    ngx_buf_t   *b;
    ngx_uint_t   i;

    b = f->data->buf;   // Stream Data 

    p = start;
    // 读取帧类型(type)
    p = ngx_quic_parse_int(p, end, &varint);
    if (p == NULL) {
        pkt->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic failed to obtain quic frame type");
        return NGX_ERROR;
    }

    if (varint > NGX_QUIC_FT_LAST) {
        pkt->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic unknown frame type 0x%xL", varint);
        return NGX_ERROR;
    }

    f->type = varint;

    // 判断是否允许的帧类型
    if (ngx_quic_frame_allowed(pkt, f->type) != NGX_OK) {
        pkt->error = NGX_QUIC_ERR_PROTOCOL_VIOLATION;
        return NGX_ERROR;
    }

    switch (f->type) {

    case NGX_QUIC_FT_CRYPTO:

        p = ngx_quic_parse_int(p, end, &f->u.crypto.offset);
        if (p == NULL) {
            goto error;
        }

        p = ngx_quic_parse_int(p, end, &f->u.crypto.length);
        if (p == NULL) {
            goto error;
        }

        p = ngx_quic_read_bytes(p, end, f->u.crypto.length, &b->pos);
        if (p == NULL) {
            goto error;
        }

        b->last = p;

        break;

    ......

    case NGX_QUIC_FT_CONNECTION_CLOSE:
    case NGX_QUIC_FT_CONNECTION_CLOSE_APP:

        p = ngx_quic_parse_int(p, end, &f->u.close.error_code);
        if (p == NULL) {
            goto error;
        }

        if (f->type == NGX_QUIC_FT_CONNECTION_CLOSE) {
            p = ngx_quic_parse_int(p, end, &f->u.close.frame_type);
            if (p == NULL) {
                goto error;
            }
        }

        p = ngx_quic_parse_int(p, end, &varint);
        if (p == NULL) {
            goto error;
        }

        f->u.close.reason.len = varint;

        p = ngx_quic_read_bytes(p, end, f->u.close.reason.len,
                                &f->u.close.reason.data);
        if (p == NULL) {
            goto error;
        }

        break;

    // 处理STREAM帧
    case NGX_QUIC_FT_STREAM:
    case NGX_QUIC_FT_STREAM1:
    case NGX_QUIC_FT_STREAM2:
    case NGX_QUIC_FT_STREAM3:
    case NGX_QUIC_FT_STREAM4:
    case NGX_QUIC_FT_STREAM5:
    case NGX_QUIC_FT_STREAM6:
    case NGX_QUIC_FT_STREAM7:
        // 是否有FIN标志
        f->u.stream.fin = (f->type & NGX_QUIC_STREAM_FRAME_FIN) ? 1 : 0;
        // 流ID
        p = ngx_quic_parse_int(p, end, &f->u.stream.stream_id);
        if (p == NULL) {
            goto error;
        }
        // OFF标志
        if (f->type & NGX_QUIC_STREAM_FRAME_OFF) {
            f->u.stream.off = 1;
            // 读取偏移
            p = ngx_quic_parse_int(p, end, &f->u.stream.offset);
            if (p == NULL) {
                goto error;
            }

        } else {
            f->u.stream.off = 0;
            f->u.stream.offset = 0;
        }
        // 长度标志
        if (f->type & NGX_QUIC_STREAM_FRAME_LEN) {
            f->u.stream.len = 1;
            // 读取长度
            p = ngx_quic_parse_int(p, end, &f->u.stream.length);
            if (p == NULL) {
                goto error;
            }

        } else {
            f->u.stream.len = 0;
            f->u.stream.length = end - p; /* up to packet end */
        }
        // 读取payload
        p = ngx_quic_read_bytes(p, end, f->u.stream.length, &b->pos);
        if (p == NULL) {
            goto error;
        }

        b->last = p;

        f->type = NGX_QUIC_FT_STREAM;
        break;

    case NGX_QUIC_FT_MAX_DATA:

        p = ngx_quic_parse_int(p, end, &f->u.max_data.max_data);
        if (p == NULL) {
            goto error;
        }

        break;

    ......

    default:
        ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                      "quic unknown frame type 0x%xi", f->type);
        return NGX_ERROR;
    }

    f->level = pkt->level;

    return p - start;

error:

    pkt->error = NGX_QUIC_ERR_FRAME_ENCODING_ERROR;

    ngx_log_error(NGX_LOG_INFO, pkt->log, 0,
                  "quic failed to parse frame type:0x%xi", f->type);

    return NGX_ERROR;
}

总结

// 核心处理函数 // 接收udp数据报, 每个UDP报都需要经过该函数的处理 ngx_quic_recvmsg // 从quic Package中获取连接ID,相同ID标识一条quic虚拟的连接。 ngx_quic_get_packet_dcid // 根据连接ID获取已建好的连接,连接是ngx_quic_listen函数插入的。 ngx_quic_lookup_connection // 分配一组连接所需要的connection结构体,quic和h3代码中,只有两个地方分配connection了 // 1.在ngx_quic_recvmsg中,为每个quic虚拟连接分配一个。 // 2.在ngx_quic_create_stream函数中,为每个stream分配一个。 ngx_get_connection // quic启动函数,需要关注的是读回调:c->read->handler = ngx_quic_input_handler; ngx_quic_run // 处理UDP数据报,该函数从ngx_quic_get_connection是否有返回分为两种情况。 ngx_quic_handle_datagram // 处理quic Packet ngx_quic_handle_packet // 解析quic Packet ngx_quic_parse_packet // 新建quic connection,一个连接ID标识一个quic connection,一个quic connection可以支持多stream。 ngx_quic_new_connection // 在quic connection上抽象出quic socket, ngx_quic_open_sockets ngx_quic_listen // 处理quic frame ngx_quic_handle_frames // 解析quic frame ngx_quic_read_bytes 读取quic frame携带的数据 ngx_quic_parse_frame // 处理stream frame, ngx_quic_parse_frame 解析得到的数据调用 ngx_quic_write_buffer 缓存到qs->recv ngx_quic_handle_stream_frame // 获取对应流ID的stream, 流ID低2位是标志位,所以会有 id >> 2、 min_id += 0x04 这样的操作。 ngx_quic_get_stream (rev->handler = ngx_quic_init_stream_handler;) // 新建一个stream,对应一个虚拟连接stream connection ngx_quic_create_stream {sc->recv = ngx_quic_stream_recv;} // 添加到事件驱动中,添加到 ngx_posted_events 全局队列等待调度执行 ngx_quic_set_event // 解析http3的HEADERS帧 ngx_http_v3_parse_headers // 解析http3的DATA帧 ngx_http_v3_parse_data // 根据索引搜索QPACK静态索引表,静态表是全局的 ngx_http_v3_lookup_static // 根据索引搜索QPACK动态索引表,动态表是基于连接的 ngx_http_v3_lookup // 处理http3的一个请求头KV ngx_http_v3_process_header // 处理伪头,也就是请求行内容(method、uri、schema、host等) ngx_http_v3_process_pseudo_header

// 初始化quic connection // quic Packet 一个连接ID标识一条连接 ngx_quic_recvmsg -> ngx_http_init_connection[ls->handler] -> ngx_http_v3_init_stream -> ngx_quic_run{c->read->handler = ngx_quic_input_handler;} -> ngx_quic_handle_datagram -> ngx_quic_handle_packet{ngx_quic_new_connection; ngx_quic_handle_payload} -> ngx_quic_new_connection -> ngx_quic_open_sockets -> ngx_quic_listen{qsock->udp.connection = c; ngx_rbtree_insert(&c->listening->rbtree, &qsock->udp.node);} -> ngx_quic_handle_payload -> ngx_quic_handle_frames -> ngx_quic_handle_stream_frame -> ngx_quic_set_event // 处理后续udp报 ngx_quic_recvmsg {ngx_quic_lookup_connection; ngx_quic_get_socket;} -> ngx_quic_input_handler[rev->handler(rev)] -> ngx_quic_handle_datagram -> ngx_quic_handle_packet -> ngx_quic_handle_payload -> ngx_quic_handle_frames -> ngx_quic_handle_stream_frame -> ngx_quic_write_buffer // 处理连接上的quic frame ngx_process_events_and_timers -> ngx_event_process_posted -> ngx_quic_init_stream_handler[rev->handler] -> ngx_http_init_connection[ls->handler] -> ngx_http_v3_init_stream -> ngx_http_v3_init_request_stream -> ngx_http_v3_wait_request_handler -> ngx_http_v3_process_request -> ngx_http_process_request // 读取http3的body帧并解析payload ngx_http_read_client_request_body -> ngx_http_v3_read_request_body{r->read_event_handler = ngx_http_v3_read_client_request_body_handler;} -> ngx_http_v3_request_body_filter -> ngx_http_v3_parse_data

// 核心结构 ngx_http_v3_session_t // quic connection 一个连接ID对应一个结构,支持多个stream ngx_quic_connection_t // 基于connection的socket ngx_quic_socket_t // 一条stream对应一个该结构体 ngx_quic_stream_t

image image

参考

https://www.taohui.tech/2021/02/04/%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE/%E6%B7%B1%E5%85%A5%E5%89%96%E6%9E%90HTTP3%E5%8D%8F%E8%AE%AE/ https://www.zhihu.com/tardis/zm/art/228723188?source_id=1003 https://datatracker.ietf.org/doc/id/draft-ietf-quic-transport-29.html https://my.oschina.net/u/4273516/blog/8597013 检查是否支持Http3: https://http3check.net/

vislee commented 1 year ago

http3.0 HEADERS帧Lua用例代码:

local function parse_headers_frame(frame_data)
    -- 读取帧标志
    local flags = string.byte(frame_data, 1)

    -- 检查是否存在头部碎片
    local has_fragment = bit32.band(flags, 0x01) ~= 0
    if has_fragment then
        -- TODO: 处理头部碎片的情况
    end

    -- 获取头部长度
    local tmp = frame_data:sub(2, 3)
    local length = string.unpack(">I2", tmp)
    print("length:", length)

    -- 解析头部
    local headers = {}
    local pos = 4
    while pos <= length do
        -- 读取头部名长度
        local name_len = string.unpack(">I2", frame_data:sub(pos, pos+1))
        print("name_len:", name_len)
        pos = pos + 2

        -- 读取头部名
        local name = frame_data:sub(pos, pos+name_len-1)
        pos = pos + name_len

        -- 读取头部值长度
        local value_len = string.unpack(">I2", frame_data:sub(pos, pos+1))
        print("value_len:", value_len)
        pos = pos + 2

        -- 读取头部值
        local value = frame_data:sub(pos, pos+value_len-1)
        pos = pos + value_len

        -- 添加头部到结果列表
        table.insert(headers, {name = name, value = value})
    end

    return headers
end

-- 测试示例
local headers_frame_data = "\x01\x00\x0c\x00\x07example\x00\x0bexample.com"
local headers = parse_headers_frame(headers_frame_data)

for _, header in ipairs(headers) do
    print("Name:", header.name)
    print("Value:", header.value)
end