tag:crieit.net,2005:https://crieit.net/tags/gRPC/feed 「gRPC」の記事 - Crieit Crieitでタグ「gRPC」に投稿された最近の記事 2024-05-02T23:53:49+09:00 https://crieit.net/tags/gRPC/feed tag:crieit.net,2005:PublicArticle/18284 2022-08-20T12:35:05+09:00 2024-05-02T23:53:49+09:00 https://crieit.net/posts/connect-go-with-cors connect-webを試そうとしたらCORSまわりでちょっとはまった話 <h1 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://future-architect.github.io/articles/20220819a/">gRPCがフロントエンド通信の第一の選択肢になる時代がやってきたかも? | フューチャー技術ブログ</a> を読んで、「HTTP/1.1で動いてcurlで投げたJSONも処理できるgRPC(もどき)って最強じゃん!」と思ったので <a target="_blank" rel="nofollow noopener" href="https://connect.build/docs/introduction">チュートリアル</a> を試してみた。</p> <p>connect-go のほうのチュートリアルは自分で書いたprotocol bufferからサーバーとクライアントを両方作る方法の解説だったのだけれど、connect-web のほうのチュートリアルは既存のWebサービスにつなぐためのクライアントの作り方の解説であったため、チュートリアルを参考にしつつ connect-go で作ったサーバーに接続する connect-web のクライアントを作ろうとした。</p> <h1 id="本文"><a href="#%E6%9C%AC%E6%96%87">本文</a></h1> <h2 id="やろうとしたこと"><a href="#%E3%82%84%E3%82%8D%E3%81%86%E3%81%A8%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">やろうとしたこと</a></h2> <p>まずは <a target="_blank" rel="nofollow noopener" href="https://connect.build/docs/web/getting-started">Getting started | Connect</a> のPrepareの節を行い、プロジェクトをつくる。</p> <p>次に、protocol bufferからTypeScriptのコードを生成。<a target="_blank" rel="nofollow noopener" href="https://connect.build/docs/web/generating-code">Generating code | Connect</a> にある通りの <code>buf.gen.yaml</code> を作った後、</p> <pre><code>ln -s ../connect-go-example/greet greet buf generate </code></pre> <p>で <code>gen</code> 以下にTypeScriptのコードを生成する。</p> <p>そして、App.tsx を以下のようにした。</p> <pre><code class="javascript">import { createConnectTransport, createPromiseClient, } from "@bufbuild/connect-web"; import React, { useState } from "react"; import { GreetService } from "../gen/greet/v1/greet_connectweb"; import './App.css' const transport = createConnectTransport({ baseUrl: "http://localhost:8080", }) const client = createPromiseClient(GreetService, transport) function App() { const [userName, setUserName] = useState(""); const [message, setMessage] = useState("ここにメッセージが入ります"); const sendName = async (e: React.FormEvent<HTMLElement>) => { e.preventDefault(); const res = await client.greet({ name: userName, }) setMessage(res.greeting) } return ( <div className="App"> <p>{message}</p> <form onSubmit={sendName}> <input value={userName} onChange={e => setUserName(e.target.value)} /> <button type="submit">送信</button> </form> </div> ) } export default App </code></pre> <p>これで <code>npm run dev</code> して表示されたブラウザで「送信」ボタンを押すといいかんじに名前が表示されるはず……だが、何も出ない。ブラウザの開発者コンソールを見ると、<strong>405 Method Not Allowed</strong> のエラーが返ってきている。</p> <p><a href="https://crieit.now.sh/upload_images/cd71f0d0d2c7cca896698c0a4bd3a31d630051c1b137f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/cd71f0d0d2c7cca896698c0a4bd3a31d630051c1b137f.png?mw=700" alt="image.png" /></a></p> <h2 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h2> <p>このエラーの意味は「POSTしか受け付けないサーバーにOPTIONSのリクエストを送っている」というものである。が、OPTIONSなんて聞いたこともないようなHTTPリクエストを送っているつもりはない。それでしばらく調べたのだが、これはCORSプリフライトというやつらしい。「単純リクエスト」以外のリクエストを送りたい時は予めOPTIONSで宛先サーバーの様子を調べるのである。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/CORS">オリジン間リソース共有 (CORS) - HTTP | MDN</a></li> </ul> <h2 id="対処"><a href="#%E5%AF%BE%E5%87%A6">対処</a></h2> <p>というわけで、このOPTIONSをちゃんと処理できるような処理をサーバー側に足せば良いということになる。Goの <code>net/http</code> の場合は <a target="_blank" rel="nofollow noopener" href="https://github.com/rs/cors">rs/cors: Go net/http configurable handler to handle CORS requests</a> という定番ライブラリがあるようなので、これを噛ませてみることにする。</p> <pre><code class="go">package main import ( "context" greetv1 "example/gen/greet/v1" "example/gen/greet/v1/greetv1connect" "fmt" "log" "net/http" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "github.com/bufbuild/connect-go" "github.com/rs/cors" ) type GreetServer struct{} func (s *GreetServer) Greet( ctx context.Context, req *connect.Request[greetv1.GreetRequest], ) (*connect.Response[greetv1.GreetResponse], error) { log.Println("Request headers: ", req.Header()) res := connect.NewResponse(&greetv1.GreetResponse{ Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name), }) res.Header().Set("Greet-Version", "v1") return res, nil } func main() { greeter := &GreetServer{} mux := http.NewServeMux() path, handler := greetv1connect.NewGreetServiceHandler(greeter) mux.Handle(path, handler) corsHandler := cors.Default().Handler(h2c.NewHandler(mux, &http2.Server{})) // corsのハンドラを追加した // corsHandler := h2c.NewHandler(mux, &http2.Server{}) // もとの実装はこれ http.ListenAndServe( "localhost:8080", corsHandler, ) } </code></pre> <p>これでサーバーを立て直すと、無事リクエストが処理されるようになった。</p> <h1 id="感想"><a href="#%E6%84%9F%E6%83%B3">感想</a></h1> <p>Connect自体とは関係ない現代の基本的なWeb技術のところではまってしまった。そろそろちゃんとCORSを理解しないといけないと思った。</p> すずしめ tag:crieit.net,2005:PublicArticle/17789 2021-11-27T13:51:05+09:00 2024-05-02T23:53:20+09:00 https://crieit.net/posts/etude-grpc-tonic tonicを使ってgRPCサーバーを立ててみる <p>gRPCサーバー+grpc-webをバックエンドにしてWiki的なのをつくろうという気持ちがあるので、そのための実験をしています。今回はgRPCサーバーをRustでつくれるかの調査です。</p> <h1 id="ライブラリ選定"><a href="#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E9%81%B8%E5%AE%9A">ライブラリ選定</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://medium.com/nttlabs/rust-and-grpc-cb10a783c9da">Rustで、Hello Worldを卒業した?次は、gRPCだよね! | by FUJITA Tomonori | nttlabs | Medium</a> というRust教な記事があり、ここで <strong><a target="_blank" rel="nofollow noopener" href="https://github.com/hyperium/tonic">tonic</a></strong> が推されています。</p> <p>私は別にRust教信者ではないですが、Rustだけで完結していると環境構築が楽そうなので、とりあえずtonicを試してみることにしました。<a target="_blank" rel="nofollow noopener" href="https://lib.rs/search?q=grpc">‘grpc’ search // Lib.rs</a> でもtonicが一番人気っぽいですし、まあたぶんよくできているのでしょう。</p> <h1 id="Hello world"><a href="#Hello+world">Hello world</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/hyperium/tonic/blob/master/examples/helloworld-tutorial.md">tonic/helloworld-tutorial.md at master · hyperium/tonic</a>に従ってHelloWorldします。</p> <h2 id="環境準備"><a href="#%E7%92%B0%E5%A2%83%E6%BA%96%E5%82%99">環境準備</a></h2> <p>Rust 2021 Editionに依存しているのでRust 1.56以降が必要というのはなかなか面白いですね。</p> <p>エディタにはVSCode、補完にはrust-analyzerを使います。ここで注意点なのですが、READMEには</p> <blockquote> <p>If you're using rust-analyzer we recommend you set "rust-analyzer.cargo.loadOutDirsFromCheck": true to correctly load the generated code.</p> </blockquote> <p>とあるものの、この通りにやると "unknown configuration setting" とVSCodeに言われてしまいます。<a target="_blank" rel="nofollow noopener" href="https://github.com/rust-analyzer/rust-analyzer/issues/6448#issuecomment-961441435">rust-analyzerのIssue</a>によると、この <code>rust-analyzer.cargo.loadOutDirsFromCheck</code> 設定は <code>rust-analyzer.cargo.runBuildScripts</code> に改名された上に既定でオンになるようになったらしいです。つまり、特に何の設定もしなくてもよいということですね。</p> <h2 id="proto/sinanoki.proto"><a href="#proto%2Fsinanoki.proto">proto/sinanoki.proto</a></h2> <p>protocol bufferでAPI定義を書きます。なお、このsinanokiというのは単なる(私が作ろうとしているものの)プロジェクト名なので意味は無いです。</p> <pre><code>syntax = "proto3"; package sinanoki; service PageManager { rpc GetPage(GetPageRequest) returns (GetPageResponce); } message GetPageRequest { string path = 1; } message GetPageResponce { string markdown = 1; } </code></pre> <h2 id="cargo.toml"><a href="#cargo.toml">cargo.toml</a></h2> <pre><code class="toml">[package] name = "sinanoki-backend" version = "0.1.0" authors = ["suzusime <***@gmail.com>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] tonic = "0.6" prost = "0.9" tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } [build-dependencies] tonic-build = "0.6" </code></pre> <p>言われる通りにdependenciesの設定を追加します。今回はサーバーだけ(複数の実行ファイルはいらない)なので、binまわりの設定は飛ばしてserver.rsの代わりにmain.rsに書くことにします。</p> <h2 id="build.rs"><a href="#build.rs">build.rs</a></h2> <p>ビルドに対して特殊な処理が必要になるので、ルートフォルダにbuild.rsを置きます。</p> <pre><code class="rust">fn main() -> Result<(), Box<dyn std::error::Error>> { tonic_build::compile_protos("./proto/sinanoki.proto")?; Ok(()) } </code></pre> <h2 id="src/main.rs"><a href="#src%2Fmain.rs">src/main.rs</a></h2> <p>今までの設定をして<code>cargo build</code>すると、protocol bufferに書いたものに対応したサーバー/クライアントの雛型となるRustのコードがtarget以下に生成されるようになります。これを利用してサーバーを実装します。</p> <p>例えば私の場合は <code>target/debug/build/sinanoki-backend-1664b6d9540c1c9f/out/sinanoki.rs</code> に雛型コードが吐かれました。この雛型コードの中に</p> <pre><code class="rust"> #[async_trait] pub trait PageManager: Send + Sync + 'static { async fn get_page( &self, request: tonic::Request<super::GetPageRequest>, ) -> Result<tonic::Response<super::GetPageResponce>, tonic::Status>; } </code></pre> <p>というのがあるので、これをコピペしたものを使い、以下のようなコードを<code>src/main.rs</code>に書きます。</p> <pre><code class="rust">use sinanoki::page_manager_server::{PageManager, PageManagerServer}; use sinanoki::{GetPageRequest, GetPageResponce}; use tonic::{transport::Server, Request, Response, Status}; pub mod sinanoki { tonic::include_proto!("sinanoki"); // The string specified here must match the proto package name } #[derive(Debug, Default)] pub struct MyPageManager {} #[tonic::async_trait] impl PageManager for MyPageManager { async fn get_page( &self, request: Request<GetPageRequest>, ) -> Result<Response<GetPageResponce>, Status> { println!("Got a request: {:?}", request); let reply = GetPageResponce { markdown: format!("you requested a file of {}", request.into_inner().path).into(), // We must use .into_inner() as the fields of gRPC requests and responses are private }; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let addr = "[::1]:50051".parse()?; let manager = MyPageManager::default(); Server::builder() .add_service(PageManagerServer::new(manager)) .serve(addr) .await?; Ok(()) } </code></pre> <h2 id="実行"><a href="#%E5%AE%9F%E8%A1%8C">実行</a></h2> <p>あとは</p> <pre><code>$ cargo run </code></pre> <p>して、</p> <pre><code>$ grpcurl -plaintext -import-path ./proto -proto ./proto/sinanoki.proto -d '{"path": "/index.md"}' localhost:50051 sinanoki.PageManager/GetPage { "markdown": "you requested a file of /index.md" } </code></pre> <p>で動作が確認できました。</p> <h1 id="所感"><a href="#%E6%89%80%E6%84%9F">所感</a></h1> <p>どうせ何かハマるだろうと思っていましたが、すんなり動きました。</p> <p>困ったところを挙げるならば、(tonicに限らず)Rustの補完があまり気持ちよく動いてくれないことでしょうか。単にバグっぽいところ(implしようとしたときに勝手に出してくれるコードが明らかにおかしい)を除いても適切な補完候補がなかなか出てくれなくて、少し厳しいです。おそらく私の設定が悪いんだろうなと思っているのですが……。</p> すずしめ tag:crieit.net,2005:PublicArticle/17761 2021-11-12T01:52:27+09:00 2024-05-02T23:52:54+09:00 https://crieit.net/posts/grpc-cs-dotnet6-on-mac C#(.Net 6)でgRPCサーバー on Mac <p>.Net 6 も出たし……と思い、C# で gRPC のサンプルをMacで動かそうとしたらエラーで落ちてちょっと戸惑ったので、手順を書いておきます。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-6.0&tabs=visual-studio-code">ASP.NET Core で .NET Core gRPC のクライアントとサーバーを作成する | Microsoft Docs</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/aspnet/core/grpc/test-tools?view=aspnetcore-6.0">ASP.NET Core で gRPCurl を使用して gRPC サービスをテストする | Microsoft Docs</a></li> </ul> <h1 id="手順"><a href="#%E6%89%8B%E9%A0%86">手順</a></h1> <h2 id="テンプレートからプロジェクトを作る"><a href="#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%81%8B%E3%82%89%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E4%BD%9C%E3%82%8B">テンプレートからプロジェクトを作る</a></h2> <p>gRPCのテンプレからプロジェクトを作ります。</p> <pre><code class="bash">$ dotnet new grpc -o GrpcGreeter </code></pre> <h2 id="実行(落ちる)"><a href="#%E5%AE%9F%E8%A1%8C%EF%BC%88%E8%90%BD%E3%81%A1%E3%82%8B%EF%BC%89">実行(落ちる)</a></h2> <p>普通に実行します。するとMacでは落ちます。</p> <pre><code class="bash">$ cd GrpcGreeter $ dotnet run ビルドしています... Unhandled exception. System.IO.IOException: Failed to bind to address https://localhost:7229. ---> System.AggregateException: One or more errors occurred. (HTTP/2 over TLS is not supported on macOS due to missing ALPN support.) (HTTP/2 over TLS is not supported on macOS due to missing ALPN support.) </code></pre> <h2 id="対処:TLSを使わない"><a href="#%E5%AF%BE%E5%87%A6%EF%BC%9ATLS%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84">対処:TLSを使わない</a></h2> <p>落ちる理由はエラーメッセージにも <a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-6.0&tabs=visual-studio-code">チュートリアル</a> にも書いてあって、Macや古いWindowsではHTTP2 on TLSがサポートされていないからです。よってTLSを使わないようにすれば解決です。</p> <p>で、<a target="_blank" rel="nofollow noopener" href="https://docs.microsoft.com/ja-jp/aspnet/core/grpc/troubleshoot?view=aspnetcore-6.0#unable-to-start-aspnet-core-grpc-app-on-macos">.NET Core での gRPC のトラブルシューティング | Microsoft Docs</a>にはProgram.csに書き足せという旨が書かれているわけですが、これが正直よくわからない。たぶんテンプレートの形式が変わってしまったのだろうなと思われます。</p> <p>というわけで、次のようにして雑に解決しました。</p> <p>Properties/launchSettings.json には</p> <pre><code class="json">{ "profiles": { "GrpcGreeter": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "http://localhost:5142;https://localhost:7229", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } </code></pre> <p>とありますが、これの <code>applicationUrl</code> からhttpsのほうのURLを削り、</p> <pre><code class="json">{ "profiles": { "GrpcGreeter": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "http://localhost:5142", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } </code></pre> <p>として保存します。</p> <h2 id="実行"><a href="#%E5%AE%9F%E8%A1%8C">実行</a></h2> <p>これで実行できます。</p> <pre><code class="bash">$ dotnet run ビルドしています... info: Microsoft.Hosting.Lifetime[14] Now listening on: http://localhost:5142 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: /Users/suzusime/sandbox/GrpcGreeter/ </code></pre> <h2 id="grpcurlでテスト"><a href="#grpcurl%E3%81%A7%E3%83%86%E3%82%B9%E3%83%88">grpcurlでテスト</a></h2> <p>これでサーバーが立つことがわかったので、grpcurlでAPIを叩いてみます。</p> <h3 id="reflectionを追加"><a href="#reflection%E3%82%92%E8%BF%BD%E5%8A%A0">reflectionを追加</a></h3> <p>grpcurlで叩くにはサーバーにreflection機能を追加する必要があるので、追加します(Goでやったときは不要だった気がするので本当は不要なのかも)。</p> <p>まずは</p> <pre><code class="bash">$ dotnet add package Grpc.AspNetCore.Server.Reflection --version 2.40.0 </code></pre> <p>でプロジェクトを追加します。</p> <p>次に、<code>Program.cs</code>を</p> <pre><code class="cs">using GrpcGreeter.Services; var builder = WebApplication.CreateBuilder(args); // Additional configuration is required to successfully run gRPC on macOS. // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 // Add services to the container. builder.Services.AddGrpc(); builder.Services.AddGrpcReflection(); // 追加 var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGrpcService<GreeterService>(); app.MapGrpcReflectionService(); // 追加 app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); app.Run(); </code></pre> <p>のように編集します。</p> <h3 id="grpcurlのインストール"><a href="#grpcurl%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">grpcurlのインストール</a></h3> <p>homebrewで入ります。</p> <pre><code class="bash">$ brew install grpcurl </code></pre> <h3 id="grpcurlで叩く"><a href="#grpcurl%E3%81%A7%E5%8F%A9%E3%81%8F">grpcurlで叩く</a></h3> <p>サーバーを立てます。</p> <pre><code class="bash">$ dotnet run </code></pre> <p>別のコンソールを開いてgrpcurlでサーバーを叩きましょう。まずは describe でAPIの使い方を調べてみます。</p> <pre><code class="bash"># 機能の一覧を出す $ grpcurl -plaintext localhost:5142 describe greet.Greeter is a service: service Greeter { rpc SayHello ( .greet.HelloRequest ) returns ( .greet.HelloReply ); } grpc.reflection.v1alpha.ServerReflection is a service: service ServerReflection { rpc ServerReflectionInfo ( stream .grpc.reflection.v1alpha.ServerReflectionRequest ) returns ( stream .grpc.reflection.v1alpha.ServerReflectionResponse ); } # 型の詳細を見る $ grpcurl -plaintext localhost:5142 describe greet.HelloRequest greet.HelloRequest is a message: message HelloRequest { string name = 1; } </code></pre> <p>これでAPIの叩き方がわかったので、リクエストを送ってみます。</p> <pre><code class="bash">$ grpcurl -plaintext -d '{ "name": "世界" }' localhost:5142 greet.Greeter/SayHello { "message": "Hello 世界" } </code></pre> <p>gRPCのサーバーが動いていることが確かめられました。</p> <h1 id="その他"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96">その他</a></h1> <ul> <li>protobufのパスは GrpcGreeter.csproj で指定できるようになっています。protobufを変更しても、dotnet runしたときにいいかんじにC#のファイルも生成してくれるようです。</li> <li>サーバーの実装は Services/GreeterService.cs にあります(見れば分かりますが)。Greeter.GreeterBaseという自動生成される基底クラスを継承してオーバーライドしていく方式みたいです。</li> </ul> <h1 id="所感"><a href="#%E6%89%80%E6%84%9F">所感</a></h1> <p>テンプレに従うだけで動かせるので、とりあえずgRPCを試してみたいという需要ならGoでやる以上に楽ですね。</p> <p>あと、Visual Studio for Mac 2022 Previewも入れてみたのですが、重くて正直微妙でした。VS Codeの出来が良すぎる……。</p> すずしめ