Takeout Central had a problem. We were running a TCP Server and a UDP Server. Both were written in Perl. Neither was taking advantage of concurrency. At the same time, our team of Delivery Heroes became frustrated while beta testing an IoT device in the field. Information sent to the ten beta test devices took an hour or more to update, which we found to be an unacceptable amount of time.
Enter Go. I’d used Go in the past. It was my language of choice for Advent Of Code 2017. As I kept up with the five days of challenges, I found that writing in Go was a breeze. After that, I kept an eye out for a problem that called for a robust systems language. Here was the opening.
Go uses concurrency rather than parallelism. While the differences between concurrency and parallelism are slight to most, co-designer of the Go programming language Rob Pike explains it like this: “Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.”*
First, I wanted to know how concurrency functioned in Go. It was so straightforward that even beginner tutorials discussed it relatively early. Next, I needed to know how to implement concurrency in a server environment. After a few more minutes of googling, I found the scaffold for the TCP server. I learned how Go handles socket communications on this blog, which also explains how these can all be leveraged using goroutines. The
go keyword before
handleRequest does all of the work for concurrency, because each function called with a preceding go starts a new concurrent process. Getting the server off the ground was easy, because all it entailed was prefacing a function call with “go”.
I’d become comfortable with concurrency and goroutines, so I turned my attention back to the problem at Takeout Central. To replace the old system that was slowing us down without losing any functionality, we needed the new TCP server to:
- Concurrently handle each Device-to-Server TCP connection using goroutines
- Respond to valid requests with a JSON packet
- Log requests, failures, updated states
- Update a MySQL database accordingly
Because our devices sent requests and expected responses in JSON, it was my next topic of interest. The json package Marshal function does the work producing the JSON string and it’s documentation lays out the possible inputs.
I chose to use an exported struct. It offered clearly-defined messages, meaning the struct was also self-documenting. I prefer to use explicit key names whenever possible, which is why I also utilized the ‘json’ tag in cases where a device’s expected key did not conform to Go’s style.
This struct is populated using the explicit assignment seen below where the variable
message is a string containing a user defined error message. The resulting struct is Marshaled into a byte slice to be returned by our connection handler.
On the flip side, requests need to be validated by the Unmarshal function. Given the
errorMessage struct above we can put together the pieces to get an error message from the client using the code below. If invalid JSON is passed, the Unmarshal function will pass an error for our server to log. This validates that messages are at least using valid JSON.
The standard Unmarshal function omits fields that don’t exist in your struct definition by default. Here at Takeout Central on this project specifically we’re working with standard messages with very little variance between them. If that’s not the case for your application, you might want to look into handling unexpected messages using a different interface than a struct.
For logging, Go provides the log package. We start here by initializing a pointer to Logger in
main using Log.new. Using this pointer and dependency injection we pass this along to the
handleConnection function and furthermore into our subsequent log helper functions.
For the formatting I prefer something along the lines of
2019–04–25 21:26:17 [error], and so our log functions look like this:
It’s important to note here that when using dependency injection on concurrent functions the objects you’re passing need to be thread-safe. This is necessary to avoid race conditions as well as ensuring one of the many concurrent processes don’t fail because it’s attempting to modify our log file while it’s in use. Luckily for us, the documentation for Logger spells that out explicitly.
If one thing has come through thus far, I hope it’s how straightforward and ‘batteries-included’ Go is for these tasks in particular. Up until now and in fact, excluding the MySQL drivers I’m about to delve into, everything in our server is powered by the Go packages from the core team.
Go doesn’t implement each database in their own core libraries, it’s been left as a challenge to the users or the reader. They do provide the sql package as an interface and a comprehensive list of drivers for your ease. I went with go-sql-driver as it best fit my projects needs.
go-sql-driver and Go sql follow standard patterns if you’ve worked with MySQL before. Starting with a pointer to the DB(database) object we use it like Logger before as this is also designed to be threadsafe as you might expect.
For our use-case, we’re monitoring and updating devices one at a time. This means there’s very few cases where we have to fetch more than one row from the database, meaning our requests and data grabs tend to look like the code below. Like
log our database pointer is initialized in
main and passed to each
The variables you see that aren’t initialized in view:
dbh, our database pointer;
log, pointer to our log object;
errors which is a package Go provides to handle passing errors between functions.
Wrapping it all together
This was one of the most enjoyable projects I’ve worked on recently and I owe it largely to Go’s internal packages and tools. In particular, goroutines as a thread abstraction. Having worked with threads manually in the past I fully appreciate just how much heavy lifting they do. The simple ‘go’ keyword provided a smooth abstraction that is less prone to error and automatically efficiently schedules threads. If you’re implementing this yourself, please keep in mind other aspects of a production server like malicious attackers and prepare accordingly. With that in mind though, this is a great TCP server app for a few IoT devices connecting to a central server; I hope it serves you well.