Jump to content

  • Log In with Google      Sign In   
  • Create Account

Interested in a FREE copy of HTML5 game maker Construct 2?

We'll be giving away three Personal Edition licences in next Tuesday's GDNet Direct email newsletter!

Sign up from the right-hand sidebar on our homepage and read Tuesday's newsletter for details!


We're also offering banner ads on our site from just $5! 1. Details HERE. 2. GDNet+ Subscriptions HERE. 3. Ad upload HERE.


Like
2Likes
Dislike
If you find this article contains errors or problems rendering it unreadable (missing images or files, mangled code, improper text formatting, etc) please contact the editor so corrections can be made. Thank you for helping us improve this resource

Introduction

I went on #gamedev (my nick is jadam) the other day and asked if anybody knew DirectPlay. I was expecting someone to just say "Yeah, go to www.directplay.com and get an online book!" Not a single person there knew it! I asked what they used for networking and everyone said WinSock. I was originally going to learn DirectPlay, but I guess there aren't enough resources at the moment.

I had used WinSock before, but that was in Visual BASIC. Back then; I was amazed at how simple it was. Well I thought that WinSock in C++ would be just as easy; big mistake...


What is WinSock?

WinSock is an API that will let you create and use sockets. Sockets are connections, usually through the Internet or a LAN. These connections are two-way, meaning that both sides can send and receive information. Each computer that is in the Internet or on a LAN has an IP address. An IP address consists of four bytes separated with periods ("."). An example of an IP address is "129.240.3.5". Notice that each of the four numbers that makes up an IP address is a byte, so it can range from 0 to 255. 256^4 = 4,294,967,296 that makes over 4 billion different addresses, which is enough for now J. So a socket can be created from the computer at address 37.143.125.23 to a computer at address 64.253.241.72. The problem is that if the computer wants to have more than one socket at a time, we will need a further address to identify our socket. This further address is called a port. We use ports all the time when we are in the Internet. For example, the standard port for HTTP (web surfing) is port 80. That means that if we type http://www.intertainment.8m.com into our browser, the browser will find out what IP address that server is and try connecting to port 80. Ports 1 - 1000 are pretty much reserved for standard protocols. Protocols are "languages" used to communicate with each other. For example HTTP is used for web page transfer, FTP is used for file transfer, and NNTP is used for newsgroups. Anyway, WinSock gives you all the power to create sockets, connect, disconnect, close, listen to a port. Listening is when a program just sits at a specific port and waits for some computer to connect. Programs that listen to ports are called daemons. For example a HTTP daemon will just listen at port 80 and then give you index.html or whatever site on connection. I think that was enough theory to learn some more theory.


TCP or UDP

What is the difference between TCP and UDP? Well, first of all, TCP and UDP are both protocols used to transport data. TCP uses the stream architecture while UDP uses the data gram architecture. Stream means that if we have a socket that is connected, data will be sent reliably. The data gram architecture is unreliable and data may be split up, lost, or even duplicated. Because of UDP's disadvantages, we will use TCP. There are still other protocols that can be used with WinSock 2, such as DecNet and a bunch of others.


Architecture

A very important thing to think about is what architecture to use when making a multiplayer game. The two (main) architectures are client - client and client - server.

Client - Client

The client-to-client (also called peer-to-peer) architecture is quite simple, yet should only be used in 2 player games. Two clients (player's computers) connect to each other using their opponent's IP address or domain/host name. One problem with this sort of connection is that exchanging IP addresses before you play gets on your nerves quickly. To play against someone, you will have to know his or her IP address. Some people trade IP addresses via email or chat (IRC, ICQ, AIM, etc). Another option is to a server that is especially set up to do nothing but help players swap IP addresses. This means that games are private and two-player. This may or may not be a good thing. The two machines will just exchange game states with each other and both will do full processing (wasting resources).

This is a diagram of the client - client architecture:

Attached Image: Image30.gif

Or in terms of more than two machines:

Attached Image: Image31.gif

Client - Server

This is the way to go. A fast computer with a fast internet/LAN connection starts a special server application. Everything goes through the server and the server may do processing for all the clients (which cuts down on processing if the game is only one screen big). The server may or may not process data, depending on the way the game was programmed. The cool thing about servers is that there can be (theoretically) unlimited players and the server can do the processing while all the clients just display what the server sent them. Another cool thing about servers is that they aren't usually started on some private computer, but on multiplayer servers, which have a domain name (www.mplayer.com, www.heat.net, etc.) so you donÆt have to type in the IP address, or exchange it. Also, client - server games will often have a lobby, where you can find other players, wait, or chat.

Here is a diagram of the client - server architecture:

Attached Image: Image32.gif

There are three sockets in the diagram. There is the client socket, which obviously is the socket belonging to the client computer. The server uses two sockets. In the beginning there is only one socket, the listen socket. The listen socket listens at a well-known port (a port that the client knows). When the listen socket accepts the connection, it creates a new "connection socket". This may seem strange, but it is perfectly logical because if the client used the listen socket to communicate and exchange data with the server, then no one else could connect during that time.

Here is a diagram of multiple computers playing a client - server game:

Attached Image: Image33.gif

The problem with the Internet or even LANs is that it is often too slow. That means that it is important that there is a balance between packet size and speed. Depending on the game, the server may do more or less. Some games also use hybrid architectures, where there is a server, but clients don't have to go through the server. My opinion is to keep it simple, fast, and efficient, and that means use client - server J.


Enough theory, on to WinSock

Okay, there are two versions of WinSock, version 1.1 and version 2. I suggest using version 2, since you don't have to use TCP/IP. Now it is time to define what we will accomplish in this article and what not. We will write a client - server rocks-paper-scissor-shoot game. The client will be a multi-threaded console application. The client will be a DirectX Win32 app.

I am not a WinSock master and have only learned it a short time ago. In fact, many things that I am writing about in this article, I am doing for the first time myself! Still, I believe that this is beneficial, since a WinSock pro may have forgotten what the most common mistakes are and what things are hard to understand in the beginning. First, I will show you how WinSock is organized, different methods of programming with it, and the prototypes of the API functions. Once you understand WinSock, we will program Rock, Paper, Scissors, Shoot using WinSock.

To begin with, you should say #include at the top of any source file that uses WinSock. Also, you should add ws2_32.lib to any project that uses WinSock. Now you're all set to program using WinSock, but how about learning how it works and what to do first?

There are different ways to program with WinSock. You can use the very basic UNIX/Berkley type functions, the Microsoft's Windows specialized version of the basic functions, or use the Object Orientated MFC version. I wanted to go for the OO version first, since classes usually wrap up an API and make it usable. But no such luck, remember these are MFC classes and that means: make it as complicated as possible. The MFC classes are just about the same as the very basic UNIX/Berkley functions and Windows extensions put together! Microsoft's Windows specialized functions are great, but infer that you will be making a Win32 Application by allowing you to hook your sockets to custom Windows Messages that will be sent to your program. In the case of making a server, this is not true. Why would we need our server to be a Win32 app? That's pointless. To keep it simple we will just use the very basic UNIX/Berkley functions for the server.

Data types:

sockaddr
Description: sockaddr is used to specify a socket connection. Use sockaddr_in whenever possible, since it is TCP orientated. This data type is used to store information about a socket (port number, IP address, etc.) that is accepted by a server. Only a server program uses this data type!
sockaddr_in
Description: sockaddr_in is used to specify a socket connection. It contains fields to specify IP address and port. This version of sockaddr is TCP orientated; use it as much as possible. This type will be used when creating sockets.

struct sockaddr_in
{
  short sin_family;     	// Protocol type (should be set to AF_INET)
  u_short sin_port;     	// Port number of socket
  struct in_addr sin_addr;  // IP address
  char sin_zero[8];     	// Unused
};

WSAData
Description: WSAData is used when you load and initialize the ws2_32.dll library. This data structure is filled in for you by the WSAStartup () function. Use this to determine if the computer running your program has the right WinSock version.
SOCKET
Description: SOCKET is a data type used to store socket handles. These handles are used to identify the socket. SOCKET actually is nothing more than an unsigned int.


The basics

To load the ws2_32.dll use this code:


// Must be done at the beginning of every WinSock program
WSADATA w;	// used to store information about WinSock version
int error = WSAStartup (0x0202, &w);   // Fill in w

if (error)
{ // there was an error
  return;
}
if (w.wVersion != 0x0202)
{ // wrong WinSock version!
  WSACleanup (); // unload ws2_32.dll
  return;
}

You may be wondering what 0x0202 means. It means version 2.2. If I wanted version 1.1, I'd change it to 0x0101. WSAStartup () fills in the WSADATA structure and loads the WinSock2 dynamic link library. WSACleanup() unloads the WinSock DLL.

To create a socket:

SOCKET s = socket (AF_INET, SOCK_STREAM, 0); // Create socket

That's all you need to create a socket, but you'll have to bind it to a port later when you want to actually use it. AF_INET is a constant defined somewhere in winsock2.h. If there is ever a function that requires you to tell it something about the address family (or int af), then just say AF_INET. SOCK_STREAM is a constant that tells Winsock that you want a stream (TCP/IP) socket. You can also have data gram (UDP) sockets, but they are unreliable. Leave the last parameter as 0, this will just select the correct protocol for you (which should be TCP/IP).

To actually assign a port to a socket (or bind a socket):

// Note that you should only bind server sockets, not client sockets
// SOCKET s is a valid socket
// WSAStartup has been called

sockaddr_in addr; // the address structure for a TCP socket

addr.sin_family = AF_INET;  	// Address family Internet
addr.sin_port = htons (5001);   // Assign port 5001 to this socket
addr.sin_addr.s_addr = htonl (INADDR_ANY);   // No destination
if (bind(s, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR)
{ // error
  WSACleanup ();  // unload WinSock
  return;     	// quit
}

This may look confusing, but it's not that bad. addr describes our socket by specifying the port. What about the IP address? We set that to INADDR_ANY, which allows it to be any IP address, since we don't really care about the IP address if we are just telling WinSock which port we want our side of the connection to be. Why do we use htons () and htonl ()? These will convert short and long, respectively, to the correct format for the network to understand. If we have the port number 7134 (which is a short), then we use htons (7134). We have to use htonl () on the IP address. But what if we want to actually specify the IP address? We don't use htonl (), we use inet_addr (). For example inet_addr ("129.42.12.241"). inet_addr parses the string and takes out the periods (".") and then converts it into a long.

To listen at the bound port:

// WSAStartup () has been called
// SOCKET s is valid
// s has been bound to a port using sockaddr_in sock
if (listen(s,5)==SOCKET_ERROR)
{ // error!  unable to listen
  WSACleanup ();
  return;
}

// listening…

Now we just have to accept a connection once some client tries to connect. The only peculiar thing about the above code is listen (SOCKET s, int backlog). What is this backlog? Backlog means the number of clients that can connect while the socket is being used. That means that these clients will have to wait until all clients before him have been dealt with. If you specify a backlog of 5 and seven people try to connect, then the last 2 will receive an error message and should try to connect again later. Usually a backlog between 2 and 10 is good, depending on how many users are expected on a server.

To try and connect to a socket:

// WSAStartup () has been called
// SOCKET s is valid
// s has been bound to a port using sockaddr_in sock
sockaddr_in target;

target.sin_family = AF_INET;       	// address family Internet
target.sin_port = htons (5001);    	// set server’s port number
target.sin_addr.s_addr = inet_addr ("52.123.72.251");  // set server’s IP

if (connect(s, target, sizeof(target)) == SOCKET_ERROR)
{ // an error connecting has occurred!
  WSACleanup ();
  return;
}

That'sall you have to do to request a connection! target obviously defines the socket that you are trying to connect to. The connect () function requires a valid socket (s), the description of the target socket (target), and the size or length of the description (sizeof(target)). This function will just send a connection request and then wait to be accepted or report any occurring errors.

Accepting a connection:

// WSAStartup () has been called
// SOCKET s is valid
// s has been bound to a port using sockaddr_in sock
// s is listening

#define MAX_CLIENTS 5;         	// just used for clearness

int number_of_clients = 0;
SOCKET client[MAX_CLIENTS];    	// socket handles to clients
sockaddr client_sock[MAX_CLIENTS]; // info on client sockets

while (number_of_clients < MAX_CLIENTS) // let MAX_CLIENTS connect
{
  client[number_of_clients] =  // accept a connection
  	accept (s, client_sock[number_of_clients], &addr_size); 
  if (client[number_of_clients] == INVALID_SOCKET)
  { // error accepting connection
	WSACleanup ();
	return;
  }
  else
  { // client connected successfully
	// start a thread that will communicate with client
	startThread (client[number_of_clients]);
	number_of_clients++;
  }
}

I hope you can follow that. MAX_CLIENTS isnÆt really necessary, but I just use it to make the code cleaner and simpler for demonstrative purposes. number_of_clients is a counter that keeps track of how many clients are connected. client[MAX_CLIENTS] is an array of SOCKETs which is used to save the handles of the sockets that are connected to the clients. client_sock[MAX_CLIENTS] is an array of sockaddr that is used to keep information about the type of connection, what port, etc. Usually, we donÆt want to mess with client_sock, but a bunch of functions will require it as a parameter. Basically this loop just waits until someone requests a connection, then it accepts it and starts a thread that communicates with the client.

Writing (or sending):

// SOCKET s is initialized
char buffer[11];  // buffer that is 11 characters big
sprintf (buffer, "Whatever…");

send (s, buffer, sizeof(buffer), 0);

Parameter two of send () is const char FAR *buf and it points to the buffer of chars that we wish to send. Parameter three is an int and it is the length (or size) of the buffer we are sending. The last parameter is for flags that we will never use; keep it 0.

Reading (or receiving):

// SOCKET s is initialized
char buffer[80]; // buffer that is 80 characters big

recv (s, buffer, sizeof(buffer), 0);

recv () is pretty much the same as send, except that this time, we are not transmitting a buffer, but receiving it.

Resolving an IP address or URL:

// const char *Host contains either a IP address or a domain name
u_long addr = inet_addr(Host);   // try and parse it if it is an IP address
if (addr == INADDR_NONE) {
  // Host isn't an IP address, try using DNS
  hostent* HE = gethostbyname(Host);
  if (HE == 0) {
	// error: Unable to parse!
	WSACleanup ();
	return;
  }
  addr = *((u_long*)HE->h_addr_list[0]);
}

Although it may be hard to understand at first, the code isn't actually that complicated. What the code does is try to parse const char *Host into u_long addr, which we can use to connect to another computer.

To close a socket:

shutdown (s, SD_SEND);  // s cannot send anymore

// you should check to see if any last data has arrived here

closesocket (s);   // close

I know it seems stupid that you must call two functions and then check if any more data has been received to close a socket, but that's life. shutdown(SOCKET s, int how) locks a specific attribute of a socket. Here are the possible attributes (that are passed in the how parameter:
  • SD_SEND means that the socket cannot send anymore
  • SD_RECEIVE means that the socket cannot receive anymore
  • SD_BOTH means that the socket cannot send or receive
Blocking, non-blocking, and asynchronous sockets

Here is where I have to stop for a bit and discuss some more theory. So far we have only been using blocking sockets. When we call accept (), the function just sits there waiting for a connection request to be made. That is what blocking means. The function will just sit there waiting for an event or an error to occur. This type of socket is the default type created with the socket () command.

The next type of socket is the non-blocking socket. With a non-blocking socket, functions such as accept () return immediately after being called, returning either an error, a good result, or nothing (meaning that the result will come in later). These sockets are computationally inefficient as you will find yourself writing tight while loops waiting for some even to finally happen. It is not good practice to use these sockets. Non-blocking sockets can be made using the select () command. I will not show you how to use them since there is a better way of making sockets that don't block.

Asynchronous sockets are Win32 specific. They are sockets that, like non-blocking sockets, return immediately. The difference is that you give the setup function a windows message that it should send when the desired event has occurred. This way you can say:

#define WM_ONSOCKET WM_USER+1

…in message handler…

  case WM_ONSOCKET:
  {
	if (WSAGETSELECTERROR(lparam))
	{
  	// error occurred
  	WSACleanup ();
  	return 0;
	}
	switch (WSAGETSELECTEVENT(lparam))
	{
  	case FD_READ: // data has been received
    	…
  	case FD_CONNECT: // connection has been accepted
    	…
	}
  } break;

This is great if you are using MFC or the Win32 API. You can do other things like draw graphics, receive user input, etc. while waiting for some socket event to occur.

What type of socket should I use now?

For server applications, use blocking sockets, as they are the most logical, simple, and practical when all you want to do is wait for a connection and then communicate with the client.

For client applications, use asynchronous sockets, as they are the most efficient when you want to do other things than just sit around and eat CPU cycles.

Using asynchronous sockets

Alright. You know how to make blocking sockets and now its time to learn the best client type socket.

This is the prototype of the function we're interested in:

int WSAAsyncSelect (SOCKET s, HWND hWnd, usigned int wMsg, long lEvent);

SOCKET s should be clear. hWnd is the window handle of your application (which is main_window_handle if you happen to be using André LaMothe's gaming console for DirectX as a framework J). wMsg is which message you want to be sent to your application if the desired event occurs. lEvent specifies the event(s) that will cause WinSock to send wMsg to your application. Here are some flags you can use:
FD_READ - On receiving data
FD_WRITE - The socket is ready to send data
FD_CONNECT - Server has accepted the socket and we're ready to go
FD_CLOSE - The socket has closed
Just OR (|) them together if you want more than one. Note that these are not all the flags you can use, but these are the only really useful ones for client applications.

When you add code to handle your new "socket" event in your application's message handler, you will need to know two macros. WSAGETSELECTERROR () is used to find out if there happened to be an error. WSAGETSELECTEVENT () is used to find what event has triggered the message.

#define WM_ONSOCKET WM_USER+1

…SOCKET s is initialized.  We will set it to asynchronous mode…

WSAAsyncSelect (s, hWnd, WM_ONSOCKET, (FD_READ | FD_CONNECT | FD_CLOSE));

…in the event handler…

  case WM_ONSOCKET:
  {
	if (WSAGETSELECTERROR(lparam))
	{ // error
  	WSACleanup ();
  	return 0;
	}

	switch (WSAGETSELECTEVENT(lparam))
	{
  	case FD_READ:
    	…receive data…
  	case FD_CONNECT:
    	…start sending data…
  	case FD_CLOSE:
    	…quit program…
  	default:
    	…do nothing…
	}
  } break;

Get it? Notice how only the lparam parameter and not the wparam is used.


Error checking (yuck)

Error checking is very annoying, but it is necessary in a commercial game. Remember that commercial software should be fool-proof and that means a hell of a lot of error checking. How do I check for errors? Most WinSock functions return an int. As a general rule, it is good to say:

if (function(…) == SOCKET_ERROR)
{
  cout << "Error!\n";
  WSACleanup ();
  return;
}

Functions that create or accept sockets usually won't return SOCKET_ERROR, but will return INVALID_SOCKET.

That was basic error checking... Here is the hard part. After SOCKET_ERROR or INVALID_SOCKET, and application should call int WSAGetLastError ( void ). WSAGetLastError will return an error code.

WinSock error codes taken from WSAPI22.DOC:

<table border="1" cellpadding="5" cellspacing="0" width="85%"><tbody><tr bgcolor="#666699"><td width="26%"><font color="white"><b>WinSock code</b></font></td><td width="25%"><font color="white"><b>Berkeley equivalent</b></font></td><td width="8%"><font color="white"><b>Error</b></font></td><td width="41%"><font color="white"><b>Interpretation</b></font></td></tr><tr bgcolor="#DDDDDD"><td>WSAEINTR</td><td>EINTR</td><td>10004</td><td>As in standard C</td></tr><tr bgcolor="#DDDDDD"><td>WSAEBADF</td><td>EBADF</td><td>10009</td><td>As in standard C</td></tr><tr bgcolor="#DDDDDD"><td>WSAEACCES</td><td>EACCES</td><td>10013</td><td>As in standard C</td></tr><tr bgcolor="#DDDDDD"><td>WSAEFAULT</td><td>EFAULT</td><td>10014</td><td>As in standard C</td></tr><tr bgcolor="#DDDDDD"><td>WSAEINVAL</td><td>EINVAL</td><td>10022</td><td>As in standard C</td></tr><tr bgcolor="#DDDDDD"><td>WSAEMFILE</td><td>EMFILE</td><td>10024</td><td>As in standard C</td></tr><tr bgcolor="#DDDDDD"><td>WSAEWOULDBLOCK</td><td>EWOULDBLOCK</td><td>10035</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEINPROGRESS</td><td>EINPROGRESS</td><td>10036</td><td>This error is returned if any WinSock function is called while a blocking function is in progress.</td></tr><tr bgcolor="#DDDDDD"><td>WSAEALREADY</td><td>EALREADY</td><td>10037</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENOTSOCK</td><td>ENOTSOCK</td><td>10038</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEDESTADDRREQ</td><td>EDESTADDRREQ</td><td>10039</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEMSGSIZE</td><td>EMSGSIZE</td><td>10040</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEPROTOTYPE</td><td>EPROTOTYPE</td><td>10041</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENOPROTOOPT</td><td>ENOPROTOOPT</td><td>10042</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEPROTONOSUPPORT</td><td>EPROTONOSUPPORT</td><td>10043</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAESOCKTNOSUPPORT</td><td>ESOCKTNOSUPPORT</td><td>10044</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEOPNOTSUPP</td><td>EOPNOTSUPP</td><td>10045</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEPFNOSUPPORT</td><td>EPFNOSUPPORT</td><td>10046</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEAFNOSUPPORT</td><td>EAFNOSUPPORT</td><td>10047</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEADDRINUSE</td><td>EADDRINUSE</td><td>10048</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEADDRNOTAVAIL</td><td>EADDRNOTAVAIL</td><td>10049</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENETDOWN</td><td>ENETDOWN</td><td>10050</td><td>As in BSD. This error may be reported at any time if the WinSock implementation detects an underlying failure.</td></tr><tr bgcolor="#DDDDDD"><td>WSAENETUNREACH</td><td><p>ENETUNREACH</p></td><td>10051</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENETRESET</td><td>ENETRESET</td><td>10052</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAECONNABORTED</td><td>ECONNABORTED</td><td>10053</td><td><p>As in BSD</p></td></tr><tr bgcolor="#DDDDDD"><td>WSAECONNRESET</td><td>ECONNRESET</td><td>10054</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENOBUFS</td><td>ENOBUFS</td><td>10055</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEISCONN</td><td>EISCONN</td><td>10056</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENOTCONN</td><td>ENOTCONN</td><td>10057</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAESHUTDOWN</td><td>ESHUTDOWN</td><td>10058</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAETOOMANYREFS</td><td>ETOOMANYREFS</td><td>10059</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAETIMEDOUT</td><td>ETIMEDOUT</td><td>10060</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAECONNREFUSED</td><td>ECONNREFUSED</td><td>10061</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAELOOP</td><td>ELOOP</td><td>10062</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAENAMETOOLONG</td><td>ENAMETOOLONG</td><td>10063</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEHOSTDOWN</td><td>EHOSTDOWN</td><td>10064</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSAEHOSTUNREACH</td><td>EHOSTUNREACH</td><td>10065</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>&nbsp;</td><td>&nbsp;</td><td>Missing 10066 thru 10071</td><td>&nbsp;</td></tr><tr bgcolor="#DDDDDD"><td>WSASYSNOTREADY</td><td>&nbsp;</td><td>10091</td><td>Returned by <b>WSAStartup()</b> indicating that the network subsystem is unusable.</td></tr><tr bgcolor="#DDDDDD"><td>WSAVERNOTSUPPORTED</td><td>&nbsp;</td><td>10092</td><td>Returned by <b>WSAStartup()</b> indicating that the WinSock DLL cannot support this app.</td></tr><tr bgcolor="#DDDDDD"><td>WSANOTINITIALISED</td><td>&nbsp;</td><td>10093</td><td>Returned by any function except <b>WSAStartup()</b> indicating that a successful <b>WSAStartup()</b> has not yet been performed.</td></tr><tr bgcolor="#DDDDDD"><td>WSAEDISCON</td><td>&nbsp;</td><td>100101</td><td>Returned by <b>WSARecv()</b>, <b>WSARecvFrom()</b> to indicate the remote party has initiated a graceful shutdown sequence.</td></tr><tr bgcolor="#DDDDDD"><td>&nbsp;</td><td>&nbsp;</td><td>Missing 10102 thru 10112</td><td>&nbsp;</td></tr><tr bgcolor="#DDDDDD"><td>WSA_OPERATION_ABORTED</td><td>&nbsp;</td><td>*</td><td>An overlapped operation has been canceled due to the closure of the socket, or the execution of the SIO_FLUSH command in <b>WSAIoctl()</b></td></tr><tr bgcolor="#DDDDDD"><td>WSAHOST_NOT_FOUND</td><td>HOST_NOT_FOUND</td><td>11001</td><td>As in BSD.</td></tr><tr bgcolor="#DDDDDD"><td>WSATRY_AGAIN</td><td>TRY_AGAIN</td><td>11002</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSANO_RECOVERY</td><td>NO_RECOVERY</td><td>11003</td><td>As in BSD</td></tr><tr bgcolor="#DDDDDD"><td>WSANO_DATA</td><td>NO_DATA</td><td>11004</td><td>As in BSD</td></tr></tbody></table>To find detailed descriptions of error codes, look at WSAPI22.DOC (MicrosoftÆs WinSock 2.2 API reference). So our fool-proof version looks like this:

if (function(…) == SOCKET_ERROR)
{
  cout << "Error:\n";
  switch (WSAGetLastError ())
  {
	case …:
  	cout << "this type of error happened!\n";
  	break;
	case …
  }
  WSACleanup ();
  cout << "If this error happens again, sue Microsoft!\n";
  return;
}

I know it is a real pain in the neck to do all that error checking, but it is necessary.


The price you pay for using asynchronous sockets

There is one disadvantage when using asynchronous sockets. Say you decide to send something to the computer you are connected with, and one command after, you try to perform another operation on the socket, chances are that the socket will return an error. The error is WSAEWOULDBLOCK (error number 10035). The good news is that this error is not a fatal error, but just means, "Try again later". The bad news is that in theory, you should be testing for this error specifically and trying to perform your action until it actually works and this error is no longer returned. Now this is annoying, since you can test for errors by just saying if (socketaction(s)==SOCKET_ERROR) (here, socketaction() doesn't mean anything. It is just an example). But WSAEWOULDBLOCK is an error also. That means you will have to specially test (call WSAGetLastError()) if WSAEWOULDBLOCK occurs and just repeatedly try again.

I have found a cheap way around this which is not 100% guaranteed to work. When you perform your action and there was an error, test if WSAEWOULDBLOCK was returned. If it was returned then just Sleep (750) and try again. WhatÆs so great about this? Well, you donÆt have to loop, instead you just delay and try once again. Chances are, that after 750 milliseconds, the socket will be ready to perform your action.


Rock, Paper, Scissor, Shoot!

Here comes the cool part. We will program a multiplayer game using WinSock. The game will be client - server because I find it a more user friendly model than client - client. We will program two independent applications: RPSS and RPSSS. RPSS is Rock, Paper, Scissor, Shoot (the client). RPSSS is Rock, Paper, Scissor, Shoot Server (the server).

The server's features are:
  • Small, simple, console application
  • Lobby type architecture that waits for two clients to join
  • Blocking sockets
The client's features are:
  • Infinite game rounds
  • Graphics
  • Asynchronous sockets
A feature that both the client and the server share is that I have implemented a quit mechanism so if the server realizes that a client has quit, he will also stop the client that is still connected. This is good since it helps eliminate pointless errors when one client has suddenly quit.

We need to develop the protocol we will use to communicate between the two clients and the server.

We can just use this:

#define RPSS_NUMOFUSERS 0x03
#define RPSS_STARTGAME 0x04
#define RPSS_SCISSOR 0x05
#define RPSS_ROCK 0x06
#define RPSS_PAPER 0x07
#define RPSS_QUIT 0x08

So each buffer of data that we transmit is 2 bytes long. The first byte is one of the above commands and the second is a parameter. For example the first byte we transmit is RPSS_NUMOFUSERS and the second is 2. If you think about it, the only command that actually needs a parameter is RPSS_NUMOFUSERS, so if we took that away, we would just be transmitting one byte instead of two.

In which order should I program client - server games? I'm trying to figure that out right now!

My opinion is:
  • Create the protocol with which client and server will communicate.
  • Start coding to the server up to where the connection is accepted.
  • Start coding the client up to the part where the server accepts the connection.
  • Now code your way, step-by-step, through sending and receiving data for both the client and the server.
How to use the demo

The source code and the EXE files for both RPSS and RPSSS are included with this document. I suggest you start by looking at the source code for RPSSS and then RPSS.

Any computer can be the server. In theory, it should be the fastest computer that also has either a static IP address or a domain name. For testing purposes, however, it can be any computer. Start RPSSS.EXE and you will just see a DOS program that does nothing but wait. You may want to minimize it and do something else.

If you want to play, you will need to execute the client (RPSS.EXE). When you start, you can enter the IP address of the server that you wish to play on. To adjust the number, use the up or down arrow keys. To switch to another byte of the IP address, press the left or right arrow keys. I know this seems overly complicated, but when you actually start the program, you'll understand.

When you've entered the IP address, click on the connect button below it. Now all you have to do is wait for another player and then you can play. Please note that RPSS.EXE supports Alt-Tab, which means that you can switch between applications. Using Alt-Tab, you can run two clients and play against yourself (for testing purposes, of course J).

When you are actually playing, you will see the different choices of moves in front of you (rock, paper, or scissor). Select one move using the left or right arrow keys and press enter. Now you must wait for your opponent to chose. When both of you have chosen, the score will change and the next round will start. Press escape or Alt-F4 to quit anytime.

Please note that the demo took me about four days to write, so it is not bug-free or user friendly. It is just mean to demonstrate the proper use of WinSock in a multiplayer game. There are a few points where I could have improved it, but all-in-all it works. I can take no responsibility for the demo programs whatsoever!


A word on lag (or latency)

Everyone who has played a high-speed multiplayer game has experienced lag. Whether it was the sluggish frame-rate in GTA2 or the being killed without getting a chance to react in Tom Clancy's: Rainbow Six. Lag is really annoying, because it doesn't give all players the same chance. If you have a 56k modem and are playing against someone with a ASDL connection in Korea, and the server is in Korea also, you're bound to experience it. While your enemy moves lightening fast and is never where you shoot, you hardly move at all and by the time you realize you're being attacked, you're already dead.

This may be a reason to develop turn-based or puzzle games.

One way around lag is optimization. Here are a few quick tips:
  • Don't send information every frame.
  • Only send coordinates when you move or change direction.
  • Don't send the entire display, only the parts which have changed.
  • Don't allow more than 32 players at once.
  • Players should be able to see an opponent's connection speed (33.6, 56k, ISDN, cable, ASDL, T1, etc.) before choosing to play with the opponent.
Even if you consider all the above aspects, you may not be able to get the speed you want or need for the game to run smoothly. The next step to reducing lag is dead-reckoning algorithms. Although some algorithms use statistics, others linear equations, others cubic splines, etc., they all work in the same way. The algorithm will predict what the player's next action is and will use its prediction until a packet has arrived that describes the action that the player actually performed. A good source for articles on dead-reckoning is http://www.GameDev.net.

The last way around lag that I know of is approximation. This is basically dead-reckoning, but not quite. A game that uses this is BaldurÆs Gate. In the manual it says something like, "If you happen to be situated close to another playerÆs computer, you may notice that you screens appear a little differently.". This seems to be a good method for multiplayer RPGs. When you log on to a multiplayer game, the level is transferred to your machine. When you start playing, all that is ever sent to you are the important actions that a player or NPC has done. Each computer independently updates its map and some things which depend on a random number will appear differently. The advantage of this is that since we are only transferring the important data, we are cutting down on the load.


Further reading, last minute stuff, and more, more, more

http://www.winsock.com

http://www.sockets.com

There are a number of books about WinSock but I donÆt know a single one since IÆm a cheapskate, just go look it up at http://www.amazon.com.

Read WSAPI22.DOC which is linked to by http://www.sockets.com. You may also want to download the WinSock 2 SDK, which, again, is linked to by http://www.sockets.com. You should also download the WinSock FAQ from http://www.sockets.com.

My email address is ceo@intertainment.8m.com, feel free to complain, suggest, or anything else.

Copyright © 2000 Stefan Hajnoczi

I take no responsibility whatsoever for this document

Use it at your own risk

This document is written IMHO







Comments

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS