Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

examples/features/dualstack: Demonstrate Dual Stack functionality #8098

Merged
merged 4 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 tot he
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: s/tot he/ to the/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

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 the include their serving port and address type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete the spurious include here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

(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
```
191 changes: 191 additions & 0 deletions examples/features/dualstack/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
*
* 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 it sends 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 1 is listening only on the IPv4 loopback address.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Server 2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the comment.

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.Fatalf("failed to serve: %v", err)
}
}()
log.Printf("serving on %s\n", bindAddr)
}
wg.Wait()
}
Loading