#http-parser #http #h2 #h1 #clevercloud #foo-bar

kawa

为Sōzu设计的HTTP/1.1和HTTP/2.0的无差别表示,用于解析、生成和转换HTTP消息,实现零拷贝。

7个版本

0.6.7 2024年7月16日
0.6.6 2024年4月5日
0.6.5 2024年1月25日
0.6.4 2023年11月15日
0.6.1 2023年5月4日

#156 in 网络编程

Download history 79/week @ 2024-04-29 58/week @ 2024-05-06 27/week @ 2024-05-13 70/week @ 2024-05-20 103/week @ 2024-05-27 131/week @ 2024-06-03 24/week @ 2024-06-10 93/week @ 2024-06-17 94/week @ 2024-06-24 51/week @ 2024-07-01 330/week @ 2024-07-08 247/week @ 2024-07-15 93/week @ 2024-07-22 62/week @ 2024-07-29 78/week @ 2024-08-05 26/week @ 2024-08-12

264 每月下载量
3 个crate中使用(通过 sozu-lib

自定义许可证

120KB
2.5K SLoC

Kawa

用于解析、生成和转换HTTP消息的HTTP/1.1和HTTP/2.0的无差别表示,实现零拷贝。

原则

考虑以下存储在Buffer中的HTTP/1.1响应

HTTP/1.1 200 OK
Transfer-Encoding: chunked     // the body of the response is streamed
Connection: Keep-Alive
User-Agent: curl/7.43.0
Trailer: Foo                   // declares a trailer header named "Foo"

4                              // declares one chunk of 4 bytes
Wiki
5                              // declares one chunk of 5 bytes
pedia
0                              // declares one chunk of 0 byte (the last chunk)
Foo: bar                       // trailer header "Foo"

HTTP通用表示

它可以在原地解析,提取基本内容(头部名称、值...)并存储为HTTP通用块的向量。Kawa是HTTP的中介,无协议表示

kawa_blocks: [
    StatusLine::Response(V11, Slice("200"), Slice("OK")),
    Header(Slice("Transfer-Encoding"), Slice("chunked")),
    Header(Slice("Connection"), Slice("Keep-Alive")),
    Header(Slice("User-Agent"), Slice("curl/7.43.0")),
    Header(Slice("Trailer"), Slice("Foo")),
    Flags(END_HEADER),
    ChunkHeader(Slice("4")),
    Chunk(Slice("Wiki")),
    Flags(END_CHUNK),
    ChunkHeader(Slice("5")),
    Chunk(Slice("pedia")),
    Flags(END_CHUNK),
    Flags(END_BODY),
    Header(Slice("Foo"), Slice("bar")),
    Flags(END_HEADER | END_STREAM),
]

注意:ChunkHeader是唯一的协议特定Block。它包含HTTP/1.1分块头中的分块大小。HTTP/2转换器可以安全忽略它们。持有上下文相关信息的Flags块,允许转换器无状态。

重要的是,Chunk块不一定包含整个分块。它们可能只包含更大的分块的一部分。这意味着这两种表示严格相同

kawa_full_chunk: [
    ChunkHeader(Slice("4")),
    Chunk(Slice("Wiki")),
    Flags(END_CHUNK),
]
kawa_fragmented_chunk: [
    ChunkHeader(Slice("4")),
    Chunk(Slice("Wi")),
    Chunk(Slice("k")),
    Chunk(Slice("i")),
    Flags(END_CHUNK),
]

注意:这样做是为了在不等待可能非常大的分块完全到达的情况下前进解析头。这种方案允许更有效的流处理,并防止解析器在无法装入其缓冲区的较大分块上软锁定。

使用切片引用缓冲区内容

注意,Blocks从不复制数据。它们使用Store::Slices引用请求的部分,这些切片只包含起始索引和长度。可以按以下方式查看Buffer,用括号标记引用的数据

HTTP/1.1 [200] [OK]
[Transfer-Encoding]: [chunked]
[Connection]: [Keep-Alive]
[User-Agent]: [curl/7.43.0]
[Trailer]: [Foo]

[4]
[Wiki]
[5]
[pedia]
0
[Foo]: [bar]

注意:从括号之外的一切都是无用的,永远不会被使用

Kawa用例

假设我们想

  • 删除“User-Agent”头部,
  • 添加“Sozu-id”头部,
  • 将头信息 "Connection" 改为 "close",
  • 将尾部 "Foo" 改为 "bazz",

无论底层协议(HTTP/1 或 HTTP/2)如何,都可以使用通用的 Kawa 表示方法完成这些操作。

    kawa_blocks.remove(3); // remove "User-Agent" header
    kawa_blocks.insert(3, Header(Static("Sozu-id"), Vec(sozu_id.as_bytes().to_vec())));
    kawa_blocks[2].val.modify("close");
    kawa_blocks[13].val.modify("bazz");

注意:应仅使用 modify 与将要丢弃的动态值,以提供适当的生命周期。对于静态值(如 "close"),请使用 Store::Static,这只是示例。例如,kawa_blocks[2].val = Static("close") 会更高效。

kawa_blocks: [
    StatusLine::Response(V11, Slice("200"), Slice("OK")),
    Header(Slice("Transfer-Encoding"), Slice("chunked")),
    // "close" is shorter than "Keep-Alive" so it was written in place and kept as a Slice
    Header(Slice("Connection"), Slice("close")),
    Header(Static("Sozu-id"), Vec("SOZUBALANCEID")),
    Header(Slice("Trailer"), Slice("Foo")),
    Flags(END_HEADER),
    ChunkHeader(Slice("4")),
    Chunk(Slice("Wiki")),
    Flags(END_CHUNK),
    ChunkHeader(Slice("5")),
    Chunk(Slice("pedia")),
    Flags(END_CHUNK),
    Flags(END_BODY),
    // "bazz" is longer than "bar" so it was dynamically allocated, this may change in the future
    Header(Slice("Foo"), Vec("bazz"))
    Flags(END_HEADER | END_STREAM),
]

现在缓冲区看起来是这样的

HTTP/1.1 [200] [OK]
[Transfer-Encoding]: [chunked]
[Connection]: [close]Alive     // "close" written in place and Slice adjusted
User-Agent: curl/7.43.0        // no references to this line
[Trailer]: [Foo]

[4]
[Wiki]
[5]
[pedia]
0
[Foo]: bar                     // no reference to "bar"

现在,响应已成功编辑,我们可以将其转换回特定的协议。为了简化,让我们将其转换回 HTTP/1

kawa_blocks: [] // Blocks are consumed
out: [
    // StatusLine::Request
    Static("HTTP/1.1"),
    Static(" "),
    Slice("200"),
    Static(" "),
    Slice("OK")
    Static("\r\n"),

    // Header
    Slice("Transfer-Encoding"),
    Static(": "),
    Slice("chunked"),
    Static("\r\n"),

    // Header
    Slice("Connection"),
    Static(": "),
    Slice("close"),
    Static("\r\n"),

    // Header
    Static("Sozu-id"),
    Static(": "),
    Vec("SOZUBALANCEID"),
    Static("\r\n"),

    // Header
    Slice("Trailer"),
    Static(": "),
    Slice("Foo"),
    Static("\r\n"),

    // Flags(END_HEADER)
    Static("\r\n"),

    // ChunkHeader
    Slice("4")
    Static("\r\n")
    // Chunk
    Slice("Wiki")
    // Flags(END_CHUNK)
    Static("\r\n")

    // ChunkHeader
    Slice("5")
    Static("\r\n")
    // Chunk
    Slice("pedia")
    // Flags(END_CHUNK)
    Static("\r\n")

    // Flags(END_BODY),
    Static("0\r\n")

    // Header
    Slice("Foo"),
    Static(": "),
    Vec("bazz"),
    Static("\r\n"),

    // Flags(END_HEADER | END_STREAM)
    Static("\r\n"),
]

每个元素都以 u8 的切片形式存储数据,无论是静态的、动态的还是来自响应缓冲区。可以从这种表示形式构建一个 IoSlice 向量,并将其高效地发送到套接字。这产生了最终的响应

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Connection: close
Sozu-id: SOZUBALANCEID
Trailer: Foo

4
Wiki
5
pedia
0
Foo: bazz

内存管理

假设套接字只写到了 "Wikipedia" 的 "Wi"(109 字节)。在每次写入后,应使用已写入的字节数调用 Kawa::consume。这会指示 Kawa 释放其 out 向量中的不必要的 Stores,并在可能的情况下回收其 Buffer 中的空间。在我们的例子中,从 out 中遍历和丢弃 Stores 后仍然保留

out: [
    // <-- previous Stores were completely written so they were removed
    Slice("ki"),    // Slice was partially written and updated accordingly
    Static("\r\n"),
    Slice("5"),
    Static("\r\n"),
    Slice("pedia"),
    Static("\r\n"),
    Static("0\r\n"),
    Slice("Foo"),
    Static(": "),
    Vec("bazz"),
    Static("\r\n"),
    Static("\r\n"),
]

请求缓冲区中的大多数数据不再被引用,现在是无用的

HTTP/1.1 200 OK
Transfer-Encoding: chunked
Connection: closeAlive
User-Agent: curl/7.43.0
Trailer: Foo

4
Wi[ki]
[5]
[pedia]
0
[Foo]: bar

这可以通过 Kawa::leftmost_ref 来测量,它返回最左边的 Store::Slice 的起始位置,指示 Buffer 中该点之前的所有内容都是未使用的。在这里,它将返回 115。将使用此值调用 Buffer::consume。如果 Buffer 认为应该将其数据移动以释放此空间(Buffer::should_shift),则将调用 Buffer::shift 以将数据通过 memmoving 移回到缓冲区的起始位置。缓冲区将看起来像这样

ki
5
pedia
0
Foo: bar

注意:这是本模块中唯一的复制数据实例,并且是必要的,除非我们更改 Buffer 的数据结构(例如使用真正的环形缓冲区)。尽管如此,这应该微不足道,因为大多数移动只复制 0 或非常少的字节。

因此,输出向量中剩余的 Store::Slices 引用了已移动的数据。

out: [
    Slice("ki"),    // references data starting at index 115
    Static("\r\n"),
    Slice("5"),     // references data starting at index 119
    Static("\r\n"),
    Slice("pedia"), // references...
    Static("\r\n"),
    Static("0\r\n"),
    Slice("Foo"),
    Static(": "),
    Vec("bazz"),
    Static("\r\n"),
    Static("\r\n"),
]

为了将 Store::Slices 与新缓冲区同步,使用丢弃的字节数调用 Kawa::push_left 以重新定位数据

out: [
    Slice("ki"),    // references data starting at index 0
    Static("\r\n"),
    Slice("5"),     // references data starting at index 4
    Static("\r\n"),
    Slice("pedia"), // references...
    Static("\r\n"),
    Static("0\r\n"),
    Slice("Foo"),
    Static(": "),
    Vec("bazz"),
    Static("\r\n"),
    Static("\r\n"),
]
[ki]
[5]
[pedia]
0
[Foo]: bar

依赖关系

~1MB
~19K SLoC