Navigation: Multiplayer Games

Tutorial 1 - Winsock


Overview


Figure 1.

Since DirectPlay is now deprecated(It's got old), I've decided Winsock(Windows Sockets) is a good alternative to creating multiplayer games in C++. So this tutorial is about setting up a peer-to-peer client and server using Winsock.

There is a difference between blocking network communication and asynchronous network communication. Blocking is where a function hangs or waits for a response before it returns, which could prevent a game from running smoothly having to wait. Asynchronous or event-driven architectures are the solution to that. Instead of polling and blocking until a network response is received, instead you can do something when an event occurs. This article focuses on asynchronous networking.

I will explain how to use Winsock with a simple windowed app but leave it to you to incorporate it into an actual game.

You more-or-less need a main WndProc that handles all network events.

The Basics

Ok, the Winsock app for this tutorial is a Win32 windowed app. It connects two computers together using TCP/IP and then the two computers can send data to each other. The data is received on one end through a windows message e.g. WM_SOCKET. We define the window message ourselves, WM_SOCKET, and that way we can identify a network event by this message in the application's main WndProc. To request network messages, use the winsock function WSAAsyncSelect(). However you must create a Server/Client socket before calling WSAAsyncSelect();

A socket is basically something that let's you talk to a computer over a network. More on sockets later.

The application for this tutorial can act as a server or a client. There is no need to create a separate executable, one for client and one for server.

127.0.0.1 is a special ip address called the loop back address. It can be used to test networking on one machine. You can find the actual ip address of your computer by running ipconfig.exe from the command line(See Figure 2).


Figure 2.

Once you have chosen to host a game(See Figure 1), you can then join that game by typing the ip address of the host and press Join.

The window uses GDI+, a graphics API, to render white text; You will need to type gdiplus.lib in Linker -> Input -> Additional Dependencies to use GDI+. You may also have to download the Windows Platform SDK, if it is not already on your system.

You need to create a socket on the server side and a socket on the client side. The server then listens for an incoming connection request from a client on the server socket and on some port, e.g. 5678. If a client tries to connect then the server accepts and gets a socket that represents a socket on the client side. The server can then communicate with the client using that socket.

So that covers the theory.

The User Interface

I will give you the code that creates a UI like in Figure 1, and you can use this as a starting point if you like for Winsock.

Get code

main.cpp is intended for an empty Win32 project.

If you start a new project you will have to add gdiplus.lib and ws2_32.lib to Additional Dependencies to be able to use GDI+ and Winsock.

Once you have a window set up and a WndProc, you can start adding Winsock code.

As far as GDI+ goes void OnPaint(HDC hdc) is probably the only method worth looking at if it's just Winsock you are interested in. using namespace Gdiplus; is necessary if you want to use GDI+ objects without Gdiplus::

First Steps

The winsock header must come before <Windows.h>:
#include <winsock2.h>
#include <windows.h>
#include <gdiplus.h>
#include <string>
I have created the following methods to handle client-Server interaction:
//Winsock methods.

bool InitializeWinsock();
bool ConnectionEstablished();
void SendChatMessage();
void ProcessReceivedMessage(string msg);
void StopConnectionActivity();

//Client side methods.
bool CreateSocketClient();
bool RequestNetworkEventsClient(HWND proc_window);
bool ConnectToServer(string ip_address);

//Server side methods.
bool CreateSocketServer();
bool RequestNetworkEventsServer(HWND proc_window);
InitializeWinsock() prepares Winsock for use. I recommend calling it only when you need to use winsock rather than when the program is started. It calls WSAStartup(MAKEWORD(2,2),&WsaDat).

To connect a Client to a Server you need to create a Socket on the client side and a Socket on the Server side. A SOCKET is a pointer variable that is part of Winsock. I have made the functions CreateSocketClient() and CreateSocketServer() to create sockets.

If you look at CreateSocketClient() you can see a socket is created with the following line:
Socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
Where "Socket" is a global variable: SOCKET Socket;

AF_INET means we want to use Internet Protocol v4 addresses (See Figure 2).

CreateSocketServer() is a bit different from CreateSocketClient().
htonl(INADDR_ANY) is to do with byte-order, which I won't go into. bind() associates the SockAddr object and Port number with the socket. The Port is a number used to identify an incoming connection request from the client. For example a client may try to connect to a socket on port 5678.

Network Messages

#define WM_SOCKET WM_USER + 1
By calling WSAAsyncSelect(...) with a valid socket as a parameter requests network events in the form of a windows message e.g. WM_SOCKET. When WM_SOCKET is received we can use WSAGETSELECTEVENT(lParam) to get the associated event FD_READ, FD_CLOSE or FD_ACCEPT for example. Take a look at the code for these events to see how they are handled by the WndProc or see The Window Procedure below.

Host or Client?

You can examine the code to see what happens when the Host button is pressed or if Join is pressed. Just look at void HandleButtonPressed(HWND hWnd, int notification_code).

When Host is pressed the program tries to inizialize winsock. If successful, it tries to create a socket, a socket on the server side because Host is chosen. If successful a request for Network windows messages is made with the line: RequestNetworkEventsServer(main_window).
(See code below)
if(hWnd == hWnd_BtnHost)
{
	...

	if(InitializeWinsock())
	{
		if(CreateSocketServer())
		{
			if(RequestNetworkEventsServer(main_window))
			{
				msg_status = L"Hosting Game, Waiting for client to join. ";
The RequestNetworkEventsServer(main_window) function simply calls WSAAsyncSelect() to request the WM messages(Notice WM_SOCKET is passed as a parameter).

If the socket was created and request for WM_SOCKET successfull then the host listens on the socket with the following code:
	//Now the socket is set up,
	//Listen for incoming connections on the socket:
	if(listen(Socket,(1))==SOCKET_ERROR)
	{
	    ...


A similar thing is done for the client if Join is pressed:
if(InitializeWinsock())
{
	if(CreateSocketClient())
	{
		if(RequestNetworkEventsClient(main_window))
		{
			msg_status = L"Joining Game";
			//Update Status.
			Repaint(main_window);

			string ip = GetIPFromTextBox();
			ConnectToServer(ip);

			...
		}
	}
}
ConnectToServer(ip) calls the Winsock connect() method to try to connect to the host computer.

StopConnectionActivity() can then be used to cleanup winsock or stop any network activity. You might for example want to cancel joining or hosting a game and end any network activity such as listening on a socket. After freeing Winsock you can reinitialize it when needed with InitializeWinsock().

bool ConnectionEstablished() depends on the variables bConnectedToClient or bConnectedToServer. These variables need to be managed to reflect the state of the application. A possible improvement might be to create some class to manage a network session.

The Window Procedure

If all is good then network events will be sent to the WndProc. Here is how you might handle them:
//Handle Winsock messages.
switch(msg)
{
	case WM_SOCKET:
	{
		if(WSAGETSELECTERROR(lParam))
		{	
			MessageBox(hWnd,
				L"Connection to server failed",
				L"Error",
				MB_OK|MB_ICONERROR);

			msg_status = L"Connection to Host failed.";
			//Update Status.
			Repaint(main_window);

			break;
		}

		switch(WSAGETSELECTEVENT(lParam))
		{
			//FD_READ on Client side and Server side.
			case FD_READ:
			{
				//read incoming data
				char szIncoming[1024];
				ZeroMemory(szIncoming,sizeof(szIncoming));

				int inDataLength=recv(Socket,(char*)szIncoming,sizeof(szIncoming)/sizeof(szIncoming[0]),0);

				string received_msg = szIncoming;
				ProcessReceivedMessage(received_msg);
			}
			break;

			//FD_CLOSE on Client side and Server side.
			case FD_CLOSE:
			{
				//Server has disconnected from client or
				//client has disconnected from server.
				closesocket(Socket);

				msg_status = L"Connection Lost.";
				//Update Status.
				Repaint(main_window);

				StopConnectionActivity();
			}
			break;

			//FD_ACCEPT on Server side only.
			case FD_ACCEPT:
			{
				//Decide whether to allow client to connect:

				int size=sizeof(sockaddr);

				//Get client sock addr:
				//(And Socket):
				Socket=accept(wParam,&sockAddrClient,&size);                
				if (Socket==INVALID_SOCKET)
				{
					int nret = WSAGetLastError();
					WSACleanup();
					break;
				}
				else{
					//Client connected.
					bConnectedToClient = true;

					msg_status = L"Client Connected!";

					//Update Status.
					Repaint(main_window);

					send(Socket,"connection_response",strlen("connection_response"),0);
					break;
				}
			}
			break;
		}
	}
}

ProcessReceivedMessage(string msg)

I have made it so the server sends the message "connection_response" to the client to let the client know a connection was established. ProcessReceivedMessage() updates the status on receipt to let the user know too.

Lastly, if the user presses the send button, a text message is sent to the other computer. ProcessReceivedMessage() will set the msg_received string to this text message.

Global Variables

The global variables I have used for the example application are defined as:
//Global Socket.
SOCKET Socket=NULL;
sockaddr sockAddrClient;
int Port = 5678;
bool bWinsockInitialized = false;
bool bConnectedToServer = false;
bool bConnectedToClient = false;

//Is this computer a host or a client?
bool bClient = false;
bool bHost = false;

Conclusion

Download Tut Files

Download Application