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