http trailer 与 http2

前言

最近做项目,发现一些项目使用 grpc,尤其是容器 K8S下,笔者在很久以前文章:https://blog.csdn.net/fenglllle/article/details/127829481

有一点非常奇怪的现象,在 h2 的返回的结果后面还有一个 header,这个实际上是 http trailer。

准备示例

在 github 为例:https://github.com/ma6174/blog/issues/22

go 语言

Go 复制代码
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"os/signal"
	"syscall"
)

func grpcStatus(w http.ResponseWriter, r *http.Request) {
	fmt.Printf("header: %+v\n", r.Header)
	fmt.Printf("trailer before read body: %+v\n", r.Trailer)
	data, _ := io.ReadAll(r.Body)
	w.Header().Add("Transfer-Encoding", "chunked")
//	w.Header().Add("Content-Length", "-1")
	w.Header().Add("Trailer", "grpc-status")
	fmt.Println("------------", string(data))
	w.Write([]byte("haha"))

	w.WriteHeader(200)
	w.Header().Set("grpc-status", "0")
}

func main() {
	go func() {
		http.HandleFunc("/demo", grpcStatus)
		err := http.ListenAndServe(":8080", nil)
		if err != nil {
			fmt.Println(err)
		}
	}()
	signalCh := make(chan os.Signal, 1)
	signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)

	sig := <-signalCh
	fmt.Printf("Received signal: %v\n", sig)

	fmt.Println("hello")
}

使用 postman 发送请求 localhost:8080/demo

没什么奇特的地方,非常常规,但是当我们抓包时

奇怪的事情发生了,在 chunked 的结束时,发现了 trailer header 数据

这里是返回数据,同理,如果对请求数据进行处理也会出现类似的效果

go 示例

Go 复制代码
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	_ "strconv"
	"strings"
)

type headerReader struct {
	reader io.Reader
	header http.Header
}

func (r *headerReader) Read(p []byte) (n int, err error) {
	n, err = r.reader.Read(p) //先读 body
	if err == io.EOF {
		//写 trailer header
		r.header.Set("grpc-status", "0")
	}
	return
}

func main() {
	h := &headerReader{
		reader: strings.NewReader("body"),
		header: http.Header{},
	}
	req, err := http.NewRequest("POST", "http://localhost:8080", h)
	if err != nil {
		panic(err)
	}
	req.ContentLength = -1
	req.Trailer = h.header //设置 trailer
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.Status)
	_, err = io.Copy(os.Stdout, resp.Body)
	if err != nil {
		panic(err)
	}
}

mac 下使用 nc -l 8080

nc 命令

-l Listen mode, for inbound connects

监听发现 trailer 的 header 在 body 后被读取出来了

因为 postman 没法在发送 body 后,再写 header,所以只能通过代码的方式

通过上面的实践,发现 http trailer 的执行方式:

  1. 使用 http chunked

  2. header 设置key 为Trailer的header key,多个 key 逗号,分割

  3. 先写,或者先读 body,然后设置 trailer 定义的 header key 和 value

使用 h2

回到最初的问题,如果真是 grpc,那么使用的 h2 的方式 trailer?换成 java 写一个springboot demo

java 复制代码
@RestController
public class DemoController {

    @GetMapping("/demo")
    public void demo(HttpServletResponse response) throws IOException {
        response.addHeader("Transfer-Encoding", "chunked");
        response.addHeader("Content-Length", String.valueOf(-1));
        response.addHeader("Trailer", "grpc-status");
        PrintWriter printWriter = response.getWriter();
        printWriter.print("body");
        printWriter.flush();
        response.addHeader("grpc-status", "0");
        printWriter.close();
    }
}

server.http2.enabled = true

然后强制发送 http2 的请求

huahua@huahuadeMac-mini ~ % curl --http2 http://localhost:8080/demo

curl: (92) Invalid HTTP header field was received: frame type: 1, stream: 1, name: [transfer-encoding], value: [chunked]

根据 http2 的标准:https://datatracker.ietf.org/doc/html/rfc7540#section-8.1

不能使用 chunked 编码,其实 http2 本身就是支持分块传输

但是 trailer 需要 chunked 传输,所以暂时还没有头绪解决,怎么使用 http2 执行 chunked http trailer

直到一篇文章:https://carlmastrangelo.com/blog/why-does-grpc-insist-on-trailers

讲述了 grpc 的设计,到是怎么失控的,在 http2 中分帧,所以不需要 chunked 编码了,可以直接发送 trailer

java 复制代码
@RestController
public class DemoController {

    @GetMapping("/demo")
    public void demo(HttpServletResponse response) throws IOException {
//        response.addHeader("Transfer-Encoding", "chunked");
//        response.addHeader("Content-Length", String.valueOf(-1));
        response.addHeader("Trailer", "grpc-status");
        PrintWriter printWriter = response.getWriter();
        printWriter.print("body");

//        response.addHeader("grpc-status", "0");
        response.setTrailerFields(()->{
            Map<String, String> map = new HashMap<>();
            map.put("grpc-status", "0");
            return map;
        });
        printWriter.flush();
        printWriter.close();
    }
}

发送请求,抓包,成功还原 http2 grpc 的设计理念

总结

经过实践还原了 grpc 的通信内容,通过 springboot 的 h2 达到 grpc 的 trailer 的方式,以后在 springboot 中试试,算是解决上一篇文章的遗留问题。不过居然有 http trailer 这玩意,笔者以前没来没用过,也没听到哪里有使用,算是对这个设计有了深刻的了解了。

相关推荐
野熊佩骑18 小时前
一文读懂Nginx 之 Ubuntu使用apt方式安装Nginx官方最新版本
linux·运维·服务器·nginx·ubuntu·http
minji...21 小时前
Linux 网络基础之传输层TCP(七)确认应答机制,超时重传机制,连接管理机制(三次握手四次挥手),流量控制,滑动窗口,快重传
linux·运维·服务器·网络·网络协议·tcp/ip·http
专吃海绵宝宝菠萝屋的派大星1 天前
spring Ai 开发的mcp-由sse改成Streamable HTTP
人工智能·spring·http
成空的梦想1 天前
免费 vs 付费国密 SSL 怎么选?
服务器·网络·网络协议·http·https·ssl
丑八怪大丑1 天前
XML_Tomcat_HTTP
xml·http·tomcat
minji...1 天前
Linux 网络基础之传输层TCP(六)TCP报头格式,TCP可靠性,序号/确认序号,窗口大,标志位,初识三次握手四次挥手
linux·运维·服务器·网络·网络协议·tcp/ip·http
Arman_2 天前
02 rusty-cat 实战:MeowClient 配置、任务参数、进度回调与暂停恢复
http·https·rust·tokio·文件分片上传·文件分片下载
wangjialelele2 天前
HTTP Cookie 和 Session
http·cookie·session
艾莉丝努力练剑2 天前
【Linux网络】Linux 网络编程:HTTP(一)协议初识
linux·运维·服务器·网络·tcp/ip·计算机网络·http
Arman_2 天前
01 Rust 大文件断点上传下载入门:用 rusty-cat 让上传下载更可靠
http·https·rust·tokio·大量阅读·文件分片上传下载