你的第一个 gRPC API 在 Node.js 中

发布: (2026年2月2日 GMT+8 13:08)
6 min read
原文: Dev.to

Source: Dev.to

Your first gRPC API in Node.js 的封面图片

Cheulong Sear

为什么使用 gRPC?

gRPC 是一个高性能、开源的通用 RPC 框架,可在微服务和无服务器架构上原生运行。

示例

Rest API

一个简单的餐厅服务 REST API 示例,根据餐厅 ID 返回餐厅列表。

// restaurant-service

import express from "express";

const app = express();
app.use(express.json());

const restaurantsData = [
  {
    id: "res_001",
    name: "Spicy Garden",
    cuisine: "Indian",
    location: "New York",
    rating: 4.5,
    isOpen: true,
  },
  {
    id: "res_002",
    name: "Sushi Zen",
    cuisine: "Japanese",
    location: "San Francisco",
    rating: 4.7,
    isOpen: false,
  },
  {
    id: "res_003",
    name: "Pasta Palace",
    cuisine: "Italian",
    location: "Chicago",
    rating: 4.2,
    isOpen: true,
  },
  {
    id: "res_004",
    name: "Burger Hub",
    cuisine: "American",
    location: "Austin",
    rating: 4.0,
    isOpen: true,
  },
  {
    id: "res_005",
    name: "Dragon Wok",
    cuisine: "Chinese",
    location: "Seattle",
    rating: 4.6,
    isOpen: false,
  },
];

app.post("/restaurants/bulk", (req, res) => {
  const { restaurantIds } = req.body;

  const result = restaurantsData.filter((r) =>
    restaurantIds.includes(r.id)
  );

  res.json(result);
});

app.listen(3001, () => {
  console.log("Server is running on port 3001");
});

将此 REST API 转换为 gRPC

1. 定义 .proto 文件

.proto 文件是一种语言无关、平台无关、协议无关的接口定义语言(IDL),用于描述服务和消息格式。

查看文档了解更多细节:

// restaurants.proto

syntax = "proto3";

package restaurants;

service RestaurantService {
  rpc GetRestaurantsBulk (BulkRestaurantRequest) returns (BulkRestaurantResponse);
}

message BulkRestaurantRequest {
  repeated string restaurantIds = 1;
}

message Restaurant {
  string id = 1;
  string name = 2;
  string cuisine = 3;
  string location = 4;
  double rating = 5;
  bool isOpen = 6;
}

message BulkRestaurantResponse {
  repeated Restaurant restaurants = 1;
}

2. 实现一个 gRPC 服务器 来替代 Express 路由

安装依赖

bun add @grpc/grpc-js @grpc/proto-loader

服务器代码 (grpc-server.js)

// restaurant-service/grpc-server.js

import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
import path from "path";

const PROTO_PATH = path.resolve("restaurants.proto");

// Load proto definition
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const restaurantProto = grpc.loadPackageDefinition(packageDefinition).restaurants;

// Mock data (same as Express)
const restaurantsData = [
  {
    id: "res_001",
    name: "Spicy Garden",
    cuisine: "Indian",
    location: "New York",
    rating: 4.5,
    isOpen: true,
  },
  {
    id: "res_002",
    name: "Sushi Zen",
    cuisine: "Japanese",
    location: "San Francisco",
    rating: 4.7,
    isOpen: false,
  },
  {
    id: "res_003",
    name: "Pasta Palace",
    cuisine: "Italian",
    location: "Chicago",
    rating: 4.2,
    isOpen: true,
  },
  {
    id: "res_004",
    name: "Burger Hub",
    cuisine: "American",
    location: "Austin",
    rating: 4.0,
    isOpen: true,
  },
  {
    id: "res_005",
    name: "Dragon Wok",
    cuisine: "Chinese",
    location: "Seattle",
    rating: 4.6,
    isOpen: false,
  },
];

// gRPC method implementation
function getRestaurantsBulk(call, callback) {
  const { restaurantIds } = call.request;

  const restaurants = restaurantsData.filter((r) =>
    restaurantIds.includes(r.id)
  );

  callback(null, { restaurants });
}

// Create and start the server
const server = new grpc.Server();

server.addService(restaurantProto.RestaurantService.service, {
  GetRestaurantsBulk: getRestaurantsBulk,
});

server.bindAsync(
  "0.0.0.0:50051",
  grpc.ServerCredentials.createInsecure(),
  (port, error) => {
    if (error) {
      console.error("Error binding server:", error);
      return;
    }
    console.log("gRPC Server running on port", port);
    server.start();
  }
);

3. 创建一个 gRPC 客户端 来替代 Express 客户端

// user-service/grpc-client.ts

import express from "express";
import grpc from "@grpc/grpc-js";
import protoLoader from "@grpc/proto-loader";
import path from "path";

const app = express();

// Load proto definition
const PROTO_PATH = path.resolve("restaurants.proto");

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const restaurantProto: any = grpc.loadPackageDefinition(packageDefinition).restaurants;

// Create a client that talks to the gRPC server
const restaurantClient = new restaurantProto.RestaurantService(
  "localhost:50051",
  grpc.credentials.createInsecure()
);

// Example endpoint that uses the gRPC client
app.get("/my-visited-restaurants", (req, res) => {
  // In a real scenario, you would get these IDs from the user's history
  const visitedIds = ["res_001", "res_003", "res_005"];

  restaurantClient.GetRestaurantsBulk(
    { restaurantIds: visitedIds },
    (err: any, response: any) => {
      if (err) {
        console.error("gRPC erro

#### 附加客户端示例

```ts
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import express from 'express';

const app = express();

const restaurantProtoPath = __dirname + '/proto/restaurant.proto';
const restaurantPackageDefinition = protoLoader.loadSync(restaurantProtoPath, {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});
const restaurantProto: any = grpc.loadPackageDefinition(restaurantPackageDefinition).restaurant;

const restaurantClient = new restaurantProto.RestaurantService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

const userData = [
  {
    id: 'user_001',
    name: 'John Doe',
    email: 'john.doe@example.com',
    vistedRestaurant: ['res_001', 'res_002', 'res_003'],
  },
];

app.get('/users/:id/visit-restaurants', async (req, res) => {
  const userId = req.params.id;
  const userDetailData = userData.find((user) => user.id === userId);

  restaurantClient.GetRestaurantsBulk(
    {
      restaurantIds: userDetailData?.vistedRestaurant,
    },
    (err: grpc.ServiceError | null, response: any) => {
      if (err) {
        console.error(err);
        return res.status(500).json({ message: 'gRPC error' });
      }

      res.json({
        visitedRestaurantsDetail: response.restaurants,
      });
    }
  );
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

通过 .proto 定义、gRPC 服务器和匹配的客户端,你已经将一个简单的 REST 端点转换为高性能的 gRPC 服务。 🎉

Comparison: Express (REST) vs. gRPC

特性REST (JSON over HTTP)gRPC (Protobuf over HTTP/2)
传输层HTTP/1.1HTTP/2
负载格式JSONProtobuf(二进制)
性能中等高(二进制 + HTTP/2)
安全性TLS/HTTPSTLS/HTTPS + 内置认证
可扩展性良好在高流量下表现卓越
工具链生态系统丰富正在成长但尚不成熟
学习曲线较高(proto 文件、桩代码)
灵活性非常灵活灵活性较低(严格的契约)

其他优势

  • 性能:gRPC 比 REST 更快,因为它使用二进制编码和 HTTP/2,能够实现更好的压缩和更快的数据传输。
  • 安全性:gRPC 支持身份验证和授权,安全性高于 REST。
  • 可扩展性:gRPC 设计用于处理高流量和大数据集,因而比 REST 更具可扩展性。

缺点

  • 学习曲线:gRPC 的学习曲线比 REST 陡峭,学习和实现难度更大。
  • 工具链:相较于 REST,gRPC 的工具支持有限,调试和测试更具挑战性。
  • 灵活性:gRPC 的灵活性低于 REST,适应需求变化的难度更大。

结论

gRPC 是构建微服务的强大且高效的方式,但它并非万用解决方案。它在高性能、低延迟和高吞吐量的应用场景中表现出色。

请保持联系

  • 作品集:
  • 领英:
  • GitHub:
  • YouTube:
Back to Blog

相关文章

阅读更多 »

Koa 的零配置支持

Vercel 现在支持 applications,一个富有表现力的 HTTP 中间件框架,使编写 Web 应用和 API 更加愉快,且无需配置。Koa…