This guide provides an overview of how to implement a specific simple scenario with grpc (see here for a more in-depth/general one). It assumes that you have the necessary proto installed (otherwise see in-depth guide). We want to construct a scenario with two entities:
- A server
- A client
and with the functionality that:
- The client can ask the server for the current time and supply its ID in the request.
- The server will respond with its server name and the current time.
NOTE: while going through the guide, refer to the files in the repo for more context and details.
- Open an empty folder in your editor.
- Run
go mod init someNamewhere 'someName' can be anything relevant. This will create ago.modfile with your chosen name at the top. - Create a folder named grpc (or something else) with a file proto file inside (for example
proto.proto). - Create a client folder with a
client.gofile. - Create a server folder with a
server.gofile. - Put both the client and server into the same package by stating the package at the top of the files (for example
package main).
Inside the proto.proto file we need to set up the service, service functions, and message type(s) we need. The functions are used for communicating between the entities, while the message types are used for input/output in the functions. The service contains the functions.
In our case, in the proto file, we want to have a:
- TimeAsk service
- function for the client to ask the server for the time.
- Type for the message the client will send to ask for the time.
- Type for the message the server will send back to the client with the time.
Therefore, we add (where line 2-3 depends on the values above and simpleGuide is the name used in the go.mod file)
syntax = "proto3";
package simpleGuide;
option go_package = "grpc/proto";
message AskForTimeMessage {
int64 clientId = 1;
}
message TimeMessage {
string serverName = 1;
string time = 2;
}
service TimeAsk {
rpc AskForTime(AskForTimeMessage) returns (TimeMessage);
}Then we run the command:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative grpc/proto.proto
where the last part specifies the path to the proto file. This should create two files inside the grpc folder (proto.pb.go and proto_grpc.go). These files contain autogenerated code based on the things we implemented in the proto file.
NOTE: imports are not always explicitly mentioned here (such as time, etc.). They should be added to the imports at the top. If in doubt, refer to the files in the repo.
Now we need to set up the server. We will let the user determine which port it should run on via the flag "-port".
Create a server struct with the necessary fields. You can read more about why the unimplemented part is needed here.
type Server struct {
proto.UnimplementedTimeAskServer // Necessary
name string
port int
}Add a port variable and create a main that parses the flag, sets up a server struct, starts the server, and ensures that main will keep running.
var port = flag.Int("port", 0, "server port number")
func main() {
// Get the port from the command line when the server is run
flag.Parse()
// Create a server struct
server := &Server{
name: "serverName",
port: *port,
}
// Start the server
go startServer(server)
// Keep the server running until it is manually quit
for {
}
}To implement the startServer function, we have to use the "google.golang.org/grpc" (grpc) and netpackage. We first create a new grpc server and make it listen at the port the user specified. If that is successful, then we register a proto server with the grpc server and the server struct and serve its listener.
func startServer(server *Server) {
// Create a new grpc server
grpcServer := grpc.NewServer()
// Make the server listen at the given port (convert int port to string)
listener, err := net.Listen("tcp", ":"+strconv.Itoa(server.port))
if err != nil {
log.Fatalf("Could not create the server %v", err)
}
log.Printf("Started server at port: %d\n", server.port)
// Register the grpc server and serve its listener
proto.RegisterTimeAskServer(grpcServer, server)
serveError := grpcServer.Serve(listener)
if serveError != nil {
log.Fatalf("Could not serve listener")
}
}NOTE: After adding "google.golang.org/grpc", you will need to run go mod tidy (specifically to be able to use the NewServer function).
Now you should be able to start the server at some port like go run server/server.go -port 5454 and get the output some_time_stamp Started server at port: 5454 (while the server keeps running).
We now need to implement the AskForTime function we defined in the proto file, so that the client can call it.
To do so, we have to find the corresponding function inside the proto_grpc.pb.go file and copy its signature (in my case on line 32):
func (c *timeAskClient) AskForTime(ctx context.Context, in *AskForTimeMessage, opts ...grpc.CallOption) (*TimeMessage, error)Copy this into the server.go file, turn it into a function, and change some things:
- Import the context package.
- Change timeAskClient to Server, so that it matches our server struct definition.
- Add *proto.AskForTimeMessage instead of just AskForTimeMessage as the
invalue (to point to the right type in the proto file). - Add *proto.TimeMessage instead of just TimeMessage as the output.
- Remove the
opts ...grpc.CallOptionfrom the input. - Temporarily make it return
nil, nil
We now have:
func (c *Server) AskForTime(ctx context.Context, in *proto.AskForTimeMessage) (*proto.TimeMessage, error) {
return nil, nil
}and there should not be any compile-errors.
Now we will make the server actually return something and log which client is requesting the time. To do so, update the function to:
func (c *Server) AskForTime(ctx context.Context, in *proto.AskForTimeMessage) (*proto.TimeMessage, error) {
log.Printf("Client with ID %d asked for the time\n", in.ClientId)
return &proto.TimeMessage{Time: time.Now().String()}, nil
}Brief overview of what we have done so far with the server:
- Created a server struct.
- Created a variable for getting the desired port from the user.
- Created a main which parses the port flag, creates an instance of the server struct, starts the server, and keeps running.
- Implemented a function we specified in the proto file.
NOTE: if you have more functions in your proto file for the TimeAsk service, then you also need to implement those. Otherwise, your IDE will complain because the necessary interface functions for the server have not been fully implemented.
Similar to the server create a client struct, a port variable, set up the struct in main, and ensure that the client keeps running:
type Client struct {
id int
}
var (
port = flag.Int("port", 0, "client port number")
)
func main() {
// Parse the flags to get the port for the client
flag.Parse()
// Create a client
client := &Client{
id: *port,
}
// Wait for the client (user) to ask for the time
go waitForTimeRequest(client)
for {
}
}Now we have to implement a function to connect to the server that can be used to obtain a connection, which can be used in the waitForTimeRequest function.
func connectToServer() (proto.TimeAskClient, error) {
// Dial the server at the specified port.
conn, err := grpc.Dial("localhost:"+strconv.Itoa(*serverPort), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Could not connect to port %d", *serverPort)
} else {
log.Printf("Connected to the server at port %d\n", *serverPort)
}
return proto.NewTimeAskClient(conn), nil
}Lastly, we can implement the actual function:
func waitForTimeRequest(client *Client) {
// Connect to the server
serverConnection, _ := connectToServer()
// Wait for input in the client terminal
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
input := scanner.Text()
log.Printf("Client asked for time with input: %s\n", input)
// Ask the server for the time
timeReturnMessage, err := serverConnection.AskForTime(context.Background(), &proto.AskForTimeMessage{
ClientId: int64(client.id),
})
if err != nil {
log.Printf(err.Error())
} else {
log.Printf("Server %s says the time is %s\n", timeReturnMessage.ServerName, timeReturnMessage.Time)
}
}
}We first set up a connection to the server with the helper function. Then we create a scanner and wait for any input (+ enter keyboard press) from the user, after which we get the time from the server.
- Run the server:
go run server/server.go -port 5454. - In a different terminal, run the client:
go run client/client.go -cPort 8080 -sPort 5454. - In the client terminal input something and press enter. You should now get the time and be able to see in the server terminal that the client requested the time.
Below is an example of running the server and client:
- Every time you update the proto file, you have to run the
protoccommand (the long command in the proto file section). - Keep track of naming. For example, if the name of the service is changed to YearAsk then we e.g. also have to change the return statement in
connectToServerto instead useproto.NewYearAskClient. This also goes for message and function changes. - Make sure you have imported
"google.golang.org/grpc"in the server and rango mod tidy. - Ensure that you are using * and & properly. If you feel a bit shaky about this, you might want to consider using IntelliJ with the Go plugin, since it provides better intellisense.
