Chapter 10. Network Programming Primer

Much of this chapter is really only applicable to online real-time network games, which need to communicate using a local area network (LAN) or wide area network (WAN) connection. Some parts deal with logic that is equally applicable to other gaming models, and these are noted where appropriate.

Other types of game—for example, Web games played through the browser and so on—don’t directly manipulate the network interface, and therefore do not necessarily require an understanding of the details of network programming in order to make them work.

However, it is essential that we follow up the theories and abstract implementation details with some concrete discussion of the building blocks of network programming. These fundamental topics include the following:

  • Sockets and socket programming

  • Polling and other techniques for multi-player processing

  • Client/server game programming

  • Open source code libraries

The aim of the chapter is to ensure that, having designed and developed a well-behaved network game, the underlying networking can be implemented using off-the-shelf code to speed development, whilst keeping the principles of stability and security that such projects require. It is, however, merely a discussion of the fundamentals. There are details and implementation tricks that network game programmers will learn over time. Nonetheless, what is presented here should allow you to hit the ground running.

An Introduction to Socket Programming

A “socket” is a programming construct that allows the software application (in this case, a game) to talk to other instances of software applications over a network connection. It hides the actual implementation of the bits and bytes communicating with hardware behind a logical abstraction.

Each piece of network hardware (wireless, wired, Bluetooth, Wi-Fi, etc.) has its own particular foibles. These are encoded in the driver, thereby abstracting the problem away from the programmer—that is, the programmer only needs to know what he or she wants to implement, and leave the actual hardware control to the driver. What we are left with is a clear interface, common across most (if not all) code libraries, that allows us to implement networking easily and fairly portably.

Sockets are associated with ports as a way to identify them in the system. While it is perfectly possible to open a socket that is not linked to a specific port, it is often a great help if the port is used as a way to separate communication streams. It is one possible way of identifying multiple connections. As we shall discover, however, there are many other ways of implementing multiple network connections from the point of view of the server. And that is the crux of network gaming: It is multi-player, which implies multiple network connections.

Before we go on, it is worth pointing out that the networking bottleneck is more likely to be the processor than allocating sockets and ports. By the time you run out of ports, the chances are that the machine is approaching saturation.

Types of Sockets

There are two principle types of sockets:

  • Stream sockets (sometimes referred to as SOCK_STREAM)

  • Datagram sockets (sometimes referred to as SOCK_DGRAM)

A “stream socket” is oriented around a permanent connection, and is part of the TCP protocol. This connection may bounce up and down, causing jitter, but every packet sent ought to be received in the correct order by the client system. This statement glosses over some of the more technical aspects of how this is achieved, but as far as the network library is concerned, it is more or less true. This doesn’t mean that you can dispense with logic to detect out-of-sequence packets, but in principle, they should not be prevalent in the system.

A “datagram socket,” on the other hand, is one that operates on the principle of “open, send, close.” In other words, it is not permanently connected to the remote system, and as such offers no guarantee of flow control or deliverability whatsoever. In practice, this means that if you want to build a game over UDP/IP, you need to add a layer that makes sure that the packets arrive as intended. This includes checking that they are in the right order, because the only guarantee in the UDP protocol is that if the data arrives, it will be error-free.

The networking protocols that we look at here are TCP/IP and UDP/IP. The “IP” stands for Internet protocol, and deals with routing issues, which are not covered in any detail here. They mainly concern themselves with how the data gets from A to B. We are more concerned with the data itself.

Many streaming data services, including multi-player video games, use UDP. These implementations have a layer that deals with the vagaries of the UDP protocol. UDP can be very useful simply because no connection is needed, rather than despite it. Due to this, we care much less about things like dropped packets and jitter because UDP is part of the equation. In many ways, UDP makes the whole networking paradigm easier to implement, as long as we accept that there will be some natural wastage in the information exchange.

This means that we have to explicitly compensate for the following:

  • Lost packets

  • Delayed packets

  • Out-of-sequence packets

  • Dropped connections

As long as we can compensate for this inherent lack of a permanent connection (by only exchanging data in very small, isolated chunks for example) then UDP is a great protocol. However, if we have to exchange larger, more frequent chunks of data, then the TCP protocol offers much better intrinsic flow control.

Naturally, TCP does have some overhead, which might make it less attractive, but this overhead is there to make sure that the data arrives correctly. This is vital for things like Web pages and MUDs, where any loss of information means that the page or displayed data is corrupted.

For a game like Quake, however, it can be relatively unimportant that there is the occasional bit of lag, network latency, or dropped packet causing jitter because it can be compensated for. Academic studies (for example, “The Effect of Latency and Network Limitations on MMORPGs” by Tobias Fritsch, Hartmut Ritter, and Jochen Schiller [FRI01] show that players are very resilient (and tolerant) toward these kinds of issues, as long as the outcome of the game is not unduly affected.

In short, as jitter and latency increase, the game flows less and less well—but players are willing to put up with an enormous number of problems before they abandon the game completely. It’s nice to know, but not something we should build into our network handling; we must always strive for the best communication possible.

In addition to the two protocol types, there are also two ways in which we can use sockets:

  • Blocking sockets

  • Non-blocking sockets

We deal with these in detail later on; for now, we should just mention that blocking sockets are the usual default. The problem is that lots of functions used in socket programming run the risk of blocking the sockets.

The worst-case scenario is that a program can find itself in a busy waiting loop, blocking the socket even when there is nothing to do, as control will not be returned to the program until data arrives to be processed. One way of avoiding this is by using non-blocking sockets. Non-blocking sockets are simply not allowed to block. Each call to the socket will return regardless of the presence of data. This has its own problems, however, as polling for data in this way eats up CPU time. We look at some ways to make the polling process slightly more efficient later on in the chapter.

There is, however another option, called “multiplexing.” Multiplexing means that you need only to monitor a set of sockets, and then send or receive over the ones that actually have data. This removes the risk of causing a block on one of the sockets, and removes the need for using CPU-intensive non-blocking sockets.

Multiplexing is necessary for server applications or smart clients having more than one socket connection. For example, any system that is communicating with multiple remote hosts will benefit from multiplexing, but it is less necessary for clients that are running one connection, as they can use non-blocking sockets.

The point is this: The game should never, as a client or a server, get stuck in a loop waiting for data, because if it does so, there is a risk that nothing else in the game will be able to run. No more screen updates, database accesses, or serving other clients. Blocking is bad and has to be avoided at all costs.

Protocol Layers

The socket is at the sharp end of the network programming paradigm. It is the point of communication with the outside world. However, the data itself has to be correctly presented to the interface for the network communication to work.

For each protocol that is used (TCP, UDP, IP, etc.), the data needs to be correctly encapsulated. This essentially means that the data is put into an envelope, with the envelope itself containing information that is pertinent to the network protocol but not necessarily to the game. For example, the envelope might have instructions as to what to do with the data, usually contained in some kind of header, when it is picked apart by the recipient. This might include specific encoding, encryption, or in-game identification data. The outermost layers (envelopes) contain address information so that the transport layer knows what to do with the packet of data. This is depicted in Figure 10.1, which shows layers of data encapsulation.

Packet headers and layers.

Figure 10.1. Packet headers and layers.

Figure 10.1 is a very simplified representation of the layers that are involved in network communication. There can be more or fewer layers, some added by the protocol (networking) library, and some that are proprietary to the game itself.

At the heart is the payload data, which is then usually encapsulated in a layer that is part of the game logic. This inner envelope may or may not be present, depending on the complexity of the error checking and security that is deployed. Then the resulting data is treated as payload and encapsulated in a layer that is used by the network to deliver the data to the end point. To understand this separation more fully, we can look at the OSI Layered Network Model in gaming terms:

Application

Data delivered to the game

Presentation

How the data is represented for transport

Session

How the communication between systems is managed

Transport

How end-to-end communication is managed (TCP versus UDP)

Network

Addressing and routing (IP)

Data Link

Physical addressing between network (MAC) and system

Physical

Mechanical transmission of data

In this book, we deal with issues at several levels of this list, but not the physical, data link, or network layers. These are transparent to the network game developer. We are more interested by the logical representation of the data. The implementation of the transport and session layers are also usually transparent but included in the networking code supplied by the API developer. For PCs, the network is handled by the operating system, with drivers supplied by the network equipment manufacturer. The code-level interface to the hardware is standardized. For other platforms, the development kit supplied by the platform manufacturer will supply appropriate code used to access the specific hardware.

The presentation layer also deals with various encryption issues that we need to prevent cheating and ensure robustness as well as identify packets and check for out-of-sequence packets. These can also be a good clue as to whether there is some underhanded play going on (see Chapter 8, “Removing the Cheating Elements”).

Finally, the application layer deals with how the data is represented by the game. At this level, we are concerned with the various compression and data-decoding standards that we have used for the representation of state information in the game, and so on.

So, we build the data in the application core, then pass it to the code that represents the presentation layer (proprietary). Next we must make a call to the underlying operating system (or equivalent), giving it enough data that it can process the request. Everything else from the session layer down to the physical layer ought to be dealt with by the network library. Part of that responsibility might be in a third-party library, and some will be in the manufacturer’s own driver and development kit.

The smallest unit of communication is the socket, and provides the logical bridge between the game world and the real world.

Server-Side Sockets

Generally speaking, these are “listening” sockets. This means that they do nothing until data arrives, at which point the data is decoded, processed, and an answer sent, if one is available. Of course, game servers may also need to broadcast information from time to time, but they can only do this if a connection is available (a TCP stream, or previous UDP connection). Part of the server’s responsibilities, therefore, includes processing incoming data and multiplexing to make sure that no incoming updates are missed, whilst not blocking any of the pending communications.

Using a request-response mechanism simplifies things because it allows the client to indicate that it is ready to receive an update, whilst also flagging to the server that it needs to perform some processing. If the game developer chooses to allow broadcast updates over UDP (as opposed to request-response over TCP), then the control algorithms become more difficult to implement. Part of this is being able to match the incoming data with the game state and preparing some kind of response having updated that game state.

There will be times when there is so much going on that this becomes difficult, and the temptation is to allow the clients to present the results of the actions rather than working it out on the server. The networked versions of Civilization use this approach. Since the game logic is present in the clients anyway, it seemed logical. However, it does open the doors for cheating, because if the data can be unraveled, fake updates can be passed to the server. These are hard to catch, because the server does not maintain the same level of detail, nor do any of the processing.

So, however hard the implementation seems to be, it is worth making a robust server-side algorithm that can match game states properly and simultaneously handle the network connections to prevent the above scenario.

Finally, having worked out the appropriate response, the server has to have a mechanism for sending data—more importantly, sending the right data to the right client, at the right time. This is also something we deal with in this chapter.

Client-Side Sockets

On the face of it, the client side is much easier to appreciate. After all, all it needs to do is open a socket, send the data, and wait for a response. However, it also needs to maintain a hard real-time application and cope with network problems, whilst simultaneously accepting input from the player.

The network-communication programming is therefore the smallest part of the equation, but also has some important challenges of its own to contend with. These include response matching—which is made easier if a strict request-response mechanism is used, and only slightly more tricky if broadcasts are allowed—and inter-client communication.

Using Polling

Polling is a technique that allows the game client or server to check to see if network data is available while also giving the appearance of continuing to process normal game events. In fact, most game programmers should be well aware of polling techniques, as they are used in real-time systems to prevent them from becoming blocked in a busy waiting loop.

There are many different kinds of polling algorithms, some more sensible and effective than others, and some that have specific significance in game programming. It is important to remember that at the core, any program has to be broken down into a series of sequential steps.

Even if they run in parallel in a multi-threaded, multi-processor environment, the lowest unit of execution is still a step-by-step process. Polling is the mechanism that allows other things to continue, even when the system is also waiting for something to happen in a specific sequence.

Server-Side Polling

In principle, the server is constantly polling to see if it has to do anything. This does not just include the network layer, as it may be polling for a number of reasons:

  • Polling for incoming data

  • Polling for changes to the game environment

  • Polling the status of the system

And polling can happen at many levels—from the system down to the interactions between clients. After all, it is no good for the game server to be able to play out the in-game decisions if the database falls over, so some polling selection needs to take place.

One part of the system should be able to choose (intelligently, we hope) between different things to poll, at a high level. This might need to be weighted according to activity in different areas of the system as a whole. Those things can select further children to perform polling for them, eventually winding up at the game-environment level, where polling for specific in-game events takes place. In this model, the game itself is just one part of a much larger system that needs to be maintained in real time. The lowest-level actors in the system then need to poll hardware (for example), connections (such as network sockets), or software event queues. Everything is governed by the need to poll specific areas at specific times.

The polling frequency might change depending on changes in the system as a whole. Intelligently scheduling polling activities and splitting the load according to actual activity is something that might be less important for some games than others. The most complex environment will be clustered servers (such as those used for Eve Online), where the communication between servers can be as important as the communication between client and server.

This means, for example, that if the network is not active, the scheduling system might choose to poll it less than another, more urgent, aspect of the system. The polling mechanism itself then needs to be applied across the various system services (i.e., sockets, in this case) to determine whether there is any work to do.

Those, in a nutshell, are the server side–polling challenges. The client side is not nearly so complex an environment.

Client-Side Polling

Client-side polling is really only about the game on the one hand, and the network on the other. Leaving aside the game itself, as the game is beyond this book’s scope, we shall concentrate on the polling mechanisms that enable the network and multi-player aspects of the game.

If a request/response architecture that only reacts to the player is used (i.e., a Web game), then a robust polling mechanism might not even be necessary. After all, these tend to be oriented toward the player scheduling the action as opposed to being a flowing gaming experience, due to the nature of the connectionless communications medium. (A side note: AJAX can be used for asynchronous game-environment polling, if that proves to be something that the game developer wants to offer. In addition, games that are played through the browser, such as Adobe Flash games, are grouped together with traditional networked games for the purpose of this discussion, and will definitely need a robust polling mechanism.)

A network game client might poll for one of the following reasons:

  • To see if there is a response to a previous request

  • To check for game-environment changes

  • To check for things like personal messages

At the same time as the network polling is going on (i.e., asking the game server for updates and monitoring the connection for replies), the client also has to continue playing the game. This includes rendering the results of the last round of data that came in in response to polling the server, and polling the player for reactions to that rendering.

So, the scheduling mechanism will also have to deal with the possibility that the client is not polling the game server uniquely—i.e., it will need to poll other, related services in order to manage all the system tasks. These system tasks then process in a sequential fashion, as does the polling mechanism itself.

Sequential Processing Techniques

Programming is often thought of as the act of breaking down a complex problem into a series of steps. Game programming is no different, and computers must have tasks expressed to them in a sequential fashion.

Even if the result is that two processes run side by side, doing different things, because there are two processors available, the decision that spawned those two processes was based on sequential processing, and each process will achieve its goal through sequential processing.

This step-by-step approach means that we cannot poll every possible aspect of the system simultaneously. We can do it very quickly, so that it seems simultaneous, but it will not be simultaneous in the eyes of the processor. And the more activity there is to process, the less simultaneous it becomes, until it is patently obvious that there is a sequence to the way that the services are being polled. For this reason, we need to have a model that predictably polls in a way that makes sure that we never leave something out, and that every aspect of the system gets the attention that it deserves.

Socket Polling Example

As an illustration of polling, we are going to tie it in with the subject of this chapter: network programming. A server, after all, has to have a good way to poll multiple network connections, even if multiplexing is being used.

We will cover the implementation of multiplexing in the next section; for now, you can assume that the result is a list of sockets that have data waiting on them that can be polled to retrieve that data. This actually solves half the problem, as you don’t strictly need to poll anymore, but just process the data in a logical order.

The basic model that we use is called “round robin,” and applies equally to polling as data processing. The model is simplicity itself: Each socket is polled in turn, and data processed in a sequential pattern. Once a socket has been processed, the round-robin model states that control is passed to the next socket, which is then processed, and so on in a circle. When the last socket has been processed, then the processing begins anew with the first socket.

Whether you are polling the sockets for data or processing data that you know is there, the round-robin approach makes sure that

  • Each socket gets equal attention.

  • None of the sockets are missed.

  • Sockets are processed in a predictable fashion.

All of these are good. But the round-robin model has some basic flaws. Chiefly, it is possible to spend an inordinate amount of time processing small amounts of data, while “busier” sockets stack data that never gets processed. This is due to the fact that each socket receives the same level of attention, whilst it might not have the same level of activity. Put another way, assuming that we really are polling in a round-robin fashion, what happens when only one socket has activity, and 99 are dormant?

Clearly, the round-robin approach really only works in a system where all clients require equal attention, which is rarely going to be the case in a gaming system.

Enhancements to Basic Round Robin

By way of an illustration, we are going to look at two possible enhancements to the basic round-robin scheduling model for real-time systems. These are

  • Active time slicing

  • Most-active polling

Both of these require that we maintain a dynamic list that gives us the order in which we will process the items. This list will change over time as the criteria for activity measurement changes the processing priority and amount of processing power devoted to each item.

Implicitly, the items at the top of the list should receive the most attention, as they are the ones with the most activity. Other than this premise, the processing is identical to the round-robin model described above.

Where the enhancements differ is in how this attention difference is applied. In the case of active time slicing, each item is specifically linked to a number that indicates the number of operations (or pieces of data) that may be processed by the system. More active items get a larger slice of processor time.

The most active–polling enhancement takes a slightly different approach, and simply returns to the most active items more often. This removes the need for an accurate time-slicing algorithm, which can become messy when a thread needs to be interrupted and suspended prematurely.

Both approaches can be fine-tuned to the game’s own requirements, and both retain the predictable essence of round-robin scheduling whilst allowing the dynamic flexibility that ensures that the most-deserving items get the most attention.

Socket Programming

This section puts sockets into practice, and is full of code that can be used in basic socket programming tasks. Of course, the snippets will need to be extended to provide the functionality that is required for a game, but the underlying code provides a solid foundation.

The discussion of socket programming is based on Unix-style calls, but similar functions can be found in most operating systems or development kits. The language that I have chosen for the implementation is C, for speed and portability. C++ variations also exist, and work in much the same way.

Really all that is needed is a handful of functions and data types. These are all well documented in the appropriate programming guides. The following is more of a recipe than it is a guide to every possible network function call available to the programmer. If the recipe is followed, then you will have a working prototype—but as for any good chef, this is only the starting point. You can work with the recipe to make the result your own. Depending on the game being developed, these changes could be quite extensive.

On the client side, the basic steps are as follows:

  1. Create a socket of the appropriate type (i.e., TCP or UDP).

  2. Bind it to a local socket (for security reasons).

  3. Connect to the server (address and protocol).

  4. Query the server (using send and receive).

  5. Interact with the user (the game).

  6. Interrupt to get updates from the server (receive data).

  7. Send data to the server (post-interaction).

On the server side, as we have seen, the steps are as follows:

  1. Create a socket of the appropriate type (i.e., TCP or UDP).

  2. Bind it to a local socket (because that’s where the incoming calls will be placed).

  3. Listen for incoming calls (from clients).

  4. Connect to client calls.

  5. Send and receive data whilst updating the game state.

  6. Ad infinitum....

Obviously, in both cases, there will be some multiplexing and/or polling required to make sure that, once connected, the correct flow of events for the game system as a whole is maintained. But the network-communication steps are more or less as presented above.

The remainder of this section details the steps required to actually create code that enables the game to follow these processes. We start with a look at the data types provided by most C implementations in the sockets.h library.

Data Types

There are some special data types provided by the sockets.h library that are also duplicated in platform-specific socket application programming interfaces (APIs), such as WinSock. These contain information about the socket type, address, status, and so on. For example, there is a function that returns a socket identifier. In order to create the socket, and hence the identifier that represents it, it is first necessary to tell the network library everything about the nature of the connection.

The standard Unix data type for this information is called sockaddr_in. Other APIs will have something that is identical, or at least very similar. The Unix variant looks like the following:

struct sockaddr_in {
       short int           sin_family;   // address family,
                                            AF_something
       unsigned short int  sin_port;     // the port
       struct in_addr      sin_addr;     // the IP address
       unsigned char       sin_zero[8];  // padding
};

Windows also has an equivalent, as do other libraries; they are laid out in more or less the same way.

You will notice that the IP address is inconveniently contained inside a sub-structure called in_addr. The in_addr sub-structure is also defined in a way that makes it hard to access directly. For example, it actually contains one single member, s_addr, which is a four-byte number representing the IP address—not programmer friendly, in the least!

Luckily, however, there are a number of functions that can help us build the data types that we need, so we don’t need to become experts in translating numbers between arcane formatting specifications. The first of these functions is used to turn a string of IP numbers (i.e., 12.34.567.89) into an s_addr value, which can be placed in the sockaddr_in structure. The inet_addr function that provides this is used in the following way:

struct sockaddr_in my_socket_addr;
my_socket_addr.sin_addr.s_addr = inet_addr(' '12.34.567.89' ');

Again, notice that the actual IP address is assigned to the s_addr member of the sub-structure sin_addr, which is itself a member of the sockaddr_in structure, which is the base type of the variable my_socket_addr. It is all quite confusing at first, but the value of s_addr is the four-byte number mentioned previously.

In addition to the inet_addr function, there are some functions used to ensure that the byte ordering for numerical data types is correct. This is an important point, because the byte ordering (little endian or big endian) is not always the same from one platform to another.

So, the client and the server might run on hardware with different byte ordering, as might the development platform and the deployment platform (i.e., PC versus console). The two byte-ordering schemes are known as “host” and “network.” The four worker functions that can be used to translate between them are as follows:

  • htons(): host-to-network short

  • htonl(): host-to-network long

  • ntohs(): network-to-host short

  • ntohl(): network-to-host long

So, the line of code from the preceding snippet now becomes:

my_socket_addr.sin_addr.s_addr = htonl (inet_addr(' '12.34.567.89' '));

The additional code is presented in bold font. We can also use the hton and ntoh functions with the preceding structure to, for example, set the port number correctly when the socket is created. This is the next step in building the network connection.

Sockets and Ports

When we first start using the network, we need to open a socket that is bound to a specific IP address and port. The actual IP address and port that are used will depend on whether we are looking at the problem from the point of view of the client or server.

If we are writing a server communications layer, we bind the socket to our local IP address. If we are writing from the point of view of a client, we bind to a remote IP address representing the server (or another client). This IP address will probably have been obtained by calling a DNS function to convert a text address (for example www.mygameserver.com) to an IP address.

A socket is created with the socket() function, which takes a few key parameters. The first is the protocol family (PF_INET), along with the type of socket (SOCK_STREAM or SOCK_DGRAM). There is also the possibility to select a protocol, but we can set this to 0 to choose it based on the socket type. That way, the function will create a valid socket for stream or datagram communications without us worrying about what the value should be for each one.

The socket() call returns a descriptor that can be used to reference the socket. In standard implementations based on the traditional Unix model, this descriptor is just a simple integer number. This clearly limits the amount of available sockets to the size of an int at the operating system level.

The next part of the sequence depends on how you want to implement the network layer. It can be bound to a specific port or left unbound. The bind() function links the socket descriptor to a specific port. This is the equivalent of the IP:port convention used to connect to MUDs using a Telnet application:

telnet xxx.xxx.xxx.xxx:port

A socket will be associated with a port in the end, even implicitly, because it’s needed at the system level. You should remember that the port that we are talking about here is only local, and has no impact on the remote port, because that is dealt with when we actually connect to the remote system.

The calls used to bind the two together use the identifier returned by the socket function. What follows is the whole stanza in C code for setting up a socket and associating it with a port (which can then be used for calling or listening for data). Note that for the sake of clarity, we have left out the error-checking code that would normally be present to ensure that the socket has been allocated correctly, that the IP address could be found, and so on.

    // set up the socket address structure
    struct sockaddr_in       my_socket_addr;
    my_socket_addr.sin_family = AF_INET;
    my_socket_addr.sin_port = htons(PORT_NUMBER); // PORT_NUMBER
#defined somewhere
    my_socket_addr.sin_addr.s_addr = inet_addr("12.34.567.89");

    // set up the socket itself
    int socket_id = socket ( PF_INET, SOCK_STREAM, 0);

    // bind the socket, port, and address together
    bind ( socket_id, (struct sockaddr *)&my_socket_addr, sizeof
my_socket_addr);

    // put error checking here

There is one little trick that can be used with this code. If you want the system to pick a socket and local IP address, then you can specify a 0 for the port and INADDR_ANY as the address. Since these are all local values, it doesn’t really matter, unless the incoming call will be on a specific port.

Connecting to Remote Machines

On the client side, we can call the connect() function to connect to a remote host, using a socket and socket address set up with code similar to the preceding. For this we need another address information structure, which, this time, will relate to the remote address that we want to call.

So, having set up a socket and stored the identifier, we can now populate another sockaddr_in structure (this time called remote_addr) and call the connect() function:

connect ( socket_id, (struct sockaddr *)&remote_addr, sizeof
remote_addr);

Note that the connect() call looks rather similar to the bind() call, and that we don’t need to call bind(). The remote IP address and port are implicitly bound to the socket by the connect() function.

From the point of view of the client system, that is really all that is necessary. As long as the server has accepted rather than rejected the connect() request, the two machines can exchange data using whatever protocol they have set up (i.e., Telnet, HTTP, FTP, etc.) The server side is a little more involved, because it needs to listen for a connection and then accept the call if it wants to exchange data.

Server-Side Connections

For server-side communications, the bind() operation is not optional because the system will assign a random port if the local port is not bound to the socket. This is a bad idea because the client on the other side will not know the port number to connect to.

Of course, there’s no way to tell it, there being no network available, so for server-side network programming, the bind() call becomes extremely important. However, since we are going to be listening on the socket, the IP address can just be set to the local host.

So, we use the same code as before to set up the socket and bind it locally (probably using a known value for sin_port, and INADDR_ANY to pick up the local IP address) before calling the listen function, thus:

listen ( socket_id, 10 );

The value 10 in this case specifies that we are willing to let 10 calls queue up before we deal with them (by calling the accept() function). This is a reasonable number, and the 11th caller will be automatically rejected to keep the queue clean.

So far, so good. However, a point to remember is that when the program accepts the call, the call will be assigned a new socket ID. This means that the current one can be left in place, listening for more incoming calls. This behavior is the cornerstone for the multiplexing system.

For each client that connects on the port, a new socket is allocated to allow communication with the client. Because we’re expecting more than one client connecting, we also need to make sure that we have enough places in memory for as many socket identifiers as the game server is willing and able to deal with.

In addition, the accept() call is a blocking call. In other words, it doesn’t return if there’s nothing to talk to, and the program will sleep until a connection to accept comes along. This means that there is no point blindly calling accept() as the first operation of the server, because it runs the risk of also being the last.

Luckily, there is another useful multiplexing function, select(), that lets us get around this by monitoring sockets that we have set up to receive connections. The select() call operates on sets of sockets.

Generally speaking, there are three kinds of sets defined in the network library:

  • Sets of sockets that we read from

  • Sets that we write to

  • Exception sets

Since the client is in charge, we usually have a use for the first set only. In addition, just because the socket set is identified as one that we read from, it doesn’t mean that we cannot write to it. We can, but the call to select() will return a positive value for only those sockets that have data ready to be read from.

The way the select() function works is that the server software is responsible for managing a master set of current sockets. These are then copied to a set that can be passed to select(), which then removes those that are not ready to be read from. The resulting set can then be processed in a round-robin (or an enhanced variation) fashion.

The master set will always contain the so-called listener (the socket that we give to the listen() function). If there is a new connection to deal with, the master socket will still be in the copied set after we have called the select() function. Naturally, it is best to process the listener socket first, even before the rest are dealt with, as pending connections should be given priority to avoid them stacking up on the queue.

The following code example assumes that we set up the listener socket as per the previous code samples, and then just want to check for incoming connections in an appropriate fashion:

    // listening_socket is an int, and has been created with a call
to socket()
    listen( listening_socket, 10); // wait for a connection
    // set up the master socket set
    fd_set master_socket_set;
    FD_ZERO(&master_socket_set); // remove any existing sockets
    FD_SET( listening_socket, &master_socket_set ); //put the
listener in the set
    max_socket_id = listening_socket; // need to track the biggest id
for select()

    // Off we go!
    while (1) {
      // copy the master list and call select
      fd_set tmp_socket_set = master_socket_set;
      select ( max_socket_id+1, &tmp_socket_set, NULL, NULL, NULL);
      // check to see if there are any "new" connections
      if (FD_ISSET (listening_socket, &tmp_socket_set)) // is it set?
      {
        // allocate a new socket id
        struct sockaddr_in remote_socket_addr;
        socklen_t addrlen = sizeof(remote_socket_addr);

        // accept the connection
        int new_socket = accept ( listening_socket,
     (struct sockaddr *)&remote_socket_addr, &addrlen);
        FD_SET(new_socket, &master_socket_set); // add it to the
master set
        if (new_socket > max_socket_id)
          max_socket_id = new_socket; // update the max
      } else { // there is no new connection on the listener
        // check the other sockets in a round-robin fashion
        for ( i = listening_socket+1; i <= max_socket_id; i++) {
         if (FD_ISSET(i, &tmp_socket_set)) // is there any data?
         {
           // read the data from the socket
         }
      }
   }
}

In the for loop that represents the round-robin processing of the sockets, we start from the lowest identifier and then proceed to the highest identifier that has been present in the system. It would also be worth keeping better track of the socket identifiers so that only possible ones are tested. In the preceding code, a lower-order socket, once closed, would still be tested by the for loop.

The NULL values in the select() call represent sets for write and exception sockets and a timeout value, none of which we need in this simple example. The FD_ macros used are provided by the socket library for dealing with sets of sockets and the select() function.

The ones used here are as follows:

  • FD_ZERO: Remove all sockets from the set.

  • FD_SET: Set the socket in the set.

  • FD_ISSET: Test to see if a socket is set in the set.

We also introduced the accept() function that allows the client to establish the socket prior to allowing the client to send its data. As you can see, it also provides for capturing the remote address, so that the connection can be terminated if checking is being done on this level for security reasons.

Finally, the one part that we have not put in the above is the code to read data from the socket that we now know (through the select() function) has data waiting. Now that we can set up connections, we should look at data-transfer functions.

Sending and Receiving Data

On the client side, we assume that the following tasks have already been carried out:

  • Resolve the host to an IP address.

  • Open a socket (on a specific port).

  • Wait for the acknowledgement that the socket is open.

At this point, the server is listening and ready to receive data from the client. The first burst of data ought to be from the client to identify itself to the server. After this, assuming that the credentials are correct, the game dialog can begin.

Data is sent down a socket with the send() function. This acts rather like file handling, and the function takes a socket identifier (descriptor) as its first parameter and then a pointer to the data, the length, and some flags. To use these flags, it is necessary to look up the definition in the platform’s online guide (man page, or documentation provided in a Windows help file). In the following examples, we shall ignore them, and concentrate on making sure that the data is correctly encapsulated.

The easiest example is sending character data. This is no more extravagant than the following:

char szData [1024];
strcpy(szData, ''Hello TCP/IP'');
send ( sock_id, szData, strlen(szData), 0);

The send() function also returns the number of bytes sent. Clearly, this can be less than the actual number that the application needs to send, due to packet-size limitations. It is up to the programmer to send the rest of the data at some point in the future; if the packet length is set to 128 bytes, and there are 270 bytes to be sent, then three attempts will need to be made before all the data is transmitted.

Receiving data is no more complex. The recv() function, which takes the same parameters as send() and returns the number of bytes received, is used. The value of 0 is reserved for cases where the other side has closed the connection. A negative value is used to indicate an error.

On the server side, the server can do nothing until the client or clients connect and identify themselves. At this point, it can send a first update, and wait for further instructions thereafter. The same send() and recv() functions are used to do so.

Any data can be sent, be it character based or binary. However, there is a small problem with sending data such as integers as integers. Those who regularly deal with binary files on heterogeneous platforms will attest to this; it is worth taking a small detour here to examine the issue.

We shall assume that you are writing a game that distributes a level file across Unix (Mips/Alpha based) and Windows (Intel based) platforms. Furthermore, let us assume that these level files are created on the Windows platform. To identify blocks of data (records), you decided to use a length indicator of two bytes (classic integer), which will tell the reading application how many bytes are in the block, or record. This is sometimes known as the “record-length indicator.”

The following is a fairly simple C rendition of a possible function to write the data to a file, minus the error checking for clarity:

void WriteRecord( char * szRecordData, FILE * hFile )
{
  int nLength = strlen(szRecordData);
  fwrite ( &nLength, sizeof(int), 1, hFile); // write the length
      indicator
  fwrite ( szRecordData, sizeof(char), nLength, hFile); // write
      the data
}

Now, if you were to run the code on Intel and Motorola architectures, the resulting file would look very different. This is because the byte ordering is different on these two architectures. We saw this briefly above when dealing with IP addresses and port numbers: The network and host architectures may be different.

This means that, in binary, 128 bytes (for example) is represented as the following:

00 80

Intel architecture

80 00

Motorola architecture

So, if you write out the value 00 80 in hexadecimal on the Intel platform and read it in blindly, using the fread() function, on the Motorola platform, then the result will be incorrect. In fact, it will be an integer with the value 32,768, because Motorola platforms expect the least significant byte to arrive first.

Typically, we have a lot of control over level files and where they end up. This might not be the case with network architectures because of the fact that we are likely to try to use a very powerful server, possibly running a different byte-ordering architecture, and we might not know the architectures of all the clients.

Using a similar approach as above, the following might be possible, to stream level data to a client:

// THE FOLLOWING IS A BAD IDEA
void SendRecord( char * szRecordData, int nSocket_ID )
{
  int nLength = strlen(szRecordData);
  send ( nSocket_ID, &nLength, sizeof(int), 0 ); // send the
      data length
  send ( nSocket_ID, szRecordData, nLength, 0); // send the data
}

This code will only ever work when both systems use the same byte ordering. Luckily, there are many solutions to work around the problem.

You should remember that whenever data is sent as binary information, such as int values, this problem will occur. It is much better, if marginally less efficient, to send information as single bytes and do the byte ordering in code, either using the ntohs or ntohl (and htons/htonl) functions or through a custom solution.

To send the number 128 using a custom solution, having chosen to use a two-byte scheme, the application would send 00 and then 80 (hexadecimal representation), explicitly in that order. To send a value of 256, the application would then send 01 followed by 00; the recipient would know that it should count the 255 in the least significant byte, add it to the 01, then add the 00 of the least significant byte—the value 00 FF, of course, being equal to 255. In this way, it is possible to replace any byte ordering that is implicit by platform with an explicit one.

There is another option: to tell the receiving system what byte ordering is in use by sending a character 1 or 0. The end result is the same. The receiving system still needs to compensate for an eventual mismatch, so it is probably better just to send explicitly byte by byte (or at least pack the data into a data string, and send the whole lot at once) or use the built-in byte-ordering functions.

Datagram Socket Send/Receive

All of the above relates to stream sockets, which are permanently connected. Datagram sockets work in a slightly different way, and have some functions that specifically support their unconnected nature.

You will remember that datagram sockets (AF_DGRAM) are not connected in the same way as the sockets we’ve been looking at so far. Each packet of data that we want to send has to have an address specified as part of the stanza, and each packet is sent independently of the others.

The way to do this is just to create a local socket in the usual manner, and then populate another struct sockaddr to contain the destination address, which is passed to a variation of the send() function, called sendto(), as follows:

sendto ( nSocket_ID, szRecordData, nLength, 0,
       &sock_addr_to, sizeof(*sock_addr_to));

The last parameter is just the length of the sockaddr structure. Putting it in this way makes the code a bit more portable, as we explicitly measure the structure in the function call. The sendto() returns an integer that indicates the result, just like the send() function.

The companion function to sendto() is recvfrom(), which is the exact opposite and takes the same parameters. It is used in the opposite direction, and is a blocking call.

If all of this seems a bit tricky, remember that you can still use a datagram socket in a connected way by specifying connect() and using send() and recv() as normal, and specifying _DGRAM instead of _STREAM in the various address and socket-setup structures. However, this will make the socket a connected datagram socket. Any advantages that were being exploited by using a disconnected socket are therefore lost.

Closing the Socket

Once the initial data exchange has been completed, the application can use the shutdown() function to specify that traffic in one direction is no longer allowed. This can be useful in some circumstances, and the following possibilities are offered:

shutdown ( nSocket_ID, 0 ); // receives no longer allowed
shutdown ( nSocket_ID, 1 ); // sends no longer allowed
shutdown ( nSocket_ID, 0 ); // sends/receives no longer allowed

The last option should be used only if no traffic may be exchanged at all, but the system still needs to reserve the connection. Once the system knows that it no longer needs the nSocket_ID, it can call the close() function (or, on Windows, using WinSock, closesocket()). The close() function takes the socket descriptor, as in:

close ( nSocket_ID );

Now that we have examined all the worker functions, it is time to look at how these get put together in some examples of how the various snippets can be integrated.

Client Example

The first example is a function to open a socket to a named host on a specific port. We assume that the server is a real name (like www.mygameserver.com), and therefore need to use gethostbyname() to look up the IP address.

This step can be omitted if the game infrastructure uses only a static IP address. This aside, the code is as follows:

// returns a socket descriptor, or an error code
int OpenSocket ( char * szHostName, int nPort )
{
  struct hostent *host_entry; // host entry for the lookup
  struct sockaddr_in remote_socket_addr;
  int nSocket_ID; // we'll return this, hopefully
  // look up the IP address
  host_entry = gethostbyname( szHostName );
  if ( host_entry == NULL ) return -1; // doesn't exist?
  // open a socket
  nSocket_ID = socket ( PF_INET, SOCK_STREAM, 0 );
  if ( nSocket_ID == -1 ) return -2; // can't get a local socket
  // create the remote address structure
  remote_socket_addr.sin_family = AF_INET;
  remote_socket_addr.sin_port = nPort;
  // get the IP address that we looked up
  remote_socket_addr.sin_addr = *((struct in_addr *)host_entry-
      >h_addr);
  // zero out the padding
  memset(remote_socket_addr.sin_zero, '',
         sizeof(remote_socket_addr.sin_zero));
  // connect the socket to the address
  if (connect ( nSocket_ID, (struct sockaddr
      *)&remote_socket_addr,
      sizeof(remote_socket_addr)) == -1)
  {
    // we don't need the socket anymore
    close ( nSocket_ID );
    return -3; // connect failed
  }
// finally, return the socket descriptor
return nSocket_ID;
}

You will also note that we have put in some very basic error checking to make sure that everything is correctly assigned and that all the various pieces are in place before returning the socket descriptor.

Once the socket is open, we might want to send a status request and then wait for updated information. It is assumed that the game is going to unfold using a request-response model. You should be aware that there are many other possibilities, however—for example, the server might want to pre-empt by broadcasting information. In addition, some level of probing might still be needed to ensure that all clients are still connected. In our model, however, we assume that this is dealt with elsewhere.

In addition, we are going to use a simple seeded random-number generator to try to add a little security. This is not very robust, and can be cracked, but it shows the principle of encapsulating sensitive data in an encoding scheme. The complete function is as follows:

int SendRequest ( int nRequest, int nSocket_ID )
{
  int nSeed;
  // pick a seed at random...
  srand(time(null));
  nSeed = (int) rand();
  // use the seed to encode the request
  int nEncodedRequest = nRequest;
  srand(nSeed);
  nEncodedRequest += rand() % 1000; // keep them numbers low
  // build the data string
  char szData [1024];
  sprintf( szData, ''%d|%d'', nSeed, nEncodedRequest );
  // finally, send...
  int nBytesLeft = strlen (nEncodedRequest);
  int nSentSoFar = 0;
  do {
    int nSent = send ( nSocket_ID, szData+nSentSoFar, nBytesLeft, 0 );
    if ( nSent == -1 ) return -1; // something went wrong!
    nBytesLeft -= nSent;
  } while ( nBytesLeft > 0 );
}

The kicker here is that the seed is sent as plain text, along with the encoded request, which makes it easy to crack if the encoding mechanism is known. This makes it much less secure than other possibilities mentioned in Chapter 8.

Also, you will notice that we’ve put in a little bit of code to make sure that all the data gets sent. A real implementation would probably put this in its own SendAll() function so that it could be reused across other data-sending functions.

Now that we can open a socket to an address and send a request, the next thing we need to be able to do is receive the response, decode it, and return the result. We assume that this result is an integer in this case:

int ReceiveResponse ( int nSocket_ID )
{
  char szData[1024];
  int nReceived = recv ( nSocket_ID, szData, 1023, 0 );
  if ( nReceived == 0 ) return 0; // nothing to report
  // get the seed, using strtok, and then the data
  int nSeed, nEncodedResponse;
  char * tok = strtok( szData, ''|'' );
  nSeed = atoi ( tok ); // we need error checking here!!
  tok = strtok( NULL, ''|'' );
  nEncodedResponse = atoi ( tok ); // we need error checking here!!
  // decode
  srand( nSeed );
  return nEncodedResponse % (rand() % 1000);
}

For the sake of simplicity, this code merely grabs a single blob of data. A real-world application will probably need to encapsulate that data such that there is a start and end tag that lets us know when all the data is received. Real-world data exchanges usually extend over several blobs of game-related data.

Server Example

In a sense, the server is just the partner to the client, which has to be able to read the seed, determine the request, and provide the response. For these basic tasks, the preceding code can be re-used, but there are also some additional things that the server needs to be able to do—mainly because it is talking to multiple clients, whereas each client typically only communicates with a single server.

So, all of the preceding code works for the basic communication, but what about a solution for the multiple-client problem? We have seen one, using the select() function that lets us know when there is data pending that needs to be dealt with, but there are many variations on the basic theme, which we shall now put into code.

Selecting Sources

Sometimes it is necessary for the server to be able to “listen” selectively; this may be because it is more efficient to concentrate scheduling on busy connections as per the augmented round robin discussed previously, or maybe for other reasons.

The select() function only tells us, based on a set of sockets, which ones have data waiting to be read. It doesn’t tell us which have recently been busy. For example, if a client is doing nothing but monitoring the server, then this is probably going to be dealt with less often than some of the connections to clients involved in (for example) a battle or some other interaction. However, the call to select() reveals that there are multiple connections, and all have data waiting—so what should the server do? There are several solutions to this conundrum.

Randomly Process Sockets

The first is the easiest, and could be seen as the fairest way to try to give as much attention to the sockets as possible. It is also a simple illustration of how to selectively listen to specific sockets upon demand. The approach just requires that we select one of the sockets at random, test to see if it has data, and if it does, process that data:

while (1) {
  select ( nMaxSocket+1, &socket_set, NULL, NULL, NULL) // test
      all sockets
  // omitted for brevity :
  //  CHECK THE LISTENER FIRST FOR NEW CONNECTIONS
  // pick one
  int nSocket = rand() % nMaxSocket;
  if (FD_ISSET(nSocket, &socket_set))
  {
    // process the data on nSocket
  }
}

We have left out the part of the code where the listener is interrogated for new connections. There are some other issues with this code, too, but it is a possible way to proceed. If we know that all connections have equal weight, then it is a fair distribution of processor time. Sometimes, however, we want to do it a bit differently.

Keep an Activity Log

An activity log can be used to specifically weight the application’s attention to sockets that have a high activity level. This activity could be measured on a volume or quality basis (some actions have a higher activity quotient than others, regardless of the number of requests made). The only issue is that it also requires that we continually sort the list to be accurate—using, for example the C qsort() function. If this is an acceptable overhead, then the code is really quite simple.

First, we need to put the socket identifiers in an array allocated using the malloc() function. Each entry needs a place for the descriptor and a value that indicates how active it has been relative to the system behavior. This might look something like the following:

struct ACTIVITY_ENTRY
{
  int nSocket_ID;
  int nActivityLevel;
};

Following this, we need a function to compare two entries so that they can be sorted using the C qsort() function. This code might be akin to the following:

int CompareActivity ( struct ACTIVITY_ENTRY sA, struct
      ACTIVITY_ENTRY sB )
{
  if ( sA.nActivityLevel < sB.nActivityLevel ) return -1;
  if ( sA.nActivityLevel > sB.nActivityLevel ) return 1;
  return 0; // they must be equal, in this case
}

With the comparison function and data structure in hand, we have the ability to manipulate and sort a list of socket descriptors.

To prevent processor hogging, we also probably need a system to reduce the activity counter as well as increase it. Otherwise, a very busy process (client) will force the system into a loop, only ever dealing with one or two clients roundly ignoring and the rest.

One approach would be to give more attention to high– transaction volume clients by visiting them once for every cycle. So, if there are 100 sockets open, the busiest one gets visited 99 times for every cycle, whereas the middle-ranking ones get visited 49 times, and the end one gets visited just once.

Of course, this can be adjusted according to data volumes and speed of play, but the principle remains the same: We attach more weight and give more attention to those sockets producing most of the interaction. If they have no data left to process, of course, their place in the queue is relinquished, and their activity count reduced accordingly. These principles, when implemented, ought to provide a reasonably well distributed attention model.

Sequentially Process Sockets

The opposite end of the spectrum is to use the list to process sockets sequentially, from top to bottom, having either sorted them at random (for some attempt at attention fairness) or chosen some other criteria.

There is not much else to say about this approach; it is really just mentioned here for completeness sake. The underlying algorithm is just like the FD_ISSET one above, except that the application will need to perform a select() to update the socket set and then loop through the socket pool (array), checking FD_ISSET to see if there’s any work to do, in a sequential fashion.

One important thing to note with using the array is that the programmer is also responsible for making sure that any sockets that are closed are removed from the array, as well as from the master socket set.

The other side to this is that any new sockets are added (by checking the listener socket, usually socket 0 in the set) both to the master set and the array. The game developer has to decide whether the overhead and additional complexity is balanced by the benefits that it brings.

Remember that each layer of network complexity that is added takes away from some other aspect of the system. Sometimes, the additional overhead is just not worth the extra effort.

Use Timeouts and Interrupts

Finally, any of the above can be combined with a system of timeouts or interrupts. A “timeout” occurs when something that has been happening for a while seems to be taking too long, and the system decides to do something else. The option then remains to see if the process can be completed at a later date. On the other hand, an “interrupt” occurs when something happens that has to be dealt with immediately and cannot wait for the next time slice to be allocated. Balancing time in this way is a key function of any hard real-time system (such as aircraft-control systems or a video game), and this complexity is increased due to the multi-user nature of the gaming environment.

There are many potential ways to implement timeouts, but all of them rely on being able to do two things:

  • Time the current process that has the system’s attention.

  • Use criteria to gauge whether to continue processing once the preallocated time slice has come to and end.

This technique of time slicing is also very common in real-time systems, and allows a process to have a certain amount of time allocated for processing. As we have already mentioned, there could be fixed time slices or there could be variable ones used by the system.

For example, we might decide to combine the time slice with the socket processing described above such that more active sockets get bigger time slices in which their status is checked several times. If they have no data, their time slice could be ended prematurely, and control returned to the system. This could provide a better solution than simply increasing the frequency by which their sockets are checked.

An interrupt-based system requires more complex programming because every piece of data is initially given the same weight. Subsequently, each socket gets processed by some kind of scheduling daemon in a round-robin fashion until something happens that interrupts the current task. Polling is then used to determine the order by which the sockets will be processed, and might necessitate a system whereby each incoming request for attention requires a response from the server to tell the client that it can go ahead.

Potentially, this adds yet another piece of data to the network exchange. Again, this could have a negative impact on performance. However, it would mean that the most urgent of the requests can be dealt with first, so even a low-activity client can get attention fast if something happens to it that requires immediate attention.

Finally, you should not forget that the server system is in complete control, and has the possibility to equate player data with their network connections. The two pieces of information can be used together to make sure that the socket reading and system updates get done in a way that is best for everyone.

Buffers

One other aspect of server-side programming is that effective network communication will often require the use of buffers. The assumption is that the bottleneck is not the processor, but the network itself. A “buffer” is just a place where information that can be gathered together quickly is stored before it is processed by a more lengthy operation—i.e., a network write (send). Compared to processing speeds and data-transfer rates around a computer system, a network is slow, and buffering is supposed to make sure that it is never the weak link.

To try to help smooth out the latency effects of the network, predictive buffers can also be used to queue up data in advance of receiving confirmation of the prediction. This is helpful, for example, when processing movement commands for an in-game entity that is moving in a direction that is unlikely to diverge. Using prediction and buffers together in this way can help make communication more efficient.

Sessions

Finally, sessions are a very important part of network-game programming for Web games and other varieties of game models. They are the responsibility of the server, which is why they are mentioned here.

Sessions are required to help prevent hacking and cheating, and to protect the player’s own account. They might not offer complete protection against all possible attacks, but they do go some way to making attacks more difficult.

The easiest way to demonstrate an unsecure way to test sessions using a Web environment is as follows:

  • Have a database that contains the player’s username and password, as well as a session identifier, which is set to NULL in case the player is not logged in.

  • Upon login, a session identifier is generated and passed back to the Web client as a hidden area in a form (login via username/password by query, not by comparing in script).

  • The same form also contains the player’s own ID, and is used whenever a request is made of the system.

  • When the player logs out, the session ID is cancelled (set to NULL).

If the player then tries to perform an action, the session identifier is passed to the system, which validates it against the database. If it fails (as it will if the player is not logged in), then the player is prompted to log in again.

This is the basic session-handling model, and there are many variations. The key points to remember are that the database should never be directly queried for the password, and that these passwords should, wherever possible, be encrypted. Additional protection can then be provided by using a timer to time out sessions, and generating different session identifiers per request/response pair. The underlying mechanism remains the same, however.

Open-Source Code Libraries

The last section of this chapter is designed to encourage you to consider using open-source solutions in your projects. There is simply no need to re-invent the wheel for major platforms where open source is prevalent, and there are many benefits of using the programming skills of others to enhance one’s own creations.

Even the Torque engine (not quite open source, but still a very cheap option) supports cutting-edge consoles and takes much of the pain out of network programming. Of course, the game still needs to be correctly designed and implemented, but these frameworks make it much easier.

In addition, commercial frameworks like XNA (from Microsoft) make life even easier, as they encapsulate much of the functionality required and hide the vagaries of the underlying platform from the developer. They do, however, limit the platform to those supported by the framework technology.

Of course, for PCs running Windows and Linux, it is usually possible to get open-source code to run all the parts of the network-game model, including

  • Client TCP/IP libraries

  • Server TCP/IP libraries

  • Full server implementations

So where does one start?

MUD Libraries

Even if you have no interest in MUD development per se, open-source MUD code is worth examining as it touches on all the basic principles of a good network-game design:

  • Messaging system: inter-player communication

  • User management: username/password and access rights

  • Persistent universe: with objects and locations

  • Location-driven architecture: with ownership and locations

The point of this list is that even if a MUD is technically text only, if you exchange “data” for “text,” quite a robust MMORPG can be built on these very solid foundations. They are well understood, and have been refined over time to take advantage of many different extensions to platform technology.

What’s more, you can add data to the text and come up with a hybrid that can be played over the Internet, over the Web, or using some kind of proprietary client without changing the core on the server. The result, if designed correctly, will still allow most, if not all, of the features intended by the initial design.

With this in mind, let’s look at each one in more detail.

Messaging System

A messaging system is core to many online games. For example, at the core of a MUD is usually a very robust chat-style communication layer capable of

  • Receiving incoming messages

  • Posting messages privately

  • Posting messages to everyone

  • Etc.

This is exactly what is needed if a game is being created, and the heart of the messaging system could be reused in other communication environments to create different kinds of games. The key is how the result of each request is displayed on the screen.

Under the hood, it is just text flying around, and the representation is what will differentiate a MUD from an MMORPG, as well as things like hit points, possessions, etc. that are key to role-playing games in general.

There are even MUDs (MUCKs, MUSHes and MOOs) that can offer a graphical front-end and that interpret the data that is exchanged in the messaging system in a certain way. Those of you who are familiar with MUDs will also remember that one had to make a choice before using the system between teletype (vt100) terminals and ANSI terminals. This was because even in the early- to mid-1990s, the Internet was robust enough to allow MUD clients to display colored text or plain black and white. Moving from this to graphics is just a small step in terms of the model used to transport the data.

As long as the communications layer and messaging system is robust, it can be reused (the techniques, at least, if not the source code itself). It is worth pointing those of you who want to copy code chunks from open-source projects to the upcoming section on licensing.

User Management

Each user exists in his or her own piece of the game environment—meaning that there has to be some user management beyond the aforementioned messaging system. This goes alongside the mechanisms for storing user data, as well as the session handling that allows for logging in and out. Of course, there is also the personalization of each user, and other aspects that are common to all kinds of online games. Again, open-source solutions can be very instructive in the implementation of these aspects of network game programming.

Persistent-Environment and Location-Driven Architecture

MUDs also have a game environment that contains pieces that are persistent. This means that they continue to exist even when there is nobody logged in, and that they are predictably in the place that the player left them unless someone else has moved them. Hand in hand with this, the messaging system, and user management is the fact that the MUD is built on a location-driven architecture. This is very useful in creating network games, which tend also to be built around a sense of location.

Action Games

Networking code is also part of some major games that have been released into open source (Quake, for example). Much of the issues that they had to deal with when the first networked version was released are still prevalent today.

The code might not be cutting edge in terms of graphics, but the network communications haven’t changed that much. The problems of latency, jitter, and lag still exist, and the solutions are largely the same. There might be some advances in intelligent prediction (Street Fighter IV, for example, uses combat prediction), but many of the solutions remain the same.

Much can be learned from these pieces of code that are more or less implementations of techniques that have grown up from action games. It is also fair to say that most MMORPGs, including ones like Second Life, are less graphically accomplished than games like Eve Online or Unreal Tournament. It is almost like they feel they don’t need it—the point is that an online game can be great fun without the high-end graphics.

However, in the same way that a single-player non-networked game with networking bolted on is not a network game, a great MUD with a terrible front-end will not win any fans beyond a hard-core set who believe in the environment even without the bells, whistles, and high-realism graphics.

Bearing these thoughts in mind, there are still some great lessons to be learned from using the source.

Level/Map Management

Action games, more than most, seem to be delivered as maps, or levels. You might compare this approach with a MUD, where the game environment is delivered as it is encountered, usually as text snippets. It is a question of the volume of data that needs to be exchanged in order to describe the game environment.

Clearly, given the required quality of action game environment rendering, it is not usually possible to stream everything because too much data would be required to render the screen. However, there may be a happy halfway house such as that used in other environments, based on the placement of predefined and downloadable objects. In fact, this could even be based on a system like Google Earth, where content is refined as the viewer zooms in. Anything that can help to reduce the in-flight data will make this more achievable, including downloadable asset packs.

At the end of the day, however, nothing beats a level file, even if it does have to be downloaded before the player can get into the game. Action games use them very well, and much can be learned from their approach.

One reason that level files are so big and complex is that there is usually a very competent 3D engine under the hood that can deal with very complex environments.

On the network side, these games show exactly how to maintain congruence between the map and the clients by exchanging data. Again, these are valuable lessons that the budding network game developer will need to learn to be successful.

Attention-Based Updates

In addition to the rendering aspect, action games need some way to maintain the data-transfer rate at times when many clients might be involved in some in-game action. In Chapter 7, “Improving Network Communications,” we saw some of the causes of lag and jitter, as well as how to eliminate them. Using attention-based updates is one way to try to get the optimum performance from the system. The key is that those items that have the attention of the player must be updated first, and that things that do not can be rendered later, differently, or not at all, depending on the quality of the network connections.

Advanced Rendering

Some aspects of the rendering system for network games can get quite advanced, especially since these games usually need to be more adaptive than a locally played version of the game. These include, for example, line-of-sight rendering based on downloaded information from the game server. This is important because these may actually not even include details beyond the local area. For security reasons, it is better only to deliver to the client the data that it needs to prevent possibly rendering things that ought to remain hidden. This will serve to reduce network traffic, but also will increase server-side processing, which must be borne in mind when deciding how rendering details will be exchanged with clients.

Split Network Models

Finally, many action and strategy games also use a network model that splits the responsibility between clients and the server. This results in farming out work, which can also help increase overall system efficiency.

There is a risk, of course, associated with this. Part of the problem is that if the decision making is farmed out to the client, and a hacker is sitting on the other side of the man-machine interface, then he or she may choose to interfere with the local decision-making process. The result is passing data back to the server that is inherently wrong. Worse than this is the interception of data, so that it can be modified before it is returned to the server, thereby giving a false view of the game environment.

There are plenty of ways that this can be countered, but the question of efficiency still remains to be answered. After all the cross checking, encryption, error correction, and processing, the solution still has to be more efficient than processing everything on the server in order for it to be worthwhile. Sadly, we might not know this until it is too late: during implementation.

Web Gaming

As you are reading this, there are probably more open-source Web-gaming projects to consult than any other kind because the Web is a cheap and easy platform to develop for. The accessibility, as well as the low expense of setting up a virtual server, makes it a great starting point for many projects.

Under the hood, Web games are great because they rely on existing networking standards for communication, display, and update, and offer a split-responsibility scripting model. These key points make it a very attractive platform to develop for.

The split-scripting model means that the client and server can both be scripted to the point that the workload can be distributed between them. This can be true for game-logic as well as rendering and data-exchange scripting.

Ultimately, this can mean that there is a lot of logic in the client. As long as this does not affect gameplay, and is only used to render the effect of that gameplay to the players, then there will be no problem in adopting this model.

Perhaps the most attractive feature of Web-game programming is that the network communication is hidden behind the existing platform architecture of the World Wide Web. In addition, open-source solutions exist for practically every aspect, from user management to content delivery and secure session management. This leaves the programmer free to concentrate on implementing the best possible game, given the limitations imposed by the Web-page environment.

Final Thoughts

This chapter has dealt with three very important areas:

  • The theory of network communication

  • Implementing network communication

  • The practice of network communication

Hopefully, it has given you enough ammunition to create something special in the networked-game arena. There is certainly enough for any aspiring network-game developer to cut his or her teeth, even if he or she does not have the skills to make the next Burnout: Paradise or Halo 3.

However fast networks become, and however reliable connections appear, there are several areas of network programming that will always remain more or less the same:

  • There will be more content than bandwidth.

  • Client systems will be unpredictable.

  • Hackers, cheaters, and bots are a fact of life.

People who design games will be constantly surprised when they are told that the idea they have will cause unnecessary data exchange that cannot be catered to with current networking technology. By the same token, they will be surprised at the amount of time that programmers claim the system needs to spend error checking and correcting. That is processing time that could be used for other things, and they’re right to be a little put out, but it is vital to making sure that every player gets a consistent experience. One thing that everyone should be able to agree on, though, is the necessity to try to identify and mitigate the effects of hackers, cheaters, and those who seek to use bots to further their in-game career.

Some of this awareness can tie in with the algorithms used to counter the unpredictability of client systems. The key here is that the algorithms we use for tracking in-game activity in the hope that we can optimize the scheduling give us a hook into the data that is being exchanged. If we can spy on that data, we can analyze it and try to ensure that nothing under-handed is going on.

A simple example might be that a developer decides to use an optimized algorithm for monitoring sockets for incoming data that takes account of their activity. He then analyzes, in real time, the activity levels and decides that, above a given threshold, he will start to take interest in sockets representing clients with an unusually high activity level. He can then single them out for further analysis, such as shot accuracy, and other giveaway signals that someone is using a proxy system to play the game (see Chapter 8 for a discussion of proxy systems). Hopefully, nine times out of 10, the game will catch and penalize the culprit. Although the end decision rests with the developer, it is worthwhile to strike a balance between overzealous rat-catching and letting people get on with it.

Finally, never underestimate the workload that programming the network layer requires. Yes, there is one chapter out of 10 in this book dedicated to programming, but that is not a reflection of the amount of resources a developer must dedicate to it.

Testing is important, too—not just more testing, but smarter testing, which will inevitably mean that the network layer has to be tested as it is developed. This means that, like the AI, which goes hand in hand with network programming and testing in a multiplayer environment, it can never be left until the last minute as a bolt-on for a single-player game. The two models are just too different. A multi-player game has to be so from the ground up.

References

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

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