HTTP/2实现中PRI方法引发问题:原因及处理方式咨询
首先,咱们得搞清楚你为什么会看到PRI请求——这根本不是客户端发起的实际业务请求,而是HTTP/2连接初始化的前缀标识,核心问题出在你用了错误的工具去解析HTTP/2连接。
为什么会收到PRI?
HTTP/2是二进制帧协议,和HTTP/1.x的文本格式完全不同。当客户端通过ALPN协商确定使用h2协议后,首先会发送一段固定的连接前缀:
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
这段内容的作用是触发HTTP/1.x服务器报错(以此确认服务器是否真的支持HTTP/2),但对于正确的HTTP/2服务器来说,它是连接初始化的一部分,需要被HTTP/2的专用解析器处理,而不是当成HTTP/1的请求来解析。
你现在用http.ReadRequest来读取请求,这个方法是专门为HTTP/1.x设计的文本请求解析器,它会把这段二进制协议的前缀当成一个畸形的HTTP/1请求,所以才会读出PRI这个不存在的请求方法。
你不需要手动处理SETTINGS帧或PRI前缀
Go标准库已经内置了完整的HTTP/2支持,完全不需要你手动处理底层的帧交换(包括SETTINGS帧的发送、接收和确认)。问题的根源是你没有使用Go的HTTP/2运行时来处理连接,而是试图用HTTP/1的方式手动解析请求。
正确的实现方式
方案1:直接使用标准库http.Server(推荐)
只要你的TLS配置中NextProtos包含"h2",http.Server会自动处理ALPN协商,自动区分HTTP/1和HTTP/2连接,并且用对应的协议栈处理请求。你只需要像平时写HTTP服务一样定义Handler即可:
package main import ( "crypto/rand" "crypto/tls" "net/http" "time" ) func main() { // 加载你的证书(假设cert已经是tls.Certificate类型) cert, err := tls.LoadX509KeyPair("server.crt", "server.key") if err != nil { panic(err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: "mysrvr", NextProtos: []string{"h2", "http/1.1", "http/1.0"}, Time: time.Now, Rand: rand.Reader, } server := &http.Server{ Addr: ":8000", TLSConfig: tlsConfig, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 这里处理你的请求,不管是HTTP/1还是HTTP/2,接口完全一致 w.WriteHeader(http.StatusOK) w.Write([]byte("Hello from " + r.Proto)) }), } // 启动TLS服务,因为TLSConfig已经包含证书,所以可以传空字符串 if err := server.ListenAndServeTLS("", ""); err != nil { panic(err) } }
用这个代码启动服务后,再用你的curl命令测试:
$ curl -v https://127.0.0.1:8000 -k --http2
你会发现请求被正常处理,Handler里拿到的r.Proto会是HTTP/2.0,而且完全看不到PRI前缀——因为底层的HTTP/2栈已经帮你处理了连接初始化的所有细节。
方案2:手动管理连接时使用http2.Server
如果你因为某些原因必须手动管理连接(比如自定义监听逻辑),那应该用golang.org/x/net/http2包的Server来处理HTTP/2连接,而不是用http.ReadRequest:
package main import ( "crypto/rand" "crypto/tls" "net" "net/http" "time" "golang.org/x/net/http2" ) func yourHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("Hello from " + r.Proto)) } func main() { cert, err := tls.LoadX509KeyPair("server.crt", "server.key") if err != nil { panic(err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, ServerName: "mysrvr", NextProtos: []string{"h2", "http/1.1", "http/1.0"}, Time: time.Now, Rand: rand.Reader, } ln, err := tls.Listen("tcp", ":8000", tlsConfig) if err != nil { panic(err) } defer ln.Close() http2Server := &http2.Server{} http1Server := &http.Server{Handler: http.HandlerFunc(yourHandler)} for { conn, err := ln.Accept() if err != nil { // 处理错误 continue } tlsConn := conn.(*tls.Conn) if err := tlsConn.Handshake(); err != nil { conn.Close() continue } // 获取协商后的协议 proto := tlsConn.ConnectionState().NegotiatedProtocol go func(c net.Conn) { defer c.Close() if proto == "h2" { // 用HTTP/2栈处理连接 http2Server.ServeConn(c, &http2.ServeConnOpts{Handler: http.HandlerFunc(yourHandler)}) } else { // 用HTTP/1栈处理连接 http1Server.Serve(c) } }(conn) } }
总结
- 看到
PRI请求是因为你用HTTP/1的解析器处理了HTTP/2连接,这是错误的使用方式 - 不需要手动解析PRI前缀或发送SETTINGS帧,Go的HTTP/2运行时会自动完成这些底层操作
- 推荐直接使用标准库的
http.Server,它已经无缝支持HTTP/1和HTTP/2的自动切换
内容的提问来源于stack exchange,提问作者Sam

