Winsock Tutorial 8

Handling multiple clients

In this Winsock tutorial we are going to investigate how to accept multiple incoming connections to make a more usable server. Generally speaking, a server that can only receive information from only one client is not of much use. I am sure you would agree.

This tutorial is built on the foundation that we used previously in Winsock Tutorial 7.

Prerequisites

Project type: Windows
Include files: winsock2.h, windows.h
Library files: ws2_32.lib



Expanding the server

Now that we are expecting to receive multiple connections, we will handle the sockets slightly differently.

We will have a socket called 'ServerSocket' which we will bind as per usual. But, this time we are going to create an array of client sockets called Socket[n] for our client connections. Where 'n' is the current client that we will be dealing with.

We also introduce two new integer variables - nMaxClients and nClient.
const int nMaxClients=3; int nClient=0; SOCKET Socket[nMaxClients-1]; SOCKET ServerSocket=NULL; The purpose for nMaxClients is to set the maximum number of clients that can connect to the server. So, if nMaxClients=20, the twenty first client will be rejected.

The only difference with our bind call now, is that we are going to bind 'ServerSocket' instead of 'Socket', in the previous tutorial.
if(bind(ServerSocket,(LPSOCKADDR)&SockAddr,sizeof(SockAddr))==SOCKET_ERROR)
{
	MessageBox(hWnd,"Unable to bind socket","Error",MB_OK);
	SendMessage(hWnd,WM_DESTROY,NULL,NULL);
	break;
}

Accepting the clients

Now that we are accepting multiple clients, we need to re-think how we can do this.

The simplest way would be to create an 'if' statement, right? Right!
if(nClient<nMaxClients)
{
	int size=sizeof(sockaddr);
	Socket[nClient]=accept(wParam,&sockAddrClient,&size);                
	if (Socket[nClient]==INVALID_SOCKET)
	{
		int nret = WSAGetLastError();
		WSACleanup();
	}
	SendMessage(hEditIn,
		WM_SETTEXT,
		NULL,
		(LPARAM)"Client connected!");
	}
	nClient++;
}
So, all that is happening here is a simple test. If we haven't reached our server limit increment the client count and accept the connection.

There is a downside to this method though and that is socket re-use. Just say you have set nMaxClients=10. What if your server has five connections and four of those connections decide to leave? You are still only left with five more sockets aren't you?

A possible fix to this would be to increment your nMaxClients count each time someone leaves, so you'll have more sockets to play with. But, I'll let you think about that as my main objective is to show how you can play with multiple connections. How you put your new found knowledge into practice is up to you.

Reading incoming data

Reading the incoming data is not dissimilar to what you are used to doing. The quick and and easy way would be to loop through all of our sockets. If, for some reason, there is no data to be read, the recv() call will return -1. If this happens we don't really care we just want to move on to the next socket and see what is waiting (if anything).
for(int n=0;n<nMaxClients;n++)
{
	char szIncoming[1024];
	ZeroMemory(szIncoming,sizeof(szIncoming));

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

	if(inDataLength!=-1)
	{
		strncat(szHistory,szIncoming,inDataLength);
		strcat(szHistory,"\r\n");

		SendMessage(hEditIn,
			WM_SETTEXT,
			sizeof(szIncoming)-1,
			reinterpret_cast<LPARAM>&szHistory));
	}
}
All too easy, so far. You'll be writing the next block buster MMO before you know it!

Sending data to the clients

Once again, for small scale projects, we can take the easy way out and use a loop to send the data to all clients. In a large scale project (like the previously mentioned MMO) you would be expected to refine the process a bit. If you are dealing with ten thousand concurrent connections, you want every clock cycle and bit of bandwidth to count.

So as not to deter you too much, I tested this very server, we are working on, with one hundred dummy clients and could not visually see any delay in transmission (on a LAN). So, even a simple 'tutorial' server can go a long way!

Now, back to the job at hand. Here is our send loop.
char szBuffer[1024];
ZeroMemory(szBuffer,sizeof(szBuffer));

SendMessage(hEditOut,
	WM_GETTEXT,
	sizeof(szBuffer),
	reinterpret_cast<LPARAM>(szBuffer));
for(int n=0;n<=nMaxClients;nClient;n++)
{
	send(Socket[n],szBuffer,strlen(szBuffer),0);
}

SendMessage(hEditOut,WM_SETTEXT,NULL,(LPARAM)"");
You will notice the only real difference is the 'for' loop. Otherwise, it is business as usual.

The final task is to fire up visual studio and make it all happen!

The Full Code

#include <winsock2.h>
#include <windows.h>

#pragma comment(lib,"ws2_32.lib")

#define IDC_EDIT_IN		101
#define IDC_EDIT_OUT		102
#define IDC_MAIN_BUTTON		103
#define WM_SOCKET		104

int nPort=5555;

HWND hEditIn=NULL;
HWND hEditOut=NULL;
char szHistory[10000];
sockaddr sockAddrClient;

const int nMaxClients=3;
int nClient=0;
SOCKET Socket[nMaxClients-1];
SOCKET ServerSocket=NULL;

LRESULT CALLBACK WinProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInst,HINSTANCE hPrevInst,LPSTR lpCmdLine,int nShowCmd)
{
	WNDCLASSEX wClass;
	ZeroMemory(&wClass,sizeof(WNDCLASSEX));
	wClass.cbClsExtra=NULL;
	wClass.cbSize=sizeof(WNDCLASSEX);
	wClass.cbWndExtra=NULL;
	wClass.hbrBackground=(HBRUSH)COLOR_WINDOW;
	wClass.hCursor=LoadCursor(NULL,IDC_ARROW);
	wClass.hIcon=NULL;
	wClass.hIconSm=NULL;
	wClass.hInstance=hInst;
	wClass.lpfnWndProc=(WNDPROC)WinProc;
	wClass.lpszClassName="Window Class";
	wClass.lpszMenuName=NULL;
	wClass.style=CS_HREDRAW|CS_VREDRAW;

	if(!RegisterClassEx(&wClass))
	{
		int nResult=GetLastError();
		MessageBox(NULL,
			"Window class creation failed\r\nError code:",
			"Window Class Failed",
			MB_ICONERROR);
	}

	HWND hWnd=CreateWindowEx(NULL,
			"Window Class",
			"Winsock Async Server",
			WS_OVERLAPPEDWINDOW,
			200,
			200,
			640,
			480,
			NULL,
			NULL,
			hInst,
			NULL);

	if(!hWnd)
	{
		int nResult=GetLastError();

		MessageBox(NULL,
			"Window creation failed\r\nError code:",
			"Window Creation Failed",
			MB_ICONERROR);
	}

    ShowWindow(hWnd,nShowCmd);

	MSG msg;
	ZeroMemory(&msg,sizeof(MSG));

	while(GetMessage(&msg,NULL,0,0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return 0;
}

LRESULT CALLBACK WinProc(HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam)
{
	switch(msg)
    {
		case WM_COMMAND:
			switch(LOWORD(wParam))
            {
				case IDC_MAIN_BUTTON:
				{
					char szBuffer[1024];
					ZeroMemory(szBuffer,sizeof(szBuffer));

					SendMessage(hEditOut,
						WM_GETTEXT,
						sizeof(szBuffer),
						reinterpret_cast<LPARAM>(szBuffer));
					for(int n=0;n<=nClient;n++)
					{
						send(Socket[n],szBuffer,strlen(szBuffer),0);
					}

					SendMessage(hEditOut,WM_SETTEXT,NULL,(LPARAM)"");
				}
				break;
			}
			break;
		case WM_CREATE: 
		{
			ZeroMemory(szHistory,sizeof(szHistory));

			// Create incoming message box
			hEditIn=CreateWindowEx(WS_EX_CLIENTEDGE,
				"EDIT",
				"",
				WS_CHILD|WS_VISIBLE|ES_MULTILINE|
				ES_AUTOVSCROLL|ES_AUTOHSCROLL,
				50,
				120,
				400,
				200,
				hWnd,
				(HMENU)IDC_EDIT_IN,
				GetModuleHandle(NULL),
				NULL);
			if(!hEditIn)
			{
				MessageBox(hWnd,
					"Could not create incoming edit box.",
					"Error",
					MB_OK|MB_ICONERROR);
			}
			HGDIOBJ hfDefault=GetStockObject(DEFAULT_GUI_FONT);
			SendMessage(hEditIn,
					WM_SETFONT,
					(WPARAM)hfDefault,
					MAKELPARAM(FALSE,0));
			SendMessage(hEditIn,
					WM_SETTEXT,
					NULL,
					(LPARAM)"Waiting for client to connect...");

			// Create outgoing message box
			hEditOut=CreateWindowEx(WS_EX_CLIENTEDGE,
						"EDIT",
						"",
						WS_CHILD|WS_VISIBLE|ES_MULTILINE|
						ES_AUTOVSCROLL|ES_AUTOHSCROLL,
						50,
						50,
						400,
						60,
						hWnd,
						(HMENU)IDC_EDIT_IN,
						GetModuleHandle(NULL),
						NULL);
			if(!hEditOut)
			{
				MessageBox(hWnd,
					"Could not create outgoing edit box.",
					"Error",
					MB_OK|MB_ICONERROR);
			}

			SendMessage(hEditOut,
					WM_SETFONT,
					(WPARAM)hfDefault,
					MAKELPARAM(FALSE,0));
			SendMessage(hEditOut,
					WM_SETTEXT,
					NULL,
					(LPARAM)"Type message here...");

			// Create a push button
			HWND hWndButton=CreateWindow( 
					    "BUTTON",
						"Send",
						WS_TABSTOP|WS_VISIBLE|
						WS_CHILD|BS_DEFPUSHBUTTON,
						50,
						330,
						75,
						23,
						hWnd,
						(HMENU)IDC_MAIN_BUTTON,
						GetModuleHandle(NULL),
						NULL);
			
			SendMessage(hWndButton,
				WM_SETFONT,
				(WPARAM)hfDefault,
				MAKELPARAM(FALSE,0));

			WSADATA WsaDat;
			int nResult=WSAStartup(MAKEWORD(2,2),&WsaDat);
			if(nResult!=0)
			{
				MessageBox(hWnd,
					"Winsock initialization failed",
					"Critical Error",
					MB_ICONERROR);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}

			ServerSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
			if(ServerSocket==INVALID_SOCKET)
			{
				MessageBox(hWnd,
					"Socket creation failed",
					"Critical Error",
					MB_ICONERROR);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}

			SOCKADDR_IN SockAddr;
			SockAddr.sin_port=htons(nPort);
			SockAddr.sin_family=AF_INET;
			SockAddr.sin_addr.s_addr=htonl(INADDR_ANY);

			if(bind(ServerSocket,(LPSOCKADDR)&SockAddr,sizeof(SockAddr))==SOCKET_ERROR)
			{
				MessageBox(hWnd,"Unable to bind socket","Error",MB_OK);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}

			nResult=WSAAsyncSelect(ServerSocket,
					hWnd,
					WM_SOCKET,
					(FD_CLOSE|FD_ACCEPT|FD_READ));
			if(nResult)
			{
				MessageBox(hWnd,
					"WSAAsyncSelect failed",
					"Critical Error",
					MB_ICONERROR);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}

			if(listen(ServerSocket,SOMAXCONN)==SOCKET_ERROR)
			{
				MessageBox(hWnd,
					"Unable to listen!",
					"Error",
					MB_OK);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}
		}
		break;

		case WM_DESTROY:
		{
			PostQuitMessage(0);
			shutdown(ServerSocket,SD_BOTH);
			closesocket(ServerSocket);
			WSACleanup();
			return 0;
		}
		break;

		case WM_SOCKET:
		{
			switch(WSAGETSELECTEVENT(lParam))
			{
				case FD_READ:
				{
					for(int n=0;n<=nMaxClients;n++)
					{
						char szIncoming[1024];
						ZeroMemory(szIncoming,sizeof(szIncoming));

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

						if(inDataLength!=-1)
						{
							strncat(szHistory,szIncoming,inDataLength);
							strcat(szHistory,"\r\n");
	
							SendMessage(hEditIn,
								WM_SETTEXT,
								sizeof(szIncoming)-1,
								reinterpret_cast<LPARAM>(&szHistory));
						}
					}
				}
				break;

				case FD_CLOSE:
				{
					MessageBox(hWnd,
						"Client closed connection",
						"Connection closed!",
						MB_ICONINFORMATION|MB_OK);
				}
				break;

				case FD_ACCEPT:
				{
					if(nClient<nMaxClients)
					{
						int size=sizeof(sockaddr);
						Socket[nClient]=accept(wParam,&sockAddrClient,&size);                
						if (Socket[nClient]==INVALID_SOCKET)
						{
							int nret = WSAGetLastError();
							WSACleanup();
						}
						SendMessage(hEditIn,
							WM_SETTEXT,
							NULL,
							(LPARAM)"Client connected!");
						}
						nClient++;
					}
				break;
    			}   
			}
		}
    
    return DefWindowProc(hWnd,msg,wParam,lParam);
}


Things to try

See if you can make your 'accept' call reuse disconnected sockets.

Additional information

For additional information we have provided the following links.

Microsoft (MSDN) - The WSAAsyncSelect() function


Next tutorial

Tutorial 9 - Coming Soon