diff --git a/docs/go/advanced/grpc.md b/docs/go/advanced/grpc.md index e69de29..06c695b 100644 --- a/docs/go/advanced/grpc.md +++ b/docs/go/advanced/grpc.md @@ -0,0 +1,212 @@ +# gRPC + +GRPC is an enhancement to RPC in that both sides don't have to be written in Go, because there are libraries for a lot of other languages. It also defines it's payload types in separate proto files so they can be shared among teams. + +Documentation: https://grpc.io/docs/languages/go/basics/ + +## Install GRPC + +```bash +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.27 +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2 +``` + +## Proto Files + +```proto +// log.proto +syntax = "proto3"; + +package logs; + +option go_package = "/logs"; + +message Log{ + string name = 1; + string data = 2; +} + +message LogRequest{ + Log logEntry = 1; +} + +message LogResponse{ + string result = 1; +} + +service LogService{ + rpc WriteLog(LogRequest) returns (LogResponse); +} +``` + +## Compile Protos + +Proto compiler link: https://grpc.io/docs/protoc-installation/. This should be stored somewhere in your path, preferably in your `~/go/bin` directory. + +In directory with your proto file, run the below command. This will generate + +```bash +protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative logs.proto +``` + +## Listener + +### Install dependencies + +```bash +go get google.golang.org/gprc +go get google.golang.org/protobuf +``` + +### Implement handlers + +```go +// grpc.go +package main + +import ( + "context" + "fmt" + "log" + "logger/data" // our db models + "logger/logs" // our proto files + "net" + + "google.golang.org/grpc" +) + +type LogServer struct { + logs.UnimplementedLogServiceServer + Models data.Models // db writer +} + +// Using code generated from proto +func (l *LogServer) WriteLog(ctx context.Context, req *logs.LogRequest) (*logs.LogResponse, error) { + input := req.GetLogEntry() // auto generated function + + logEntry := data.LogEntry{ + Name: input.Name, + Data: input.Data, + } + + err := l.Models.LogEntry.Insert(logEntry) + if err != nil { + resp := &logs.LogResponse{ + Result: "failed", + } + return resp, err + } + + resp := &logs.LogResponse{ + Result: "logged!", + } + + return resp, nil +} + +func (app *Config) gRPCListen() { + lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort)) + if err != nil { + log.Fatalf("failed to listen to gRPC: %v", err) + } + + s := grpc.NewServer() + + logs.RegisterLogServiceServer(s, &LogServer{ + Models: app.Models, + }) + + log.Printf("gRPC server listening on port %s", grpcPort) + + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } +} + +``` + +### Instantiate server + +```go +// grpc.go +func (app *Config) gRPCListen() { + lis, err := net.Listen("tcp", fmt.Sprintf(":%s", grpcPort)) + if err != nil { + log.Fatalf("failed to listen to gRPC: %v", err) + } + + s := grpc.NewServer() + + // auto generated function + logs.RegisterLogServiceServer(s, &LogServer{ + Models: app.Models, + }) + + log.Printf("gRPC server listening on port %s", grpcPort) + + if err := s.Serve(lis); err != nil { + log.Fatalf("failed to serve gRPC: %v", err) + } +} +``` + +```go +// main.go +go app.grpcLisen() +``` + +## Sender + +The sender must also have access to the proto files generated by the listener. It would be a good idea to store these in a separate repo so that multiple teams can access them from a central source of truth. + +```go +import ( + "context" + "log" // proto + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func (app *Config) logItemViaGRPC(w http.ResponseWriter, r *http.Request) { + var requestPayload RequestPayload + + err := app.readJSON(w, r, &requestPayload) + if err != nil { + app.errorJSON(w, err) + return + } + + // need to pass some sort of credentials to the server + conn, err := grpc.Dial("logger:50001", grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + if err != nil { + app.errorJSON(w, err) + return + } + defer conn.Close() + + // generated in proto + client := logs.NewLogServiceClient(conn) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + _, err = client.WriteLog(ctx, &logs.LogRequest{ + LogEntry: &logs.Log{ + Name: requestPayload.Log.Name, + Data: requestPayload.Log.Data, + }, + }) + if err != nil { + app.errorJSON(w, err) + return + } + + payload := jsonResponse{ + Error: false, + Message: "logged via gRPC", + } + + app.writeJSON(w, http.StatusAccepted, payload) +} +``` diff --git a/docs/go/advanced/http.md b/docs/go/advanced/http.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/go/advanced/rpc.md b/docs/go/advanced/rpc.md index e69de29..249bd07 100644 --- a/docs/go/advanced/rpc.md +++ b/docs/go/advanced/rpc.md @@ -0,0 +1,97 @@ +# RPC + +RPC stands for Remote Procedure Calls, and allows for external Go microservices to call functions in our application directly. + +## Listener + +To do this, you need to define a RPCServer dummy type, and payload to receive, and a function that gets a pointer to a response string and returns an error. + +```go +type RPCServer struct{} + +type RPCPayload struct { + Name string + Data string +} + +func (r *RPCServer) LogInfo(payload RPCPayload, resp *string) error { + collection := client.Database("logs").Collection("logs") + _, err := collection.InsertOne(context.TODO(), data.LogEntry{ + Name: payload.Name, + Data: payload.Data, + }) + if err != nil { + log.Println("error writing to mongo ", err) + return err + } + *resp = "processed payload via RPC: " + payload.Name + + return nil +} + +``` + +To instantiate the RPCServer, use the below syntax in your main application file. Basically you define a function that creates the connection and listens on a port. Then, you register your RPC handler type and serve the RPC server. + +```go +func (app *Config) rpcListen() error { + log.Println("Starting RPC server on port " + RPC_PORT) + listen, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", RPC_PORT)) + if err != nil { + return err + } + defer listen.Close() + + for { + rpcConn, err := listen.Accept() + if err != nil { + log.Println("Error accepting connection: ", err) + continue + } + go rpc.ServeConn(rpcConn) + } +} + +// in function where you start your app... +err = rpc.Register(new(RPCServer)) +go app.rpcListen() +``` + +## Sender + +To send messages via RPC, you need to create a type for the payload that exactly matches that of the receiver. + +```go +type RPCPayload struct { + Name string + Data string +} + +func (app *Config) logItemViaRPC(r http.ResponseWriter, l LogPayload) { + client, err := rpc.Dial("tcp", "logger:5001") + if err != nil { + app.errorJSON(r, err) + return + } + + payload := RPCPayload{ + Name: l.Name, + Data: l.Data, + } + + var resp string + + err = client.Call("RPCServer.LogInfo", payload, &resp) + if err != nil { + app.errorJSON(r, err) + return + } + + returnPayload := jsonResponse{ + Error: false, + Message: resp, + } + + app.writeJSON(r, http.StatusAccepted, returnPayload) +} +``` diff --git a/docs/go/sidebar.json b/docs/go/sidebar.json index 4eeb7d0..33ac984 100644 --- a/docs/go/sidebar.json +++ b/docs/go/sidebar.json @@ -17,6 +17,7 @@ "text": "Go Advanced", "items": [ { "text": "Testing", "link": "/go/advanced/testing" }, + {"text": "HTTP", "link": "/go/advanced/http"}, {"text": "RPC", "link": "/go/advanced/rpc"}, {"text": "gRPC", "link": "/go/advanced/grpc"} ]