Connect-goとBufを使って、GoでgRPC入門してみよう
この記事の目的
タイトル通り、gRPC入門してみようという趣旨の記事です。
ただgRPCを学ぶだけでは味気ないので、開発体験をよくしてくれるConnectやBufを使っていきます。
対象読者
Goの基本的な書き方を知っている方
gRPC入門者
そもそもgRPCとは?
gRPC は、Google が開発した高性能で汎用的なオープンソース RPC フレームワークです。gRPC を使用すると、クライアント アプリケーションは、別のマシンのサーバー アプリケーション上のメソッドを、ローカル オブジェクトと同様に直接呼び出すことができます。これにより、分散アプリケーションや分散サービスを簡単に作成できます。[^1]
[^1]https://cloud.google.com/api-gateway/docs/grpc-overview?hl=ja
使うメリット
コードの自動生成
Protocol Buffers(protoファイル)でAPIを定義すると、各言語に対応したクライアントやサーバーサイドのコードを自動生成してくれる。
RESTよりも高速
HTTP/2を使用し、バイナリプロトコルとして Protocol Buffersするためデータのシリアライズとデシリアライズが高速
型チェック
Protocol Buffersのおかげで、型チェックできる。
使うデメリット
RESTよりも実装が複雑
詳しくはこちらの本が参考になります。
早速実装してみよう
環境構築
connectのgetting-startedやってみる。
まずはディレクトリ作成
mkdir connect-go-example
cd connect-go-example
init
go mod init example
install
次のChatアプリを作ってみるまでやる場合、buf と grpcurlはhomebrew などでインストールしてください。
brew install buf
brew install grpcurl
go installでも可
go install github.com/bufbuild/buf/cmd/buf@latest
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
その他、使うやつインストール
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
go get golang.org/x/net/http2
go get connectrpc.com/connect
パスを通してあげる必要があります
[ -n "$(go env GOBIN)" ] && export PATH="$(go env GOBIN):${PATH}"
[ -n "$(go env GOPATH)" ] && export PATH="$(go env GOPATH)/bin:${PATH}"
これで、必要な環境は整いました。
サービス定義
protoファイルに、サービスを定義していきます。
protoファイル作成
mkdir -p greet/v1
touch greet/v1/greet.proto
syntax = "proto3";
package greet.v1;
option go_package = "example/gen/greet/v1;greetv1";
message GreetRequest {
string name = 1;
}
message GreetResponse {
string greeting = 1;
}
service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
}
書き方
まず、バージョン定義します
syntax = "proto3";
生成されたパッケージが名前衝突しないように、packageにpackage名を指定できる
package greet.v1;
optionでコードが生成される場所を指定できます
option go_package = "example/gen/greet/v1;greetv1";
型定義
message GreetRequest {
string name = 1;
}
message GreetResponse {
string greeting = 1;
}
messageは、複数のフィールドを持つことができるもので、フィールドは、型、名前、および一意のフィールド番号を持ちます。
message MyMessage {
int32 id = 1;
string name = 2;
bool is_active = 3;
}
上記の例では、MyMessage
という名前の message
を定義していて、この message
は、id
(int32型)、name
(string型)、および is_active
(bool型)の3つのフィールドを持っています。
Enum型
enum Status {
UNKNOWN = 0;
IN_PROGRESS = 1;
COMPLETED = 2;
}
message Task {
int32 id = 1;
string description = 2;
Status status = 3;
}
enum
を使用すると、一連の定義済み定数の中から1つを選択できます。上記の例では、Status
という名前の enum
を定義し、Task
messageの中で使用しています。
repeated
message TaskList {
repeated Task tasks = 1;
}
repeated
キーワードは、フィールドが配列であることを示します。
上記の例では、TaskList
messageは、複数の Task
を持つことができます。
使用できるデータ型
Proto Type | 説明 |
---|---|
int32 | 符号付き32ビット整数 |
int64 | 符号付き64ビット整数 |
uint32 | 符号なし32ビット整数 |
uint64 | 符号なし64ビット整数 |
sint32 | 符号付き32ビット整数。ZigZagエンコーディングを使用 |
sint64 | 符号付き64ビット整数。ZigZagエンコーディングを使用 |
fixed32 | 常に4バイトの符号なし32ビット整数 |
fixed64 | 常に8バイトの符号なし64ビット整数 |
sfixed32 | 常に4バイトの符号付き32ビット整数。ZigZagエンコーディングを使用 |
sfixed64 | 常に8バイトの符号付き64ビット整数。ZigZagエンコーディングを使用 |
float | 32ビット浮動小数点数 |
double | 64ビット浮動小数点数 |
bool | 真偽値 (true または false) |
string | UTF-8文字列 |
bytes | バイト列 |
Bufの用意
buf config initでbuf.yamlを作る
buf config init
buf.gen.yamlを定義する
touch buf.gen.yaml
version: v2
plugins:
- local: protoc-gen-go
out: gen
opt: paths=source_relative
- local: protoc-gen-connect-go
out: gen
opt: paths=source_relative
ここまでセットアップできたら、protoファイルからコードを生成してみる。
buf lint
buf generate
すると、genの中にコードが生成される。
.
├── buf.gen.yaml
├── buf.yaml
├── gen
│ └── greet
│ └── v1
│ ├── greet.pb.go
│ └── greetv1connect
│ └── greet.connect.go
├── go.mod
└── greet
└── v1
└── greet.proto
生成されたコードをgoで使ってみる。
サーバー側
必要なもの作成する
mkdir -p cmd/server
touch cmd/server/main.go
package main
import (
"context"
"fmt"
"log"
"net/http"
"connectrpc.com/connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
"example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)
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)
err := http.ListenAndServe(
"localhost:8080",
h2c.NewHandler(mux, &http2.Server{}),
)
if err != nil {
return
}
}
コード解説
パッケージのインポート
import (
"context"
"fmt"
"log"
"net/http"
"connectrpc.com/connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
"example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)
- 標準ライブラリ(
context
,fmt
,log
,net/http
)をインポート - Connect関連のパッケージ(
connectrpc.com/connect
)をインポート - HTTP/2サポートのパッケージ(
golang.org/x/net/http2
,golang.org/x/net/http2/h2c
)をインポート - Bufで生成されたコード(
greetv1
とgreetv1connect
)をインポート
構造体の定義
type GreetServer struct{}
Greetメソッドの実装
protoファイルから生成されたコードを元に、Greetメゾットを実装していく。
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
}
main
func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handle(path, handler)
err := http.ListenAndServe(
"localhost:8080",
h2c.NewHandler(mux, &http2.Server{}),
)
if err != nil {
return
}
}
まず、GreetServerのインスタンス化とマルチプレクサ(multiplexer)の作成を行う
greeter := &GreetServer{}
mux := http.NewServeMux()
マルチプレクサとは
複数のハンドラの中から、特定のリクエストパスに合致するハンドラを一つ選んで実行する仕組み
例)
path: / だったら hander: log.Println("Root")
path:/greet だったら hander: log.Println("Hello")
みたいなことをやってくれるやつ
greetv1connect.NewGreetServiceHandlerを使い、pathとhandlerを取得
path, handler := greetv1connect.NewGreetServiceHandler(greeter)
mux.Handleで、pathとhanderを登録
mux.Handle(path, handler)
サーバー起動する。h2cを使えば、TLSなしでHTTP/2を起動できるみたい
err := http.ListenAndServe(
"localhost:8080",
h2c.NewHandler(mux, &http2.Server{}),
)
if err != nil {
return
}
サーバーを起動してみる
go run ./cmd/server/main.go
別のターミナルを開き、テストしてみる。
まずはcurl
curl \
--header "Content-Type: application/json" \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet
すると、以下のようなレスポンスが返ってくる
{"greeting":"Hello, Jane!"}%
connectのいいところは、grpcurl使わなくてもテストできるところですね
次は、grpcurl
grpcurl \
-protoset <(buf build -o -) -plaintext \
-d '{"name": "Jane"}' \
localhost:8080 greet.v1.GreetService/Greet
すると、以下のようなレスポンスが返ってくる
{
"greeting": "Hello, Jane!"
}
クライアント側
mkdir -p cmd/client
touch cmd/client/main.go
完成系のコード
package main
import (
"context"
"log"
"net/http"
greetv1 "example/gen/greet/v1"
"example/gen/greet/v1/greetv1connect"
"connectrpc.com/connect"
)
func main() {
client := greetv1connect.NewGreetServiceClient(
http.DefaultClient,
"http://localhost:8080",
)
res, err := client.Greet(
context.Background(),
connect.NewRequest(&greetv1.GreetRequest{Name: "Jane"}),
)
if err != nil {
log.Println(err)
}
log.Println(res)
log.Println(res.Msg.Greeting)
}
コード解説
パッケージのインポート
import (
"context"
"log"
"net/http"
greetv1 "example/gen/greet/v1"
"example/gen/greet/v1/greetv1connect"
"connectrpc.com/connect"
)
main
gRPCサーバーにリクエストを送るためのクライアントを作成
client := greetv1connect.NewGreetServiceClient(
http.DefaultClient,
"http://localhost:8080",
)
サーバー側のGreetメゾットを呼び出す
context.Background()で空のコンテキストを作成
connect.NewRequestでリクエスト
res, err := client.Greet(
context.Background(),
connect.NewRequest(&greetv1.GreetRequest{Name: "Jane"}),
)
レスポンスの処理
if err != nil {
log.Println(err)
}
log.Println(res.Msg.Greeting)
クライアントを使ってみる
サーバーを起動した状態で、clientを実行
go run ./cmd/client/main.go
良さそうですね
❯ go run cmd/client/main.go ✨ 00:01
2025/06/05 00:01:30 Hello, Jane!
Chatアプリ作ってみる
getting-startedだと、Unary RPCしかやってなかったので他も触っていきます
参考)
サーバー側をGo、クライアント側をGoにします。
新しくディレクトリ作成する
mkdir chat-example
mkdir -p chat/v1/
touch chat/v1/chat.proto
欲張って、Server streaming RPC、Client streaming RPC、Bidirectional streaming RPC使えるようにしてみましょう。
syntax = "proto3";
package chat.v1;
option go_package = "chat/gen/chat/v1;chatv1";
message ChatMessageRequest {
string user = 1;
string message = 2;
}
message ChatMessageResponse {
string message = 1;
}
service ChatService {
rpc ClientChat(stream ChatMessageRequest) returns (ChatMessageResponse) {}
rpc ServerChat(ChatMessageRequest) returns (stream ChatMessageResponse) {}
rpc BidirectionalChat(stream ChatMessageRequest) returns (stream ChatMessageResponse) {}
}
コンフィグ初期化して
buf config init
リント
buf lint
すると、すっごい怒られますね
❯ buf lint 4m54s✨ 00:28
chat/v1/chat.proto:17:3:"chat.v1.ChatMessageRequest" is used as the request or response type for multiple RPCs.
chat/v1/chat.proto:17:3:"chat.v1.ChatMessageResponse" is used as the request or response type for multiple RPCs.
chat/v1/chat.proto:17:25:RPC request type "ChatMessageRequest" should be named "ClientChatRequest" or "ChatServiceClientChatRequest".
chat/v1/chat.proto:17:54:RPC response type "ChatMessageResponse" should be named "ClientChatResponse" or "ChatServiceClientChatResponse".
chat/v1/chat.proto:18:3:"chat.v1.ChatMessageRequest" is used as the request or response type for multiple RPCs.
chat/v1/chat.proto:18:3:"chat.v1.ChatMessageResponse" is used as the request or response type for multiple RPCs.
chat/v1/chat.proto:18:18:RPC request type "ChatMessageRequest" should be named "ServerChatRequest" or "ChatServiceServerChatRequest".
chat/v1/chat.proto:18:54:RPC response type "ChatMessageResponse" should be named "ServerChatResponse" or "ChatServiceServerChatResponse".
chat/v1/chat.proto:19:3:"chat.v1.ChatMessageRequest" is used as the request or response type for multiple RPCs.
chat/v1/chat.proto:19:3:"chat.v1.ChatMessageResponse" is used as the request or response type for multiple RPCs.
chat/v1/chat.proto:19:32:RPC request type "ChatMessageRequest" should be named "BidirectionalChatRequest" or "ChatServiceBidirectionalChatRequest".
chat/v1/chat.proto:19:68:RPC response type "ChatMessageResponse" should be named "BidirectionalChatResponse" or "ChatServiceBidirectionalChatResponse".
言われた通りに直してみましょう。
syntax = "proto3";
package chat.v1;
option go_package = "chat/gen/chat/v1;chatv1";
message ClientChatRequest {
string user = 1;
string message = 2;
}
message ClientChatResponse {
string message = 1;
}
message ServerChatRequest {
string user = 1;
string message = 2;
}
message ServerChatResponse {
string message = 1;
}
message BidirectionalChatRequest {
string user = 1;
string message = 2;
}
message BidirectionalChatResponse {
string message = 1;
}
service ChatService {
rpc ClientChat(stream ClientChatRequest) returns (ClientChatResponse) {}
rpc ServerChat(ServerChatRequest) returns (stream ServerChatResponse) {}
rpc BidirectionalChat(stream BidirectionalChatRequest) returns (stream BidirectionalChatResponse) {}
}
リント
buf lint
すると、何も言われなくなりました
❯ buf lint 0s✨ 00:29
基本的に、buf lintに従って直すしたほうが基本protoファイルが綺麗になるので、generateする前にbuf lintして確認してあげましょう。
generateするための準備する。
go mod init chat
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
go get golang.org/x/net/http2
go get connectrpc.com/connect
[ -n "$(go env GOBIN)" ] && export PATH="$(go env GOBIN):${PATH}"
[ -n "$(go env GOPATH)" ] && export PATH="$(go env GOPATH)/bin:${PATH}"
touch buf.gen.yaml
version: v2
plugins:
- local: protoc-gen-go
out: gen
opt: paths=source_relative
- local: protoc-gen-connect-go
out: gen
opt: paths=source_relative
サーバー側
まずmain.goを用意
mkdir -p cmd/server/
touch cmd/server/main.go
コード
package main
import (
chatv1 "chat/gen/chat/v1"
"chat/gen/chat/v1/chatv1connect"
"connectrpc.com/connect"
"context"
"fmt"
"io"
"log"
"net/http"
"time"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
type ChatServer struct{}
func (s *ChatServer) ClientChat(
ctx context.Context,
stream *connect.ClientStream[chatv1.ClientChatRequest],
) (*connect.Response[chatv1.ClientChatResponse], error) {
log.Println("ClientChat開始")
var messages []string
for stream.Receive() {
req := stream.Msg()
log.Printf("受信: ユーザー=%s, メッセージ=%s", req.User, req.Message)
messages = append(messages, fmt.Sprintf("%s: %s", req.User, req.Message))
}
if err := stream.Err(); err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
response := &chatv1.ClientChatResponse{
Message: fmt.Sprintf("受信した%d件のメッセージ: %v", len(messages), messages),
}
log.Printf("ClientChatレスポンス: %s", response.Message)
return connect.NewResponse(response), nil
}
func (s *ChatServer) ServerChat(
ctx context.Context,
req *connect.Request[chatv1.ServerChatRequest],
stream *connect.ServerStream[chatv1.ServerChatResponse],
) error {
log.Printf("ServerChat開始: ユーザー=%s, メッセージ=%s", req.Msg.User, req.Msg.Message)
for i := 1; i <= 5; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
response := &chatv1.ServerChatResponse{
Message: fmt.Sprintf("サーバーからのメッセージ %d/5: %sさん、「%s」への応答です",
i, req.Msg.User, req.Msg.Message),
}
if err := stream.Send(response); err != nil {
return err
}
log.Printf("送信: %s", response.Message)
time.Sleep(1 * time.Second)
}
log.Println("ServerChat完了")
return nil
}
func (s *ChatServer) BidirectionalChat(
ctx context.Context,
stream *connect.BidiStream[chatv1.BidirectionalChatRequest, chatv1.BidirectionalChatResponse],
) error {
log.Println("BidirectionalChat開始")
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
req, err := stream.Receive()
if err != nil {
if err == io.EOF {
log.Println("BidirectionalChat正常終了")
return nil
}
return err
}
log.Printf("双方向受信: ユーザー=%s, メッセージ=%s", req.User, req.Message)
response := &chatv1.BidirectionalChatResponse{
Message: fmt.Sprintf("エコー: %sさんから「%s」を受信しました", req.User, req.Message),
}
if err := stream.Send(response); err != nil {
return err
}
log.Printf("双方向送信: %s", response.Message)
}
}
func main() {
chatServer := &ChatServer{}
mux := http.NewServeMux()
path, handler := chatv1connect.NewChatServiceHandler(chatServer)
mux.Handle(path, handler)
err := http.ListenAndServe(
"localhost:8080",
h2c.NewHandler(mux, &http2.Server{}),
)
if err != nil {
log.Fatalf("サーバー起動エラー: %v", err)
}
}
クライアント側 (Go)
mkdir -p cmd/client/
touch cmd/client/main.go
package main
import (
chatv1 "chat/gen/chat/v1"
"chat/gen/chat/v1/chatv1connect"
"connectrpc.com/connect"
"context"
"crypto/tls"
"io"
"log"
"net"
"net/http"
"time"
"golang.org/x/net/http2"
)
func main() {
httpClient := &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}
client := chatv1connect.NewChatServiceClient(
httpClient,
"http://localhost:8080",
)
ctx := context.Background()
log.Println("\n1. ClientChatをテスト中...")
testClientChat(ctx, client)
time.Sleep(2 * time.Second)
log.Println("\n2. ServerChatをテスト中...")
testServerChat(ctx, client)
time.Sleep(2 * time.Second)
log.Println("\n3. BidirectionalChatをテスト中...")
testBidirectionalChat(ctx, client)
log.Println("\n=== すべてのテスト完了 ===")
}
func testClientChat(ctx context.Context, client chatv1connect.ChatServiceClient) {
stream := client.ClientChat(ctx)
messages := []struct {
user string
message string
}{
{"田中", "こんにちは!"},
{"田中", "今日はいい天気ですね"},
{"田中", "よろしくお願いします"},
}
for _, msg := range messages {
req := &chatv1.ClientChatRequest{
User: msg.user,
Message: msg.message,
}
if err := stream.Send(req); err != nil {
log.Fatalf("ClientChat送信エラー: %v", err)
}
log.Printf("送信: %s: %s", msg.user, msg.message)
time.Sleep(500 * time.Millisecond)
}
resp, err := stream.CloseAndReceive()
if err != nil {
log.Fatalf("ClientChatクローズエラー: %v", err)
}
log.Printf("ClientChatレスポンス: %s", resp.Msg.Message)
}
func testServerChat(ctx context.Context, client chatv1connect.ChatServiceClient) {
req := connect.NewRequest(&chatv1.ServerChatRequest{
User: "佐藤",
Message: "サーバーストリーミングのテストです",
})
stream, err := client.ServerChat(ctx, req)
if err != nil {
log.Fatalf("ServerChat開始エラー: %v", err)
}
log.Printf("ServerChatリクエスト送信: %s: %s", req.Msg.User, req.Msg.Message)
for stream.Receive() {
resp := stream.Msg()
log.Printf("ServerChatレスポンス: %s", resp.Message)
}
if err := stream.Err(); err != nil {
log.Fatalf("ServerChatストリームエラー: %v", err)
}
log.Println("ServerChat完了")
}
func testBidirectionalChat(ctx context.Context, client chatv1connect.ChatServiceClient) {
stream := client.BidirectionalChat(ctx)
go func() {
messages := []struct {
user string
message string
}{
{"山田", "双方向チャット開始"},
{"山田", "メッセージ1"},
{"山田", "メッセージ2"},
{"山田", "メッセージ3"},
}
for _, msg := range messages {
req := &chatv1.BidirectionalChatRequest{
User: msg.user,
Message: msg.message,
}
if err := stream.Send(req); err != nil {
log.Printf("BidirectionalChat送信エラー: %v", err)
return
}
log.Printf("双方向送信: %s: %s", msg.user, msg.message)
time.Sleep(1 * time.Second)
}
if err := stream.CloseRequest(); err != nil {
log.Printf("BidirectionalChatクローズエラー: %v", err)
}
}()
for {
resp, err := stream.Receive()
if err != nil {
if err == io.EOF {
log.Println("BidirectionalChat正常終了")
break
}
log.Fatalf("BidirectionalChatストリームエラー: %v", err)
}
log.Printf("双方向受信: %s", resp.Message)
}
log.Println("BidirectionalChat完了")
}
クライアント側 (Flutter) おまけ
flutterプロジェクト作成
flutter create chat_flutter
cd chat_flutter
protoファイル作成
mkdir -p chat/v1/
touch chat/v1/chat.proto
syntax = "proto3";
package chat.v1;
option go_package = "chat/gen/chat/v1;chatv1";
message ClientChatRequest {
string user = 1;
string message = 2;
}
message ClientChatResponse {
string message = 1;
}
message ServerChatRequest {
string user = 1;
string message = 2;
}
message ServerChatResponse {
string message = 1;
}
message BidirectionalChatRequest {
string user = 1;
string message = 2;
}
message BidirectionalChatResponse {
string message = 1;
}
service ChatService {
rpc ClientChat(stream ClientChatRequest) returns (ClientChatResponse) {}
rpc ServerChat(ServerChatRequest) returns (stream ServerChatResponse) {}
rpc BidirectionalChat(stream BidirectionalChatRequest) returns (stream BidirectionalChatResponse) {}
}
必要なもの追加
flutter pub add connectrpc
config init
buf config init
gen.yaml書く
touch buf.gen.yaml
version: v2
plugins:
- remote: buf.build/connectrpc/dart
out: lib/gen
- remote: buf.build/protocolbuffers/dart
out: lib/gen
include_wkt: true
include_imports: true
generate
buf generate
main.dartにclientコードを書いていきます。
*なお、FlutterミリしらなのでClaudeに書かせています。
動作は確認しています。
import 'package:flutter/material.dart';
import 'package:connectrpc/http2.dart' as http2;
import 'package:connectrpc/protocol/connect.dart' as connect;
import 'package:connectrpc/protobuf.dart' as protobuf;
import 'gen/chat/v1/chat.pb.dart';
import 'gen/chat/v1/chat.connect.client.dart';
import 'dart:async';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Chat Client',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const ChatPage(),
);
}
}
class ChatMessage {
final String user;
final String message;
final DateTime timestamp;
final bool isFromServer;
ChatMessage({
required this.user,
required this.message,
required this.timestamp,
this.isFromServer = false,
});
}
class ChatPage extends StatefulWidget {
const ChatPage({super.key});
State<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final TextEditingController _messageInputController = TextEditingController();
final TextEditingController _userController = TextEditingController();
final List<ChatMessage> _messages = [];
final ScrollController _scrollController = ScrollController();
late ChatServiceClient _client;
StreamController<ChatMessage>? _messageController;
StreamController<ClientChatRequest>? _clientStreamController;
Stream<BidirectionalChatResponse>? _bidirectionalStream;
StreamController<BidirectionalChatRequest>? _bidirectionalController;
int _clientMessageCount = 0;
bool _isConnected = false;
String _connectionStatus = "未接続";
String _streamType = 'server'; // 'client', 'server', 'bidirectional'
final String _host = 'localhost'; // 必要に応じて変更
// 開発用の代替:
// - エミュレータの場合: '10.0.2.2'
// - iOSシミュレータの場合: 'localhost' または '127.0.0.1'
// - 実機の場合: PCのローカルIPアドレス
final int _port = 8080;
void initState() {
super.initState();
_initializeClient();
_userController.text = 'User${DateTime.now().millisecondsSinceEpoch % 1000}';
}
void _initializeClient() async {
try {
_addDebugMessage("クライアント初期化開始: $_host:$_port");
// HTTP/2対応のConnect Transportを作成
final transport = connect.Transport(
baseUrl: 'http://$_host:$_port',
codec: protobuf.ProtoCodec(),
httpClient: http2.createHttpClient(),
);
_client = ChatServiceClient(transport);
_messageController = StreamController<ChatMessage>();
// 接続テスト
await _testConnection();
if (_isConnected) {
_startStreaming();
}
} catch (e) {
_addDebugMessage("初期化エラー: $e");
_updateConnectionStatus("初期化失敗: $e");
}
}
void _addDebugMessage(String message) {
_addMessage('Debug', message, isFromServer: true);
}
void _updateConnectionStatus(String status) {
setState(() {
_connectionStatus = status;
});
}
Future<void> _testConnection() async {
try {
_updateConnectionStatus("接続テスト中...");
final request = ServerChatRequest(
user: 'system',
message: 'connection_test',
);
final responseStream = _client.serverChat(request);
final response = await responseStream.first.timeout(
const Duration(seconds: 5),
);
setState(() {
_isConnected = true;
_connectionStatus = "接続成功";
});
_addDebugMessage("接続テスト成功: ${response.message}");
} catch (e) {
setState(() {
_isConnected = false;
_connectionStatus = "接続失敗: $e";
});
_addDebugMessage("接続テスト失敗: $e");
}
}
void _onStreamTypeChanged() {
_addDebugMessage("ストリームタイプを $_streamType に変更");
_cleanupStreams();
_clientMessageCount = 0;
if (_isConnected) {
_startStreaming();
}
}
void _cleanupStreams() {
_clientStreamController?.close();
_clientStreamController = null;
_bidirectionalController?.close();
_bidirectionalController = null;
_bidirectionalStream = null;
}
void _startStreaming() {
_addDebugMessage("$_streamType ストリーミング開始");
switch (_streamType) {
case 'client':
_startClientStreaming();
break;
case 'server':
break;
case 'bidirectional':
_startBidirectionalStreaming();
break;
}
}
void _startClientStreaming() {
_clientStreamController = StreamController<ClientChatRequest>();
_clientMessageCount = 0;
_client.clientChat(_clientStreamController!.stream).then((response) {
_addMessage('Server', response.message, isFromServer: true);
_addDebugMessage("Client stream完了");
}).catchError((error) {
_addMessage('System', 'Client stream error: $error', isFromServer: true);
});
}
void _startBidirectionalStreaming() {
_bidirectionalController = StreamController<BidirectionalChatRequest>();
_bidirectionalStream = _client.bidirectionalChat(_bidirectionalController!.stream);
_bidirectionalStream!.listen(
(response) {
_addMessage('Server', response.message, isFromServer: true);
},
onError: (error) {
_addMessage('System', 'Bidirectional stream error: $error', isFromServer: true);
},
onDone: () {
_addDebugMessage("Bidirectional stream終了");
},
);
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _addMessage(String user, String message, {bool isFromServer = false}) {
setState(() {
_messages.add(ChatMessage(
user: user,
message: message,
timestamp: DateTime.now(),
isFromServer: isFromServer,
));
});
_scrollToBottom();
}
Future<void> _sendMessage() async {
if (_messageInputController.text.trim().isEmpty || _userController.text.trim().isEmpty) {
return;
}
final message = _messageInputController.text.trim();
final user = _userController.text.trim();
_addMessage(user, message);
_messageInputController.clear();
try {
switch (_streamType) {
case 'client':
await _sendClientMessage(user, message);
break;
case 'server':
await _sendServerMessage(user, message);
break;
case 'bidirectional':
await _sendBidirectionalMessage(user, message);
break;
}
} catch (e) {
_addMessage('System', 'Error: $e', isFromServer: true);
}
}
Future<void> _sendClientMessage(String user, String message) async {
if (_clientStreamController == null) {
_startClientStreaming();
}
final request = ClientChatRequest(
user: user,
message: message,
);
_clientStreamController!.add(request);
_clientMessageCount++;
if (_clientMessageCount >= 2) {
_addDebugMessage("2つのメッセージを送信したため、client streamを閉じます");
_clientStreamController!.close();
_clientStreamController = null;
_clientMessageCount = 0;
}
}
Future<void> _sendServerMessage(String user, String message) async {
final request = ServerChatRequest(
user: user,
message: message,
);
final responseStream = _client.serverChat(request);
await for (final response in responseStream) {
_addMessage('Server', response.message, isFromServer: true);
}
}
Future<void> _sendBidirectionalMessage(String user, String message) async {
if (_bidirectionalController == null) {
_startBidirectionalStreaming();
}
final request = BidirectionalChatRequest(
user: user,
message: message,
);
_bidirectionalController!.add(request);
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chat Client'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
actions: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_isConnected ? Icons.circle : Icons.circle_outlined,
color: _isConnected ? Colors.green : Colors.red,
size: 12,
),
const SizedBox(width: 4),
Text(
_connectionStatus,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
],
),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
bottom: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
),
child: Column(
children: [
Row(
children: [
const Text('Username: '),
Expanded(
child: TextField(
controller: _userController,
decoration: const InputDecoration(
hintText: 'Enter your username',
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
),
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Stream Type: '),
Expanded(
child: SegmentedButton<String>(
segments: const [
ButtonSegment(
value: 'client',
label: Text('Client'),
icon: Icon(Icons.upload),
),
ButtonSegment(
value: 'server',
label: Text('Server'),
icon: Icon(Icons.download),
),
ButtonSegment(
value: 'bidirectional',
label: Text('Bidi'),
icon: Icon(Icons.sync),
),
],
selected: {_streamType},
onSelectionChanged: (Set<String> newSelection) {
setState(() {
_streamType = newSelection.first;
});
_onStreamTypeChanged();
},
),
),
],
),
],
),
),
Expanded(
child: _messages.isEmpty
? const Center(
child: Text(
'No messages yet. Start chatting!',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
)
: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(16.0),
itemCount: _messages.length,
itemBuilder: (context, index) {
final message = _messages[index];
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Card(
color: message.isFromServer
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.secondaryContainer,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
message.user,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const Spacer(),
Text(
'${message.timestamp.hour.toString().padLeft(2, '0')}:'
'${message.timestamp.minute.toString().padLeft(2, '0')}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
const SizedBox(height: 4),
Text(message.message),
],
),
),
),
);
},
),
),
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border(
top: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
),
child: Row(
children: [
Expanded(
child: TextField(
controller: _messageInputController,
decoration: const InputDecoration(
hintText: 'Type a message...',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _sendMessage(),
),
),
const SizedBox(width: 8),
FilledButton(
onPressed: _isConnected ? _sendMessage : null,
child: const Text('Send'),
),
],
),
),
],
),
);
}
void dispose() {
_messageInputController.dispose();
_userController.dispose();
_scrollController.dispose();
_messageController?.close();
_cleanupStreams();
super.dispose();
}
}
Discussion