跳到主要内容

gRPC 在 Node.js 客户端和 Go 服务端通信

项目源码链接

定义 Proto 文件

首先,需要定义一个 .proto 文件来描述服务和消息格式。

syntax = "proto3";

package proto;

// 定义 TodoList 服务
service TodoList {
rpc AddTodo (AddingTodoRequest) returns (TodoResponse);
rpc ToggleTodo (TogglingTodoRequest) returns (TodoResponse);
rpc RemoveTodo (RemovingTodoRequest) returns (TodoResponse);
}

// 请求和响应消息
message AddingTodoRequest {
string content = 1;
}

message TogglingTodoRequest {
int64 id = 1;
}

message RemovingTodoRequest {
int64 id = 1;
}

message TodoResponse {
int64 id = 1;
string content = 2;
bool completed = 3;
}

Go 服务端实现

在服务端,我使用 Go 语言实现了定义的 gRPC 服务。

package main

import (
"context"
"fmt"
"goLearning/gRPC/basic1/proto"
"google.golang.org/grpc"
"net"
"time"
)

type Todo struct {
ID int64
Content string
Completed bool
}

type TodoListServer struct {
proto.UnimplementedTodoListServer
todos []Todo
}

func (s *TodoListServer) AddTodo(ctx context.Context, req *proto.AddingTodoRequest) (*proto.TodoResponse, error) {
todo := Todo{
ID: time.Now().Unix(),
Content: req.Content,
Completed: false,
}
s.todos = append(s.todos, todo)

return &proto.TodoResponse{
Id: todo.ID,
Content: todo.Content,
Completed: todo.Completed,
}, nil
}

func (s *TodoListServer) ToggleTodo(ctx context.Context, req *proto.TogglingTodoRequest) (*proto.TodoResponse, error) {
var updatedTodo *Todo
for i := range s.todos {
if s.todos[i].ID == req.Id {
s.todos[i].Completed = !s.todos[i].Completed
updatedTodo = &s.todos[i]
break
}
}
if updatedTodo == nil {
return nil, fmt.Errorf("Todo not found")
}
return &proto.TodoResponse{
Id: updatedTodo.ID,
Content: updatedTodo.Content,
Completed: updatedTodo.Completed,
}, nil
}

func (s *TodoListServer) RemoveTodo(ctx context.Context, req *proto.RemovingTodoRequest) (*proto.TodoResponse, error) {
var removedTodo Todo
for i, todo := range s.todos {
if todo.ID == req.Id {
removedTodo = todo
s.todos = append(s.todos[:i], s.todos[i+1:]...)
break
}
}
return &proto.TodoResponse{
Id: removedTodo.ID,
Content: removedTodo.Content,
Completed: removedTodo.Completed,
}, nil
}

func main() {
server := grpc.NewServer()
proto.RegisterTodoListServer(server, &TodoListServer{
todos: make([]Todo, 0),
})
listener, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err.Error())
}
if err := server.Serve(listener); err != nil {
panic(err.Error())
}
}

在上述代码中,我实现了 AddTodoToggleTodoRemoveTodo 方法,用于添加、切换状态和删除待办事项。

Node.js 客户端实现

gRPC 客户端配置

// grpcClient.js

const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const PROTO_PATH = "../proto/todolist.proto";

// 加载 .proto 文件
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});

// 创建 gRPC 客户端
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition).proto;
const client = new protoDescriptor.TodoList(
"localhost:8080",
grpc.credentials.createInsecure()
);

module.exports = client;

Express 服务

// app.js

const express = require("express");
const bodyParser = require("body-parser");
const grpcClient = require("./grpcClient");

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

// 跨域设置
app.all("*", (req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type");
res.header("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS");
next();
});

// 添加待办事项
app.post("/add_todo", (req, res) => {
const { content } = req.body;

grpcClient.AddTodo({ content }, (err, response) => {
if (err) {
res.status(500).json({ error: err.details });
} else {
res.json(response);
}
});
});

// 切换待办事项状态
app.post("/toggle_todo", (req, res) => {
const { id } = req.body;

grpcClient.ToggleTodo({ id }, (err, response) => {
if (err) {
res.status(500).json({ error: err.details });
} else {
res.json(response);
}
});
});

// 删除待办事项
app.post("/remove_todo", (req, res) => {
const { id } = req.body;

grpcClient.RemoveTodo({ id }, (err, response) => {
if (err) {
res.status(500).json({ error: err.details });
} else {
res.json(response);
}
});
});

app.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});

在客户端,我使用 Express 框架创建了一个简单的 HTTP 服务器,通过调用 gRPC 客户端的方法与 Go 服务端通信。

注意事项

  • 变量命名:为了提高代码可读性,我将变量名优化为更具描述性的名称,例如将 gClient 改为 grpcClient

  • 错误处理:在 Node.js 客户端中,添加了错误处理,确保在发生错误时返回详细的错误信息。

  • 跨域配置:修正了跨域头部的设置,添加了缺少的 Content-Type

  • Proto 文件:确保 .proto 文件中的服务和消息定义准确,以便正确生成客户端和服务端代码。

  • 版本兼容性:在修改内容时,保持了使用的包的版本不变,针对现有版本进行了优化。

  • 代码块标注:所有代码块均标明了语言类型,便于阅读和语法高亮。