gRPC + gRPC-Web + gRPC-Gateway + HTTP(S) hybrid server
package main
import (
"github.com/sarafanfm/mtserver"
)
func main() {
server := mtserver.New()
// optionally you can handle any waitgroup job errors
server.OnJobError = func(err error) {
panic(err)
}
// Add endpoints, register gRPC services
server.Run()
}
Each endpoint is a gRPC or HTTP server, or both. You can specify positive port for enable transport or non-positive for disable it.
http := server.AddEndpoint("http", &mtserver.EndpointOpts{PORT_HTTP: 80})
grpc := server.AddEndpoint("grpc", &mtserver.EndpointOpts{PORT_GRPC: 50021})
hybrid := server.AddEndpoint("hybrid", &mtserver.EndpointOpts{PORT_GRPC: 50021, PORT_HTTP: 80})
Also EndpointOpts can:
AllowCORS
- enable CORSCORS_Wrapper
- custom CORS func. See default used func hereUnaryInterceptors
- gRPC unary Middlewares. See Go gRPC Middleware for more infoStreamInterceptors
- gRPC stream Middlewares. See Go gRPC Middleware for more infoGrpcCredentials
- gRPC credentials.insecure.NewCredentials()
will be used by defaultTlsDomains
- List of domains for auto-certificates (HTTPS). See autocert
Full EndpointOpts declaration:
type Callback func()
type CallbackError func(error)
&mtserver.EndpointOpts{
PORT_GRPC int
PORT_HTTP int
AllowCORS bool
CORS_Wrapper func(h http.HandlerFunc) http.HandlerFunc
UnaryInterceptors []grpc.UnaryServerInterceptor
StreamInterceptors []grpc.StreamServerInterceptor
GrpcCredentials credentials.TransportCredentials
TlsDomains []string
OnStart Callback
OnStartError CallbackError
OnShutdown Callback
OnForceShutdown Callback // if cannot shutdown in 5 secs
}
Any HTTP-enabled endpoint have a Mux
. So you can register any HTTP handler over it.
package main
import (
"net/http"
"github.com/sarafanfm/mtserver"
)
func main() {
server := mtserver.New()
ep := server.AddEndpoint("http", &mtserver.EndpointOpts{PORT_HTTP: 80})
root := http.Dir("./static")
fs := http.FileServer(root)
ep.Mux.Handle("/", fs)
server.Run()
}
More complex example you can see here
If you have configured your protoc
correctly, you should have some interfaces and methods for each Service
defined in your *.proto
after compiling proto-files.
Among other things, there should be an XServer
interface and RegisterXServer
method where X
is your service name. Now you can register your service in gRPC endpoint:
type X struct {
XServer
}
func NewX() *X {
return &X{}
}
server := mtserver.New()
grpc := server.AddEndpoint("grpc", &mtserver.EndpointOpts{PORT_GRPC: 50021})
mtserver.RegisterGrpcService(
grpc,
&mtserver.GrpcService[XServer]{
Service: NewX(),
RegisterOnGRPC: RegisterXServer,
},
)
server.Run()
The only thing left for you to do is to implement the methods of your service in X
.
Will be automatically enabled if HTTP is enabled in your endpoint. So your web-clients must search gRPC server on HTTP port.
First, you need to install and configure gRPC-Gateway correctly.
From the example above, your protoc
compiler must generate one more method - RegisterXHandlerFromEndpoint
.
Now you can enable handling google.api.http
proto annotations by changing gRPC service registration with following:
hybrid := server.AddEndpoint("grpc", &mtserver.EndpointOpts{PORT_GRPC: 50021, PORT_HTTP: 80})
mtserver.RegisterGrpcService(
hybrid,
&mtserver.GrpcService[XServer]{
Service: NewX(),
RegisterOnGRPC: RegisterXServer,
RegisterOnGateway: RegisterXHandlerFromEndpoint, // <- here. Don't forget to enable HTTP
},
)
Now your server can handle HTTP requests defined in google.api.http
proto annotations.
In the standard error
interface, the code
field is not taken.
This module provides the ability to conveniently manage errors.
// somewhere in business logic layer
func GetSomething() error {
return mtserver.NewError("something not found", mtserver.ErrNotFound)
}
// inside gRPC service implementation
func (s *Server) Do(context context.Context, in *SomeInput) (*SomeOutput, error) {
if err := GetSomething(); err != nil {
return nil, mtserver.GrpcError(err) // <- convert to correct gRPC codes.NotFound
} else {
return &SomeOutput{}, nil
}
}
// or inside HTTP handler
func Handler(w http.ResponseWriter, r *http.Request) {
if err := GetSomething(); err != nil {
w.WriteHeader(mtserver.HttpCode(err)) // <- convert to correct HTTP http.StatusNotFound
w.Write([]byte(err.Error()))
} else {
// ...
}
}
Sometimes we need to receive events from the server. For example: user changed his profile. But we must remember that many other users can subscribe to one event. For these cases we have included this struct.
Imagine that we have two methods in the service: unary SaveProfile
and server-side stream ListenProfile
.
As we know, protoc
compiler will generate Svc_ListenProfileServer
struct which will be "response stream" for ListenProfile method.
So if we have integer key as user id then we can do something like this:
type Svc struct {
// generics is: map key (user id is int), stream type, payload type
notifyStreams *mtserver.StreamMap[int, Svc_ListenProfileServer, *SomeProfile]
}
func NewSvc() *Svc {
return &Svc{notifyStreams: mtserver.NewStreamMap[int, Svc_ListenProfileServer, *SomeProfile]()}
}
func (s *Svc) SaveProfile(context context.Context, in *SomeProfile) (*SomeProfile, error) {
var result *SomeProfile := SomeBusinessLogicSaveMethod()
s.notifyStreams.Send(result.UserID, result) // <- notify listeners
return result, nil
}
func (s *Svc) ListenProfile(in *SomeIntegerMessage, stream Svc_ListenProfileServer) error {
return s.notifyStreams.Add(in.UserID, stream, func() { log.Print("New listener was added to map") }) // <- add listener
}
that's all. All disconnected/died streams will be removed automatically. All connected streams will be notified when profile will be changed.
See example here
MTServer will try to preload env-formatted (comma separated) files provided in environment variable ENV_FILE
.
ENV_FILE=/etc/config.env,./.env go run .
See godotenv for more info.
Also you can use mtserver.RequiredEnv(string) string
func that will panic
if no required env var present.
port := mtserver.RequiredEnv("HTTP_PORT")