Programming with Asynchronous Sockets

Published February 01, 2001 by Drew Sikora, posted by Gaiiden
Do you see issues with this article? Let us know.
Advertisement

Introduction

Hello again. It's been a while - I've been keeping myself quite busy with studies, work, school, and more. Ugh. But I saw a post on the boards asking for a DirectPlay or WinSock tutorial so here I am. I had to speed learn (is that even a term?) WinSock for work so we could develop a (big word here) multiplexing program for DSL modems. Blah - that got canceled. So here I am with all this knowledge and nothing to do with it - except share it, of course.

This article is written about a topic I only recently came to fully understand. It's kinda confusing at first, but I had to rely on these stinking textbook-like books that gave no explanations whatsoever. Hopefully I can help save you from having to learn this way and then reduce yourself to begging for help and understanding from the boards. Anyway enough friggin talk - I have work to do after this. Now I have to learn (well, understand) Java in three days. Good grief. What do these people think I am, Superman? Hey I kinda like that...

This article assumes you have prior experience in WinSock programming - this article does not cover the basic WinSock conventions used within.

Asynchronous != Non-Blocking

Since you are just learning this, I will assume that up till now you have been using blocking sockets. With that in mind, I will assume that you may have toyed around with non-blocking sockets. What's the difference? A socket that is blocked sends out a call and then waits for an answer. A non-blocking socket sends out a call and the program execution continues. This is exactly what asynchronous sockets do. However, there is a major difference.

When using non-blocking sockets, you have to continually poll the socket to see whether or not you have any actions that need to be performed. These actions can include sending and receiving data, establishing a connection, sending back a response to a connection request, etc. Non-blocking sockets have the advantage in that they allow normal program execution to continue - meaning you can process data while waiting for more to come in. This immediately makes them superior to blocking sockets in many cases (but not all).

However, asynchronous sockets take polling to a new level - they do it for you. This means that you can focus on other things while WinSock keeps watch over your sockets. (Sounds better than non-blocking sockets eh? Not always, as you'll soon see.) So how does it do this? Well, um - I kinda like don't really know, sorry. I do know it calls a library and that creates a thread or something and, and - well I can tell you how you are notified, even if I can't tell you how it knows to notify you. J Had ya going there, didn't I?

Windows Socket Messages

That's right - read it and weep. Hope you weren't planning on a nice simple console app there, bucko. This requires a nice spiffy Win32 app, and nothing less. If you don't know how to create a Win32 app (namely, a window, a message handler, and a main loop) then I can suggest the first part of Game Programming Genesis, by that cool dude Ironblayde, which explains all you need to know that will get you started. Once you're done reading that, get yourself back in here and keep going!

For those of you in the know, this is the one major advantage asynchronous has over non-blocking sockets. Of course, this can also give non-blocking sockets an advantage when you want to design a simple console app. Non-blocking sockets can be used in console apps, asynchronous sockets cannot.

But I still haven't gotten to the topic of this section yet! Socket Messages - right. WinSock makes use of the Windows message queue in order to notify you when socket events take place. These are the events that WinSock can generate for you:

  • FD_READ: This message indicates that you have just received data, and you must use the recv() function to get it.
  • FD_WRITE: This means that there is room in the buffer to send, and we must send using the send() function.
  • FD_CONNECT: Used only by the client program. This message indicates that the server has received the connection request and has accepted.
  • FD_ACCEPT: Used only by the server program. This message indicates the receipt of the connect() request and you must send back an accept().
  • FD_CLOSE: This message indicates that the other side's socket has shut down. This can be for several reasons.

These five events (which are piggybacked on a message, as we'll soon see) are all you need to manage your connections. I say connections, plural, because the beauty of this setup is that each message has the socket instance that called it stored in its wParam field. This means you can manage multiple connections with only one FD_READ or FD_WRITE message! I'll point this out a little later.

Setting Up the Asynchronous Sockets

It's not as hard as you may think. All it takes is for you to create the socket and then make the call to WSAAsyncSelect(), whose definition is shown below.

int PASCAL FAR WSAAsyncSelect(SOCKET s, HWND, hwnd, unsigned int wMsg, long lEvent);

And here we have the parameter explanations.

  • s - the socket instance for which we want to enable event notification.
  • hwnd - identifies the window to which the messages will be posted, since WinSock uses PostMessage() to get the notification to you (hey, now we know how it does it!).
  • wMsg - the message type for the notification message(s).
  • lEvent - a bit mask identifying the events for which we want notification. Ex: FD_READ | RD_WRITE | FD_CONNECT. Now then! We know how to do it, so let's do it already. Below is a short code segment involving creating a socket and making it asynchronous, first for a client, then for a server.
// the message we'll use for our async notification
#define WM_WSAASYNC (WM_USER +5)

// create and test the socket
Port = socket(AF_INET, SOCK_STREAM, 0);
if (Port == INVALID_SOCKET)
return(FALSE);

// make the socket asynchronous and notify of read, write, connect and close events
// this is the client socket
WSAAsyncSelect(Port, hwnd, WM_WSAASYNC, FD_WRITE | FD_CONNECT | FD_READ | FD_CLOSE);

// make the socket asynchronous and notify of read, write, accept and close events
// this is the server socket
WSAAsyncSelect(Port, hwnd, WM_WSAASYNC, FD_READ | FD_WRITE | FD_ACCEPT | FD_CLOSE);

Of course, we wouldn't put the client and server sockets in the same app, I'm just trying to save space here. Now, take note because this is important: You cannot perform any actions on the socket until you have declared it asynchronous. Why? Let's say you create the socket and then send() something off quick quick. Well congratulations, you just made it a blocking socket, and now your program is hung up waiting for the send() to complete. Oh yes, you can still change it to asynchronous with WSAAsyncSelect() it's just that it's going to cause some problems stopping execution like that.

Also notice I had to create a custom message called WM_WSAASYNC. I did this using one of Windows' custom messages. Nothing big or worth explaining - just follow my lead. By the way, you can call this message whatever the hell you want, it doesn't have to be WM_WSAASYNC. (Although keeping the WM_ part would be nice for people reading your code)

Handling Notification Messages

Notification Messages is really a misleading term. In reality, WinSock only generates one message that it posts to the queue, and attached to it is the event that occurred. This is shown by the example below.

switch(msg)
{
	case WM_WSAASYNC:
	{
		// what word?
		switch(WSAGETSELECTEVENT(lParam))
		{
			case FD_ACCEPT:
			{
				// check for an error
				if (!WSAGETSELECTERROR(lParam))
					return(FALSE);
				// process message
				sAccept = accept(wParam, NULL, NULL);
				return(0);
			}
		}
	}
}

Whoa! Talk about nests! And what are all these macros doing? LParam and wParam? What the heck? Let's now take a look at the notification message structure

Ahhh, now it's beginning to make some sense. Let's cover where each parameter plays its part. We'll start with wParam. Where is it? Right here:

...
sAccept = accept(wParam, NULL, NULL);
...

And here's what I was telling you earlier. Notice I used the variable wParam in place of the socket instance. Why? So that if I had more than one socket (s1 and s2) I could use the same message for both. So whichever socket invokes the message is passed along with it. For instance, say it was s2 that needs to receive data. Well instead of having a switch statement inside the message handler to decide which socket to place in the accept() call, I just use wParam. Translation:

sAccept = accept(wParam, NULL, NULL);

is equal to

sAccept = accept(s2, NULL, NULL);

Now that is cool. Saves you the time and the trouble now doesn't it? I know I probably repeated myself a bit there but it's an important point to remember and understand because it's very useful, as you can see.

Now then, lParam is no more complicated. But there is a difference. The lParam variable has a high field and a low field, and both fields can be set to separate values. Normally we would extract these with the Windows' API HIWORD() and LOWORD() macros. But Winsock makes our lives even easier and has two of its own macros, WSAGETSELECTEVENT() and WSAGETSELECTERROR(). These do the exact same thing as the Windows' macros, except they relieve us of the need to remember which field holds what. If you look back up at the example above, you can see how both macros are used. The WSAGETSELECTEVENT() is your ticket to figuring out just what WinSock event was triggered. Here is the full case statement inside the WM_WSAASYNC message handler:

switch(WSAGETSELECTEVENT(lParam))
{
	case FD_READ:
		{
		} break;
	case FD_WRITE:
		{
		} break;
	case FD_CONNECT:
	// or
	case FD_ACCEPT:
		{
		} break;
	case FD_CLOSE:
		{
		} break;
}

Hope I didn't trip you up with the FD_CONNECT and the FD_ACCEPT events - just remember that they will always depend on whether it's the server or the client.

And finally we have the WSAGETSELECTERROR() macro, which we can use to see if an error has occurred before we decide to do anything. Usually this is not necessary, but there is one good time to error check - and that's when the FD_CLOSE event is called. As I said earlier, there are many reasons a socket could be shut down. Here are the error codes:

  • 0 = either the other side did a graceful close or did a shutdown() for send. Either way there was no real error.
  • WSACONNRESET = the other side did a closesocket() with reset close behavior.
  • WSACONNABORTED = the connection may have timed out. This is an abrupt abort and could even be from the user just quitting.
  • WSANETDOWN = network code has completely failed, which means you're in trouble J It's always best to check for these error codes when processing an FD_CLOSE event to determine just what went wrong. All other times you should just use the WSAGetLastError() function to determine what went bad.

Shaking Hands - Connecting and Accepting

Now the fun begins. We are going to place a call to connect() with the client app, send an accept() with the server app, and then process an FD_CONNECT message. Are we ready? Then let's first call the connect() function. Where you ask? Well, where you normally would. Let's not get this mixed up - the FD_CONNECT event happens after the connection has been established, so you don't call connect from the FD_CONNECT event handler, we'll get there soon. Just call connect() as you normally would, after you set all the IP addresses and port numbers and so on.

connect(Port, (LPSOCKADDR)&Server, sizeof(Server));

NOTE: It's important to know that all asynchronous functions will return the error WSAWOULDBLOCK at some point. This error can trip you up if you aren't ready for it, even though it is vital to the FD_WRITE event, as we'll see. So if you do any error checking, be sure to pass over this error, since it really isn't one.

Now, the call to connect has been sent, and your program goes into a wait state (unless you have something else for it to do in the meantime). So since it's so boring over here, let's hop on over to the server where it is just now receiving your connection request. And what happens? An FD_ACCEPT event is generated! Quick! To the Batcave Robin! Err, sorry - got carried away there. So now the server grabs the message and it pops into its queue. The message handler picks it up and - what do we do? Have no fear, we simply have to be friendly and call accept().

...
case FD_ACCEPT:
{
	// holds size of client struct
	int lenclient = sizeof(client_sock);

	// connect to the server
	Port = accept(wParam, &client_sock, &lenclient);
} break;
...

Now that the server has done his job, let's jump on back over to the client and see what he's up to. And look at that. He's received the acceptance, and now he's generated an FD_CONNECT event. Here we go - off to the message handler. Now what? Well, this event is a place where you can perform any number of tasks that need to be done right after connection has been established. While in an FD_ACCEPT event you have to call accept(), in an FD_CONNECT event you don't have to do anything - although it is a good idea to set a variable or let the user know you're connected.

Re-enabling Functions

Now some people may whack me for getting out of line here, but I feel it's a good time to explain this concept before have to cover the FD_WRITE message. The way WinSock handles messages is that it only posts one at a time. Once a message is posted, no other messages are posted until they are handled, and a re-enabling function is used.

For example, the re-enabling function for FD_ACCEPT is accept(). When an FD_ACCEPT event is posted, no other message will be posted until the application handles the current one and then calls accept(). Once accept() is called, if there is already another connection request, FD_ACCEPT is posted again immediately. If not, WinSock waits for one or posts another pending event.

The point is that without calling accept() you do not re-enable WinSock to send more FD_ACCEPT messages (or any other messages, for that matter). Needless to say, this can be a problem J. There are only two other events that require a re-enabling function. Those are FD_READ and FD_WRITE. Their re-enabling functions are recv() and send(). Gee. What a surprise.

Sending and Receiving Data

I wanted to split this into two parts, but I decided just to explain FD_READ first and save the more complicated FD_WRITE for last. That said, the FD_READ event is easy enough to handle. WinSock will notify you when the network buffer has more data for you to collect. For every FD_READ event you will call recv() just like so:

int bytes_recv = recv(wParam, &data, sizeof(data), 0);

Nothing terribly different, except you can't forget to stick in that wParam. Also, a word of warning: You are not guaranteed that you will receive the entire buffer in one shot. This means your data may not come through all at once. Be sure to implement a test using the bytes received (which happens to be the return value of the recv() function) to determine if you have all of your data before manipulating it.

Now the FD_WRITE event is a bit more complicated. Mainly because of the way it is called. First of all, an FD_WRITE event is always generated when you first establish a connection with another socket. So you think all you have to do is plunk in a send() function with whatever else you'll need and you'll be fine. Oh if that were only true. Listen carefully: the FD_WRITE event is triggered only when there is more room in the buffer with which to write data. Huh? Allow me to explain.

First off, the buffer, as I use it here, is the network buffer, which you write information to. That information is then sent over the network (intranet or internet) to the receiving socket. So you'd think that since there will always be space left to write data if you don't fill it up all at once, that FD_WRITE events will just keep coming and coming right? Wrong! Read the rules again. It says when there is more room. Not enough room, more room. This means you have to fill it up first! How do you do that?

The general idea is to create an infinite while loop in which you will continuously send data until you max out the buffer. When is happens, send() will return the error WSAWOULDBLOCK. This means that if it were a blocking socket, it would wait (stop execution) for more room in the buffer and then send. But since it isn't, you get the error. So now that you've filled up the buffer, you just have to wait until more room becomes available so you can write again. And bingo! Up pops another FD_WRITE event. Do you have any idea how much trouble I had to go through to figure this out? You people are so darned lucky! Here's an example of an FD_WRITE event handler:

case FD_WRITE: // we can send data
{
	// enter an infinite loop
	while(TRUE)
	{
		// read in more data from the file and store it in packet.data.
		in.read((char*)&packet.data, MAX_PACKET_SIZE);

		// increment the amount of data sent
		data_sent += strlen(packet.data);

		// send the packet off to the Server if it is filled
		if (send(wparam, (char*)(&packet), sizeof(PACKET), 0) == SOCKET_ERROR)
		{
			// check if the network buffer is full and can send no more
			// data. If so then break from the loop
			if (WSAGetLastError() == WSAEWOULDBLOCK)
			{
				// break from the loop - buffer is full
				break;
			}
			else // another error
			{
				// display an error message and clean up
				CleanUp();
				return(0);
			}
		}
	}
} break;

There, you see? The implementation isn't so hard after all! It was only the concept that was messing with your head. If you use this technique, the loop will end when the network buffer becomes full. Then, when more space is available - another FD_WRITE event will be triggered, and you can send more data.

Before you jump to make use of your newfound knowledge, allow me to clarify the use of the FD_WRITE event. Do not expect to be using this event at all if you are not going to be sending large pieces of data at once. The reason for this is simple - if you base all you information transactions on FD_WRITE and never send enough to fill up the buffer, after that first trial FD_WRITE event generated at the beginning of the connection - no other FD_WRITE event will be triggered! Therefore for games, where you send as little data as possible, just forget about setting a socket for the FD_WRITE event and use send() wherever you need it. FD_WRITE is best used for transferring files, in my experience.

Conclusion

Well, so far this is the longest article I've written. I've tried to keep it as short as possible to hold your attention but there was just so much information to cover. Asynchronous sockets can be really confusing at first if they are not explained correctly, and I hope I did a good job of teaching you how to use them properly. If you have any questions or comments, please [email="drew@gamedev.net"]email me[/email]. I will try to answer as many as I can. Also, by emailing me, I'll have your address in case I have any new information or corrections to give out. Hope you enjoyed reading the article as much as I enjoyed writing it. I'm looking forward to my next one. Until then...

Note on the Source

The source code attached to this article demonstrates an asynchronous application. Source is free for any use you wish, credit would be nice!

Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement