0Pricing
Node.js Backend Development Bootcamp · 课时

一元、服务器、客户端与双向流式 RPC

实现四种 gRPC 调用类型,并为每种交互选择合适的模式。

一元、服务器、客户端与双向流式 RPC 是 CoddyKit 上的免费 Node.js Backend Development Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Node.js Backend Development Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Node.js Backend Development Bootcamp 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

Four Ways to Call an RPC

gRPC defines four kinds of method in your .proto file, and each one maps to a different streaming shape:

  • Unary — one request, one response (like a normal function call).
  • Server streaming — one request, a stream of responses.
  • Client streaming — a stream of requests, one response.
  • Bidirectional streaming — both sides stream independently.

The stream keyword in the service definition is what decides the shape. In this lesson you'll implement all four in Node.js with @grpc/grpc-js and learn when to reach for each.

syntax = "proto3";
package chat;

service ChatService {
  // unary
  rpc GetUser (UserRequest) returns (User);
  // server streaming
  rpc ListMessages (RoomRequest) returns (stream Message);
  // client streaming
  rpc UploadLogs (stream LogLine) returns (UploadSummary);
  // bidirectional streaming
  rpc Chat (stream Message) returns (stream Message);
}

Loading the Proto in Node.js

Before you implement any handler, you load the .proto definition at runtime with @grpc/proto-loader and turn it into a gRPC service object with @grpc/grpc-js.

The loaded packageDefinition mirrors your proto packages and services. You attach handlers to the service with server.addService() — the names you provide must match the rpc names exactly.

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const pkgDef = protoLoader.loadSync('chat.proto', {
  keepCase: true,
  longs: String,
  enums: String,
  defaults: true,
  oneofs: true,
});

const proto = grpc.loadPackageDefinition(pkgDef).chat;
const server = new grpc.Server();

// handlers get attached here
server.addService(proto.ChatService.service, { /* ... */ });

Unary RPC — One In, One Out

A unary handler receives (call, callback). The request payload is on call.request, and you reply by invoking the Node-style callback(error, response).

  • Pass null as the first argument for success.
  • Pass a { code, details } error object to signal failure.

Unary is the right choice for classic request/response work: fetching a record, validating input, performing a single mutation.

function getUser(call, callback) {
  const { id } = call.request;
  if (!id) {
    return callback({
      code: grpc.status.INVALID_ARGUMENT,
      details: 'id is required',
    });
  }
  const user = { id, name: 'Ada Lovelace' };
  callback(null, user);
}

server.addService(proto.ChatService.service, { GetUser: getUser });

Server Streaming — One In, Many Out

A server-streaming handler receives only (call) — there is no callback. You push each response with call.write(message), then signal completion with call.end().

This pattern shines when the server produces a sequence the client can consume incrementally: paginated results, log tails, progress events, or large result sets you don't want to buffer in memory.

function listMessages(call) {
  const { roomId } = call.request;
  const messages = loadMessagesForRoom(roomId); // array

  for (const msg of messages) {
    call.write({ id: msg.id, text: msg.text });
  }
  call.end(); // closes the stream to the client
}

server.addService(proto.ChatService.service, {
  ListMessages: listMessages,
});

Backpressure in Server Streaming

call.write() returns a boolean. When it returns false, the internal buffer is full and you should wait for the 'drain' event before writing more. Ignoring this on large streams can balloon memory usage.

The robust pattern wraps writes in a promise that resolves on drain, so an async loop naturally pauses when the consumer is slow.

function writeAsync(call, msg) {
  return new Promise((resolve) => {
    if (call.write(msg)) resolve();
    else call.once('drain', resolve);
  });
}

async function streamBigResult(call) {
  for (let i = 0; i < 100000; i++) {
    await writeAsync(call, { id: i, text: 'row ' + i });
  }
  call.end();
}

Client Streaming — Many In, One Out

A client-streaming handler receives (call, callback). You listen for incoming items with call.on('data', ...), do final work on call.on('end', ...), and send the single response via the callback.

Use it when the client feeds many items but you only need one aggregate result: bulk uploads, batched metrics ingestion, or computing a summary/average over a stream.

function uploadLogs(call, callback) {
  let count = 0;
  let bytes = 0;

  call.on('data', (logLine) => {
    count += 1;
    bytes += Buffer.byteLength(logLine.text || '');
  });

  call.on('end', () => {
    callback(null, { received: count, totalBytes: bytes });
  });

  call.on('error', (err) => console.error('client stream error', err));
}

Bidirectional Streaming — Many In, Many Out

A bidirectional handler receives only (call) and treats it as a duplex stream: read incoming items with call.on('data', ...) and emit responses with call.write(...) at any time. Both directions are fully independent.

This is ideal for chat, multiplayer state sync, or live request/response negotiation where either side may speak first or out of lockstep.

function chat(call) {
  call.on('data', (msg) => {
    // echo every message back to the sender, augmented
    call.write({ id: msg.id, text: 'echo: ' + msg.text });
  });

  call.on('end', () => call.end());
  call.on('error', (err) => console.error('chat error', err));
}

server.addService(proto.ChatService.service, { Chat: chat });

Calling From a gRPC Client

The client API mirrors the server shapes. Unary returns via a callback; server streaming returns a readable stream you iterate with 'data'; client streaming gives you a writable stream you write() to and then end(); bidirectional gives you both at once.

The single rule: the stream keyword on each side of the proto determines whether you get a callback or a stream object.

const client = new proto.ChatService(
  'localhost:50051',
  grpc.credentials.createInsecure(),
);

// unary
client.GetUser({ id: '1' }, (err, user) => console.log(user));

// server streaming
const stream = client.ListMessages({ roomId: 'general' });
stream.on('data', (m) => console.log(m.text));
stream.on('end', () => console.log('done'));

Mental Model: Pick by Cardinality

Choosing the right RPC type is almost always a question of cardinality on each side:

  • One request, one response → unary.
  • One request, results arrive over time → server streaming.
  • Many inputs collapsed into one result → client streaming.
  • Continuous, independent two-way flow → bidirectional.

Don't reach for streaming just because data is large — pagination over unary calls is often simpler. Reach for streaming when the data is open-ended in time or genuinely incremental.

A Runnable Cardinality Picker

Here's a tiny, framework-free helper that encodes the decision rule above. It takes whether each side streams and returns the gRPC method type — useful as a sanity check when designing a service.

This is plain JavaScript with no gRPC dependency, so an online judge can run it directly.

function rpcType(clientStreams, serverStreams) {
  if (!clientStreams && !serverStreams) return 'unary';
  if (!clientStreams && serverStreams) return 'server-streaming';
  if (clientStreams && !serverStreams) return 'client-streaming';
  return 'bidirectional';
}

console.log(rpcType(false, false)); // unary
console.log(rpcType(false, true));  // server-streaming
console.log(rpcType(true, false));  // client-streaming
console.log(rpcType(true, true));   // bidirectional

Errors, Deadlines, and Cleanup

Across all four types, a few production habits matter:

  • Always attach call.on('error', ...) on streaming handlers — an unhandled stream error can crash the process.
  • Respect deadlines: check call.cancelled in long loops and stop writing if the client gave up.
  • For client/bidi streams, do final work in 'end', not after the first 'data'.
  • Send domain errors with grpc.status codes (e.g. NOT_FOUND, INVALID_ARGUMENT) rather than throwing raw exceptions.
function listMessages(call) {
  let i = 0;
  const timer = setInterval(() => {
    if (call.cancelled) { clearInterval(timer); return; }
    if (i >= 10) { clearInterval(timer); return call.end(); }
    call.write({ id: i, text: 'tick ' + i++ });
  }, 100);

  call.on('error', () => clearInterval(timer));
}

Quick Check

Test your understanding of choosing the right RPC type.

Recap

You now know all four gRPC call shapes and how to implement them in Node.js with @grpc/grpc-js:

  • Unary — (call, callback); read call.request, reply once.
  • Server streaming — (call); call.write() repeatedly, then call.end(), minding backpressure via 'drain'.
  • Client streaming — (call, callback); aggregate on 'data', respond once on 'end'.
  • Bidirectional — (call); read and write independently for live, two-way flows.

Choose by cardinality and whether the data is open-ended in time, always handle stream errors, and respect client deadlines with call.cancelled.

常见问题解答

「一元、服务器、客户端与双向流式 RPC」课时是免费的吗?

是的 — 「一元、服务器、客户端与双向流式 RPC」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Node.js Backend Development Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 Node.js Backend Development Bootcamp 课程共包含 4 节课。

「一元、服务器、客户端与双向流式 RPC」这节课中我会学到什么?

实现四种 gRPC 调用类型,并为每种交互选择合适的模式。 你通过在浏览器中直接运行的动手代码来练习 Node.js Backend Development Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Node.js Backend Development Bootcamp 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Node.js Backend Development Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「一元、服务器、客户端与双向流式 RPC」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Node.js Backend Development Bootcamp 课中编写并运行代码吗?

能。每节 Node.js Backend Development Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 使用 Protobuf IDL 定义服务与消息
  2. 一元、服务器、客户端与双向流式 RPC
  3. 拦截器、截止时间与元数据
  4. Proto 演进与向后兼容
← 返回 Node.js Backend Development Bootcamp