Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / game

Simple client-server network using C++ and Windows Winsock

4.79/5 (47 votes)
21 Apr 2013CPOL10 min read 360.4K   15.4K  
How to create a client-server network for multiplayer game programming using C++ and Windows Winsock

Introduction

I recently finished a video game design and implementation course at UC San Diego and was in charge of the networking side of the video game along with another teammate. I want to write a guide to illustrate how to set up a simple client-server network using C++, the Windows Winsock 2 library, and TCP. By the end of this guide, you will have simple packets sent and received between the server and the client.

Background

Microsoft provides very useful guides on how to go about creating a client and a server using their Winsock library but I just wanted to add clarification and guide those who are using it for game programming and explain how I used it. I connected the dots so that you don't have to Smile | <img src=

I highly recommend that you follow this tutorial using Microsoft Visual Studio. I am using version 2010 Ultimate and my project is a Visual C++ Win32 Console Application.

Client connecting to Server

The server and the client will each have their own sockets, which they will use to send and receive data through a TCP connection. First we will create a class which will wrap the send and receive functions of the Winsock 2 library into a more simpler form for better readability. We will use this class inside our server and client network classes to send and receive data. This class is not completely necessary but will make our code more simpler to understand in the future.

Lets create a class called "NetworkServices". In its header file ("NetworkService.h") include the following libraries:

C++
#pragma once
#include <winsock2.h>
#include <Windows.h>

The above libraries contain the functions required for the receive and send functions of the Winsock library.

This class will not need a constructor/deconstructor. Also, declare the following static wrapper functions which will be used by both the server and the client:

C++
class NetworkServices
{

public:

static int sendMessage(SOCKET curSocket, char * message, int messageSize);
static int receiveMessage(SOCKET curSocket, char * buffer, int bufSize);

};

Now lets write the definitions to our wrapper functions and use the Winsock library functions to send and receive data inside the class (NetworkServices.cpp):

**Note: I have #included "stdafx.h", a file that is automatically generated by Visual Studio, in my cpp files. You may not need this, in which case you may remove the line. However, users have reported errors when not including this file when working in VS.

C++
#include "stdafx.h" 
#include "NetworkServices.h"

int NetworkServices::sendMessage(SOCKET curSocket, char * message, int messageSize)
{
    return send(curSocket, message, messageSize, 0);
}

sendMessage() takes a socket type object to send the message through, a pointer to the buffer where the message we want to send is located, and the size of the message. We then call send() of the Winsock library and provide it with the necessary information. The "0" provided to send() is usually used if you'd want to set flags for send() to work in a different way than its default but in our case, we do not need to set any flags. The send function will return an int value representing the number of bytes it successfully sent or an error value if there was a problem sending through the socket. Make sure to error check this value in your real application. If you'd like to see how the send function works then go here.

C++
int NetworkServices::receiveMessage(SOCKET curSocket, char * buffer, int bufSize)
{
    return recv(curSocket, buffer, bufSize, 0);
}

receiveMessage() takes a socket type object to check for any data that is available for reading at that socket on the network. It will place any data read into our "buffer", and will require a buffer size to indicate how much is the maximum it can read each time receiveMessage() is called. We then provide this information to recv() which is the Winsock library function for receiving data from a socket. It will also return an int value representing the number of bytes it read into our buffer or an error if there was a problem receiving from the socket. Make sure to error check this value in your real application. The "0" provided to recv() works the same way as the one in send, here is more information on recv().

We now create the client side of the network. Declare a ClientNetwork header file called "ClientNetwork.h" and include the following libraries:

C++
// Networking libraries
#include <winsock2.h>
#include <Windows.h>
#include "NetworkServices.h"
#include <ws2tcpip.h>
#include <stdio.h> 

and define the following constants and library links.

C++
// size of our buffer
#define DEFAULT_BUFLEN 512
// port to connect sockets through 
#define DEFAULT_PORT "6881"
// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

Declare the following variables/functions in the header file:

C++
class ClientNetwork
{

public:

    // for error checking function calls in Winsock library
    int iResult;

    // socket for client to connect to server
    SOCKET ConnectSocket;

    // ctor/dtor
    ClientNetwork(void);
    ~ClientNetwork(void);
};

We are ready to implement client connection to server. In the constructor for ClientNetwork inside ClientNetwork.cpp, initialize the Winsock. I will not go in depth with the explanation for how this works. You may check the Microsoft library if you wish to learn more. However, you will not most likely need to change these values.

C++
#include "stdafx.h"
#include "ClientNetwork.h"

ClientNetwork::ClientNetwork(void) {

    // create WSADATA object
    WSADATA wsaData;

    // socket
    ConnectSocket = INVALID_SOCKET;

    // holds address info for socket to connect to
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);

    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        exit(1);
    }



    // set address info
    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;  //TCP connection!!!

Currently, I have set the address for the server to be localhost, you may change this if you'd like.

C++
//resolve server address and port
iResult = getaddrinfo("127.0.0.1", DEFAULT_PORT, &hints, &result);

if( iResult != 0 )
{
    printf("getaddrinfo failed with error: %d\n", iResult);
    WSACleanup();
    exit(1);
}

// Attempt to connect to an address until one succeeds
for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {

    // Create a SOCKET for connecting to server
    ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
        ptr->ai_protocol);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        WSACleanup();
        exit(1);
    }

    // Connect to server.
    iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);

    if (iResult == SOCKET_ERROR)
    {
        closesocket(ConnectSocket);
        ConnectSocket = INVALID_SOCKET;
        printf ("The server is down... did not connect");
    }
}



// no longer need address info for server
freeaddrinfo(result);



// if connection failed
if (ConnectSocket == INVALID_SOCKET)
{
    printf("Unable to connect to server!\n");
    WSACleanup();
    exit(1);
}

Here is an important piece of information. We are going to set our socket to be non-blocking so that it will not wait on send() and receive() functions when there is no data to send/receive. This is necessary for our multiplayer game since we'd like the game to keep going if there isn't anything to send or receive to or from a client.

C++
// Set the mode of the socket to be nonblocking
u_long iMode = 1;

iResult = ioctlsocket(ConnectSocket, FIONBIO, &iMode);
if (iResult == SOCKET_ERROR)
{
    printf("ioctlsocket failed with error: %d\n", WSAGetLastError());
    closesocket(ConnectSocket);
    WSACleanup();
    exit(1);
}

we will also disable nagle. (you don't have to)

C++
    //disable nagle
    char value = 1;
    setsockopt( ConnectSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof( value ) );

}

This is the end of our constructor for our ClientNetwork.

Now lets switch and go to setting up our ServerNetwork class.

In the header file, include the following libraries:

C++
#include <winsock2.h>
#include <Windows.h>
#include "NetworkServices.h"
#include <ws2tcpip.h>
#include <map>
using namespace std; 
#pragma comment (lib, "Ws2_32.lib")

and define the following constants:

C++
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "6881" 

Here is also what we need to declare in the header file:

C++
class ServerNetwork
{

public:

    ServerNetwork(void);
    ~ServerNetwork(void);

    // Socket to listen for new connections
    SOCKET ListenSocket;

    // Socket to give to the clients
    SOCKET ClientSocket;

    // for error checking return values
    int iResult;

    // table to keep track of each client's socket
    std::map<unsigned int, SOCKET> sessions; 
};

Let's define the ServerNetwork constructor inside the cpp file:

C++
#include "stdafx.h" 
#include "ServerNetwork.h"

ServerNetwork::ServerNetwork(void)
{

    // create WSADATA object
    WSADATA wsaData;

    // our sockets for the server
    ListenSocket = INVALID_SOCKET;
    ClientSocket = INVALID_SOCKET;



    // address info for the server to listen to
    struct addrinfo *result = NULL;
    struct addrinfo hints;

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        exit(1);
    }

    // set address information
    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;    // TCP connection!!!
    hints.ai_flags = AI_PASSIVE;

The server will not need an address since it will be on the local machine.

C++
    // Resolve the server address and port
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);

    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        exit(1);
    }

    // Create a SOCKET for connecting to server
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);

    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        exit(1);
    }

    // Set the mode of the socket to be nonblocking
    u_long iMode = 1;
    iResult = ioctlsocket(ListenSocket, FIONBIO, &iMode);

    if (iResult == SOCKET_ERROR) {
        printf("ioctlsocket failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        exit(1);
    }

    // Setup the TCP listening socket
    iResult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen);

    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        exit(1);
    }

    // no longer need address information
    freeaddrinfo(result);

    // start listening for new clients attempting to connect
    iResult = listen(ListenSocket, SOMAXCONN);

    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        exit(1);
    }
}

This completes our server initialization. Now we will need to accept clients that attempt to connect.

Add the following declaration to the ServerNetwork header file:

C++
// accept new connections
bool acceptNewClient(unsigned int & id);

And here is the definition. This function will take an ID (a way for us to track this specific client) and once a client is connected, will add their assigned socket and ID to our table. It will then return true if there was a new client added.

C++
// accept new connections
bool ServerNetwork::acceptNewClient(unsigned int & id)
{
    // if client waiting, accept the connection and save the socket
    ClientSocket = accept(ListenSocket,NULL,NULL);

    if (ClientSocket != INVALID_SOCKET) 
    {
        //disable nagle on the client's socket
        char value = 1;
        setsockopt( ClientSocket, IPPROTO_TCP, TCP_NODELAY, &value, sizeof( value ) );

        // insert new client into session id table
        sessions.insert( pair<unsigned int, SOCKET>(id, ClientSocket) );

        return true;
    }

    return false;
}

Now we will create a ServerGame class. In there we will declare and initialize a ServerNetwork object which will be used as our network. In the larger scope of the game, ServerGame should hold every information about the game on the server, including the information about the clients on the network. You will see as we go along. Create a ServerGame.h file:

C++
#include "ServerNetwork.h"

class ServerGame
{

public:

    ServerGame(void);
    ~ServerGame(void);

    void update();

private:

   // IDs for the clients connecting for table in ServerNetwork 
    static unsigned int client_id;

   // The ServerNetwork object 
    ServerNetwork* network;
};

In its cpp file:

C++
#include "stdafx.h" 
#include "ServerGame.h"

unsigned int ServerGame::client_id; 

ServerGame::ServerGame(void)
{
    // id's to assign clients for our table
    client_id = 0;

    // set up the server network to listen 
    network = new ServerNetwork(); 
}

void ServerGame::update() 
{

    // get new clients
    if(network->acceptNewClient(client_id))
    {
        printf("client %d has been connected to the server\n",client_id); 

        client_id++;
    }
}

The code inside the constructor of ServerGame will set up all of the networking we did earlier inside of ServerNetwork and will have its socket listen for incoming connections. In ServerGame::update() we will accept new clients that are trying to connect and assign them the current client_id and increment if the ID is assigned to them to prepare for the next client.

Lets create a ClientGame which should hold all of the client side information, including the network.

Create a header file named "ClientGame.h":

C++
#include <winsock2.h>
#include <Windows.h>
#include "ClientNetwork.h"

and declare these variables/functions:

C++
class ClientGame
{

public:

    ClientGame();
    ~ClientGame(void);

    ClientNetwork* network; 
};

Inside the cpp file:

C++
#include "stdafx.h"
#include "ClientGame.h" 

ClientGame::ClientGame(void)
{
    network = new ClientNetwork();
}

We are finally ready to check if the client will actually connect.

Create a main.cpp and include these libraries at the top:

C++
// may need #include "stdafx.h" in visual studio
#include "stdafx.h"
#include "ServerGame.h"
#include "ClientGame.h"
// used for multi-threading
#include <process.h>

and make these global variables/functions in main.cpp:

C++
void serverLoop(void *);
void clientLoop(void);

ServerGame * server;
ClientGame * client;

Here is the main function:

C++
int main()
{

   // initialize the server
    server = new ServerGame();

We will run the server on a separate thread, so that it will always be checking for new clients. It will also be sending and receiving data in the future.

C++
// create thread with arbitrary argument for the run function
_beginthread( serverLoop, 0, (void*)12);

// initialize the client
client = new ClientGame();

This is our game loop. It will hold the console so that we can see the output from ServerNetwork and it can be extended to do other things.

C++
    clientLoop();
}

Add these functions inside main.cpp, serverLoop will be ran by the server thread and clientLoop by the main thread :

C++
void serverLoop(void * arg) 
{ 
    while(true) 
    {
        server->update();
    }
}

void clientLoop()
{
    while(true)
    {
        //do game stuff
       //will later run client->update();
    }
}

Congratulations! you now have a client connected to the server. If you run the program, you should see a message in console stating that client 0 has been connected to the server.

Sending and Receiving Data

Now we need to set up some data to send. Lets create a header file called "NetworkData.h". We do not need a cpp file. This file will just define some data types for us.

In this header file:

C++
#include <string.h>

#define MAX_PACKET_SIZE 1000000

enum PacketTypes {

    INIT_CONNECTION = 0,

    ACTION_EVENT = 1,

}; 

and now we will define a packet struct to serve as a container to our data to send:

C++
struct Packet {

    unsigned int packet_type;

    void serialize(char * data) {
        memcpy(data, this, sizeof(Packet));
    }

    void deserialize(char * data) {
        memcpy(this, data, sizeof(Packet));
    }
};

The packet_type field will be filled using the values in the enumerator we just created.

The serialize function is used to convert the packet_type data into bytes that we can send over the network.

The deserialize function is used to convert bytes received over the network back into packet_type data that we can interpret.

You will need to include NetworkData.h inside your ServerGame, ServerNetwork, ClientGame, and ClientNetwork classes!

Lets have the client send an INIT_CONNECTION packet when it is first connected to the server inside its constructor:

C++
ClientGame::ClientGame(void)
{

    network = new ClientNetwork();

    // send init packet
    const unsigned int packet_size = sizeof(Packet);
    char packet_data[packet_size];

    Packet packet;
    packet.packet_type = INIT_CONNECTION;

    packet.serialize(packet_data);

    NetworkServices::sendMessage(network->ConnectSocket, packet_data, packet_size);
}

We just created a packet and gave it an INIT_CONNECTION type and sent it over the network using the sendMessage() function which we wrote earlier. We used the socket that the client had used to connect to the server.

Now lets read that message on the server side.

Add the following function to ServerNetwork's header file:

under public:

C++
// receive incoming data
int receiveData(unsigned int client_id, char * recvbuf);

Then this for the cpp definition:

C++
// receive incoming data
int ServerNetwork::receiveData(unsigned int client_id, char * recvbuf)
{
    if( sessions.find(client_id) != sessions.end() )
    {
        SOCKET currentSocket = sessions[client_id];
        iResult = NetworkServices::receiveMessage(currentSocket, recvbuf, MAX_PACKET_SIZE);
        if (iResult == 0)
        {
            printf("Connection closed\n");
            closesocket(currentSocket);
        }
        return iResult;
    }
    return 0;
} 

The above function will receive data waiting on the socket for a given client ID and fill the passed buffer (recvbuf) with the data read from the network.

Lets call it from the ServerGame to read data sent from the client.

Add the following to the header file of ServerGame:

under public:

C++
void receiveFromClients();

under private:

C++
// data buffer
char network_data[MAX_PACKET_SIZE];

Define receiveFromClients() in the cpp file. In this function we go through all the clients that we saved in the sessions table earlier as each were connected. Then we call receiveData() on all of them and get the data insideour network_data buffer. We then deserialize the packet and switch between whether it is an INIT or ACTION packet. In this same way, you may extend this program for different packets and struct containers of different sizes to do different things.

C++
void ServerGame::receiveFromClients()
{
    Packet packet;

    // go through all clients
    std::map<unsigned int, SOCKET>::iterator iter;

    for(iter = network->sessions.begin(); iter != network->sessions.end(); iter++)
    {
        // get data for that client
        int data_length = network->receiveData(iter->first, network_data);

        if (data_length <= 0) 
        {
            //no data recieved
            continue;
        }

        int i = 0;
        while (i < (unsigned int)data_length) 
        {
            packet.deserialize(&(network_data[i]));
            i += sizeof(Packet);

            switch (packet.packet_type) {

                case INIT_CONNECTION:

                    printf("server received init packet from client\n");

                    break;

                case ACTION_EVENT:

                    printf("server received action event packet from client\n");

                    break;

                default:

                    printf("error in packet types\n");

                    break;
            }
        }
    }
}

Now call receiveFromClients() inside of ServerGame::update():

C++
void ServerGame::update()
{
    // get new clients
   if(network->acceptNewClient(client_id))
   {
        printf("client %d has been connected to the server\n",client_id);

        client_id++;
   }

   receiveFromClients();
}

If you run the program now, you should see a message for client connecting to the server and then another message stating that the server has received an INIT packet.

Lets extend the program for the clients to send ACTION packets to the server and the server to send ACTION packets to the clients.

The client will send an INIT packet, then the server will send an ACTION packet and upon receipt, the client will send an ACTION packet, and then the server will receive the ACTION packet, and send another, and etc.

In ServerNetwork, add a function which will send messages to all connected clients.

In the header file:

C++
// send data to all clients
void sendToAll(char * packets, int totalSize);

In the cpp file:

C++
// send data to all clients
void ServerNetwork::sendToAll(char * packets, int totalSize)
{
    SOCKET currentSocket;
    std::map<unsigned int, SOCKET>::iterator iter;
    int iSendResult;

    for (iter = sessions.begin(); iter != sessions.end(); iter++)
    {
        currentSocket = iter->second;
        iSendResult = NetworkServices::sendMessage(currentSocket, packets, totalSize);

        if (iSendResult == SOCKET_ERROR) 
        {
            printf("send failed with error: %d\n", WSAGetLastError());
            closesocket(currentSocket);
        }
    }
}

Inside ServerGame.h, add the following declaration under public fields:

C++
void sendActionPackets();

Inside the ServerGame.cpp, add the following function which will send action packets to all connected clients:

C++
void ServerGame::sendActionPackets()
{
    // send action packet
    const unsigned int packet_size = sizeof(Packet);
    char packet_data[packet_size];

    Packet packet;
    packet.packet_type = ACTION_EVENT;

    packet.serialize(packet_data);

    network->sendToAll(packet_data,packet_size);
}

Modify the ServerGame::receiveFromClients() to the following:

C++
void ServerGame::receiveFromClients()
{

    Packet packet;

    // go through all clients
    std::map<unsigned int, SOCKET>::iterator iter;

    for(iter = network->sessions.begin(); iter != network->sessions.end(); iter++)
    {
        int data_length = network->receiveData(iter->first, network_data);

        if (data_length <= 0) 
        {
            //no data recieved
            continue;
        }

        int i = 0;
        while (i < (unsigned int)data_length) 
        {
            packet.deserialize(&(network_data[i]));
            i += sizeof(Packet);

            switch (packet.packet_type) {

                case INIT_CONNECTION:

                    printf("server received init packet from client\n");

                    sendActionPackets();

                    break;

                case ACTION_EVENT:

                    printf("server received action event packet from client\n");

                    sendActionPackets();

                    break;

                default:

                    printf("error in packet types\n");

                    break;
            }
        }
    }
}

Lets add functionality for clients to receive packets from the server.

In ClientNetwork.h add the following under public fields:

C++
int receivePackets(char *);

and in its cpp file:

C++
int ClientNetwork::receivePackets(char * recvbuf) 
{
    iResult = NetworkServices::receiveMessage(ConnectSocket, recvbuf, MAX_PACKET_SIZE);

    if ( iResult == 0 )
    {
        printf("Connection closed\n");
        closesocket(ConnectSocket);
        WSACleanup();
        exit(1);
    }

    return iResult;
}

In ClientGame's header file, add the following under public fields:

C++
void sendActionPackets();

char network_data[MAX_PACKET_SIZE];

void update();

In the cpp file, add the sendActionPackets() function similar to the one in ServerGame:

C++
void ClientGame::sendActionPackets()
{
    // send action packet
    const unsigned int packet_size = sizeof(Packet);
    char packet_data[packet_size];

    Packet packet;
    packet.packet_type = ACTION_EVENT;

    packet.serialize(packet_data);

    NetworkServices::sendMessage(network->ConnectSocket, packet_data, packet_size);
}

Then write the update function which will continuously receive action packets from the server and send action packets in repsonse:

C++
void ClientGame::update()
{
    Packet packet;
    int data_length = network->receivePackets(network_data);

    if (data_length <= 0) 
    {
        //no data recieved
        return;
    }

    int i = 0;
    while (i < (unsigned int)data_length) 
    {
        packet.deserialize(&(network_data[i]));
        i += sizeof(Packet);

        switch (packet.packet_type) {

            case ACTION_EVENT:

                printf("client received action event packet from server\n");

                sendActionPackets();

                break;

            default:

                printf("error in packet types\n");

                break;
        }
    }
}

And finally, call client's update function inside the clientLoop in main.cpp so that the network will send and receive:

C++
void clientLoop()
{
    while(true)
    {
        //do game stuff
        client->update();
    }
}

Congratulations!! We are finally done. If you run the program, you should be able to see messages being sent and received from the client and the server!! Smile | <img src=

Just a few last minute notes. I have not covered closing the connection but it should be pretty straight forward. You will need to remove clients from our sessions table and then close the socket using the closesocket(). This I will leave to you as an exercise.

You may extend the program by adding more data types other than the Packet type that we already defined. Just add it to the NetworkData.h file and write the corresponding serialize/deserialize functions. Make sure to account for their sizes when receiving them over the network.

Also, I had an error when building the program with Visual Studio, you may need "#pragma once" in your header files.

Good luck in your endeavors!!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)