gRPCのserver-side streamをclientから切る方法@Golang
gRPCのサーバサイドストリームって、クライアント側はどうやって切るのかが分かりづらい。
サーバ側は単純にreturn nilするだけで、クライアントにio.EOFが飛ぶからいいんですけどねー。
ってことで、どこが切れて、切ったらどうなるかをまとめてみます。
ソースはこちら
クライアントから切れそうな場所
クライアント側で切れそうな場所がいくつかあるので、とりあえず外側から順に並べてみます
grpc.ClientConn
context.Context
protocl buffersで作られたソースにあるクライアント = stream
それぞれ切断したらどんな動きをするのかなーと試してみました。
Stream
StreamにはCloseSend()というメソッドがあります。
というのもgrpc.ClientStreaminterfaceを埋め込まれているからだと思いますが、そもそも普通にかんがえたらStreamをCloseするなんてできるわけないですよね。
だって、サーバからクライアントへのストリームなんですよ。もしそこでクライアントからサーバに閉じるよーってのを送るってのはできない感じしません?
ってことで、これに関しては試していません。サーバ側でもそれを受け取って動くようなものはないですし。
Context
コンテキストってのが何かってお話なんですけど、親が終わるでーっていうたら、親から生まれた子も処理を終わらせるって感じのもの。
gRPCではクライアントのストリームを作る前に絶対にContextを渡すと思うんですが、それが親から子に渡されたContextです。
ここでいう親はgRPCで受け取ったメッセージをハンドリングする側で、ここでいう子っていうのはメッセージを受け取るためにつないでるコネクションみたいなイメージです。
つまり、親が受け取るのやめるから子もうけとらんでええでーってなったときに、親はContextに対して終わるでってのを伝えて、それが子にも伝播して子もおわるでってなるって感じ。
これを親から切れるようにするには、事前にCancelFuncってのを手に入れておく必要がある。
code:golang
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
context.Background()ってのはContextの基本的な奴で、特別な機能を持っていないContextです。
まずBackgroundでピュアなContextを作って、そこにcontext.WithCancel()でcancelFuncをもらっておきます。
cancelFuncは関数なので、実行すればContextのキャンセルが走るものです。
これでキャンセルの準備は整いました。
code:golang
// 10秒後にログを吐きながらキャンセルを叩く
go func() {
time.Sleep(10 * time.Second)
log.Println("切断開始")
cancel()
log.Println("切断終了")
}()
// ずっとgRPCからのメッセージを受け取り続ける
for {
resp, err := stream.Recv()
if err == io.EOF {
log.Println("streamが切れました")
break
} else if err != nil {
if ctx.Err() == context.Canceled {
log.Println("streamがキャンセルされました")
break
}
log.Fatalln(err)
}
log.Println(resp)
}
10秒後にcancelFuncが叩かれるようにして、あとは普通にgRPCでサーバからのメッセージを待ち受けます。
10秒後にcancelFuncが叩かれると、streamに渡していたContextがキャンセル状態になり、stream.Recv()がエラーを返します。
それを受け取ってハンドリングしているのが、if ctx.Err() == context.Canceled {}の部分。
ctx.Err()でContextからErrorを取り出して、それをContextが事前に用意しているContext.Cancelと一致するかをチェックするという単純な方法です。
これでクライアント側のStreamは切れます。
この時サーバにはまだ何も起こっていませんが、次にサーバがメッセージを送ろうとしたときにエラーになり、そこで切断を検知します。
切断方法としてはこれが大本命かなーといった印象です。
ClientConn
コネクション自体を切ることもできます。
code:golang
conn, err := grpc.Dial("localhost:5432", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %+v\n", err)
}
defer conn.Close()
たぶんつなぐときにはこんな感じで書くと思うのですが、最後にはdeferでコネクション自体を閉じるようにしていると思います。
これを切りたいときにやるって方法です。
ContextのCancelと、ConnectionのCloseとで、サーバ側では変わりはありません。
次に送信するときに送信できなくてエラーになるというだけ。
なのでどちらを選択するかは完全にクライアント側の自由です。
そして大きな違いとしては、エラーのハンドリング難易度です。
Contextのエラーは前述のとおり、context.Cancelと比べるだけでOKでしたが、Connectionはそうもいきません。
Connectionのエラー文はこんな感じです。
rpc error: code = Canceled desc = grpc: the client connection is closing
一応grpcパッケージにErrClientConnClosingというのがあり、これと一致するんですが、Deprecatedとして使用は非推奨です。
正直廃止予定のものを使うのはどうかと思うので、Contextがおすすめです。
ErrClientConnClosingのところにステータスコードを使ったほうがいいよ。とは書かれていますが、
どうすればステータスコードのCancelを取り出せるのかがいまいちわからなかったので、Contextで解決しちゃってます。
おわりに
HTTP2のストリーミングですが、これまでのHTTPのメソッドとは違った存在でまだ直感的には分からない部分が多々あります。
そのなかでもgRPCは分かりやすくドキュメントも多い印象なので、とりあえず勉強してみようってのにはいいんじゃないですかねー。
最終更新日: 2019/06/26