-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
examples/features/dualstack: Demonstrate Dual Stack functionality (#8098
) (#8115)
- Loading branch information
Showing
4 changed files
with
306 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Dualstack | ||
|
||
The dualstack example uses a custom name resolver that provides both IPv4 and | ||
IPv6 localhost endpoints for each of 3 server instances. The client will first | ||
use the default name resolver and load balancers which will only connect to the | ||
first server. It will then use the custom name resolver with round robin to | ||
connect to each of the servers in turn. The 3 instances of the server will bind | ||
respectively to: both IPv4 and IPv6, IPv4 only, and IPv6 only. | ||
|
||
Three servers are serving on the following loopback addresses: | ||
|
||
1. `[::]:50052`: Listening on both IPv4 and IPv6 loopback addresses. | ||
1. `127.0.0.1:50050`: Listening only on the IPv4 loopback address. | ||
1. `[::1]:50051`: Listening only on the IPv6 loopback address. | ||
|
||
The server response will include its serving port and address type (IPv4, IPv6 | ||
or both). So the server on "127.0.0.1:50050" will reply to the RPC with the | ||
following message: `Greeting:Hello request:1 from server<50052> type: IPv4 | ||
only)`. | ||
|
||
## Try it | ||
|
||
```sh | ||
go run server/main.go | ||
``` | ||
|
||
```sh | ||
go run client/main.go | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
/* | ||
* | ||
* 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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* | ||
* 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 server is a server for the dualstack example. | ||
package main | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"log" | ||
"net" | ||
"sync" | ||
|
||
"google.golang.org/grpc" | ||
hwpb "google.golang.org/grpc/examples/helloworld/helloworld" | ||
) | ||
|
||
type greeterServer struct { | ||
hwpb.UnimplementedGreeterServer | ||
addressType string | ||
address string | ||
port uint32 | ||
} | ||
|
||
func (s *greeterServer) SayHello(_ context.Context, req *hwpb.HelloRequest) (*hwpb.HelloReply, error) { | ||
return &hwpb.HelloReply{ | ||
Message: fmt.Sprintf("Hello %s from server<%d> type: %s)", req.GetName(), s.port, s.addressType), | ||
}, nil | ||
} | ||
|
||
func main() { | ||
servers := []*greeterServer{ | ||
{ | ||
addressType: "both IPv4 and IPv6", | ||
address: "[::]", | ||
port: 50051, | ||
}, | ||
{ | ||
addressType: "IPv4 only", | ||
address: "127.0.0.1", | ||
port: 50052, | ||
}, | ||
{ | ||
addressType: "IPv6 only", | ||
address: "[::1]", | ||
port: 50053, | ||
}, | ||
} | ||
|
||
var wg sync.WaitGroup | ||
for _, server := range servers { | ||
bindAddr := fmt.Sprintf("%s:%d", server.address, server.port) | ||
lis, err := net.Listen("tcp", bindAddr) | ||
if err != nil { | ||
log.Fatalf("failed to listen: %v", err) | ||
} | ||
s := grpc.NewServer() | ||
hwpb.RegisterGreeterServer(s, server) | ||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
if err := s.Serve(lis); err != nil { | ||
log.Panicf("failed to serve: %v", err) | ||
} | ||
}() | ||
log.Printf("serving on %s\n", bindAddr) | ||
} | ||
wg.Wait() | ||
} |