Protocol extension
Overview
Thanks to the layered design of Hertz, in addition to the HTTP1/HTTP2 (to be open source) protocol server that comes with the Hertz framework by default, users can easily add/customize protocol processing logic that meets the needs of their own business scenarios according to their own needs.
In short, a server that implements the following interface can be added to Hertz as a custom extension server:
type Server interface {
Serve(c context.Context, conn network.Conn) error
}
Three elements of protocol layer extension
Protocol layer server initialization
Because the interface mentioned in the overview is actually a standard callback after the data is prepared at the network layer, the processing logic of our protocol layer will only be entered after a new request is established for a connection.
In this logic, we can customize the protocol parsing method, introduce business Handler execution, write data back and other standard behaviors of the protocol layer. This is also the core logic of our custom server.
type myServer struct{
xxx
xxx
}
func (s *myServer) Serve (c context.Context, conn network.Conn) error{
// protocol parsing
...
// Go to the logic function of business registration (Route, Middleware, Handler...)
...
// write data back
...
}
Defining a protocol processing logic is as simple as that! However, the two steps of parsing the protocol and writing data back can be easily achieved through the conn
interface provided in the input parameters, but how to go to the logical function of business registration?
Interaction with upper-level logic
A complete protocol must introduce business logic control (except for very few special situations), so how does the custom protocol in the Hertz framework realize this part of the ability? In fact, in the process of custom server initialization, the framework has naturally handed over this part of the capabilities to the custom protocol server.
type ServerFactory interface {
New(core Core) (server protocol.Server, err error)
}
// Core is the core interface that promises to be provided for the protocol layer extensions
type Core interface {
// IsRunning Check whether engine is running or not
IsRunning() bool
// A RequestContext pool ready for protocol server impl
GetCtxPool() *sync.Pool
// Business logic entrance
// After pre-read works, protocol server may call this method
// to introduce the middlewares and handlers
ServeHTTP(c context.Context, ctx *app.RequestContext)
// GetTracer for tracing requirement
GetTracer() tracer.Controller
}
A custom server only needs to implement a protocol server generation factory according to the above interface. The Core in the parameters actually includes the introduction of upper-layer logic interaction and the specific implementation of other core application layer interfaces. When initializing a custom server, normally you only need to save the Core to the server. When you need to transfer to the business logic, you can guide the process to the application layer processing logic (Route, Middleware, Logic Handler) through the Core. When the business logic is executed and returned, further packets can be written back based on the business data.
type myServer struct{
suite.Core
xxx
}
func (s *myServer) Serve (c context.Context, conn network.Conn) error{
// protocol parsing
...
Core.ServeHTTP(c, ctx)
// write data back
...
}
So far, a custom protocol layer server has been developed.
Registration of custom protocol server into Hertz
After completing the development of the server generation factory according to the above interface, it is very easy to load it into Hertz. Hertz’s core engine naturally provides an interface for registering a custom protocol server:
func (engine *Engine) AddProtocol(protocol string, factory suite.ServerFactory) {
engine.protocolSuite.Add(protocol, factory)
}
It is only necessary to register the user’s custom server generation factory with the engine according to the parameters specified by the interface. But it is worth noting that the protocol (string) registered here actually corresponds to the protocol negotiation key in ALPN (Application-Layer Protocol Negotiation), so if you want to access a custom protocol server through ALPN , directly specify the key as the corresponding key during ALPN negotiation. Currently, Hertz integrates an HTTP1 protocol server by default (the corresponding key is “http/1.1”). If you need to customize the HTTP1 protocol processing logic, you can directly specify the key as “http/1.1” within AddProtocol
to overwrite.
Example
package main
import (
"bytes"
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/errors"
"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/cloudwego/hertz/pkg/network"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/protocol/suite"
)
type myServer struct {
suite.Core
}
func (m myServer) Serve(c context.Context, conn network.Conn) error {
firstThreeBytes, _ := conn.Peek(3)
if !bytes.Equal(firstThreeBytes, []byte("GET")) {
return errors.NewPublic("not a GET method")
}
ctx := m.GetCtxPool().Get().(*app.RequestContext)
defer func() {
m.GetCtxPool().Put(ctx)
conn.Skip(conn.Len())
conn.Flush()
}()
ctx.Request.SetMethod("GET")
ctx.Request.SetRequestURI("/test")
m.ServeHTTP(c, ctx)
conn.WriteBinary([]byte("HTTP/1.1 200 OK\n" +
"Server: hertz\n" +
"Date: Sun, 29 May 2022 10:49:33 GMT\n" +
"Content-Type: text/plain; charset=utf-8\n" +
"Content-Length: 2\n\nok\n"))
return nil
}
type serverFactory struct {
}
func (s *serverFactory) New(core suite.Core) (server protocol.Server, err error) {
return &myServer{
core,
}, nil
}
func main() {
h := server.New()
h.GET("/test", func(c context.Context, ctx *app.RequestContext) {
hlog.Info("in handler")
})
h.AddProtocol("http/1.1", &serverFactory{})
h.Spin()
}