Skip to content

Commit

Permalink
examples/features/dualstack: Demonstrate Dual Stack functionality (gr…
Browse files Browse the repository at this point in the history
  • Loading branch information
arjan-bal authored and purnesh42H committed Feb 25, 2025
1 parent 710f7a2 commit 76753e3
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 0 deletions.
4 changes: 4 additions & 0 deletions examples/examples_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ EXAMPLES=(
"features/compression"
"features/customloadbalancer"
"features/deadline"
"features/dualstack"
"features/encryption/TLS"
"features/error_details"
"features/error_handling"
Expand Down Expand Up @@ -90,6 +91,7 @@ declare -A CLIENT_ARGS=(
declare -A SERVER_WAIT_COMMAND=(
["features/unix_abstract"]="lsof -U | grep $UNIX_ADDR"
["default"]="lsof -i :$SERVER_PORT | grep $SERVER_PORT"
["features/dualstack"]="lsof -i :50053 | grep 50053"
)

wait_for_server () {
Expand All @@ -116,6 +118,7 @@ declare -A EXPECTED_SERVER_OUTPUT=(
["features/compression"]="UnaryEcho called with message \"compress\""
["features/customloadbalancer"]="serving on localhost:50051"
["features/deadline"]=""
["features/dualstack"]="serving on \[::\]:50051"
["features/encryption/TLS"]=""
["features/error_details"]=""
["features/error_handling"]=""
Expand All @@ -142,6 +145,7 @@ declare -A EXPECTED_CLIENT_OUTPUT=(
["features/compression"]="UnaryEcho call returned \"compress\", <nil>"
["features/customloadbalancer"]="Successful multiple iterations of 1:2 ratio"
["features/deadline"]="wanted = DeadlineExceeded, got = DeadlineExceeded"
["features/dualstack"]="Successful multiple iterations of 1:1:1 ratio"
["features/encryption/TLS"]="UnaryEcho: hello world"
["features/error_details"]="Greeting: Hello world"
["features/error_handling"]="Received error"
Expand Down
29 changes: 29 additions & 0 deletions examples/features/dualstack/README.md
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
```
189 changes: 189 additions & 0 deletions examples/features/dualstack/client/main.go
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)
}
84 changes: 84 additions & 0 deletions examples/features/dualstack/server/main.go
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()
}

0 comments on commit 76753e3

Please sign in to comment.