WinSock2 for Games
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…
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 J.
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.
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.
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:
Or in terms of more than two
machines:
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:
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:
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.
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
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.
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.
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’s all 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
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.
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.
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 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:
WinSock code |
Berkeley equivalent |
Error |
Interpretation |
WSAEINTR |
EINTR |
10004 |
As in
standard C |
WSAEBADF |
EBADF |
10009 |
As in
standard C |
WSAEACCES |
EACCES |
10013 |
As in
standard C |
WSAEFAULT |
EFAULT |
10014 |
As in
standard C |
WSAEINVAL |
EINVAL |
10022 |
As in
standard C |
WSAEMFILE |
EMFILE |
10024 |
As in
standard C |
WSAEWOULDBLOCK |
EWOULDBLOCK |
10035 |
As in
BSD |
WSAEINPROGRESS |
EINPROGRESS |
10036 |
This
error is returned if any WinSock function is called while a blocking function
is in progress. |
WSAEALREADY |
EALREADY |
10037 |
As in
BSD |
WSAENOTSOCK |
ENOTSOCK |
10038 |
As in
BSD |
WSAEDESTADDRREQ |
EDESTADDRREQ |
10039 |
As in
BSD |
WSAEMSGSIZE |
EMSGSIZE |
10040 |
As in
BSD |
WSAEPROTOTYPE |
EPROTOTYPE |
10041 |
As in
BSD |
WSAENOPROTOOPT |
ENOPROTOOPT |
10042 |
As in
BSD |
WSAEPROTONOSUPPORT |
EPROTONOSUPPORT |
10043 |
As in
BSD |
WSAESOCKTNOSUPPORT |
ESOCKTNOSUPPORT |
10044 |
As in
BSD |
WSAEOPNOTSUPP |
EOPNOTSUPP |
10045 |
As in
BSD |
WSAEPFNOSUPPORT |
EPFNOSUPPORT |
10046 |
As in
BSD |
WSAEAFNOSUPPORT |
EAFNOSUPPORT |
10047 |
As in
BSD |
WSAEADDRINUSE |
EADDRINUSE |
10048 |
As in
BSD |
WSAEADDRNOTAVAIL |
EADDRNOTAVAIL |
10049 |
As in
BSD |
WSAENETDOWN |
ENETDOWN |
10050 |
As in
BSD. This error may be reported at any time if the WinSock implementation
detects an underlying failure. |
WSAENETUNREACH |
ENETUNREACH |
10051 |
As in
BSD |
WSAENETRESET |
ENETRESET |
10052 |
As in
BSD |
WSAECONNABORTED |
ECONNABORTED |
10053 |
As in BSD |
WSAECONNRESET |
ECONNRESET |
10054 |
As in
BSD |
WSAENOBUFS |
ENOBUFS |
10055 |
As in
BSD |
WSAEISCONN |
EISCONN |
10056 |
As in
BSD |
WSAENOTCONN |
ENOTCONN |
10057 |
As in
BSD |
WSAESHUTDOWN |
ESHUTDOWN |
10058 |
As in
BSD |
WSAETOOMANYREFS |
ETOOMANYREFS |
10059 |
As in
BSD |
WSAETIMEDOUT |
ETIMEDOUT |
10060 |
As in
BSD |
WSAECONNREFUSED |
ECONNREFUSED |
10061 |
As in
BSD |
WSAELOOP |
ELOOP |
10062 |
As in
BSD |
WSAENAMETOOLONG |
ENAMETOOLONG |
10063 |
As in
BSD |
WSAEHOSTDOWN |
EHOSTDOWN |
10064 |
As in
BSD |
WSAEHOSTUNREACH |
EHOSTUNREACH |
10065 |
As in
BSD |
|
|
Missing
10066 thru 10071 |
|
WSASYSNOTREADY |
|
10091 |
Returned
by WSAStartup() indicating that the network subsystem is unusable. |
WSAVERNOTSUPPORTED |
|
10092 |
Returned
by WSAStartup() indicating that the WinSock DLL cannot support this
app. |
WSANOTINITIALISED |
|
10093 |
Returned
by any function except WSAStartup() indicating that a successful WSAStartup()
has not yet been performed. |
WSAEDISCON |
|
100101 |
Returned
by WSARecv(), WSARecvFrom() to indicate the remote party has
initiated a graceful shutdown sequence. |
|
|
Missing
10102 thru 10112 |
|
WSA_OPERATION_ABORTED |
|
* |
An
overlapped operation has been canceled due to the closure of the socket, or
the execution of the SIO_FLUSH command in WSAIoctl() |
WSAHOST_NOT_FOUND |
HOST_NOT_FOUND |
11001 |
As in
BSD. |
WSATRY_AGAIN |
TRY_AGAIN |
11002 |
As in
BSD |
WSANO_RECOVERY |
NO_RECOVERY |
11003 |
As in
BSD |
WSANO_DATA |
NO_DATA |
11004 |
As in
BSD |
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.
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.
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:
The client’s features are:
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:
1.
Create
the protocol with which client and server will communicate.
2.
Start
coding to the server up to where the connection is accepted.
3.
Start
coding the client up to the part where the server accepts the connection.
4.
Now
code your way, step-by-step, through sending and receiving data for both the
client and the server.
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!
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:
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.
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