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

5. A Simple Echo Client/Server Program

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

In the last chapter, you learned the basics of the WORKSPACE file, learning how to add external dependencies, including new languages. In this chapter, we are going to build off of that work to create a simple pair of programs, in different languages (one in Java, the other in Go), to round trip messages between the two of them.

Setting Up Your Workspace

Let’s create a new directory for our work:
$ mkdir chapter_05
$ cd chapter_05
chapter_05$ touch WORKSPACE
We are going to pull in the Go rules that we utilized in the last chapter. Open your newly created WORKSPACE file and add the following.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive (
        name = "io_bazel_rules_go",
        urls = ["https://github.com/bazelbuild/rules_go/releases/download/v0.19.5/rules_go-v0.19.5.tar.gz"],
)
load("@io_bazel_rules_go/go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")
go_rules_dependencies()
go_register_toolchains()
Listing 5-1

Adding in the Go rules

Save your WORKSPACE file.

For this particular example, we are going to put all of our code into a single directory. In practice, you will likely want to separate out the code by language for the sake of organization.

Now let’s create a directory for our work and an accompanying BUILD file:
chapter_05$ mkdir src
chapter_05$ cd src
chapter_05/src$ touch BUILD

As you have seen previously, some languages (e.g., C++, Java, etc.) come out of the box with Bazel; as such, we don’t need to explicitly load in those rules. However, as you saw in the previous chapter, we need to explicitly load the rules for other languages. In this case, we are going to load in the Go rules.

Open the BUILD file and add the following.
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_binary")
Listing 5-2

Loading the Go language rules

Save your BUILD file.

Now let’s create some programs. We will start with the Go version.

Go Echo Server

We will start with a simple Go echo server. As the name implies, its main job is to accept incoming connections, read the bytes off the connection, and return a (modified) version of the same bytes back.
package main
import (
    "log"
    "net"
)
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()
    connection, error := listen.Accept()
    if error != nil {
        log.Panicln("Cannot accept a connection! Error: " + error.Error())
    }
    log.Println("Receiving on a new connection")
    defer connection.Close()
    defer log.Println("Connection now closed.")
    buffer := make([]byte, 2048)
    size, error := connection.Read(buffer)
    if error != nil {
        log.Println("Cannot read from the buffer! Error: " + error.Error())
    }
    data := string(buffer[:size])
    log.Println("Received data: " + data)
    connection.Write([]byte("Echoed from Go: " + data))
}
Listing 5-3

Simple Go echo server

Save this to chapter_05/src/echo_server.go.

Just to walk through the code a bit, the Go echo server will start listening for connections on port “1234.” Once it has accepted a connection, it will read off the data from that connection, modify it slightly (i.e., prepending “Echoed from Go:”), and then send that modified data back to the sender. For the sake of simplicity, once it has echoed the data, it will close up shop.

Now let’s create the entry within the BUILD file for the build target.
go_binary(
    name = "echo_server",
    srcs = ["echo_server.go"],
)
Listing 5-4

Go echo server build target

Save the BUILD file.

Now it’s time to actually build and run the echo server:
chapter_05/src$ bazel run :echo_server
INFO: Analysed target //src:echo_server (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:echo_server up-to-date:
  bazel-bin/src/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 0.125s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
2019/06/02 17:49:07 Spinning up the Echo Server in Go...

At this point, it is going to just be hanging around forever, since it has no one to connect to it. Kill the process and let’s fix that. We’ll be putting down Go for a moment and jumping over to the Java.

Java Echo Client

As the name implies, we will be creating a program to connect to our echo server. The echo client will attempt to connect to the server, read in some data from the user, transmit that to the server, and then write out whatever it gets back from the server.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class EchoClient {
    public static void main (String args[]) {
        System.out.println("Spinning up the Echo Client in Java...");
        try {
            final Socket socketToServer = new Socket("localhost", 1234);
            final BufferedReader inputFromServer = new BufferedReader(
               new InputStreamReader(socketToServer.getInputStream()));
            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) {
                System.out.println("Received by Java: " + inputFromUser);
                final PrintWriter outputToServer =
                    new PrintWriter(socketToServer.getOutputStream(), true);
                outputToServer.println(inputFromUser);
                System.out.println(inputFromServer.readLine());
            }
            socketToServer.close();
        } catch (Exception e) {
            System.err.println("Error: " + e);
        }
    }
}
Listing 5-5

Simple Java echo client

Save the preceding code to the file chapter_05/src/EchoClient.java.

The EchoClient attempts to create a local connection to a server on port 1234. Assuming success, it then reads a single line of data from the user, sends it over the connection, and prints the response from the server. Once again, for the sake of simplicity, this will run only once and then shut down.

Now let’s add its entry into the BUILD file.
java_binary(
    name = "EchoClient",
    srcs = ["EchoClient.java"],
)
Listing 5-6

Java echo client build target

Save this to the BUILD file.

Let’s run this on its own to make sure that we can build and run:
chapter_05/src$ bazel run :EchoClient
INFO: Analysed target //src:EchoClient (0 packages loaded, 2 targets configured).
INFO: Found 1 target...
Target //src:EchoClient up-to-date:
  bazel-bin/src/EchoClient.jar
  bazel-bin/src/EchoClient
INFO: Elapsed time: 0.342s, Critical Path: 0.12s
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...
Error: java.net.ConnectException: Connection refused (Connection refused)

Congratulations! Your program successfully runs… and then terminates because there is nothing to connect to. No worries, we will correct this in a moment.

Naming the Echo Client and Server

An astute reader would notice that the name of our Java target has a different style from the name in our Go target.

Specifically, we have one name formed primarily through underscores and one through camel casing.
go_binary(
    name = "echo_server",
    srcs = ["echo_server.go"],
)
java_binary(
    name = "EchoClient",
    srcs = ["EchoClient.java"],
)
Listing 5-7

Build targets

To be clear, this is not an editorial mistake in this case; there is actually some reason to the difference, at least for Java. For the Java binary build target, the name of the target is also used as a shorthand to inform Bazel what is the main class within the set of sources.

To illustrate, let’s modify the name of the java_binary build target to match the style of the go_binary build target.
java_binary(
    name = "echo_client",
    srcs = ["EchoClient.java"],
)
Listing 5-8

Modified java_binary name style

Save this change to the BUILD file.

Building still works as expected:
chapter_05/src$ bazel build :echo_client
INFO: Analysed target //src:echo_client (1 packages loaded, 2 targets configured).
INFO: Found 1 target...
Target //src:echo_client up-to-date:
  bazel-bin/src/echo_client.jar
  bazel-bin/src/echo_client
INFO: Elapsed time: 0.261s, Critical Path: 0.09s
INFO: 1 process: 1 worker.
INFO: Build completed successfully, 3 total actions
However, you will find that you cannot actually run the program:
chapter_05/src$ bazel run :echo_client
INFO: Analysed target //src:echo_client (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:echo_client up-to-date:
  bazel-bin/src/echo_client.jar
  bazel-bin/src/echo_client
INFO: Elapsed time: 0.137s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Error: Could not find or load main class echo_client

Without any additional guidance, the java_binary rule makes use of the name of the build target to infer the class in which the main function lives (or more to the point, the class whose main function should be used). In our modified case, Bazel is attempting to find the class echo_client, which does not exist.

This outcome should make sense. Although the Bazel rule has a convenience to elide the specification of the main class with the build target’s name, failing that convention, it will not attempt to implicitly select a class with a potential main class. As illustrated, this is even in the case where there is only one class.

Fortunately, we can add an explicit declaration to make things work again.
java_binary(
    name = "echo_client",
    srcs = ["EchoClient.java"],
    main_class = "EchoClient",
)
Listing 5-9

Explicitly listing the java_binary main class

Save this change to the BUILD file.

Running will now work as before:
chapter_05/src$ bazel run :echo_client
INFO: Analysed target //src:echo_client (1 packages loaded, 2 targets configured).
INFO: Found 1 target...
Target //src:echo_client up-to-date:
  bazel-bin/src/echo_client.jar
  bazel-bin/src/echo_client
INFO: Elapsed time: 0.204s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 2 total actions
INFO: Build completed successfully, 2 total actions
Spinning up the Echo Client in Java...
Error: java.net.ConnectException: Connection refused (Connection refused)

The preceding code is a reminder that Bazel wants everything as explicit as possible. True, some of the rules do provide some shortcuts to make life easier, but ultimately Bazel does not want dependency management or build specification to be “magical.” By making our dependencies, build specifications, and so on explicit, Bazel is able to perform optimizations and guarantees to make our builds fast, stable, and well understood.

Now, let’s actually run our programs together.

Echoing Between Programs

Having created our client and our server programs, now it is time to run them together. You will need two different instances of your shell to run each of these.

Within your first shell, let’s get the server back up and running:
chapter_05/src$ bazel run :echo_server
INFO: Analysed target //src:echo_server (0 packages loaded, 2 targets configured).
INFO: Found 1 target...
Target //src:echo_server up-to-date:
  bazel-bin/src/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 0.210s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
2019/06/02 18:50:31 Spinning up the Echo Server in Go...
That will hold until it actually gets a connection; let’s provide one. Open your second shell and run the client program:
chapter_05/src$ bazel run :echo_client
INFO: Analysed target //src:echo_client (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:echo_client up-to-date:
  bazel-bin/src/echo_client.jar
  bazel-bin/src/echo_client
INFO: Elapsed time: 0.284s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Spinning up the Echo Client in Java...
Waiting on input from the user...
Now, just enter in some text that you want to send and press enter. In your second shell (the client), you should have something like this:
chapter_05/src$ bazel run :echo_client
<omitted from above>
Spinning up the Echo Client in Java...
Waiting on input from the user...
Hello, friends!
Received by Java: Hello, friends!
Echoed from Go: Hello, friends!

The client program should have also cleanly exited.

Looking at your first shell (the server), you should have something like this:
chapter_05/src$ bazel run :echo_server
<omitted from above>
2019/06/02 18:50:31 Spinning up the Echo Server in Go...
2019/06/02 18:52:28 Receiving on a new connection
2019/06/02 18:53:25 Received data: Hello, friends!
2019/06/02 18:53:25 Connection now closed.

As with the client, your server should now have also cleanly exited. Congratulations! You have created an echo client and server in different languages in Bazel!

Upgrading to JSON

Sending byte strings back and forth is a good start, but it is not a scalable way to send messages. In this section, we will pivot over to using JSON to transmit data over the data connection. In the process, we will tie together some concepts we have been building over the last few chapters.

For the sake of illustration, we will construct a simple JSON message, which has the following data.
{
  "message": "This is my message",
  "value": 1234.56
}
Listing 5-10

Simple JSON message

JSON in Go

Go provides out-of-the-box support for marshaling and unmarshaling data between JSON and an instance of a Go struct. First, we will create a Go struct to define the transmission message.

For the most part, we need only create a plain data struct in Go with the necessary fields. The only small change is that we need to add some annotation to specify the mapping of the JSON key to the particular member of the struct.
package transmission_object
type TransmissionObject struct {
    Message string `json:"message"`
    Value  float32  `json:"value"`
}
Listing 5-11

Simple JSON object in Go

Within the chapter_05/src directory, save the preceding file to transmission_object.go.

Now let’s create a go_library in the BUILD file to provide a build target for this functionality. Open the chapter_05/src/BUILD file and add the following to it.
go_library(
    name = "transmission_object_go",
    srcs = ["transmission_object.go"],
    importpath = "transmission_object",
)
Listing 5-12

Adding the TransmissionObject as a go_library

Save the BUILD file

Now, let’s add the necessary functionality within your Go echo server. In this case, we will read the incoming message, make a bit of modification to the values within the message, and then send the modified message back.

Open echo_server.go and add the following lines.
package main
import (
      "encoding/json"
      "fmt"
      "log"
      "net"
      "transmission_object"
)
func main() {
<omitted from above>
       data := buffer[:size]
       var transmissionObject transmission_object.TransmissionObject
       error = json.Unmarshal(data, &transmissionObject)
       if error != nil {
              log.Panicln(
                     "Unable to unmarshal the buffer! Error: " +
                     error.Error())
       }
       log.Println("Message = " + transmissionObject.Message)
       log.Println("Value = " + fmt.Sprintf("%f", transmissionObject.Value))
       transmissionObject.Message =
              "Echoed from Go: " + transmissionObject.Message
       transmissionObject.Value = 2 * transmissionObject.Value
       message, error := json.Marshal(transmissionObject)
       if error != nil {
              log.Panicln(
                     "Unable to marshall the object! Error: " +
                     error.Error())
       }
       connection.Write(message)
}
Listing 5-13

Unmarshaling, modifying, and marshaling a JSON object in Go

Save the changes to chapter_05/src/echo_server.go.

Finally, we need to update the BUILD file to have the correct dependencies for echo_server. Open the BUILD file and make the following changes.
go_binary(
    name = "echo_server",
    srcs = ["echo_server.go"],
    deps = [":transmission_object_go"],
)
Listing 5-14

Updating the BUILD file

Save the changes to chapter_05/src/BUILD.

Now let’s move onto the echo_client to make the necessary changes.

JSON in Java

Go has built-in facilities for transforming JSON into instances of Go structs; Java, unfortunately, does not have the same capabilities out of the box. Fortunately, we can acquire similar behavior by using the GSON library.

GSON Setup

Back in Chapter 3, you downloaded a jar file to the third_party directory; here we will do the same again, this time to retrieve the GSON library.

First, create the third_party/gson directory:
chapter_05$ mkdir third_party
chapter_05$ cd third_party
chapter_05/third_party$ mkdir gson

Download the JAR from the following location:

http://central.maven.org/maven2/com/google/code/gson/gson/2.8.5/gson-2.8.5.jar.

Copy the JAR file to the third_party/gson directory. As before, we now create the BUILD file for the external dependency.
package(default_visibility = ["//visibility:public"])
java_import(
 name = "gson",
 jars = ["gson/gson-2.8.5.jar"]
)
Listing 5-15

Contents of the BUILD file for the third_party dependency

Save the BUILD file to the third_party directory.

Adding the Transmission Object to EchoClient

Now we will create a TransmissionObject in Java (equivalent to what we did in Go) for sending and receiving structured and typed info over the wire.
public class TransmissionObject {
    public String message;
    public float value;
}
Listing 5-16

Transmission Object in Java

Save the preceding code to chapter_05/src/TransmissionObject.java. Now let’s update the BUILD file with a new java_library build target for this object.
java_library(
    name = "transmission_object_java",
    srcs = ["TransmissionObject.java"],
)
Listing 5-17

Updating the BUILD file

Save the changes to chapter_05/src/BUILD.

Now let’s add the changes to EchoClient.java to marshal the data, send the message, and then print out the response from the echo server.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
<omitted from above>
    System.out.println("Waiting on input from the user...");
    final String inputFromUser = commandLineInput.readLine();
    if (inputFromUser != null) {
        System.out.println("Received by Java: " + inputFromUser);
        TransmissionObject transmissionObject =
            new TransmissionObject();
        transmissionObject.message = inputFromUser;
        transmissionObject.value = 3.145f;
        GsonBuilder builder = new GsonBuilder();
        Gson gson = builder.create();
        final PrintWriter outputToServer =
            new PrintWriter(socketToServer.getOutputStream(), true);
        outputToServer.println(gson.toJson(transmissionObject));
        System.out.println(inputFromServer.readLine());
    }
    socketToServer.close();
}
Listing 5-18

Adding transmission to the echo client

Save the preceding code to chapter_05/src/EchoClient.java. Finally, we can now add the necessary updates to the BUILD file.
java_binary(
 name = "echo_client",
 srcs = ["EchoClient.java"],
 main_class = "EchoClient",
 deps = [
      ":transmission_object_java",
      "//third_party:gson",
 ]
)
Listing 5-19

Updating the Java client with the new dependencies

Save the preceding code to chapter_05/src/BUILD.

Executing the Echo Client/Server with JSON

With all the pieces in place, let’s run the server and the client. Once again, you will need two terminal instances in order to properly the run the client and server.

First, we will run the echo server:
chapter_05/src$ bazel run :echo_server
INFO: Analysed target //src:echo_server (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:echo_server up-to-date:
  bazel-bin/src/darwin_amd64_stripped/echo_server
INFO: Elapsed time: 0.132s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
2019/06/04 00:27:23 Spinning up the Echo Server in Go...
In the second terminal instance, we will run the echo client:
chapter_05/src$ bazel run :echo_client
INFO: Analysed target //src:echo_client (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //src:echo_client up-to-date:
  bazel-bin/src/echo_client.jar
  bazel-bin/src/echo_client
INFO: Elapsed time: 0.115s, Critical Path: 0.00s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
Spinning up the Echo Client in Java...
Waiting on input from the user...
Let’s add the user input:
chapter_05/src$ bazel run :echo_client
<omitted from above>
Spinning up the Echo Client in Java...
Waiting on input from the user...
My Client Message
{"message":"Echoed from Go: My Client Message","value":6.29}
Finally, let’s look at the console output from the echo server:
chapter_05/src$ bazel run :echo_server
<omitted from above>
2019/06/04 00:27:23 Spinning up the Echo Server in Go...
2019/06/04 00:29:08 Receiving on a new connection
2019/06/04 00:30:04 Message = My Client Message
2019/06/04 00:30:04 Value = 3.145000

Congratulations! You have successfully augmented your client and server programs for transmitting JSON data between different languages.

Final Word: Duplication of Effort

One unfortunate reality of using JSON is that we need to duplicate the data definitions for each language. Should the data contract change, this means that every instance of the data definition needs to be changed. This is an error-prone process and, while better than simple strings transmission, still has a number of shortcomings.

As stated previously, one of the areas that Bazel excels is in multi-language support; in the next chapter, we are going to lean into this capability as we add Protocol Buffer support to our project.

Exercise – Python Client And/Or Server

As with Go, Python has some handy capabilities for marshaling to/from Python objects from JSON. Create either a new client or a server in Python.

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

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