前回からの続きです。
1. Metadataを扱う
まずgrpc-jsに定義されているMetadata型のインスタンスを生成し、Metadataのキーと値をaddします。
生成したMetadataインスタンスをcallback関数の第三引数に渡してあげることで、クライアントにMetadataを返却することができます。
//protoで定義したServiceを実装するクラス定義。
import * as grpc from "@grpc/grpc-js";
import { IGreeterServer } from "../generate/greeter_grpc_pb";
import { HelloRequest, HelloReply } from "../generate/greeter_pb";
export class GreeterServer implements IGreeterServer {
[name: string]: grpc.UntypedHandleCall;
sayHello(call: grpc.ServerUnaryCall<HelloRequest, HelloReply>, callback: grpc.sendUnaryData<HelloReply>): void {
const reply = new HelloReply();
reply.setMessage('Hello ' + call.request.getName());
//メタデータをセットする
const metadata = new grpc.Metadata();
metadata.add("id", "hogehoge");
//第一引数がエラーオブジェクト。第二引数がレスポンスオブジェクト。
//第三引数がメタデータ
callback(null, reply, metadata);
}
}
結果
grpCurlに-vオプションを追加することでMetadataなどの詳細情報をコンソールに出力することができます。
下から2行目で「id: hogehoge」がレスポンスのtrailersとして返ってきていることが確認できます。
$ grpcurl -v -plaintext -d '{"name": "hoge"}' -import-path . -proto proto/greet.proto localhost:50051 greet.Greeter/SayHello
Resolved method descriptor:
rpc SayHello ( .greet.HelloRequest ) returns ( .greet.HelloReply );
Request metadata to send:
(empty)
Response headers received:
content-type: application/grpc+proto
date: Sat, 15 Apr 2023 13:57:36 GMT
grpc-accept-encoding: identity,deflate,gzip
Response contents:
{
"message": "Hello hoge"
}
Response trailers received:
id: hogehoge
Sent 1 request and received 1 response
2. エラーレスポンスを返す
callbackの第一引数にPartial<grpc.StatusObject>のオブジェクトを渡します。
grpc.StatusObjectの定義は以下のようになっており、code, details, metadataをキーに持つことが分かります。
export interface StatusObject {
code: Status;
details: string;
metadata: Metadata;
}
では、定義を確認したところでcallbackにPartialを渡しましょう。
if(call.request.getName() === 'nobody'){
callback({
code: grpc.status.NOT_FOUND,
details: "誰でもない",
metadata: metadata,
});
}else{
//第一引数がエラーオブジェクト。第二引数がレスポンスオブジェクト。
//第三引数がメタデータ
callback(null, reply, metadata);
}
上記ではリクエストに含まれるnameが"nobody"だった場合、NOT_FOUNDを返すようにしています。
今回はすべてのプロパティを指定しましたが、Partialで定義されているのでcodeだけとか、codeとmetadataだけ、といった指定も可能です。
codeを指定しなかった場合はUNKNOWN、detailsを指定しなかった場合はUnknown Errorというメッセージがクライアントに返却されます。
上記のgreeterServer.tsに対して、実際にnobodyにしてリクエストを送信した結果が以下になります。
$ grpcurl -v -plaintext -d '{"name": "nobody"}' -import-path . -proto proto/greeter.proto localhost:50051 greet.Greeter/SayHello
Resolved method descriptor:
rpc SayHello ( .greet.HelloRequest ) returns ( .greet.HelloReply );
Request metadata to send:
(empty)
Response headers received:
(empty)
Response trailers received:
content-type: application/grpc+proto
date: Tue, 18 Apr 2023 07:12:55 GMT
id: hogehoge
Sent 1 request and received 0 responses
ERROR:
Code: NotFound
Message: 誰でもない
ちなみに
第一引数のmetadataプロパティと第三引数を同時に指定した場合、第一引数のmetadataがクライアントに返却され第3引数は無視されます。
逆に第一引数にmetadataプロパティを指定しなかった場合は、第三引数のmetadataがクライアントに返却されます。
callback({
code: grpc.status.NOT_FOUND,
metadata: metadata,
}, null, metadata2); //metadata2は無視される
3. bytes型を扱う
JavaScript/TypeScriptではprotoファイルのbytes型はUint8Array型に変換されます。
message HelloReply {
string message = 1;
google.protobuf.Timestamp current_time = 2;
bytes encoded_string = 3;
}
export class HelloReply extends jspb.Message {
getEncodedString(): Uint8Array | string;
getEncodedString_asU8(): Uint8Array;
getEncodedString_asB64(): string;
setEncodedString(value: Uint8Array | string): HelloReply;
なので自動生成されたクラスに値をセットするにはUint8Array型に変換してあげればOKです。
以下ではStringをTextEncoderを使ってエンコードしていますが、画像などのバイナリもArrayBufferを用いてUint8Array型に変換すれば同様に扱えます。
//文字列をエンコードしバイト配列(Uint8Array)を得る
const byteMessage = new TextEncoder().encode("ABCDEFG");
console.log(byteMessage);
reply.setEncodedString(byteMessage);
リクエストを送信(grpCurlから)
grpCurlでリクエストを送信するとBASE64でエンコードされたレスポンスが確認できます。
grpcurl -plaintext -d '{"name": "me"}' -import-path . -proto proto/greeter.proto localhost:50051 greet.Greeter/SayHello
{
"message": "Hello me",
"currentTime": "2023-04-19T14:12:33Z",
"encodedString": "QUJDREVGRw=="
}
サーバー側のログには16進数でABCDEFGを表すバイト配列が出力されています。
リクエストを送信(TypeScriptで書かれたクライアントから)
レスポンスに含まれるbytesはstring | Uint8Arrayの合併型なのでbyte配列として取得するためにUint8Arrayにキャストします。
const encodedString = response.getEncodedString() as Uint8Array;
const resStr = convertByte2String(encodedString);
console.log(`bytes型のレスポンス: ${resStr}`);
function convertByte2String(bytes: Uint8Array): string {
return new TextDecoder().decode(bytes)
}
クライアントを実行
サーバーから送られてきた文字列がデコードできていることが確認できました。
$ npm run client
> node_grpc_qiita_verify@1.0.0 client
> ts-node src/client.ts
Greeting: Hello
bytes型のレスポンス: ABCDEFG
4. 事前定義された型を扱う
Protocol bufferではTimestampや、何もリクエスト・レスポンスボディに設定しないときに使えるEmptyなどよく使う型が事前定義されています。
ここではTimestamp型を例に使い方を紹介します。
//実態は「node_modules/@types/google-protobuf/google/protobuf/timestamp_pb.d.ts」にある
import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
//現在日時を取得し、epoch秒に変換する
const currentDate = new Date();
let currentTime = new Timestamp();
currentTime.setSeconds(Math.floor(currentDate.getTime() / 1000));
reply.setCurrentTime(currentTime);
google-protobufと@types/google-protobufをインストールしていれば、上記のように事前定義された型をimportすることができるようになります。
Timestamp型の注意点として、タイムゾーン情報を保持することができません。
JS/TSではDate型はタイムゾーン情報を持たないので心配は少ないですが、タイムゾーン情報を持つライブラリを使用している際などは意図せず誤ったエポック秒をsecondsフィールドにセットしないよう気をつけて下さい。
まとめ
以上のサンプルはこのリポジトリにも上げているので合わせてご参考下さい。
https://github.com/Aniokrait/gRPC-TypeScript-Sample