Jump to content
  • Advertisement
  • 06/15/00 12:20 PM
    Sign in to follow this  

    WinSock2 for Games

    Networking and Multiplayer

    Myopic Rhino
    [size="5"]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...


    [size="5"]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 [font="Wingdings"]J[/font]. 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.


    [size="5"]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.


    [size="5"]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.

    [size="3"]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:

    Image30.gif


    Or in terms of more than two machines:

    Image31.gif


    [size="3"]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 donAEt 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:

    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:

    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 [font="Wingdings"]J[/font].


    [size="5"]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 [font="Courier New"][color="#000080"]#include[/color][/font] at the top of any source file that uses WinSock. Also, you should add [font="Courier New"][color="#000080"]ws2_32.lib[/color][/font] 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.

    [size="3"] Data types:

    [font="Courier New"][color="#000080"]sockaddr[/color][/font]
    [bquote]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![/bquote][font="Courier New"][color="#000080"]sockaddr_in[/color][/font]
    [bquote]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
    };
    [/bquote][font="Courier New"][color="#000080"] WSAData[/color][/font]
    [bquote]Description: WSAData is used when you load and initialize the [font="Courier New"][color="#000080"]ws2_32.dll[/color][/font] library. This data structure is filled in for you by the [font="Courier New"][color="#000080"]WSAStartup ()[/color][/font] function. Use this to determine if the computer running your program has the right WinSock version.[/bquote][font="Courier New"][color="#000080"]SOCKET[/color][/font]
    [bquote]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 [font="Courier New"][color="#000080"]unsigned int[/color][/font].[/bquote]

    [size="5"] The basics

    To load the [font="Courier New"][color="#000080"]ws2_32.dll[/color][/font] 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 [font="Courier New"][color="#000080"]int af[/color][/font]), 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 isnAEt 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 donAEt 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 [font="Courier New"][color="#000080"]const char *Host[/color][/font] into [font="Courier New"][color="#000080"]u_long addr[/color][/font], 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. [font="Courier New"][color="#000080"]shutdown(SOCKET s, int how)[/color][/font] locks a specific attribute of a socket. Here are the possible attributes (that are passed in the [font="Courier New"][color="#000080"]how[/color][/font] parameter:
    • [font="Courier New"][color="#000080"]SD_SEND[/color][/font] means that the socket cannot send anymore
    • [font="Courier New"][color="#000080"]SD_RECEIVE[/color][/font] means that the socket cannot receive anymore
    • [font="Courier New"][color="#000080"]SD_BOTH[/color][/font] means that the socket cannot send or receive
      [size="5"]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 [font="Courier New"][color="#000080"]while[/color][/font] 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.

      [size="3"]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.

      [size="3"]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 [font="Wingdings"]J[/font]). 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:
      [bquote][font="Courier New"][color="#000080"]FD_READ[/color][/font] - On receiving data
      [font="Courier New"][color="#000080"]FD_WRITE[/color][/font] - The socket is ready to send data
      [font="Courier New"][color="#000080"]FD_CONNECT[/color][/font] - Server has accepted the socket and we're ready to go
      [font="Courier New"][color="#000080"]FD_CLOSE[/color][/font] - The socket has closed[/bquote] 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.


      [size="5"]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:

      WinSock codeBerkeley equivalentErrorInterpretation
      WSAEINTREINTR10004As in standard C
      WSAEBADFEBADF10009As in standard C
      WSAEACCESEACCES10013As in standard C
      WSAEFAULTEFAULT10014As in standard C
      WSAEINVALEINVAL10022As in standard C
      WSAEMFILEEMFILE10024As in standard C
      WSAEWOULDBLOCKEWOULDBLOCK10035As in BSD
      WSAEINPROGRESSEINPROGRESS10036This error is returned if any WinSock function is called while a blocking function is in progress.
      WSAEALREADYEALREADY10037As in BSD
      WSAENOTSOCKENOTSOCK10038As in BSD
      WSAEDESTADDRREQEDESTADDRREQ10039As in BSD
      WSAEMSGSIZEEMSGSIZE10040As in BSD
      WSAEPROTOTYPEEPROTOTYPE10041As in BSD
      WSAENOPROTOOPTENOPROTOOPT10042As in BSD
      WSAEPROTONOSUPPORTEPROTONOSUPPORT10043As in BSD
      WSAESOCKTNOSUPPORTESOCKTNOSUPPORT10044As in BSD
      WSAEOPNOTSUPPEOPNOTSUPP10045As in BSD
      WSAEPFNOSUPPORTEPFNOSUPPORT10046As in BSD
      WSAEAFNOSUPPORTEAFNOSUPPORT10047As in BSD
      WSAEADDRINUSEEADDRINUSE10048As in BSD
      WSAEADDRNOTAVAILEADDRNOTAVAIL10049As in BSD
      WSAENETDOWNENETDOWN10050As in BSD. This error may be reported at any time if the WinSock implementation detects an underlying failure.
      WSAENETUNREACH

      ENETUNREACH

      10051As in BSD
      WSAENETRESETENETRESET10052As in BSD
      WSAECONNABORTEDECONNABORTED10053

      As in BSD

      WSAECONNRESETECONNRESET10054As in BSD
      WSAENOBUFSENOBUFS10055As in BSD
      WSAEISCONNEISCONN10056As in BSD
      WSAENOTCONNENOTCONN10057As in BSD
      WSAESHUTDOWNESHUTDOWN10058As in BSD
      WSAETOOMANYREFSETOOMANYREFS10059As in BSD
      WSAETIMEDOUTETIMEDOUT10060As in BSD
      WSAECONNREFUSEDECONNREFUSED10061As in BSD
      WSAELOOPELOOP10062As in BSD
      WSAENAMETOOLONGENAMETOOLONG10063As in BSD
      WSAEHOSTDOWNEHOSTDOWN10064As in BSD
      WSAEHOSTUNREACHEHOSTUNREACH10065As in BSD
        Missing 10066 thru 10071 
      WSASYSNOTREADY 10091Returned by WSAStartup() indicating that the network subsystem is unusable.
      WSAVERNOTSUPPORTED 10092Returned by WSAStartup() indicating that the WinSock DLL cannot support this app.
      WSANOTINITIALISED 10093Returned by any function except WSAStartup() indicating that a successful WSAStartup() has not yet been performed.
      WSAEDISCON 100101Returned 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_FOUNDHOST_NOT_FOUND11001As in BSD.
      WSATRY_AGAINTRY_AGAIN11002As in BSD
      WSANO_RECOVERYNO_RECOVERY11003As in BSD
      WSANO_DATANO_DATA11004As in BSD
      To find detailed descriptions of error codes, look at WSAPI22.DOC (MicrosoftAEs 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.


      [size="5"]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 [font="Courier New"][color="#000080"]WSAEWOULDBLOCK[/color][/font] (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 [font="Courier New"][color="#000080"]if (socketaction(s)==SOCKET_ERROR)[/color][/font] (here, [font="Courier New"][color="#000080"]socketaction()[/color][/font] doesn't mean anything. It is just an example). But [font="Courier New"][color="#000080"]WSAEWOULDBLOCK[/color][/font] is an error also. That means you will have to specially test (call [font="Courier New"][color="#000080"]WSAGetLastError()[/color][/font]) if [font="Courier New"][color="#000080"]WSAEWOULDBLOCK[/color][/font] 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 [font="Courier New"][color="#000080"]WSAEWOULDBLOCK[/color][/font] was returned. If it was returned then just [font="Courier New"][color="#000080"]Sleep (750)[/color][/font] and try again. WhatAEs so great about this? Well, you donAEt 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.


      [size="5"]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:
          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.

          [size="5"]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 [font="Wingdings"]J[/font]).

          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!


          [size="5"]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 BaldurAEs Gate. In the manual it says something like, "If you happen to be situated close to another playerAEs 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.


            [size="5"]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 donAEt know a single one since IAEm 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 [email="ceo@intertainment.8m.com"]ceo@intertainment.8m.com[/email], feel free to complain, suggest, or anything else.

            Copyright (C) 2000 Stefan Hajnoczi

            I take no responsibility whatsoever for this document

            Use it at your own risk

            This document is written IMHO


      Report Article
    Sign in to follow this  


    User Feedback


    There are no comments to display.



    Create an account or sign in to comment

    You need to be a member in order to leave a comment

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!