I often find that networking is a very useful tool when working on my projects, and almost as often I find that networking becomes a core aspect of the design of a project — what functionality belongs to a server versus a client; what happens in the absence of a server or network connection; can (and should) a client act as a server? Many aspects of some projects come down to how a client should communicate with a server, how I can mock that interaction for unit testing, and especially how I can design a robust framework that doesn’t explode when something goes wrong.
Unfortunately, I do not have a general solution to establishing networking in my applications, and they all end up having project-specific networking frameworks. Typically I will design the project-specific structure as somewhat of a copy of the last one I wrote, adding in any of the lessons I learned and ideas I formed while reflecting on my previous implementations.
Upon higher-level reflection, I realize that I am eschewing the practice of code reuse completely. Why should I rewrite the code to be project specific? Why wouldn’t I just take a moment to design a framework that I could reuse across multiple projects so that when I add improvements, all of my projects receive the benefit? This is that moment.
Developing My Framework
Protocol Header
My first step is to figure out what a “general” networking framework even looks like. I would like my framework to have as little overhead as possible so that things such as message processing do not cause a slowdown in the consuming applications; further, I want to minimize the information that I need to transfer so that I can minimize the portion of the consumer’s bandwidth that they sacrifice for using my framework.
So then, what is the minimum amount of information that I can send to get useful work done? It might help if I rephrase it and give myself the responsibility of a server.
Consider this: you are a contractor who receives work by mail, and you will respond to all of your clients with the result of the job when finished. What is the minimum information you need to perform a job received by mail?
I know that I need to respond later on the status of the job, so clearly I need to know who I am communicating with. Luckily for me letters have the return address clearly printed on them, so we get that for free just from receiving the letter. That was pretty easy.
Next I know that I need to perform a job for the sender; clearly that implies that I need to know what the job is. As a contractor, I perform multiple duties, so the sender definitely needs to specify what job I am to perform.
Unfortunately, just specifying a particular job is simply too vague: you want me to build a house? How many rooms, how many square feet, etc…? Thus it is clear that in addition to knowing what job to perform, I probably also need to have specifics about the job itself.
At this point, I feel that I know enough to complete a job. I know what job I am going to complete, specifics about that job, and who to communicate with when its finished. Thus we can advertise our clients to provide the following information to allow us to perform a job:
- Contact information
- What job to complete
- Any specifics about the job
However, it’s important to remember that this communication is a two-way street. What information do you need to send to them so they know how the job went?
Clearly, as a client, we need to know how the job went when it finishes, but is that all? Consider the following: you are a client who knows about a contractor that takes jobs by mail; you really like the staircase they built you, so you decide to send them two more jobs: build a new front porch, and build a new rear porch. Because each is a different job, you need to send two letters. A few days later you get a letter back from the contractor that says: “I finished your porch; it went well.” Which porch did they complete?
Here it becomes clear that job status is not the only information the client requires, they also need more information as to which job the response pertains to.
In short, the response needs to contain at least the following:
- Exactly which job this response pertains to
- The status of that job
So how does this help me build a networking framework? Here we decided that the contractor (server) needed a minimum amount of information to perform its duties. In my networking framework, this information is likely to be encapsulated in some sort of header that I can parse for the needed information. My design for this header now looks like so:
- 1-byte job specifier
- 4-byte payload size (in bytes)
- n-byte payload
Let me explain these values. The single-byte job specified is an arbitrary decision under the assumption that 256 discrete jobs is a worthy compromise between space and usability. The payload size is 4-bytes simply because the easily accessible sizes I can use are 8-bit, 16-bit, and 32-bit; 8-bit would limit payloads to 255 bytes, which I feel is far too limited; 16-bit would limit payload size to about 64KB, which seems like a large value, but if the payload were, for example, an image, this size would be very limiting; finally, 32-bit yields a whopping 4GB payload size. I feel that this is overly sufficient for a network transaction. Finally the payload is variable in size, specified by the payload size just discussed.
However, we know that the client is going to need some way to specify individual messages from one another if the job specifier is the same — some kind of transaction identifier. One of the goals was to minimize the header size in order to preserve bandwidth, so I am hesitant to add another field and expand the size.
Here I make a compromise: the payload size is massive for a single network transaction; if we stole one byte from the size we could keep the current header size the same and get an 8-bit transaction ID. This would bring the maximum payload size from 4GB to 16MB; ouch, but 16MB is still a very large payload size to be able to send. Also consider the buffer required to receive a message on the other end: with 32-bit payload sizes, the receiving end may be liable to receive a message up to 4GB and allocate a buffer for that; this is infeasible on many systems and an unrealistic size to require. 16MB is still very large, but is ultimately doable on most modern machines; anything larger can likely be split apart into digestible chunks, and anything that can’t is likely to benefit from a different networking structure anyway.
Thus, to satisfy the needs of the client and the server, we arrive at the following header:
- 8-bit job specifier
- 16-bit transaction ID and payload size (in bytes)
- Upper 8-bits are the transaction ID
- Lower 24-bits are the payload size
- n-byte payload
This header is fairly minimal, assuming only 5 bytes of space per message. As the payload size increases, the cost of this header drops significantly. Even better, the cost is virtually removed when we factor in that a majority of this information is strictly necessary to complete the transaction, and would exist in the payload if absent from the header; the job specifier and payload size would be absolutely required at a minimum, meaning only 1 byte of the header could potentially be considered wasted space.
Additionally, I can logically reduce all of my previous networking efforts to fit cleanly within this framework, seemingly suggesting that I am on the correct path to generalizing a solution that I could employ among future projects.
503