Building a Go service for gRPC from scratch

introduce

Protocol Buffers and gRPC are popular techniques for defining microservices that communicate efficiently over a network. Many companies build gRPC microservices in Go and publish the frameworks they develop. This article will start with gRPC getting started and build a gRPC service step by step.

The source code of this article has been uploaded to Github.

background

I saw a gRPC teaching at station B before video , I tried to follow the video but stepped on a lot of pits, so I decided to start from the official tutorial and complete a gRPC project myself.

start

Environment configuration

First of all, I need to configure some environments required by gRPC. Since I use Go language for development and the operating system is Ubuntu 20.04, the steps to configure the environment for gRPC-go are very simple.

Install Go

To install Go under Ubuntu, you need to download the source code of Go first. The version of Go I use is 1.18.3, and the source code download address is Go language Chinese website: go1.18.3.linux-amd64.tar.gz.

After the download is complete, first check whether there is an old version of Go on the machine, delete it if it exists, and then extract the source code to /usr/local.

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.3.linux-amd64.tar.gz

Add /usr/local/go/bin to the environment variable, which can be executed directly on the command line:

export PATH=$PATH:/usr/local/go/bin

Note: Executing the above statement on the command line will only take effect in the current command line environment. If you close the command line and then execute the go command, an error will be reported. To solve this problem, you need to add this statement to $HOME/.profile or / etc/profile, and use the source command to take effect

After the above steps are completed, check whether the Go environment is installed successfully:

go version

If the corresponding version number is output, the environment configuration is successful.

go version go1.18.3 linux/amd64

Here, configure Go's proxy as a domestic proxy to facilitate network speed problems when downloading and installing package s later. Since the installed Go is version 1.13 and above, execute the following commands directly.

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

Install the Protocol buffer compiler

Use apt or apt-get to install the Protocol buffer compiler on Ubuntu. The command is as follows:

sudo apt install -y protobuf-compiler

Check if the installation was successful:

protoc --version # Ensure compiler version is 3+

If the corresponding version number is output, the environment configuration is successful.

libprotoc 3.6.1

Configure Go plugins

When configuring Go plugins, I encountered a lot of errors.

--go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC

or

protoc-gen-go-grpc: program not found or is not executable

The online solution may not work, and finally choose to install the corresponding versions of protoc-gen-go and protoc-gen-go-grpc according to the steps on the official website.

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

Note that here are the package s downloaded from goole.golang.org

To update the environment variables, add the following command to $HOME/.profile or /etc/profile, source to make it take effect.

export PATH="$PATH:$(go env GOPATH)/bin"

At this point, the gRPC-go environment is configured.

gRPC interface definition

.proto file

The first step begins by defining the gRPC service and method request and response types. To define a service, specify the naming service in the .proto file:

service NewService {
  rpc GetHotTopNews(Request) returns (News) {}
}

Then define the RPC methods in the service definition, specifying their request and response types. gRPC allows you to define four service methods:

  • A simple RPC where the client sends a request to the server using a stub and waits for the response to come back, just like a normal function call.
// Obtains the feature at a given position.
rpc GetFeature(Point) returns (Feature) {}
  • Server-side streaming RPC, the client sends a request to the server and gets the stream to read back a series of messages. The client reads from the returned stream until there are no more messages.
// Obtains the Features available within the given Rectangle.  Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • Client-side streaming RPC, where the client writes a series of messages and sends them to the server, again using the provided stream. Once the client finishes writing messages, it waits for the server to read all messages and return its response. The client-side streaming method can be specified by placing the stream keyword before the request type.
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • Bidirectional streaming RPC where two parties send a series of messages using read and write streams. The two streams operate independently, so the client and server can read and write in any order they like: for example, the server can wait to receive all client messages before writing a response, or it can read messages alternately Messages are then written, or some other combination of reads and writes, preserving the order of messages in each stream. This type of method can be specified by placing the stream keyword before the request and response.
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

We are going to implement a gRPC interface for getting hot news, the .proto file contains protocol buffer message type definitions for all request and response types used in the service method. For example, here is the Request message type:

message Request {
  string type = 1;
  int64 page = 2;
  int64 size = 3;
  int64 is_filter = 4;
}

and the Response definition:

message Response { repeated New news = 1; }

The structure of New is defined as:

message New {
  string uniquekey = 1;
  string title = 2;
  string date = 3;
  string category = 4;
  string author_name = 5;
  string url = 6;
  string thumbnail_pic_s = 7;
  int64 is_content = 8;
}

Finally define the RPC interface:

syntax = "proto3";

```go
 insert code snippet here

option go_package = "./;protobuf";

package protobuf;

service NewService {
rpc GetHotTopNews(Request) returns (Response) {}
}

Note that added here option go_package = "./;protobuf";,Description generated pb.go of package name.
### protoc command
 Next, we need to start.proto generated in the service definition gRPC Client and server interfaces. We use special gRPC Go plug-in protobuf compiler to do this.
```bash
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative protobuf/*.proto

The following go files will be generated in the same directory as the .proto file:

  • news.pb.go, which contains all protocol buffer code for populating, serializing, and retrieving request and response message types
  • news_grpc.pb.go, contains: 1) the interface type (or stub) called by the client using the method defined in the service; 2) the interface type to be implemented by the server, which also uses the method defined in the service.

Here I use VS code for development, and two plugins are recommended when writing .proto files:

  • vscode-proto3: some syntax for identifying .proto files
  • clang-format: used to format .proto files, you need to use sudo apt install clang-format, and configure it according to the plugin instructions

Go service build

server

The server needs to implement the gRPC interface, first define a structure:

type Server struct {
	protobuf.UnimplementedNewServiceServer
}

It inherits the UnimplementedNewServiceServer in the generated pb.go file, and then implements the methods in the interface:

func (s *Server) GetHotTopNews(ctx context.Context, req *protobuf.Request) (*protobuf.Response, error) {
	ret := srv.RequestPublishAPI()
	return &protobuf.Response{
		News: ret,
	}, nil
}

In this way, the most basic gRPC service can be started.

func main() {
	// register grpc service
	s := grpc.NewServer()
	protobuf.RegisterNewServiceServer(s, &Server{})

	// listen tcp connection
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// start grpc server
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

client

Similarly, we write a client in go to request and test whether the gRPC service works.

var (
	addr = flag.String("addr", "localhost:50051", "the address to connect to")
)

func main() {
	flag.Parse()
	// Set up a connection to the server.
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := protobuf.NewNewServiceClient(conn)

	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.GetHotTopNews(ctx, &protobuf.Request{})

	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	for _, v := range r.GetNews() {
		fmt.Println(v)
	}
}

So far, a simple gRPC service has been completed, but our interface for getting hot news is fake, so we add a free hot news API to the project, so that the client can actually return. The main logic of the API is as follows:

// NewService contains services that fetch new and convert to grpc protobuf
type NewService struct {
	apiUri   string
	apiKey   string
	reqType  string
	page     int
	size     int
	isFilter int
}

func (s *NewService) RequestPublishAPI() []*protobuf.New {
	reqUrl := fmt.Sprintf("%s?type=%s&page=%d&page_size=%d&is_filter=%d&key=%s", s.apiUri, s.reqType, s.page, s.size, s.isFilter, s.apiKey)
	log.Printf("request url: %s", reqUrl)

	method := "GET"

	client := &http.Client{}
	req, err := http.NewRequest(method, reqUrl, nil)

	if err != nil {
		panic(err)
	}

	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		panic(err)
	}

	var resp ApiResponse
	err = json.Unmarshal(body, &resp)
	if err != nil {
		panic(err)
	}

	var ret []*protobuf.New
	for _, n := range resp.Result.Data {
		isContent, _ := strconv.Atoi(n.IsContent)
		ret = append(ret, &protobuf.New{
			Uniquekey:     n.Uniquekey,
			Title:         n.Title,
			Date:          n.Date,
			Category:      n.Category,
			AuthorName:    n.AuthorName,
			Url:           n.Url,
			ThumbnailPicS: n.ThumbnailPicS,
			IsContent:     int64(isContent),
		})
	}

	return ret
}

Test

Let's take a look at the current test results, first start the gRPC service:

cd cmd/server && go build -o server . && ./server

The output results are as follows, indicating normal startup.

2022/07/08 22:56:19 server listening at [::]:50051

Then start the client to send gRPC requests:

cd cmd/client && go build -o client . && ./client

It can be seen that the hot news will be output as expected by the client program logic:

uniquekey:"e36249942bd61b566293a0f658a70861"  title:"Drunk passenger "lost" huge amount of property, it turned out to be..."  date:"2022-07-08 22:28:00"  category:"headlines"  author_name:"daily news"  url:"https://mini.eastday.com/mobile/220708222828059733118.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/news/20220708/20220708222828_7250d5750196c6ca896094cf9e9b7910_1_mwpm_03201609.jpeg"  is_content:1
uniquekey:"d0b9d2392e764b05be7fc3903ae8cf0e"  title:"Shanghai pharmacy strictly guards the epidemic prevention position and sells fever and cold medicine according to epidemic prevention requirements"  date:"2022-07-08 22:28:00"  category:"headlines"  author_name:"Shangguan News, Feed: People's Information"  url:"https://mini.eastday.com/mobile/220708222828022564952.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/news/20220708/20220708222828_59a73fae2c240c9d4dc56877af1cf021_1_mwpm_03201609.jpeg"  is_content:1
uniquekey:"22d3605020cdcd1b3e36389812d9f57f"  title:"There is a county in Xinjiang, but it ushered in the first airport of its own, let's take a look"  date:"2022-07-08 22:27:00"  category:"headlines"  author_name:"joke about social phenomenon"  url:"https://mini.eastday.com/mobile/220708222748804215251.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/minimodify/20220708/640x376_62c83ee45b71b_mwpm_03201609.jpeg"  is_content:1
uniquekey:"ee7520b15386bb24835556621135b7c7"  title:"A Porsche SUV in Changsha burst into flames! How to avoid summer vehicles "getting hot"?"  date:"2022-07-08 22:27:00"  category:"headlines"  author_name:"Changsha Evening News, provided by: People's Information"  url:"https://mini.eastday.com/mobile/220708222722680289302.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/news/20220708/20220708222722_20a12760617fdaf73ba22cbeaae5a670_1_mwpm_03201609.jpeg"  is_content:1
uniquekey:"5b3346570ca64b911934c9c4c958150f"  title:"The well-known brand baby water education franchise store people go to the empty building and mothers encounter difficulty in refunding"  date:"2022-07-08 22:27:00"  category:"headlines"  author_name:"Changsha Evening News, provided by: People's Information"  url:"https://mini.eastday.com/mobile/220708222722516745726.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/news/20220708/20220708222722_476ac09f92bc5047938cbeecdef5a293_1_mwpm_03201609.jpeg"  is_content:1
uniquekey:"4b47df2a78934af1cacaf6fac844579b"  title:"illustration│thrilling! Driver trapped after van hits tree, firefighters "clamp" to rescue"  date:"2022-07-08 22:26:00"  category:"headlines"  author_name:"Wen Wei Po, provided by: People's Information"  url:"https://mini.eastday.com/mobile/220708222616778303564.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/news/20220708/20220708222616_07827127554548d4dd870205b517fda5_1_mwpm_03201609.jpeg"  is_content:1
uniquekey:"9beb3c60231daa82a18c03bbad43280c"  title:"6 Home-operated households are rectified within a limited time! Qingdao takes action on the "ice cream assassin""  date:"2022-07-08 22:25:00"  category:"headlines"  author_name:"Peninsula Metropolis Daily, provided by: People's Information"  url:"https://mini.eastday.com/mobile/220708222514489900651.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/news/20220708/20220708222514_c973a3b8b0ab7308158acf353cc32afa_1_mwpm_03201609.jpeg"  is_content:1
uniquekey:"0849aacfb2488478bd2a9147ff6d70c2"  title:"Mainland and Taiwanese companies actively respond to the "dual carbon" strategy"  date:"2022-07-08 22:24:00"  category:"headlines"  author_name:"Xinhuanet, source: People's Information"  url:"https://mini.eastday.com/mobile/220708222407637690082.html"  is_content:1
uniquekey:"d1a5bed91210467f0536fa1a77dfbf3a"  title:"Quality issues are concerned!In the first half of the year, the Sichuan Consumer Council organized 10,277 related complaints"  date:"2022-07-08 22:23:00"  category:"headlines"  author_name:"Chuanguan News, provided by: People's Information"  url:"https://mini.eastday.com/mobile/220708222331274896200.html"  is_content:1
uniquekey:"98161b2c5703e64a5881a3b1e778a04a"  title:"Sanhe will launch free nucleic acid testing services in key areas on July 9"  date:"2022-07-08 22:20:00"  category:"headlines"  author_name:"Islander Watch"  url:"https://mini.eastday.com/mobile/220708222048455355795.html"  thumbnail_pic_s:"https://dfzximg02.dftoutiao.com/minimodify/20220708/1080x593_62c83d400941c_mwpm_03201609.jpeg"  is_content:1

Visual display

One of the features of gRPC is cross-platform and cross-language communication, so we can use a simple react project for front-end visual display.

Preparation

After ensuring that nodejs and npm commands are available, use create-react-app to create a react project

npx create-react-app web
  • Now just like we did for Go before, we need to generate client and server code for Javascript. For this, our news.proto file can be used again. Create a directory called newspb/protobuf in the web/src directory to store our generated files. But since our client will be a browser client, we will have to use grpc-web.

Most modern browsers do not yet support HTTP/2. Since gRPC uses HTTP/2, grpc-web is required for the browser client to communicate with the gRPC server. grpc-web allows HTTP/1 to be used with proxies like Envoy, which helps in converting HTTP/1 to HTTP/2.

  • Make sure the protoc-gen-grpc-web plugin is installed → https://github.com/grpc/grpc-web
  • Run the following command to generate the corresponding code
protoc protobuf/*.proto --js_out=import_style=commonjs:./web/src/newspb --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./web/src/newspb
  • The generated news_pb.js and news_grpc_web_pb.js can be found under web/src/newspb/protobuf

set up envoy

Create a new envoy.yaml with the following configuration:

admin:
  access_log_path: /tmp/admin_access.log
  address:
    socket_address: { address: 0.0.0.0, port_value: 9000 }

static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 0.0.0.0, port_value: 8000 }
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        config:
          codec_type: auto
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match: { prefix: "/" }
                route:
                  cluster: news_service
                  max_grpc_timeout: 0s
              cors:
                allow_origin:
                - "*"
                allow_methods: GET, PUT, DELETE, POST, OPTIONS
                allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
                max_age: "1728000"
                expose_headers: custom-header-1,grpc-status,grpc-message
          http_filters:
          - name: envoy.grpc_web
          - name: envoy.cors
          - name: envoy.router
  clusters:
  - name: news_service
    connect_timeout: 50s
    type: logical_dns
    http2_protocol_options: {}
    lb_policy: round_robin
    hosts: [{ socket_address: { address: 172.17.0.1, port_value: 50051 }}]
  • Among them, in the configuration of clusters, hosts point to the address of the backend service, so the port number is 50051
  • The envoy.yaml file is essentially asking Envoy to run a listener on port 8000 to listen for downstream traffic. Then direct any traffic that reaches it to news_service, which is the gRPC server running on port 0051

After completing the configuration, create a new Dockerfile

FROM envoyproxy/envoy:v1.12.2

COPY ./envoy/envoy.yaml /etc/envoy/envoy.yaml

CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

Package a docker image:

docker build -t grpc-starter-envoy:1.0 .

Then run:

docker run --network=host grpc-starter-envoy:1.0

In this way, our envoy proxy is set up.

Improve the react project

First add some dependencies:

npm install grpc-web --save
npm install google-protobuf --save

We implement all the logic of the react project in web/src/App.js:

import { Request } from "./newspb/protobuf/news_pb";
import { NewServiceClient } from "./newspb/protobuf/news_grpc_web_pb";

First, import Request and NewServiceClient to send requests and generate clients, respectively.

var client = new NewServiceClient("http://localhost:8000", null, null);

Core request logic:

var request = new Request();
client.getHotTopNews(request, {}, (error, reply) => {
  if (!error) {
    console.log(reply.getNewsList());
  } else {
    console.log(error);
  }
});

When the request is successful, the JavaScript console will print out a list of hot news. We can then add some UI frameworks to beautify the display, here we choose the most popular material ui framework to integrate into the project.

npm install @mui/material @emotion/react @emotion/styled

Add the following code to web/src/App.js:

import React, { useEffect } from "react";
import { Request } from "./newspb/protobuf/news_pb";
import { NewServiceClient } from "./newspb/protobuf/news_grpc_web_pb";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import Grid from "@mui/material/Grid";
import Avatar from "@mui/material/Avatar";
import Typography from "@mui/material/Typography";
import Card from "@mui/material/Card";
import CardHeader from "@mui/material/CardHeader";
import CardMedia from "@mui/material/CardMedia";
import CardContent from "@mui/material/CardContent";
import CardActions from "@mui/material/CardActions";
import Link from "@mui/material/Link";
import { red } from "@mui/material/colors";
import NotFound from "./notfound.gif";

var client = new NewServiceClient("http://localhost:8000", null, null);

function App() {
  const [newsList, setNewsList] = React.useState([]);

  const getHotNews = () => {
    var request = new Request();
    client.getHotTopNews(request, {}, (error, reply) => {
      if (!error) {
        setNewsList(reply.getNewsList());
      } else {
        console.log(error);
      }
    });
  };

  useEffect(() => {
    getHotNews();
  }, []);

  return (
    <Container>
      <Box>
        <FormControl sx={{ m: 1, minWidth: 120 }}>
          <InputLabel htmlFor="type-select">Type</InputLabel>
          <Select defaultValue="top" id="type-select" label="Type">
            <MenuItem value={"top"}>default</MenuItem>
            <MenuItem value={"guonei"}>domestic</MenuItem>
            <MenuItem value={"guoji"}>internationality</MenuItem>
            <MenuItem value={"yule"}>entertainment</MenuItem>
            <MenuItem value={"tiyu"}>physical education</MenuItem>
            <MenuItem value={"junshi"}>military</MenuItem>
            <MenuItem value={"keji"}>Technology</MenuItem>
            <MenuItem value={"caijing"}>Finance</MenuItem>
            <MenuItem value={"youxi"}>game</MenuItem>
            <MenuItem value={"qiche"}>car</MenuItem>
            <MenuItem value={"jiankang"}>healthy</MenuItem>
          </Select>
        </FormControl>
        <FormControl sx={{ m: 1, minWidth: 120 }}>
          <TextField id="page-select" label="Page" variant="outlined" />
        </FormControl>
        <FormControl sx={{ m: 1, minWidth: 120 }}>
          <InputLabel htmlFor="size-select">Size</InputLabel>
          <Select defaultValue="5" id="size-select" label="Size">
            <MenuItem value={5}>5</MenuItem>
            <MenuItem value={10}>10</MenuItem>
            <MenuItem value={20}>20</MenuItem>
            <MenuItem value={30}>30</MenuItem>
          </Select>
        </FormControl>
      </Box>
      <Box>
        <Grid
          container
          spacing={{ xs: 2, md: 3 }}
          columns={{ xs: 4, sm: 8, md: 12 }}
        >
          {newsList.map((value, index) => (
            <Grid item xs={2} sm={4} md={4}>
              <Card>
                <CardHeader
                  avatar={
                    <Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
                      {value.array[4][0]}
                    </Avatar>
                  }
                  title={value.array[4] + value.array[3]}
                  subheader={value.array[2]}
                />
                {value.array[6] === null ||
                value.array[6] === undefined ||
                value.array[6] === "" ? (
                  <CardMedia
                    component="img"
                    height="194"
                    image={NotFound}
                    alt="News cover"
                  />
                ) : (
                  <CardMedia
                    component="img"
                    height="194"
                    image={value.array[6]}
                    alt="News cover"
                  />
                )}
                <CardContent>
                  <Typography variant="body2" color="text.secondary">
                    {value.array[1]}
                  </Typography>
                </CardContent>
                <CardActions>
                  <Link
                    href={value.array[5]}
                    underline="none"
                    target="_blank"
                    rel="noopener"
                  >
                    Original link
                  </Link>
                </CardActions>
              </Card>
            </Grid>
          ))}
        </Grid>
      </Box>
    </Container>
  );
}

export default App;

Display of results:

Finally, to solve the problem of request parameters, the changes to the server are as follows. First, modify the server implementation method of gRPC.

cmd/server/main.go

func (s *Server) GetHotTopNews(ctx context.Context, req *protobuf.Request) (*protobuf.Response, error) {
        // Add the req parameter
	ret := srv.RequestPublishAPI(req)
	return &protobuf.Response{
		News: ret,
	}, nil
}

Modify the logic for sending requests to the public API:

service/news.go

func (s *NewService) RequestPublishAPI(request *protobuf.Request) []*protobuf.New {
	// check request param
	if request.GetType() != "" {
		s.reqType = request.GetType()
	}
	if request.GetPage() != 0 {
		s.page = int(request.GetPage())
	}
	if request.GetSize() != 0 {
		s.size = int(request.GetSize())
	}
        ...
}

Add related event handlers to web/src/App.js.

import React, { useEffect } from "react";
import { Request } from "./newspb/protobuf/news_pb";
import { NewServiceClient } from "./newspb/protobuf/news_grpc_web_pb";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import FormControl from "@mui/material/FormControl";
import Select from "@mui/material/Select";
import TextField from "@mui/material/TextField";
import Grid from "@mui/material/Grid";
import Avatar from "@mui/material/Avatar";
import Typography from "@mui/material/Typography";
import Card from "@mui/material/Card";
import CardHeader from "@mui/material/CardHeader";
import CardMedia from "@mui/material/CardMedia";
import CardContent from "@mui/material/CardContent";
import CardActions from "@mui/material/CardActions";
import Link from "@mui/material/Link";
import { red } from "@mui/material/colors";
import NotFound from "./notfound.gif";

var client = new NewServiceClient("http://localhost:8000", null, null);

function App() {
  const [newsList, setNewsList] = React.useState([]);
  const [type, setType] = React.useState("top");
  const [page, setPage] = React.useState(1);
  const [size, setSize] = React.useState(10);

  const handleTypeChange = (event) => {
    setType(event.target.value);
    console.log(event.target.value);
    getHotNews(event.target.value, page, size);
  };

  const handleSizeChange = (event) => {
    setSize(event.target.value);
    console.log(event.target.value);
    getHotNews(type, page, event.target.value);
  };

  const handlePageChange = (event) => {
    setPage(event.target.value);
    console.log(event.target.value);
    getHotNews(type, event.target.value, size);
  };

  const getHotNews = (type, page, size) => {
    console.log(type, page, size);
    var request = new Request();
    request.setType(type);
    request.setPage(page);
    request.setSize(size);
    client.getHotTopNews(request, {}, (error, reply) => {
      if (!error) {
        setNewsList(reply.getNewsList());
      } else {
        console.log(error);
      }
    });
  };

  useEffect(() => {
    getHotNews(type, page, size);
  }, [type, page, size]);

  return (
    <Container>
      <Box>
        <FormControl sx={{ m: 1, minWidth: 120 }}>
          <InputLabel htmlFor="type-select">Type</InputLabel>
          <Select
            defaultValue="top"
            id="type-select"
            label="Type"
            value={type}
            onChange={handleTypeChange}
          >
            <MenuItem value={"top"}>default</MenuItem>
            <MenuItem value={"guonei"}>domestic</MenuItem>
            <MenuItem value={"guoji"}>internationality</MenuItem>
            <MenuItem value={"yule"}>entertainment</MenuItem>
            <MenuItem value={"tiyu"}>physical education</MenuItem>
            <MenuItem value={"junshi"}>military</MenuItem>
            <MenuItem value={"keji"}>Technology</MenuItem>
            <MenuItem value={"caijing"}>Finance</MenuItem>
            <MenuItem value={"youxi"}>game</MenuItem>
            <MenuItem value={"qiche"}>car</MenuItem>
            <MenuItem value={"jiankang"}>healthy</MenuItem>
          </Select>
        </FormControl>
        <FormControl sx={{ m: 1, minWidth: 120 }}>
          <TextField
            id="page-select"
            label="Page"
            variant="outlined"
            value={page}
            onChange={handlePageChange}
          />
        </FormControl>
        <FormControl sx={{ m: 1, minWidth: 120 }}>
          <InputLabel htmlFor="size-select">Size</InputLabel>
          <Select
            defaultValue="5"
            id="size-select"
            label="Size"
            value={size}
            onChange={handleSizeChange}
          >
            <MenuItem value={5}>5</MenuItem>
            <MenuItem value={10}>10</MenuItem>
            <MenuItem value={20}>20</MenuItem>
            <MenuItem value={30}>30</MenuItem>
          </Select>
        </FormControl>
      </Box>
      <Box>
        <Grid
          container
          spacing={{ xs: 2, md: 3 }}
          columns={{ xs: 4, sm: 8, md: 12 }}
        >
          {newsList.map((value, index) => (
            <Grid item xs={2} sm={4} md={4}>
              <Card>
                <CardHeader
                  avatar={
                    <Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
                      {value.array[4][0]}
                    </Avatar>
                  }
                  title={value.array[4] + value.array[3]}
                  subheader={value.array[2]}
                />
                {value.array[6] === null ||
                value.array[6] === undefined ||
                value.array[6] === "" ? (
                  <CardMedia
                    component="img"
                    height="194"
                    image={NotFound}
                    alt="News cover"
                  />
                ) : (
                  <CardMedia
                    component="img"
                    height="194"
                    image={value.array[6]}
                    alt="News cover"
                  />
                )}
                <CardContent>
                  <Typography variant="body2" color="text.secondary">
                    {value.array[1]}
                  </Typography>
                </CardContent>
                <CardActions>
                  <Link
                    href={value.array[5]}
                    underline="none"
                    target="_blank"
                    rel="noopener"
                  >
                    Original link
                  </Link>
                </CardActions>
              </Card>
            </Grid>
          ))}
        </Grid>
      </Box>
    </Container>
  );
}

export default App;

Dockerlize

We will build three docker images to provide go-grpc-server, envoy proxy and react-web service respectively, so create a new docker-compose.yaml file in the project root directory

version: '3'
services:
  proxy:
    build:
      context: ./envoy
      dockerfile: Dockerfile
    ports:
      - "8000:8000"

  go-grpc-server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
    - "50051:50051"
    depends_on:
      - proxy
  
  web-client:
    build: 
      context: ./web
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    depends_on:
      - go-grpc-server
      - proxy
    tty: true

The Dockerfile of envoy has been introduced before. Here, the previous Dockerfile is moved to the envoy directory, and the path is slightly modified:

envoy/Dockerfile

FROM envoyproxy/envoy:v1.12.2

COPY ./envoy.yaml /etc/envoy/envoy.yaml

CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml

Create a new Dockerfile in the web directory to provide the react-web image.

web/Dockerfile

FROM node:16.15.1-alpine as build
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY package-lock.json ./
RUN npm ci --silent
RUN npm install react-scripts@5.0.1 -g --silent
COPY . ./
RUN npm run build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Finally, create a new Dockerfile in the project root directory to provide gRPC services.

FROM golang:1.18-alpine

ENV GO111MODULE=on \
    GOPROXY=https://goproxy.cn,direct

WORKDIR $GOPATH/src/github.com/surzia/grpc-starter

COPY . .
RUN go mod download

RUN go build -o server .

EXPOSE 50051

CMD [ "./server" ]

Compile docker-compose.yaml as an image:

docker compose build

run the entire project

docker compose up -d

After the project starts, open the browser and enter http://localhost:3000 to access

in conclusion

This article builds a gRPC service from scratch. The technology stack used includes Go+react+gRPC+Docker+envoy. The source code has been uploaded to Github - surzia/grpc-starter

Tags: Go Microservices rpc

Posted by Drezek on Mon, 11 Jul 2022 22:26:55 +0530