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-1Adding 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-2Loading 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.
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-3Simple 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-4Go 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-5Simple 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-6Java 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"],
)
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-8Modified 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-9Explicitly 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-10Simple 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-11Simple 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-12Adding 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.
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-13Unmarshaling, 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-14Updating 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 , 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-15Contents 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-16Transmission 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-17Updating 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;
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-18Adding 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-19Updating 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.