This post will be higher-level than most. Rather than getting into the nitty-gritty details of writing a server (there are many posts doing that already), I am going to explain some general concepts that need to be understood to properly write client-server software. Specifically, by the end of this post you should have a stronger idea of what it even means to have a client-server model and what the major components of such a system are.
What is a Server?
The first obvious question is simply: what is a server? How do I know the code Im looking at (or have written) is defined as a server. Well, simply put, a server is a piece of code which is composed of inputs and outputs (like all software) that provides a service. More often than not, a server is accessible remotely but there is no reason you cannot have server software running on your local box.
As you can see, what it means to be a server is somewhat loosely defined. We will go into more detail later, but take a moment to think of some examples below and well try to formalize this sense of I know it when I see it:
- Chat Server. The server provides the means of connecting and sending messages to your friends.
- Game Server. Similar to the chat server, a game server allows you to connect with friends and play a game together.
- Web Server. A web server allows you to connect to a website and download all the information required to view a web page.
What is a Client?
The analog to a server is a client. This is what usually people realize they are interacting with in day-to-day usage of their software. Clients use the service provided by the server and present it to users in meaningful ways. Typically, client-server protocols are not very easy or pleasant for humans to parse or follow. Instead, the client provides an interface so that the user may have a pleasant experience.
Similar to the exercise we performed with servers, lets briefly identify their clients:
- Chat Client. Consider any chat client you may use: Snapchat, Whats App, HipChat, IRC, etc. All of these are clients which communicate with a server in order to send correspondence to your contacts.
- Game. For a network-based game, the client is the game itself. It understands how to communicate with the server and update the game based on a combination of your input while playing the game and the input coming from the server (think of the server as player 2³ if you were sitting right next to each other playing the game).
- Web browser. The most common client for a web server is our web browser. Whether it be Chrome or Firefox (read: I do not advocate the use of other browsers), these speak the HTTP protocol (more on this in a minute).
So how do I connect the two concepts?
Well, now that we have loosely separated the functionality of what a client vs. a server does (recap: usually servers perform some service or common function that clients rely on to provide a user with a reasonable user experience), we are ready to discuss how to actually build such systems and what we need to know. As I have already mentioned, clients and servers communicate through protocols; this is the heart of server communication (it is, more literally, the definition on how a server behaves). This is a fancy word for convention. In proper design, this is usually well-defined for the supported functionality.
So how are protocols designed or what exactly does it mean to design a protocol? Unfortunately, there is no great answer to these questions since they are engineering questions (i.e. very specific to your problem). However, the very first item you must identify is what service/functionality must your server provide. After you have solved that problem, you then factor in other constraints (i.e. I need it to be fast or I need to minimize network communication, etc.).
For instance, we will go through an example of a game protocol. Since game protocols are usually highly-specific and specialized to the game itself, its harder to point to an example (like we will later with IRC and the web). Lets take a look.
Our example: A game
I am not sure how many of you remember Unreal Tournament 2k4, but I had wasted many hours as a kid playing this game. In summary, it is a first person shooter. For sake of example, we are going to distill its functionality to shoot, run, damage player, and player eliminated. Two comments before we proceed: first, we are focusing strictly on game elements which occur while a player is in-game and playing (i.e. we are neglecting joining/leaving but after the example this should be an obvious case) and secondly, we are ignoring other elements such as creating a protocol conducive to low-latency requirements (for some definition of low-latency). That is, we only care that our server provides a sufficient service otherwise our protocol would likely turn into binary which is not very readable.
So the first thing Im going to do is layout our protocol right away. After we have seen it, Ill explain it in further detail.
RUN <PlayerName> (x, y, z)
DAMAGE <PlayerName> <amount>
SHOOT <PlayerName> (x, y, z)
Without defining a formal grammar, notice that our protocol consists of the following format:
ACTION RECIPIENT VALUE
where ACTION is our keyword to describe an event, RECIPIENT is the name of the player who receives the event (we assume that this is unique for now), and VALUE is an optional bit of data that the server may or may not need for a particular action. Pretty simple right?
Lets take a look at our game in action. Suppose we have two players named Syzygy and Harlequin. We will show how our protocol works for a firefight between the two where Syzygy defeats Harlequin:
Client: RUN Syzygy (1, 0, 0) # Syzygy is running
Client: RUN Harlequin (0, 0, 0) # Harlequin stops moving
Client: RUN Syzygy (0, 0, 0) # Syzygy stops moving
Client: SHOOT Syzygy (1, 0, 0) # Syzygy shoots along a vector <1,0,0>
Server: DAMAGE Harlequin 20 # Harlequin is hit by that shot
Client: RUN Harlequin (1, 1, 0.5) # Harlequin tries to run away
Client: RUN Syzygy (1, 0, 0.5) # Syzygy chase Harlequin
Client: SHOOT Syzygy (0.97, 0.1, 0.23) # Syzygy shoots along vector
Server: DAMAGE Harlequin 100 # Harlequin is hit with a headshot
Server: ELIMINATE Harlequin # Harlequin has been defeated
Client: RUN Syzygy (0, 0, 0) # Syzygy stops moving
You can read the plain-English description of my scenario after the # marks.
NOTE: To keep things clean, I am implying that the server echos relevant messages from the client to all other clients (i.e. SHOOT or RUN) otherwise the remote player would never see these actions.
Now that we have our protocol and weve seen how it works, lets think a little bit about the implementation. One thing which should be apparent is that the server holds much of the state for the entire game. Since our client actions (RUN and SHOOT) provide relative vectors in this case, the server must know the absolute position of each player. Similarly, in the case that a SHOOT event happens to hit an enemy, the server must first calculate that this intersection has occurred (between player and projectile) as well as how much damage it causes. Finally, the server must determine whether or not a player is eliminated after receiving damage. Above all, after these calculations. the server is responsible for updating all the clients with this information (by sending them the appropriate protocol messages).
The client, on the other hand, must simulate what these events look like in-game. The client has two major roles: (1) for its player, update the server state (i.e. RUN or SHOOT) and (2) update the state based on all other players actions which are communicated through the server. What this means is that when the server sends any message for some player, the client must update its graphics renderings, audio, etc. to reflect players moving, shooting, and so forth.
Real world examples?
There are many real world examples. The IRC protocol (RFC 1459) is an example of a chat protocol used by IRC servers and IRC clients. Similarly, the HTTP protocol (RFC 2616) is the protocol used to communicate with websites across the web (i.e. between web server and your browser). With the knowledge of those protocols, you can open any open source server or client code and follow along with their communication.
Both protocols I have listed are actually human-readable, plain-text protocols (i.e. no binary, no encryption). However, it is not uncommon for protocols to have binary formats where bits are well-defined instead of using keywords (see lower-level protocols such as TCP). As I mentioned earlier, these constraints are based on the problem youre trying to solve.
How do I really get started?
First, you want to find a problem youre going to solve. After that, start going through and determine the functionality required from a server and client. Once that is laid out, you can start designing your protocol on paper (this is an important step before coding). This sounds pretty fuzzy, but thats because the formality comes in the design portion of your protocol. That is, when you have a design written out to work with is when you usually actually discuss design trade-offs and prioritize different system constraints.
If you want to write client-server software, the best way is to simply do it. Once you have written one, the code tends to become easier and you will find that most of the hard work comes from designing an appropriate and extensible protocol so that you can evolve your system over time. If youre just starting out or just needed a refresher, I hope this article has shed some light on the topic. If not, thats what the comments are for. Until next time!comments powered by Disqus