package http3 import ( "context" "crypto/tls" "fmt" "io" "net" "time" "grpccanary/lib/grpc/http3/protoapi" "github.com/quic-go/quic-go" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) func quicDialer(tlsConf *tls.Config) func(context.Context, string) (net.Conn, error) { return func(ctx context.Context, addr string) (net.Conn, error) { qconn, err := quic.DialAddr(ctx, addr, tlsConf, &quic.Config{ KeepAlivePeriod: 10 * time.Second, }) if err != nil { return nil, fmt.Errorf("quic dial failed: %w", err) } stream, err := qconn.OpenStreamSync(ctx) if err != nil { _ = qconn.CloseWithError(0, "failed to open stream") return nil, fmt.Errorf("failed to open stream: %w", err) } return &quicNetConn{Stream: stream, conn: qconn}, nil } } func ClientRun(addr string) error { tlsConf := &tls.Config{ InsecureSkipVerify: true, NextProtos: []string{"grpc-http3-canary"}, } conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithContextDialer(quicDialer(tlsConf)), ) if err != nil { return fmt.Errorf("failed to create grpc client: %w", err) } defer conn.Close() client := protoapi.NewHttp3ServiceClient(conn) // Call 1: First Unary Ping ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() res1, err := client.Ping(ctx, &protoapi.PingRequest{Message: "First Message"}) if err != nil { return fmt.Errorf("unary ping 1 failed: %w", err) } fmt.Printf("[HTTP3 Client] Received Ping 1 Response: Message='%s', Transport='%s'\n", res1.GetMessage(), res1.GetTransport()) // Call 2: Second Unary Ping (to verify stream multiplexing / reuse over same QUIC connection) ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second) defer cancel2() res2, err := client.Ping(ctx2, &protoapi.PingRequest{Message: "Second Message"}) if err != nil { return fmt.Errorf("unary ping 2 failed: %w", err) } fmt.Printf("[HTTP3 Client] Received Ping 2 Response: Message='%s', Transport='%s'\n", res2.GetMessage(), res2.GetTransport()) // Call 3: Bidirectional Streaming Ping streamCtx, streamCancel := context.WithTimeout(context.Background(), 5*time.Second) defer streamCancel() stream, err := client.StreamPing(streamCtx) if err != nil { return fmt.Errorf("failed to open bi-directional stream: %w", err) } for i := 1; i <= 3; i++ { msg := fmt.Sprintf("Stream Message %d", i) err := stream.Send(&protoapi.PingRequest{Message: msg}) if err != nil { return fmt.Errorf("failed to send stream message %d: %w", i, err) } res, err := stream.Recv() if err == io.EOF { break } if err != nil { return fmt.Errorf("failed to receive stream response %d: %w", i, err) } fmt.Printf("[HTTP3 Client] Received Stream Response %d: Message='%s', Transport='%s'\n", i, res.GetMessage(), res.GetTransport()) } _ = stream.CloseSend() return nil }