HTTP2
HTTP/2 是对 HTTP “在线” 表达方式的一种替代。它并不是对协议的彻底重写;HTTP 方法、状态码和语义都是一样的,而且应该可以使用与 HTTP/1.x 相同的 API(可能会有一些小的补充)来表示协议。
协议的侧重点是性能:缩短用户感知的延迟、减少网络和服务器资源的使用。一个主要目标是允许使用从浏览器到网站的单一连接。
Hertz 同时支持 h2 和 h2c。参考了 net/http2 的实现。
示例代码
h2
package main
import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/network/standard"
	"github.com/cloudwego/hertz/pkg/protocol"
	"github.com/hertz-contrib/http2/config"
	"github.com/hertz-contrib/http2/factory"
)
const (
	keyPEM = `<your key PEM>`
	certPEM = `<your cert PEM>`
)
func runClient() {
	cli, _ := client.NewClient()
	cli.SetClientFactory(factory.NewClientFactory(
		config.WithDialer(standard.NewDialer()),
		config.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})))
	v, _ := json.Marshal(map[string]string{
		"hello":    "world",
		"protocol": "h2",
	})
	for {
		time.Sleep(time.Second * 1)
		req, rsp := protocol.AcquireRequest(), protocol.AcquireResponse()
		req.SetMethod("POST")
		req.SetRequestURI("https://127.0.0.1:8888")
		req.SetBody(v)
		err := cli.Do(context.Background(), req, rsp)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("[client]: received body: %s\n", string(rsp.Body()))
	}
}
func main() {
	cfg := &tls.Config{
		MinVersion:       tls.VersionTLS12,
		CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
		CipherSuites: []uint16{
			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
		},
	}
	cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
	if err != nil {
		fmt.Println(err.Error())
	}
	cfg.Certificates = append(cfg.Certificates, cert)
	h := server.New(server.WithHostPorts(":8888"), server.WithALPN(true), server.WithTLS(cfg))
	// register http2 server factory
	h.AddProtocol("h2", factory.NewServerFactory(
		config.WithReadTimeout(time.Minute),
		config.WithDisableKeepAlive(false)))
	cfg.NextProtos = append(cfg.NextProtos, "h2")
	h.POST("/", func(ctx context.Context, c *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(c.Request.Body(), &j)
		fmt.Printf("[server]: received request: %+v\n", j)
		r := map[string]string{
			"msg": "hello world",
		}
		for k, v := range j {
			r[k] = v
		}
		c.JSON(http.StatusOK, r)
	})
	go runClient()
	h.Spin()
}
h2c
package main
import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/protocol"
	"github.com/hertz-contrib/http2/config"
	"github.com/hertz-contrib/http2/factory"
)
func runClient() {
	c, _ := client.NewClient()
	c.SetClientFactory(factory.NewClientFactory(config.WithAllowHTTP(true)))
	v, _ := json.Marshal(map[string]string{
		"hello":    "world",
		"protocol": "h2c",
	})
	for {
		time.Sleep(time.Second * 1)
		req, rsp := protocol.AcquireRequest(), protocol.AcquireResponse()
		req.SetMethod("POST")
		req.SetRequestURI("http://127.0.0.1:8888")
		req.SetBody(v)
		err := c.Do(context.Background(), req, rsp)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("client received body: %s\n", string(rsp.Body()))
	}
}
func main() {
	h := server.New(server.WithHostPorts(":8888"), server.WithH2C(true))
	// register http2 server factory
	h.AddProtocol("h2", factory.NewServerFactory())
	h.POST("/", func(ctx context.Context, c *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(c.Request.Body(), &j)
		fmt.Printf("server received request: %+v\n", j)
		r := map[string]string{
			"msg": "hello world",
		}
		for k, v := range j {
			r[k] = v
		}
		c.JSON(http.StatusOK, r)
	})
	go runClient()
	h.Spin()
}
配置
服务端
| 配置 | 默认值 | 介绍 | 
|---|---|---|
ReadTimeout | 0 | 建立连接后,从服务器读取到可用资源的超时时间 | 
DisableKeepAlive | false | 是否关闭 Keep-Alive 模式 | 
示例代码:
package main
import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/network/standard"
	"github.com/cloudwego/hertz/pkg/protocol"
	"github.com/hertz-contrib/http2/config"
	"github.com/hertz-contrib/http2/factory"
)
const (
	keyPEM = `<your key PEM>`
	certPEM = `<your cert PEM>`
)
func runClient() {
	cli, _ := client.NewClient()
	cli.SetClientFactory(factory.NewClientFactory(
		config.WithDialer(standard.NewDialer()),
		config.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})))
	v, _ := json.Marshal(map[string]string{
		"hello":    "world",
		"protocol": "h2",
	})
	for {
		time.Sleep(time.Second * 1)
		req, rsp := protocol.AcquireRequest(), protocol.AcquireResponse()
		req.SetMethod("POST")
		req.SetRequestURI("https://127.0.0.1:8888")
		req.SetBody(v)
		err := cli.Do(context.Background(), req, rsp)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("[client]: received body: %s\n", string(rsp.Body()))
	}
}
func main() {
	cfg := &tls.Config{
		MinVersion:       tls.VersionTLS12,
		CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
		CipherSuites: []uint16{
			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
		},
	}
	cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
	if err != nil {
		fmt.Println(err.Error())
	}
	cfg.Certificates = append(cfg.Certificates, cert)
	h := server.New(server.WithHostPorts(":8888"), server.WithALPN(true), server.WithTLS(cfg))
	// register http2 server factory
	h.AddProtocol("h2", factory.NewServerFactory(
		config.WithReadTimeout(time.Minute),
		config.WithDisableKeepAlive(false)))
	cfg.NextProtos = append(cfg.NextProtos, "h2")
	h.POST("/", func(ctx context.Context, c *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(c.Request.Body(), &j)
		fmt.Printf("[server]: received request: %+v\n", j)
		r := map[string]string{
			"msg": "hello world",
		}
		for k, v := range j {
			r[k] = v
		}
		c.JSON(http.StatusOK, r)
	})
	go runClient()
	h.Spin()
}
WithReadTimeout
用于设置 ReadTimeout,默认值为 0。
函数签名:
func WithReadTimeout(t time.Duration) Option
WithDisableKeepAlive
用于设置是否禁用 keep-alive,默认不禁用。
函数签名:
func WithDisableKeepAlive(disableKeepAlive bool) Option
客户端
| 配置 | 默认值 | 介绍 | 
|---|---|---|
MaxHeaderListSize | 0,指使用默认的限制(10MB) | 指 http2 规范中的 SETTINGS_MAX_HEADER_LIST_SIZE。 | 
AllowHTTP | false | 设置是否允许 http,h2c 模式的开关 | 
ReadIdleTimeout | 0,即不进行健康检查 | 若连接在该段时间间隔内未接收到任何帧,将使用 ping 帧进行健康检查。 | 
PingTimeout | 15s | 超时时间,如果未收到对 Ping 的响应,连接将在该超时时间后关闭。 | 
WriteByteTimeout | 0 | 若在该段时间间隔内未写入任何数据,将关闭连接。 | 
StrictMaxConcurrentStreams | false | 设置服务器的 SETTINGS_MAX_CONCURRENT_STREAMS 是否应该被全局使用。 | 
DialTimeout | 1s | 与主机建立新连接的超时时间。 | 
MaxIdleConnDuration | 0 | 闲置的长连接在该段时间后关闭。 | 
DisableKeepAlive | false | 是否在每次请求后关闭连接。 | 
Dialer | netpoll.NewDialer() | 用于设置拨号器。 | 
TLSConfig | nil | TLS 配置 | 
RetryConfig | nil | 所有与重试有关的配置 | 
示例代码:
package main
import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
	"github.com/cloudwego/hertz/pkg/app/client/retry"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/client"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/network/standard"
	"github.com/cloudwego/hertz/pkg/protocol"
	"github.com/hertz-contrib/http2/config"
	"github.com/hertz-contrib/http2/factory"
)
const (
	keyPEM = `<your key PEM>`
	certPEM = `<your cert PEM>`
)
func runClient() {
	cli, _ := client.NewClient()
	cli.SetClientFactory(factory.NewClientFactory(
		config.WithDialTimeout(3*time.Second),
		config.WithReadIdleTimeout(1*time.Second),
		config.WithWriteByteTimeout(time.Second),
		config.WithPingTimeout(time.Minute),
		config.WithMaxIdleConnDuration(2*time.Second),
		config.WithClientDisableKeepAlive(true),     //Close Connection after each request
		config.WithStrictMaxConcurrentStreams(true), // Set the server's SETTINGS_MAX_CONCURRENT_STREAMS to be respected globally.
		config.WithDialer(standard.NewDialer()),     // You can customize dialer here
		config.WithMaxHeaderListSize(0xffffffff),    // Set SETTINGS_MAX_HEADER_LIST_SIZE to unlimited.
		config.WithMaxIdempotentCallAttempts(3),
		config.WithRetryConfig(
			retry.WithMaxAttemptTimes(3),
			retry.WithInitDelay(2*time.Millisecond),
			retry.WithMaxDelay(200*time.Millisecond),
			retry.WithMaxJitter(30*time.Millisecond),
			retry.WithDelayPolicy(retry.FixedDelayPolicy),
		),
		config.WithStrictMaxConcurrentStreams(true), // Set the server's SETTINGS_MAX_CONCURRENT_STREAMS to be respected globally.
		config.WithTLSConfig(&tls.Config{
			SessionTicketsDisabled: false,
			InsecureSkipVerify:     true,
		}),
	))
	v, _ := json.Marshal(map[string]string{
		"hello":    "world",
		"protocol": "h2",
	})
	for {
		time.Sleep(time.Second * 1)
		req, rsp := protocol.AcquireRequest(), protocol.AcquireResponse()
		req.SetMethod("POST")
		req.SetRequestURI("https://127.0.0.1:8888")
		req.SetBody(v)
		err := cli.Do(context.Background(), req, rsp)
		if err != nil {
			fmt.Println(err)
			return
		}
		fmt.Printf("[client]: received body: %s\n", string(rsp.Body()))
	}
}
func main() {
	cfg := &tls.Config{
		MinVersion:       tls.VersionTLS12,
		CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
		CipherSuites: []uint16{
			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
		},
	}
	cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
	if err != nil {
		fmt.Println(err.Error())
	}
	cfg.Certificates = append(cfg.Certificates, cert)
	h := server.New(server.WithHostPorts(":8888"), server.WithALPN(true), server.WithTLS(cfg))
	// register http2 server factory
	h.AddProtocol("h2", factory.NewServerFactory(
		config.WithReadTimeout(time.Minute),
		config.WithDisableKeepAlive(false)))
	cfg.NextProtos = append(cfg.NextProtos, "h2")
	h.POST("/", func(ctx context.Context, c *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(c.Request.Body(), &j)
		fmt.Printf("[server]: received request: %+v\n", j)
		r := map[string]string{
			"msg": "hello world",
		}
		for k, v := range j {
			r[k] = v
		}
		c.JSON(http.StatusOK, r)
	})
	go runClient()
	h.Spin()
}
WithMaxHeaderListSize
用于设置 SETTINGS_MAX_HEADER_LIST_SIZE。
与 HTTP2 规范不同,这里的 0 表示使用默认限制(目前是 10MB)。如果想表示无限,可以设置为一个尽可能大的值(0xffffffff 或 1<<32-1)。
函数签名:
func WithMaxHeaderListSize(maxHeaderListSize uint32) ClientOption
WithReadIdleTimeout
用于设置读取超时时间,超时后将使用 ping 帧进行健康检查。
注意,一个 ping 响应将被视为一个接收帧,所以如果连接上没有其他流量,健康检查将在每一个读取超时时间间隔内进行。
默认值为 0 表示不执行健康检查。
函数签名:
func WithReadIdleTimeout(readIdleTimeout time.Duration) ClientOption
WithWriteByteTimeout
用于设置写入超时时间,超时后连接将被关闭。当数据可以写入时开始计时,并随数据的写入不断延长。
函数签名:
func WithWriteByteTimeout(writeByteTimeout time.Duration) ClientOption
WithStrictMaxConcurrentStreams
用来设置服务器的 SETTINGS_MAX_CONCURRENT_STREAMS 是否应该被全局使用。
函数签名:
func WithStrictMaxConcurrentStreams(strictMaxConcurrentStreams bool) ClientOption
WithPingTimeout
设置 ping 响应的超时时间,如果未收到对 Ping 的响应,连接将在该超时时间后关闭。
默认为 15s
函数签名:
func WithPingTimeout(pt time.Duration) ClientOption
WithAllowHTTP
用于设置是否允许 http。如果启用,客户端将使用 h2c 模式。默认不启用。
函数签名:
func WithAllowHTTP(allow bool) ClientOption
WithDialer
支持自定义拨号器,默认为 netpoll.NewDialer()。
函数签名:
func WithDialer(d network.Dialer) ClientOption
接口定义:
type Dialer interface {
	// DialConnection is used to dial the peer end.
	DialConnection(network, address string, timeout time.Duration, tlsConfig *tls.Config) (conn Conn, err error)
	// DialTimeout is used to dial the peer end with a timeout.
	//
	// NOTE: Not recommended to use this function. Just for compatibility.
	DialTimeout(network, address string, timeout time.Duration, tlsConfig *tls.Config) (conn net.Conn, err error)
	// AddTLS will transfer a common connection to a tls connection.
	AddTLS(conn Conn, tlsConfig *tls.Config) (Conn, error)
}
WithDialTimeout
用于设置与主机建立新连接的超时时间,默认为 1s。
函数签名:
func WithDialTimeout(timeout time.Duration) ClientOption
WithTLSConfig
用于自定义 TLS 配置。
函数签名:
func WithTLSConfig(tlsConfig *tls.Config) ClientOption
WithMaxIdleConnDuration
用于设置长连接的最长闲置时间,超过该时间后连接关闭。默认为 0。
函数签名:
func WithMaxIdleConnDuration(d time.Duration) ClientOption
WithMaxIdempotentCallAttempts
设置idempotent calls的最大尝试次数。
函数签名:
func WithMaxIdempotentCallAttempts(n int) ClientOption
WithRetryConfig
用于设置与重试有关的配置。
函数签名:
func WithRetryConfig(opts ...retry.Option) ClientOption
WithClientDisableKeepAlive
用于设置是否在每次请求后关闭连接。默认为 false。
函数签名:
func WithClientDisableKeepAlive(disable bool) ClientOption
更多用法示例详见 hertz-contrib/http2。