© P.J. McNerney 2020
P. McNerneyBeginning Bazelhttps://doi.org/10.1007/978-1-4842-5194-2_8

8. gRPC and Bazel

P. J. McNerney1 
(1)
Blackhawk, CO, USA
 

In Chapter 6, you discovered the use of Protocol Buffers to provide a succinct, well-typed, and easily serialized data description that worked across languages. As a corollary, Bazel provided an easy way to depend upon the Protocol Buffers. In this chapter, we will explore the use of Protocol Buffers to also easily define APIs to work across various languages.

The Protocol Buffer format is used to define APIs via gRPC, which is Google’s way of creating remote procedure calls (RPCs). In a similar fashion to Protocol Buffers normalizing data access across multiple languages, gRPC normalizes making RPCs from clients to servers.

Setup

We will build off of everything done in the last chapter. We will start by first copying everything from the last chapter into a new directory (after verifying that we have cleaned out all build products):
$ cd chapter_07
chapter_07$ bazel clean
chapter_07$ ls
WORKSPACE   client    proto   server
chapter_07$ cd ..
$ cp -rf chapter_07 chapter_08
$ cd chapter_08
chapter_08$
Finally, we need to add more dependencies within the WORKSPACE file. Add in the highlighted changes into your WORKSPACE file.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
skylib_version = "0.8.0"
http_archive(
    name = "bazel_skylib",
    url = "https://github.com/bazelbuild/bazel-skylib/releases/download/{}/bazel-skylib.{}.tar.gz".format(skylib_version, skylib_version),
)
<existing dependencies omitted for brevity>
http_archive(
    name = "io_grpc_grpc_java",
    strip_prefix = "grpc-java-1.24.0",
    urls = ["https://github.com/grpc/grpc-java/archive/v1.24.0.tar.gz"],
)
load("@io_grpc_grpc_java//:repositories.bzl", "grpc_java_repositories")
grpc_java_repositories()
http_archive(
    name = "bazel_gazelle",
    urls = ["https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.19.1/bazel-gazelle-v0.19.1.tar.gz"],
)
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
gazelle_dependencies()
go_repository(
    name = "org_golang_google_grpc",
    build_file_proto_mode = "disable",
    importpath = "google.golang.org/grpc",
    sum = "h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw=",
    version = "v1.22.0",
)
go_repository(
    name = "org_golang_x_net",
    importpath = "golang.org/x/net",
    sum = "h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=",
    version = "v0.0.0-20190311183353-d8887717615a",
)
go_repository(
    name = "org_golang_x_text",
    importpath = "golang.org/x/text",
    sum = "h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=",
    version = "v0.3.0",
)
Listing 8-1

Adding the gRPC dependencies to the WORKSPACE

Save to the WORKSPACE file.

As one final item, we are going to create an empty BUILD file right next to our WORKSPACE file. This is related to the use of Gazelle (discussed next).
chapter_08$ touch BUILD

Dependency Discussion

It is worthwhile to take a moment and discuss some of the dependencies that we just added, particularly for Go. The use of http_archive should be rote by this point in time.

Skylib

The Skylib library contains a number of useful functions and rules that are used when creating custom build rules. It is a common dependency that is used by many packages. As such, you can run into issues when dependencies use multiple versions of this library. In this case, we are adding in this library explicitly and asserting a version that will be used throughout your WORKSPACE.

One thing to note about how we are including this particular version of Skylib. If you notice, we are using the .format() function on the string in order to insert the version into the path. This allows us to easily change the version later on. It also demonstrates the use of Python-like features of Starlark to create richer specification of our dependencies.

Gazelle

Gazelle is unique in that it is a build file generator for Bazel projects. That is, it can auto-magically generate BUILD files for a language from code (assuming, of course, that the code is well formed). Gazelle supports Go out-of-the-box. Since Go has the very nice property of being able to very explicitly specify dependencies within the code itself, Gazelle is able to take advantage of this fact and generate BUILD files for you.

Gazelle also defines a repository rule for Go, aptly named go_repository. As you might have already guessed, go_repository allows you to (a) specify an import path from which to retrieve the necessary dependency and (b) auto-generate the necessary BUILD files for the packages therein. As you can imagine, this can be of enormous help when incorporating third party libraries (at least for Go) that do not already have Bazel support.

The BUILD file at the root of your project functions as a location to configure options for Gazelle, if you want to use its functionality in your own project. If the BUILD file is missing, your project will fail to build.

Defining the gRPC in Protocol Buffers

In a similar fashion to how we defined messages in Protocol Buffers in a language-agnostic way, we can also define interfaces for RPCs. We will create a new file within the proto directory to house the new API.
syntax = "proto3";
import "proto/transmission_object.proto";
package transceiver;
message EchoRequest {
    transmission_object.TransmissionObject from_client = 1;
}
message EchoResponse {
    transmission_object.TransmissionObject from_server = 1;
}
service Transceiver {
    rpc Echo (EchoRequest) returns (EchoResponse);
}
Listing 8-2

Defining the interface for the RPC

Save this to proto/transceiver.proto.

Let’s take a moment just to analyze these definitions. We locally define two messages EchoRequest and EchoResponse to contain the request and response to the RPC, respectively.

Notably, within both EchoRequest and EchoResponse, we include the earlier created TransmissionObject message. Strictly speaking, there is nothing required in having the same message being included in both the request and the response; we do this here only to mirror the functionality that we have created in prior chapters. Additionally, nothing requires having the messages for the request and response defined within the same file as the interface; we do so here only for the sake of simplicity.

We then define a service Transceiver, within which we have a single RPC, Echo. In the definition of Echo, we defined the required request and response.

Finally note that all we are doing here is defining the interface for our RPC. Beyond that, nothing (here) defines its implementation.

In order to actually use and generate the RPC, we need to update our proto/BUILD file with the appropriate build targets. Open the proto/BUILD file and add the following.
<omitted for brevity>
proto_library(
    name = "transceiver_proto",
    srcs = ["transceiver.proto"],
    deps = [
        ":transmission_object_proto",
    ]
)
go_proto_library(
    name = "transceiver_go_proto_grpc",
    compiler = "@io_bazel_rules_go//proto:go_grpc",
    proto = ":transceiver_proto",
    importpath = "transceiver",
    deps = [":transmission_object_go_proto",],
    visibility = ["//server/echo_server:__pkg__"],
)
java_proto_library(
    name = "transceiver_java_proto",
    deps = [":transceiver_proto"],
    visibility = ["//client/echo_client:__subpackages__"],
)
load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library")
java_grpc_library(
    name = "transceiver_java_proto_grpc",
    srcs = [":transceiver_proto"],
    deps = [":transceiver_java_proto"],
    visibility = ["//client/echo_client:__subpackages__"],
)
Listing 8-3

Defining the build targets for the Transceiver service

Save the file to proto/BUILD.

As with the proto code, let’s examine what we’ve added here. The first new build target should look familiar; it simply defines the proto_library target for the transceiver.proto file. In a similar fashion, transceiver_java_proto should also look very familiar, as it defines the build target for the Java version of transceiver.proto.

The transceiver_go_proto_grpc looks very similar to what we have seen previously; the primary exception is addition of the compiler directive within go_proto_library target. This defines the rule that should be used when compiling the target in order to support gRPC. We use the standard rule found within the @io_bazel_rules_go dependency.

Since we are using Go only on the server side, we set the visibility to only the server subpackages.

The java_grpc_library does a similar job, except for defining the necessary target for Java. In a complementary fashion, we set the visibility to only the client subpackages.

Just to confirm that all is working well, let’s build all the targets within the package:
chapter_08$ bazel build proto:all
INFO: Analysed 7 targets (0 packages loaded, 0 targets configured).
INFO: Found 7 targets...
INFO: Elapsed time: 0.316s, Critical Path: 0.07s
INFO: 3 processes: 3 darwin-sandbox.
INFO: Build completed successfully, 4 total actions

Upgrading the Client to Use gRPC

Having defined the gRPC interface, we will now upgrade the client to use it. For the most part, the code will look very similar to what we had done previously. We will highlight a few of the changes that are needed for the basic version.

We had previously created a Java client that explicitly transmitted a serialized object. We will make some modifications to the support using gRPC.

Open client/echo_client/command_line/EchoClient.java.
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import transmission_object.TransmissionObjectOuterClass.TransmissionObject;
import transceiver.TransceiverOuterClass.EchoRequest;
import transceiver.TransceiverOuterClass.EchoResponse;
import transceiver.TransceiverGrpc;
public class EchoClient {
    public static void main(String args[]) {
        System.out.println("Spinning up the Echo Client in Java...");
        try {
            final BufferedReader commandLineInput = new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Waiting on input from the user...");
            final String inputFromUser = commandLineInput.readLine();
            if (inputFromUser != null) {
                ManagedChannel channel =
                  ManagedChannelBuilder
                    .forAddress("localhost", 1234)
                    .usePlaintext()
                    .build();
                TransceiverGrpc.TransceiverBlockingStub stub =
                  TransceiverGrpc.newBlockingStub(channel);
                EchoRequest request =  EchoRequest.newBuilder()
                  .setFromClient(
                     TransmissionObject.newBuilder()
                        .setMessage(inputFromUser)
                        .setValue(3.145f)
                        .build())
                  .build();
                EchoResponse response = stub.echo(request);
                System.out.println("Received Message from server: ");
                System.out.println(response);
                channel.shutdownNow();
            }
        } catch (Exception e) {
            System.err.println("Error: " + e);
        }
    }
}
Listing 8-4

Using gRPC on the client side

Save the file to client/echo_client/command_line/EchoClient.java.

Taking another moment to examine the changes, we first create a ManagedChannel to open a channel on a specific port. This is then used to create a stub (TransceiverBlockingStub) for actually making the RPC. For the sake of simplicity, we use the most basic of stubs, which makes blocking calls for all of the service’s RPCs (other, more flexible versions of the stub are possible, but are outside of the scope of this book).

The stub provides local methods which forward the calls through the channel. Once created, the stub makes it as easy to call an RPC as it would be a local method. The request is formulated and then used to call into the method, with the expected response.

To complete the functionality, let’s upgrade the target in the client/echo_client/command_line/BUILD file. As before, much of the original target remains the same, so we can highlight the necessary changes.
java_binary(
    name = "command_line",
    srcs = ["EchoClient.java"],
    main_class = "EchoClient",
    runtime_deps = [
        "@io_grpc_grpc_java//netty",
    ],
    deps = [
         "//proto:transmission_object_java_proto",
         "//proto:transceiver_java_proto",
         "//proto:transceiver_java_proto_grpc",
         "@io_grpc_grpc_java//api",
    ]
)
Listing 8-5

Modifications for EchoClient to support gRPC

Save the file to client/echo_client/command_line/BUILD.

For what might be obvious at this point in time, we’ve added the new dependencies from the proto subpackage. Additionally, we’ve added both a typical static dependency (@io_grpc_grpc_java//api) as well as a new runtime dependency (@io_grpc_grpc_java//netty).

In this latter case, we specify that this is a runtime dependency since it is not explicitly requested in the code. If you attempted to remove runtime_deps, you would find that building the program would work perfectly fine. However, attempting to run the program would result in an error message requesting the runtime dependency.

We will build the target just to confirm that all is well:
chapter_08$ bazel build client/echo_client/command_line
INFO: Analysed target //client/echo_client/command_line:command_line (32 packages loaded, 317 targets configured).
INFO: Found 1 target...
Target //client/echo_client/command_line:command_line up-to-date:
  bazel-bin/client/echo_client/command_line/command_line.jar
  bazel-bin/client/echo_client/command_line/command_line
INFO: Elapsed time: 6.648s, Critical Path: 6.14s
INFO: 21 processes: 18 darwin-sandbox, 3 worker.
INFO: Build completed successfully, 25 total actions

Notably, we won’t be able to actually run the client yet; although we’ve successfully created a client that uses gRPC, we still need a service that implements the RPC.

Upgrading the Server to Use gRPC

To upgrade our server to use gRPC, we will need to register and fill in the functionality for the RPCs. gRPC generates most of the scaffolding for us; we just need to register our server and provide the appropriate methods to fulfill the contract.

As with the prior section, we can make some modifications on our existing code. Notably, in this case, we will be making more extensive modifications in order to support gRPC.

Open server/echo_server/echo_server.go and make the highlighted modifications.
package main
import (
      "fmt"
      "log"
      "net"
      "transceiver"
      "transmission_object"
      "golang.org/x/net/context"
      "google.golang.org/grpc"
)
type EchoServer struct{}
func (es *EchoServer) Echo(context context.Context, request *transceiver.EchoRequest) (*transceiver.EchoResponse, error) {
      log.Println("Message = " + (*request).FromClient.GetMessage())
      log.Println("Value = " +
         fmt.Sprintf("%f", (*request).FromClient.GetValue()))
      server_message := "Received from client: " +
         (*request).FromClient.GetMessage()
      server_value := (*request).FromClient.Value * 2
      from_server := transmission_object.TransmissionObject{
             Message: server_message,
             Value:   server_value,
      }
      return &transceiver.EchoResponse{
             FromServer: &from_server,
      }, nil
}
func main() {
      log.Println("Spinning up the Echo Server in Go...")
      listen, error := net.Listen("tcp", ":1234")
      if error != nil {
              log.Panicln("Unable to listen: " + error.Error())
      }
      defer listen.Close()
      defer log.Println("Connection now closed.")
      grpc_server := grpc.NewServer()
      transceiver.RegisterTransceiverServer(grpc_server, &EchoServer{})
      error = grpc_server.Serve(listen)
      if error != nil {
              log.Panicln("Unable to start serving! Error: " + error.Error())
      }
}
Listing 8-6

Implementing the gRPC interface on the server side

Save the changes to server/echo_server/echo_server.go.

Once again, let’s take a step back to examine the major changes. In this case, you’ve created a Go struct EchoServer which has a specially named method Echo.

The Echo method takes in a Context object and the EchoRequest that we had defined in the protobuf definition and returns the EchoResponse object. This method fulfills the contract required by the interface definition. In terms of functionality, although the body differs slightly from the prior version, you should be able to recognize the same functionality.
Within the body of main, we created a new server and registered our EchoServer. Having completed the setup, we then just start listening to incoming messages.
Now let’s finish upgrading the target in the BUILD file to support this new functionality. Open server/echo_server/BUILD and make the following highlighted modifications.
load("@io_bazel_rules_go//go:def.bzl", "go_binary")
go_binary(
    name = "echo_server",
    srcs = ["echo_server.go"],
    deps = [
         "//proto:transceiver_go_proto_grpc",
         "//proto:transmission_object_go_proto",
         "@org_golang_x_net//context:go_default_library",
         "@org_golang_google_grpc//:go_default_library",
    ]
)
Listing 8-7

Modification for echo_server to support gRPC

Save your changes to server/echo_server/BUILD.

As before, the new org_golang_* dependencies are not invented from thin air; they’ve come from the Go dependency that we had defined within the WORKSPACE file.

Now, let’s just do a sanity check on the build to make sure that it is all working as expected:
chapter_08$ bazel build server/echo_server
INFO: Analysed target //server/echo_server:echo_server (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //server/echo_server:echo_server up-to-date:
  bazel-bin/server/echo_server/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 0.227s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action

Running the Client and the Server

Having completed both the client and the server, we are ready to actually use our new functionality.

Open a new terminal window. We will first start running the server.
chapter_08$ bazel run server/echo_server
INFO: Analysed target //server/echo_server:echo_server (83 packages loaded, 7969 targets configured).
INFO: Found 1 target...
Target //server/echo_server:echo_server up-to-date:
  bazel-bin/server/echo_server/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 110.524s, Critical Path: 27.33s
INFO: 466 processes: 466 darwin-sandbox.
INFO: Build completed successfully, 467 total actions
INFO: Build completed successfully, 467 total actions
2019/07/23 05:11:30 Spinning up the Echo Server in Go...
Having gotten the server listening, let’s run the client to fire a response. Open a second terminal window and run the following, adding some input at the end:
chapter_08$ bazel run client/echo_client/command_line
INFO: Analysed target //client/echo_client/command_line:command_line (60 packages loaded, 919 targets configured).
INFO: Found 1 target...
Target //client/echo_client/command_line:command_line up-to-date:
  bazel-bin/client/echo_client/command_line/command_line.jar
  bazel-bin/client/echo_client/command_line/command_line
INFO: Elapsed time: 5.728s, Critical Path: 5.33s
INFO: 3 processes: 2 darwin-sandbox, 1 worker.
INFO: Build completed successfully, 4 total actions
INFO: Build completed successfully, 4 total actions
Spinning up the Echo Client in Java...
Waiting on input from the user...
This is a test using gRPC.
Received Message from server:
from_server {
  value: 6.29
  message: "Received from client: This is a test using gRPC."
}
We’ve successfully done the echo functionality using gRPC. However, let’s take a look back at the server terminal to see the messages there:
chapter_08$ bazel run server/echo_server
<omitted for brevity>
INFO: Build completed successfully, 1 total action
2019/07/23 05:11:49 Spinning up the Echo Server in Go...
2019/07/23 05:15:02 Message = This is a test using gRPC.
2019/07/23 05:15:02 Value = 3.145000

As with your prior implementation, you see the message that was sent by the client. However, there is one important difference to the functionality here: the server has not exited. That is, the server is still running and waiting for new connections to come in.

To verify, switch back to your client terminal and do one more run:
chapter_08$ bazel run client/echo_client/command_line
<omitted for brevity>
Spinning up the Echo Client in Java...
Waiting on input from the user...
Still up and running
Received Message from server:
from_server {
  value: 6.29
  message: "Received from client: Still up and running"
}
Now let’s look back on the server terminal to see the messages:
chapter_08$ bazel run server/echo_server
<omitted for brevity>
2019/07/23 05:11:49 Spinning up the Echo Server in Go...
2019/07/23 05:15:02 Message = This is a test using gRPC.
2019/07/23 05:15:02 Value = 3.145000
2019/07/23 05:21:20 Message = Still up and running
2019/07/23 05:21:20 Value = 3.145000

This persistent functionality is available “out of the box” with gRPC. In switching over to it, we’ve actually gained more functionality for roughly the same number of lines of code.

Adding Another RPC

One complaint which could arise is that you have written roughly the same amount of code to perform the same actions as before. To further illustrate the power of what we have created, we will quickly add one more RPC to the entire system.

Open proto/transceiver.proto and add the following highlighted lines.
syntax = "proto3";
import "proto/transmission_object.proto";
package transceiver;
message EchoRequest {
    transmission_object.TransmissionObject from_client = 1;
}
message EchoResponse {
    transmission_object.TransmissionObject from_server = 1;
}
message UpperCaseRequest {
    string original = 1;
}
message UpperCaseResponse {
    string upper_cased = 1;
}
service Transceiver {
    rpc Echo (EchoRequest) returns (EchoResponse);
    rpc UpperCase (UpperCaseRequest) returns (UpperCaseResponse);
}
Listing 8-8

Adding the interface for another RPC

Save the file to proto/transceiver.proto.

Note that all that was necessary was simply adding the new RPC declaration to the service, along with some explicit messages for the request and response.

Now let’s add the implementation into the server. Open server/echo_server/echo_server.go.
import (
      "fmt"
      "log"
      "net"
      "strings"
      "transceiver"
      "transmission_object"
      "golang.org/x/net/context"
      "google.golang.org/grpc"
)
<omitted for brevity>
func (es *EchoServer) UpperCase(contest context.Context, request *transceiver.UpperCaseRequest) (*transceiver.UpperCaseResponse, error) {
      log.Println("Original = " + (*request).GetOriginal())
      return &transceiver.UpperCaseResponse{
            UpperCased: strings.ToUpper((*request).GetOriginal()),
      }, nil
}
func main() {
      log.Println("Spinning up the Echo Server in Go...")
      listen, error := net.Listen("tcp", ":1234")
      if error != nil {
              log.Panicln("Unable to listen: " + error.Error())
      }
      defer listen.Close()
      defer log.Println("Connection now closed.")
      grpc_server := grpc.NewServer()
      transceiver.RegisterTransceiverServer(grpc_server, &EchoServer{})
      error = grpc_server.Serve(listen)
      if error != nil {
              log.Panicln("Unable to start serving! Error: " + error.Error())
      }
}
Listing 8-9

Adding the implementation to the server

Save the changes to server/echo_server/echo_server.go.

Once again, note that all that was necessary was adding in the new method to the struct.

Finally, let’s make the modifications needed on the client side. Open client/echo_client/command_line/EchoClient.java.
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import transmission_object.TransmissionObjectOuterClass.TransmissionObject;
import transceiver.TransceiverOuterClass.EchoRequest;
import transceiver.TransceiverOuterClass.EchoResponse;
import transceiver.TransceiverOuterClass.UpperCaseRequest;
import transceiver.TransceiverOuterClass.UpperCaseResponse;
import transceiver.TransceiverGrpc;
public class EchoClient {
    public static void main(String args[]) {
        System.out.println("Spinning up the Echo Client in Java...");
        try {
<omitted for brevity>
                UpperCaseRequest upperCaseRequest =
                  UpperCaseRequest.newBuilder()
                    .setOriginal(inputFromUser)
                    .build();
                UpperCaseResponse upperCaseResponse =
                  stub.upperCase(upperCaseRequest);
                System.out.println("Received upper cased:");
                System.out.println(upperCaseResponse);
                channel.shutdownNow();
            }
        } catch (Exception e) {
            System.err.println("Error: " + e);
        }
    }
}
Listing 8-10

Calling the new RPC from the client

Save your changes to client/echo_client/command_line/EchoClient.java.

Once again, all that was needed was simple, to create the appropriate constructs and call the newly defined RPC. Let’s run a test to verify that all is well.

Open a terminal and start the server:
chapter_08$ bazel run server/echo_server
INFO: Analysed target //server/echo_server:echo_server (1 packages loaded, 594 targets configured).
INFO: Found 1 target...
Target //server/echo_server:echo_server up-to-date:
  bazel-bin/server/echo_server/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 1.409s, Critical Path: 0.03s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
2019/07/23 05:40:16 Spinning up the Echo Server in Go...
Now open the client terminal, and let’s run the client one more time:
chapter_08$ bazel run client/echo_client/command_line
INFO: Analysed target //client/echo_client/command_line:command_line (2 packages loaded, 432 targets configured).
INFO: Found 1 target...
Target //client/echo_client/command_line:command_line up-to-date:
  bazel-bin/client/echo_client/command_line/command_line.jar
  bazel-bin/client/echo_client/command_line/command_line
INFO: Elapsed time: 0.616s, Critical Path: 0.27s
INFO: 1 process: 1 worker.
INFO: Build completed successfully, 2 total actions
INFO: Build completed successfully, 2 total actions
Spinning up the Echo Client in Java...
Waiting on input from the user...
This is the magic.
Received Message from server:
from_server {
  value: 6.29
  message: "Received from client: This is the magic."
}
Received upper cased:
upper_cased: "THIS IS THE MAGIC."

Congratulations! You’ve just added a new RPC into your system. Notably, this required only a little bit more code in each location (definition, server, and client). Moving forward, you could easily define vastly more functionality in a very ordered and well-managed fashion.

Note

Within the body of this last section, we never made any modifications to our BUILD files, since no dependency changes were required. You were able to just operate on the code and execute, confident in the knowledge that Bazel would handle the necessary build steps.

Final Word

Over the course of the last chapter, the focus was less on an any structural knowledge on using Bazel than using it in a manner closer to actual development. You saw how Bazel’s multi-language support worked hand in hand with gRPC to easily create new client–server functionality.

Up until this point in time in the book, the focus has been mainly on looking at functionality which effectively lives on a backend. That is, both the clients and servers created thus far could easily be proxies for backend services talking to one another.

Although much of the communication theme will continue moving forward, we will start to look at using Bazel to create purely client-side functionality in the form of mobile applications. Once again, we will lean heavily on Bazel’s ability to work seamlessly across languages to create these applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.111.30