Network Game Programming - Issue 04 - cServer? I barely know her! by Dan Royer (14 July 1999) | Return to The Archives

Introduction

Here we're going to go back and flesh out a lot of older material so if you were a little dissapointed by the brevity of last week's piece, have no fear, you'll get your fill. I really am sorry it was a rush job because it was late, I was working on < shameless plug >Extreme Rock Climbing for Creative Carnage, LLC. Not as much fun as writing for flipcode, but pretty damn close.< /shameless plug >

Setting up the listening socket

The listening socket is the socket that the server "listens" on for new clients. Each new client that connects will then be assigned a unique server-side socket to transmit data through. So we're going to need two things: a way to set up the listening socket and a way to keep track of all the client sockets.
The listening socket reuses almost all of our socket creation code, with a few important changes. First, remember from last article there was a method called WSAAsyncSelect() that told winsock which window should be sent socket-related messages and which messages it wanted to receive? We need to add a new type of message. FD_ACCEPT is a server-only message that is sent to a window when a client machine is trying to connect to the listening socket.

> - FD_CONNECT: An attempt is made to create a listening socket. In client models this would mean that an attempt was made to connect to the server.
> - FD_ACCEPT: A client is trying to connect to this machine.
> - FD_READ: The socket can be read from/there is data to be read from the socket.
> - FD_WRITE: The socket can be written to.
> - FD_CLOSE: The socket is being closed.

Now that winsock has been informed that we want the window to be notified we need to create the listening socket. In order to create the listening socket we'll need to make one more little change to AddressHost().

    // Instead of our client sin_addr stuff...
    d_address.sin_addr.s_addr = INADDR_ANY;

This will tell winsock that we're not trying to connect to anyone. Lastly, AttemptConnect() needs to have listening socket creation code.

    // Don't call connect!
    // I'll be the first to admit the error management
    // here isn't terribly detailed.  Appologies.
    if( bind( d_socket, (LPSOCKADDR)&d_address, sizeof( SOCKADDR ) ) == SOCKET_ERROR ) {
      closesocket( d_socket );
      return 1;
    }
    if( listen( d_socket, SOMAXCONN ) == SOCKET_ERROR ) {
      closesocket( d_socket );
      return 2;
    }

Although it's a little late to point it out, you can add an if/else conditional around the server/client differences. That way you'd only have to call GetHost() and AttemptConnect() for either client or server.
It's very probable that you'll want to connect automatically once AddressHost() is finished. If you stuck with C++ then override the OnGetHost() with a custom method that calls cComm::OnGetHost() and then calls cComm::AttemptConnect(). This works for both client and server.

FD_ACCEPT handling

Ok, your server is now set up and the server FD_CONNECT didn't report any errors. The next step is to start your client and connect to the server. Your server window will now receive FD_ACCEPT and all it have to do is accept.

    //SOCKET      newSocket;
    SOCKADDR_IN address;
    int         size;

    size = sizeof( SOCKADDR );
    newSocket = accept( d_socket, (LPSOCKADDR)&address, &size );
    if( newSocket == INVALID_SOCKET ) {
      // Uh oh...
      return 1;
    }

If you return from FD_ACCEPT without calling accept() or if there is a problem then the client will get a error in FD_CONNECT.
Once you accept a new connection, every socket message sent to the window will have wParam = whichever newSocket is associated with that client.

cClient class

Now you'll need a nice way to store all that client related information. Better still would be to make it a base class and then build our player class on top of it. No problem! My generic client class contains:

> - Unique Name
> - Unique ID code
> - IP address
> - What time they connected
> - When the last message arrived from them (for idle timeouts)
> - Game stage (login, in menu, downloading level, in game, etc...)
> - Message Queues, linked lists of data read in from a socket / data waiting to be written to a socket.

Preventing DoS/spamming attacks

The idea here is simple: some twelve year old punk logs in and after claiming he's the baddest thing on the continent proceeds to get schooled in ass-whuppin' and trash-talkin'. Fragile ego popped like a balloon, he takes a hacking tool someone posted on the internet and through his tears proceeds to try and teach a lesson to all those people who were trying to have a good time. Since there is no known cure for stupidity the only you can do is hope to foil his plans.
The most common method of crashing a computer game is a Denial of Service (DoS) attack. This is done by trying to log into a server so many times and so fast that the server can't handle them all without compromising the game quality. In extreme cases the server can be completely locked or even crash. It's called denial of service because the server is made so busy that it can't serve anyone else.
The only tactic I know of is to make your login go as fast as possible, but recently I was reading that John Carmack of Quake fame has found an intruiging solution: UDP out of bound data. UDP is another system for sending messages over the internet, the major difference between UDP and TCP/IP is that in UDP message can arrive in a different order than the one they were sent in, whereas TCP/IP always arrives in the same order it was sent. Out of bound (OOB) data is data that is sent on a "subfrequency" (ugh, to much star trek...). The beauty of OOB data is that it cannot cause a DoS attack on a server because once a certain ammount of data is waiting to be read no client can add to that queue, or at least this is my understanding. What it means is that you would send a request to be permitted to login through OOB data and after it is approved then you could connect. How do you send the UDP OOB request without having a socket open yet? Winsock 2.0 allows you to send connectionless data. I'd just like to note that I have not tried any of this UDP OOB connectionless stuff, it is merely conjecture based on my current understanding.

If you've made it this far you've now got a client and server up and running. You basic methodology would be something like this.

Client | Server
Init() | Init()
GetHost( remote IP ) | GetHost( local IP )
SM_GETHOST with IP info and/or error code | SM_GETHOST with IP info and/or error code
Create socket. | Create socket.
WSAAsyncSelect() to set up socket flags FD_CONNECT | FD_READ | FD_SEND | FD_CLOSE | WSAAsyncSelect() to set up socket flags FD_CONNECT | FD_READ | FD_SEND | FD_CLOSE | FD_ACCEPT
| AttemptConnect( port ) creates a listening socket.
| SM_ASYNC - FD_CONNECT notifies that AttemptConnect() is done and warns if there were any problems.
AttemptConnect( port ) creates a socket to the server. | SM_ASYNC - FD_ACCEPT notifies server that remote machine wishes to connect to this machine.
SM_ASYNC - FD_CONNECT notifies client wether or not it succeeded in connecting. |
[transmit data] | [transmit data]
shutdown, cleanup & quit | shutdown, cleanup & quit

At this point there's only one socket message left to tackle, FD_CLOSE. The best way to close a socket (for either client or server) is to call shutdown( [the socket], SD_SEND ), read in any remaining data then call shutdown( [the socket], SD_BOTH ) and finally closesocket( [the socket] ). This will cause an FD_CLOSE message in the other machine. the SD_SEND flag tells the remote machine that no more data will be sent to the remote machine on this socket. That way the remote machine can read in whatever data is left pending and then use closesocket( [the socket] ) to get rid of it completely. No matter who calls shutdown() the other machine will get FD_CLOSE and the same process should be followed.

Now we have to add in all the communications between the server and the client(s). If your head doesn't hurt yet then think about this: You can send any kind of data you want back and forth but unless the other machine has some way of identifying it then it's meaningless ones and zeros. The solution is ...in next weeks' segment. BWAH HA HA, Headaches for everyone! BWAH HA HA HA...

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.