From 7c934310fda92a4d04147edeb38ec350d60dc622 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Mon, 26 Jun 2023 15:23:13 -0700 Subject: [PATCH 1/2] Add metadata example --- examples/metadata/README.md | 15 ++ examples/metadata/client.js | 267 ++++++++++++++++++++++++++++++++++++ examples/metadata/server.js | 152 ++++++++++++++++++++ examples/protos/echo.proto | 45 ++++++ 4 files changed, 479 insertions(+) create mode 100644 examples/metadata/README.md create mode 100644 examples/metadata/client.js create mode 100644 examples/metadata/server.js create mode 100644 examples/protos/echo.proto diff --git a/examples/metadata/README.md b/examples/metadata/README.md new file mode 100644 index 00000000..f8b55de2 --- /dev/null +++ b/examples/metadata/README.md @@ -0,0 +1,15 @@ +# Metadata example + +This example shows how to set and read metadata in RPC headers and trailers. + +## Start the server + +``` +node server.js +``` + +## Run the client + +``` +node client.js +``` diff --git a/examples/metadata/client.js b/examples/metadata/client.js new file mode 100644 index 00000000..83201943 --- /dev/null +++ b/examples/metadata/client.js @@ -0,0 +1,267 @@ +/* + * + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); +const parseArgs = require('minimist'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +const STREAMING_COUNT = 10; + +function unaryCallWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- unary ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.unaryEcho({message}, requestMetadata, (error, value) => { + if (error) { + console.log(`Received error ${error}`); + return; + } + console.log('Response:'); + console.log(`- ${JSON.stringify(value)}`); + }); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + }); +} + +function serverStreamingWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- server streaming ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.serverStreamingEcho({message}, requestMetadata); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('data', value => { + console.log(`Received response ${JSON.stringify(value)}`); + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + call.on('error', error => { + console.log(`Received error ${error}`); + }); + }); +} + +function clientStreamingWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- client streaming ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.clientStreamingEcho(requestMetadata, (error, value) => { + if (error) { + console.log(`Received error ${error}`); + return; + } + console.log('Response:'); + console.log(`- ${JSON.stringify(value)}`); + }); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + for (let i = 0; i < STREAMING_COUNT; i++) { + call.write({message}); + } + call.end(); + }); +} + +function bidirectionalWithMetadata(client, message) { + return new Promise((resolve, reject) => { + console.log('--- bidirectional ---'); + const requestMetadata = new grpc.Metadata(); + requestMetadata.set('timestamp', new Date().toISOString()); + const call = client.bidirectionalStreamingEcho(requestMetadata); + call.on('metadata', metadata => { + const timestamps = metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from header:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in header"); + } + const locations = metadata.get('location'); + if (locations.length > 0) { + console.log('location from header:'); + for (const [index, value] of locations.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("location expected but doesn't exist in header"); + } + }); + call.on('data', value => { + console.log(`Received response ${JSON.stringify(value)}`); + }); + call.on('status', status => { + const timestamps = status.metadata.get('timestamp'); + if (timestamps.length > 0) { + console.log('timestamp from trailer:'); + for (const [index, value] of timestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } else { + console.error("timestamp expected but doesn't exist in trailer"); + } + resolve(); + }); + call.on('error', error => { + console.log(`Received error ${error}`); + }); + for (let i = 0; i < STREAMING_COUNT; i++) { + call.write({message}); + } + call.end(); + }); +} + +function asyncWait(ms) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} + +const message = 'this is examples/metadata'; + +async function main() { + let argv = parseArgs(process.argv.slice(2), { + string: 'target' + }); + let target; + if (argv.target) { + target = argv.target; + } else { + target = 'localhost:50051'; + } + const client = new echoProto.Echo(target, grpc.credentials.createInsecure()); + await unaryCallWithMetadata(client, message); + await asyncWait(1000); + + await serverStreamingWithMetadata(client, message); + await asyncWait(1000); + + await clientStreamingWithMetadata(client, message); + await asyncWait(1000); + + await bidirectionalWithMetadata(client, message); + client.close(); +} + +main(); diff --git a/examples/metadata/server.js b/examples/metadata/server.js new file mode 100644 index 00000000..82130252 --- /dev/null +++ b/examples/metadata/server.js @@ -0,0 +1,152 @@ +/* + * + * Copyright 2023 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +const grpc = require('@grpc/grpc-js'); +const protoLoader = require('@grpc/proto-loader'); + +const PROTO_PATH = __dirname + '/../protos/echo.proto'; + +const packageDefinition = protoLoader.loadSync( + PROTO_PATH, + {keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true + }); +const echoProto = grpc.loadPackageDefinition(packageDefinition).grpc.examples.echo; + +const STREAMING_COUNT = 10; + +function unaryEcho(call, callback) { + console.log('--- UnaryEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + + console.log(`Request received ${JSON.stringify(call.request)}, sending echo`); + callback(null, call.request, outgoingTrailers); +} + +function serverStreamingEcho(call) { + console.log('--- ServerStreamingEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + console.log(`Request received ${JSON.stringify(call.request)}`); + for (let i = 0; i < STREAMING_COUNT; i++) { + console.log(`Echo message ${JSON.stringify(call.request)}`); + call.write(call.request); + } + + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + call.end(outgoingTrailers); +} + +function clientStreamingEcho(call, callback) { + console.log('--- ClientStreamingEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + let lastReceivedMessage = ''; + call.on('data', value => { + console.log(`Received request ${JSON.stringify(value)}`); + lastReceivedMessage = value.message; + }); + call.on('end', () => { + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + callback(null, {message: lastReceivedMessage}, outgoingTrailers); + }); +} + +function bidirectionalStreamingEcho(call) { + console.log('--- BidirectionalStreamingEcho ---'); + const incomingTimestamps = call.metadata.get('timestamp'); + if (incomingTimestamps.length > 0) { + console.log('Timestamp from metadata:'); + for (const [index, value] of incomingTimestamps.entries()) { + console.log(` ${index}. ${value}`); + } + } + + const outgoingHeaders = new grpc.Metadata(); + outgoingHeaders.set('location', 'MTV'); + outgoingHeaders.set('timestamp', new Date().toISOString()); + call.sendMetadata(outgoingHeaders); + + call.on('data', value => { + console.log(`Request received ${JSON.stringify(value)}, sending echo`); + call.write(value); + }); + call.on('end', () => { + const outgoingTrailers = new grpc.Metadata(); + outgoingTrailers.set('timestamp', new Date().toISOString()); + call.end(outgoingTrailers); + }); +} + +const serviceImplementation = { + unaryEcho, + serverStreamingEcho, + clientStreamingEcho, + bidirectionalStreamingEcho +}; + +function main() { + const server = new grpc.Server(); + server.addService(echoProto.Echo.service, serviceImplementation); + server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + server.start(); + }); +} + +main(); diff --git a/examples/protos/echo.proto b/examples/protos/echo.proto new file mode 100644 index 00000000..2dde5633 --- /dev/null +++ b/examples/protos/echo.proto @@ -0,0 +1,45 @@ +/* + * + * Copyright 2018 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +syntax = "proto3"; + +option go_package = "google.golang.org/grpc/examples/features/proto/echo"; + +package grpc.examples.echo; + +// EchoRequest is the request for echo. +message EchoRequest { + string message = 1; +} + +// EchoResponse is the response for echo. +message EchoResponse { + string message = 1; +} + +// Echo is the echo service. +service Echo { + // UnaryEcho is unary echo. + rpc UnaryEcho(EchoRequest) returns (EchoResponse) {} + // ServerStreamingEcho is server side streaming. + rpc ServerStreamingEcho(EchoRequest) returns (stream EchoResponse) {} + // ClientStreamingEcho is client side streaming. + rpc ClientStreamingEcho(stream EchoRequest) returns (EchoResponse) {} + // BidirectionalStreamingEcho is bidi streaming. + rpc BidirectionalStreamingEcho(stream EchoRequest) returns (stream EchoResponse) {} +} From 163597e84bf62c156d9d2db240a36ffff00a29d7 Mon Sep 17 00:00:00 2001 From: Michael Lumish Date: Wed, 28 Jun 2023 14:52:54 -0700 Subject: [PATCH 2/2] Use command line arguments more consistently --- examples/metadata/client.js | 11 +++-------- examples/metadata/server.js | 6 +++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/metadata/client.js b/examples/metadata/client.js index 83201943..e8f6f53f 100644 --- a/examples/metadata/client.js +++ b/examples/metadata/client.js @@ -242,15 +242,10 @@ const message = 'this is examples/metadata'; async function main() { let argv = parseArgs(process.argv.slice(2), { - string: 'target' + string: 'target', + default: {target: 'localhost:50052'} }); - let target; - if (argv.target) { - target = argv.target; - } else { - target = 'localhost:50051'; - } - const client = new echoProto.Echo(target, grpc.credentials.createInsecure()); + const client = new echoProto.Echo(argv.target, grpc.credentials.createInsecure()); await unaryCallWithMetadata(client, message); await asyncWait(1000); diff --git a/examples/metadata/server.js b/examples/metadata/server.js index 82130252..b061d20a 100644 --- a/examples/metadata/server.js +++ b/examples/metadata/server.js @@ -142,9 +142,13 @@ const serviceImplementation = { }; function main() { + const argv = parseArgs(process.argv.slice(2), { + string: 'port', + default: {port: '50052'} + }); const server = new grpc.Server(); server.addService(echoProto.Echo.service, serviceImplementation); - server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => { + server.bindAsync(`0.0.0.0:${argv.port}`, grpc.ServerCredentials.createInsecure(), () => { server.start(); }); }