Protocol extension

Protocol extensions provided by Hertz.

Overview

Thanks to the layered design of Hertz, in addition to the HTTP1/HTTP2/HTTP3 protocol server that comes with the Hertz framework by default, users of the framework can also customize the protocol server through the protocol.Server or protocol.StreamServer interface.

Interface Definition

protocol.Server

This interface can be used to implement protocol servers based on byte stream transmission, such as HTTP1/HTTP2.

Note: If using this interface, the underlying network library needs to implement the network.Conn interface.

type Server interface {
	Serve(c context.Context, conn network.Conn) error
}

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
}

protocol.StreamServer

This interface can be used to implement streaming based protocol servers, such as HTTP3.

Note: If using this interface, the underlying network library needs to implement the network.streamConn interface.

type StreamServer interface {
	Serve(c context.Context, conn network.StreamConn) error
}

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
}

Three elements of protocol layer extension

Taking the protocol.Server interface as an example to illustrate the three elements of protocol layer extension, the extension of the protocol.StreamServer interface is similar.

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.

Sample Code

Taking the protocol.Server interface as an example, the protocol.StreamServer interface is similar.

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()
}

Last modified June 26, 2024 : doc: add construct use case (#1098) (83b7cf9)