Network Game Programming - Issue 02 - cComm one, cComm all by Dan Royer (23 June 1999) | Return to The Archives

Introduction

It is a fortunate thing that the server and clients share so many properties, it means we'll be able to share a lot of code between them and simplify our lives. To this end cComm will be the base class for both our client and server communications. Let's start by taking a look at what the client and server have to do in order to connect to the internet via TCP/IP. This model is known as a connection-oriented.

Client
- Initialize Winsock 2.0.
- Obtain an IP and port of a game on the internet. An IP could be an address (www.flipcode.com) or a number (24.112.84.101). A port is any number between 1 and 65535, inclusive.
- Create a socket.
- Resolve the address using gethostbyname() or gethostbyaddr()
- Try to connect() to the server.
- send() and recv() data on the open socket
- closesocket() and shutdown()
- Cleanup Winsock
Server
- Initialize Winsock 2.0.
- Obtain the port number the listen socket should be opened on. This is the socket clients will connect to.
- Obtain the IP name and number of the machine for the sysadmin's benefit.
- Create a socket.
- bind() the socket.
- listen() on the socket for client connections.
- accept() connections on the socket unless there's some reason not to (such as banned IP, hammering or max clients)
- send() and recv() data on the client socket.
- closesocket() for the client
- closesocket() the listen socket
- shutdown()
- Cleanup Winsock

A surprising number of similarities, wouldn't you say? Each needs code to...
...resolve an internet address into something Winsock 2.0 can use.
...create or destroy a given socket.
...send or receive data on a given socket.
Note that a listen socket isn't quite the same as a connection socket. More on that later.
Initializing and cleaning up Winsock is easy as pie. Just ask it what version you'd like (2.0, in our case) and then check to see that there weren't any major errors. The only thing to really pay attention to is that every call to WSAStartup is matched with a call to WSACleanup because the winsock system relies on wsock32.dll. If you've never worked width DLLs, here's a ten second explanation: DLLs contain reusable system code that only take up memory once in the machine. Every time you use a DLL you increment an internal counter. When you finish you decrement the counter and when that counter reaches zero the DLL is unloaded from memory. Simple, effective and very dangerous to available memory if you don't get it right.

    int cComm::StartWinsock() {
      // WSADATA               d_winsockData;
      // unsigned short        d_winsockVersion;
      // #define WINSOCK_MAJOR_VERSION         2
      // #define WINSOCK_MINOR_VERSION         0
      char buffer[ 128 ];
      int error;

      d_winsockVersion = MAKEWORD( WINSOCK_MAJOR_VERSION, WINSOCK_MINOR_VERSION );

      error = WSAStartup( d_winsockVersion, &d_winsockData );
      if( error == SOCKET_ERROR ) {
        if( error == WSAVERNOTSUPPORTED ) {
          sprintf( buffer,
            "WSAStartup error.\nRequested Winsock v%d.%d, found v%d.%d.",
            WINSOCK_MAJOR_VERSION, WINSOCK_MINOR_VERSION,
            LOBYTE( d_winsockData.wVersion ), HIBYTE( d_winsockData.wVersion ) );
          WSACleanup();
        } else sprintf( buffer, "WSAStartup error (%d)", WSAGetLastError() );
        // It's not a good idea to try MessageBox
        // if you have a DirectX window open...
        MessageBox( NULL, buffer, APPLICATION_NAME, MB_OK );

        return 1;
      }

      return 0;  // no problems!
    }

    int cComm::EndWinsock() {
      Disconnect();  // cComm method to cleanup any remaining connections
      WSACleanup();

      return 0;
    }

The next job is to resolve the IP address of the local machine, in nnn.nnn.nnn.nnn style. This is a little more complicated because winsock 2.0 calls the network subsystem and then returns control to the application. A short while later the window you have running will receive a message indicating that the address has been resolved.

    /* cComm::GetHost will return immediately and the window pointed to by
     * d_hwnd will get a SM_GETHOST message.
     */
    HANDLE cComm::GetHost( char *pAddr, int port ) {
      // SOCKADDR_IN           d_address;
      // HANDLE                d_hostHandle;
      // char                  d_hostentbuf[ MAXGETHOSTSTRUCT ];
      // HWND                  d_hwnd;

      d_address.sin_addr.s_addr = inet_addr( pAddr );
      d_address.sin_port = htons( port );
      d_address.sin_family = AF_INET;

      if( d_address.sin_addr.s_addr == INADDR_NONE ) {
        d_hostHandle = WSAAsyncGetHostByName( d_hwnd, SM_GETHOST,
          pAddr,
          d_hostentbuf, MAXGETHOSTSTRUCT );
      } else {
        d_hostHandle = WSAAsyncGetHostByAddr( d_hwnd, SM_GETHOST,
          (const char *)&d_address.sin_addr.s_addr, sizeof( IN_ADDR ), AF_INET,
          d_hostentbuf, MAXGETHOSTSTRUCT );
      }

      return d_hostHandle;
    }

    // When the window receives SM_GETHOST I call cComm::OnGetHost()
    int cComm::OnGetHost( WPARAM wparam, LPARAM lparam ) {
      // char                  d_IP[ 64 ];

      if( (HANDLE)wparam != d_hostHandle ) return 1;

      if( WSAGETASYNCERROR( lparam ) ) {  // upper 16 bits of (DWORD?)
        Disconnect();
        return 2;
      }

      // Winsock 2.0 says use h_addr_list[ 0 ] for
      // inet_ntoa() but it didn't work for me...
      LPHOSTENT lphostent;

      lphostent = (LPHOSTENT)d_hostentbuf;
      strcpy( d_IP, inet_ntoa( *(LPIN_ADDR)lphostent->h_addr ) );

      // Here is where we'd open a socket, etc...

      return 0;
    }

Once a valid IP has been obtained our next job is to create or destroy a given socket. Again, this is a fairly simple task. The only problem is that when you want to destroy the socket you have to make sure there's no data left to be read on the socket. Though I've never tested, I bet you'd get a memory leak if you didn't read it in. Note that the following code is only version one, we'll be adding more later.

    SOCKET cComm::CreateSocket() {
      // SOCKET               d_socket;

      if( d_socket ) return d_socket;  // already exists

      // Create a streaming socket.
      d_socket = socket( AF_INET, SOCK_STREAM, 0 );

      return d_socket;
    }

    int cComm::Disconnect() {
      // SOCKET               d_socket;

      // shutdown() warns the other machine no more data
      // Will be sent on this socket.
      shutdown( d_socket, SD_SEND );

      // Read in the remaining data pending on this socket

      // Shut down the socket.
      closesocket( d_socket );

      return 0;
    }

So let's review: We can now initialize Winsock, find the local IP and create a socket. Then when the application ends we destroy the socket and cleanup Winsock. Next week we'll take that open socket and connect to a remote machine.

Article Series:

> - Network Game Programming - Issue 01 - Things that make you go "hmm..."
> - Network Game Programming - Issue 02 - cComm one, cComm all
> - Network Game Programming - Issue 03 - cClient. cClient run
> - Network Game Programming - Issue 04 - cServer? I barely know her!
> - Network Game Programming - Issue 05 - Watch your Language
> - Network Game Programming - Issue 06 - Talk about things you like to do...
> - Network Game Programming - Issue 07 - I bent my Wookie...

Copyright 1999-2008 (C) FLIPCODE.COM and/or the original content author(s). All rights reserved.
Please read our Terms, Conditions, and Privacy information.