Winsock Tutorial 6

Async Sockets in TCP/IP (The Client)

The async (otherwise known as 'asynchronous') Winsock model is very different from what we have seen in the previous tutorials.

In this Winsock tutorial, our end application is, very much, a Windows based program. We are now going to make the leap from the familiar console to a fully blown Windows GUI networked application.

Being a Windows application that we are about to create, if you have not completed Windows tutorials 1 - 3, I would highly recommend doing so. If you are familiar with programming under the Windows environment, continue on.

By the end of the next two tutorials we will have a fully functional Win32 chat program!

Prerequisites

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



The async client

The window creation code is virtually identical to the final source code used in Windows Tutorial 3. So, I am going to assume that you have gone over this code and skip any explanations relating to Window creation and so on.

The main difference between our async Winsock project and the projects we created in the previous tutorials is that, we are going to be relying on our 'callback procedure' to handle our Winsock events, making our code somewhat different from what we have seen before.

Assuming we already have a project capable of displaying a window (grab the final source from Windows tutorial 3), we need to make sure that we have two edit boxes (one for outgoing messages and one for incoming messages) and a button to send out chat messages.

So, we will define the message boxes, and send button, as follows.
#define IDC_EDIT_IN 101			// For incoming messages
#define IDC_EDIT_OUT 102		// For outgoing messages
#define IDC_MAIN_BUTTON 103		// For button
#define WM_SOCKET 104			// For async socket events
You may have noticed we have also defined WM_SOCKET as 104. This will be used to handle socket events later on.

And in the WM_CREATE event we can display the edit boxes and button at window creation time as we did in Windows tutorial 3.

Now that our window is ready to go, filled with edit boxes and a send button, we can now get down to business.

At the end of our WM_CREATE case we need to initialise Winsock (with error handling).
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;
}
No real difference here, except if initialisation fails we get a message box notifying us of the fact.

Then we need to create the socket.
Socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(Socket==INVALID_SOCKET)
{
	MessageBox(hWnd,"Socket creation failed","Critical Error",MB_ICONERROR);
	SendMessage(hWnd,WM_DESTROY,NULL,NULL);
	break;
}
Again, nothing to unfamiliar here.

This is where things start to get more interesting. We will now call upon the WSAAsyncSelect() function. WSAAsyncSelect() performs a request to use Windows messages to notify our program of any events for a socket.

Its prototype looks like this;
int WSAAsyncSelect(
	__in  SOCKET s,
	__in  HWND hWnd,
	__in  unsigned int wMsg,
	__in  long lEvent
);
The first parameter is the socket we are working with. Parameter two is the handle to the window that we are going to use. The third parameter is the Windows message that will be sent when a socket event occurs. In our case the message will be WM_SOCKET (remember we defined this as 104 earlier). The last parameter is a mask. We can set what socket events we want to listen for.

We call WSSAsyncSelect() as follows;
nResult=WSAAsyncSelect(Socket,hWnd,WM_SOCKET,(FD_CLOSE|FD_READ));
if(nResult)
{
	MessageBox(hWnd,"WSAAsyncSelect failed","Critical Error",MB_ICONERROR);
	SendMessage(hWnd,WM_DESTROY,NULL,NULL);
	break;
}
In our case we have set WSAAsyncSelect() to notify our program of FD_CLOSE and FD_READ events.



A full list of socket events we can use with WSAAsyncSelect() are;

FD_READnotifies when data arrives at our socket
FD_WRITEreceives notifications of readiness for writing
FD_OOBreceives events for arrival of 'Out Of Band' data
FD_ACCEPTnotifies of events for incoming connections
FD_CONNECTnotifies of events for socket connections
FD_CLOSEalerts when a socket has been closed
FD_QOSreceives notifications of 'Quality Of Service' changes
FD_GROUP_QOSreceives notifications of 'QOS' group changes
FD_ROUTING_INTERFACE_CHANGEnotifies of routing changes
FD_ADDRESS_LIST_CHANGEnotifies of address list changes in the protocol family

Now we can set up the hostent and SOCKADDR_IN structures as we have done previously.
struct hostent *host;
if((host=gethostbyname(szServer))==NULL)
{
	MessageBox(hWnd,"Unable to resolve host name","Critical Error",MB_ICONERROR);
	SendMessage(hWnd,WM_DESTROY,NULL,NULL);
	break;
}

// Set up our socket address structure
SOCKADDR_IN SockAddr;
SockAddr.sin_port=htons(nPort);
SockAddr.sin_family=AF_INET;
SockAddr.sin_addr.s_addr=*((unsigned long*)host->h_addr);
Once we have done that, we attempt to connect to the server.

connect(Socket,(LPSOCKADDR)(&SockAddr),sizeof(SockAddr));
That takes care of the connection to the server. Now we need to monitor the Windows messages to make use of our socket.

Getting the message

If you remember from earlier, any socket specified activity (filtered to FD_CLOSE, and FD_READ) will trigger a WM_SOCKET event.

We can check these messages by switching between WSAGETSELECTEVENT(lParam) events.
switch(WSAGETSELECTEVENT(lParam))
{
	case FD_READ:
	{
	}
	break;

	case FD_CLOSE:
	{
	}
	break;
}
To read data that arrives on the socket we can insert the following code into the FD_READ event.
char szIncoming[1024];
ZeroMemory(szIncoming,sizeof(szIncoming));

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

strncat(szHistory,szIncoming,inDataLength);
strcat(szHistory,"\r\n");

SendMessage(hEditIn,WM_SETTEXT,sizeof(szIncoming)-1,reinterpret_cast<LPARAM>(&szHistory));
There is nothing to unusual in this code. All we have done is create a buffer, received data into the buffer and sent a message instructing Windows to display the data into our 'hEditIn' edit box that we created earlier.

In order to know if the server has closed its connection to the client we need to monitor this using the FD_CLOSE message.

MessageBox(hWnd,"Server closed connection","Connection closed!",MB_ICONINFORMATION|MB_OK);
closesocket(Socket);
SendMessage(hWnd,WM_DESTROY,NULL,NULL);
That is all there is to it. We have gone over a lot of new concepts, so I recommend analysing the full source code, to fully understand the process of what is happening.

If anything is unclear, be sure to drop by the forums and let us know as this tutorial is a massive step from the previous Winsock tutorial.

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

char *szServer="localhost";
int nPort=5555;

HWND hEditIn=NULL;
HWND hEditOut=NULL;
SOCKET Socket=NULL;
char szHistory[10000];

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",
			"Windows Async Client",
			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_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)"Attempting to connect to server...");

			// 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));

			// Set up Winsock
			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;
			}

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

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

			// Resolve IP address for hostname
			struct hostent *host;
			if((host=gethostbyname(szServer))==NULL)
			{
				MessageBox(hWnd,
					"Unable to resolve host name",
					"Critical Error",
					MB_ICONERROR);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}

			// Set up our socket address structure
			SOCKADDR_IN SockAddr;
			SockAddr.sin_port=htons(nPort);
			SockAddr.sin_family=AF_INET;
			SockAddr.sin_addr.s_addr=*((unsigned long*)host->h_addr);

			connect(Socket,(LPSOCKADDR)(&SockAddr),sizeof(SockAddr));
		}
		break;

		case WM_COMMAND:
			switch(LOWORD(wParam))
            {
				case IDC_MAIN_BUTTON:
				{
					char szBuffer[1024];

					int test=sizeof(szBuffer);
					ZeroMemory(szBuffer,sizeof(szBuffer));

					SendMessage(hEditOut,
						WM_GETTEXT,
						sizeof(szBuffer),
						reinterpret_cast<LPARAM>(szBuffer));
					send(Socket,szBuffer,strlen(szBuffer),0);
					SendMessage(hEditOut,WM_SETTEXT,NULL,(LPARAM)"");
				}
				break;
			}
			break;

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

		case WM_SOCKET:
		{
			if(WSAGETSELECTERROR(lParam))
			{	
				MessageBox(hWnd,
					"Connection to server failed",
					"Error",
					MB_OK|MB_ICONERROR);
				SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				break;
			}
			switch(WSAGETSELECTEVENT(lParam))
			{
				case FD_READ:
				{
					char szIncoming[1024];
					ZeroMemory(szIncoming,sizeof(szIncoming));

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

					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,
						"Server closed connection",
						"Connection closed!",
						MB_ICONINFORMATION|MB_OK);
					closesocket(Socket);
					SendMessage(hWnd,WM_DESTROY,NULL,NULL);
				}
				break;
			}
		} 
	}

	return DefWindowProc(hWnd,msg,wParam,lParam);
}


Things to try

Once again, you can try connecting to a mail server. If you are going to try and send messages to the mail server, don't forget to hit enter after each line as the mail server expects a carriage return after each command, otherwise you will find that you don't get much of a response.

Additional information

For additional information we have provided the following links.

Microsoft (MSDN) - The WSAAsyncSelect() function


Next tutorial

Tutorial 7 - Async sockets in TCP/IP (The Server)