/* * * Copyright 2025 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // Binary client is a client for the dualstack example. package main import ( "context" "fmt" "log" "slices" "strings" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" hwpb "google.golang.org/grpc/examples/helloworld/helloworld" "google.golang.org/grpc/peer" "google.golang.org/grpc/resolver" ) const ( port1 = 50051 port2 = 50052 port3 = 50053 ) func init() { resolver.Register(&exampleResolver{}) } // exampleResolver implements both, a fake `resolver.Resolver` and // `resolver.Builder`. This resolver sends a hard-coded list of 3 endpoints each // with 2 addresses (one IPv4 and one IPv6) to the channel. type exampleResolver struct{} func (*exampleResolver) Close() {} func (*exampleResolver) ResolveNow(resolver.ResolveNowOptions) {} func (*exampleResolver) Build(_ resolver.Target, cc resolver.ClientConn, _ resolver.BuildOptions) (resolver.Resolver, error) { go func() { err := cc.UpdateState(resolver.State{ Endpoints: []resolver.Endpoint{ {Addresses: []resolver.Address{ {Addr: fmt.Sprintf("[::1]:%d", port1)}, {Addr: fmt.Sprintf("127.0.0.1:%d", port1)}, }}, {Addresses: []resolver.Address{ {Addr: fmt.Sprintf("[::1]:%d", port2)}, {Addr: fmt.Sprintf("127.0.0.1:%d", port2)}, }}, {Addresses: []resolver.Address{ {Addr: fmt.Sprintf("[::1]:%d", port3)}, {Addr: fmt.Sprintf("127.0.0.1:%d", port3)}, }}, }, }) if err != nil { log.Fatal("Failed to update resolver state", err) } }() return &exampleResolver{}, nil } func (*exampleResolver) Scheme() string { return "example" } func main() { // First send 5 requests using the default DNS and pickfirst load balancer. log.Print("**** Use default DNS resolver ****") target := fmt.Sprintf("localhost:%d", port1) cc, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("Failed to create client: %v", err) } defer cc.Close() ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() client := hwpb.NewGreeterClient(cc) for i := 0; i < 5; i++ { resp, err := client.SayHello(ctx, &hwpb.HelloRequest{ Name: fmt.Sprintf("request:%d", i), }) if err != nil { log.Panicf("RPC failed: %v", err) } log.Print("Greeting:", resp.GetMessage()) } cc.Close() log.Print("**** Change to use example name resolver ****") dOpts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"round_robin":{}}]}`), } cc, err = grpc.NewClient("example:///ignored", dOpts...) if err != nil { log.Fatalf("Failed to create client: %v", err) } client = hwpb.NewGreeterClient(cc) // Send 10 requests using the example nameresolver and round robin load // balancer. These requests are evenly distributed among the 3 servers // rather than favoring the server listening on both addresses because the // resolver groups the 3 servers as 3 endpoints each with 2 addresses. if err := waitForDistribution(ctx, client); err != nil { log.Panic(err) } log.Print("Successful multiple iterations of 1:1:1 ratio") } // waitForDistribution makes RPC's on the greeter client until 3 RPC's follow // the same 1:1:1 address ratio for the peer. Returns an error if fails to do so // before context timeout. func waitForDistribution(ctx context.Context, client hwpb.GreeterClient) error { wantPeers := []string{ // Server 1 is listening on both IPv4 and IPv6 loopback addresses. // Since the IPv6 address comes first in the resolver list, it will be // given higher priority. fmt.Sprintf("[::1]:%d", port1), // Server 2 is listening only on the IPv4 loopback address. fmt.Sprintf("127.0.0.1:%d", port2), // Server 3 is listening only on the IPv6 loopback address. fmt.Sprintf("[::1]:%d", port3), } const iterationsToVerify = 3 const backendCount = 3 requestCounter := 0 for ctx.Err() == nil { result := make(map[string]int) badRatioSeen := false for i := 1; i <= iterationsToVerify && !badRatioSeen; i++ { for j := 0; j < backendCount; j++ { var peer peer.Peer resp, err := client.SayHello(ctx, &hwpb.HelloRequest{ Name: fmt.Sprintf("request:%d", requestCounter), }, grpc.Peer(&peer)) requestCounter++ if err != nil { return fmt.Errorf("RPC failed: %v", err) } log.Print("Greeting:", resp.GetMessage()) peerAddr := peer.Addr.String() if !slices.Contains(wantPeers, peerAddr) { return fmt.Errorf("peer address was not one of %q, got: %v", strings.Join(wantPeers, ", "), peerAddr) } result[peerAddr]++ time.Sleep(time.Millisecond) } // Verify the results of this iteration. for _, count := range result { if count == i { continue } badRatioSeen = true break } if !badRatioSeen { log.Print("Got iteration with 1:1:1 distribution between addresses.") } } if !badRatioSeen { return nil } } return fmt.Errorf("timeout waiting for 1:1:1 distribution between addresses %v", wantPeers) }