HTTP2

Hertz 同时支持 h2 和 h2c。参考了 net/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(c context.Context, ctx *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(ctx.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
		}
		ctx.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(c context.Context, ctx *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(ctx.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
		}
		ctx.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(c context.Context, ctx *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(ctx.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
		}
		ctx.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(c context.Context, ctx *app.RequestContext) {
		var j map[string]string
		_ = json.Unmarshal(ctx.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
		}
		ctx.JSON(http.StatusOK, r)
	})

	go runClient()

	h.Spin()
}

WithMaxHeaderListSize

用于设置 SETTINGS_MAX_HEADER_LIST_SIZE

与 HTTP2 规范不同,这里的 0 表示使用默认限制(目前是 10MB)。如果想表示无限,可以设置为一个尽可能大的值(0xffffffff1<<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


最后修改 January 18, 2024 : Upload volo blog (#936) (1fc8abb)