SocketWrench .NET Edition

Asynchronous Sockets

One of the first issues that you’ll encounter when developing your Windows Sockets applications is the difference between blocking and non-blocking sockets. Whenever you perform some operation on a socket, it may not be able to complete immediately and return control back to your program. For example, a read on a socket cannot complete until some data has been sent by the remote host. If there is no data waiting to be read, one of two things can happen: the function can wait until some data has been written on the socket, or it can return immediately with an error that indicates that there is no data to be read.

The first case is called a synchronous or blocking socket. In other words, the program is "blocked" until the request for data has been satisfied. When the remote system does write some data on the socket, the read operation will complete and execution of the program will resume. The second case is called an asynchronous or non-blocking socket, and requires that the application recognize the error condition and handle the situation appropriately. Programs that use non-blocking sockets typically use one of two methods when sending and receiving data. The first method, called polling, is when the program periodically attempts to read or write data from the socket (typically using a timer). The second, and preferred method, is to use what is called asynchronous notification. This means that the program is notified whenever a socket event takes place, and in turn can respond to that event. For example, if the remote program writes some data to the socket, an event is generated so that program knows it can read the data from the socket at that point.

For historical reasons, the default behavior is for socket functions to block and not return until the operation has completed. However, blocking sockets in Windows can introduce some special problems in single-threaded applications. The blocking function will allow the application to continue processing messages from the operating system. Because messages are being processed, this means that the program can be re-entered at a different point with the blocked operation suspended on the program's stack. For example, consider a program that attempts to read some data from the socket when a button is pressed. Because no data has been written yet, it blocks and the program begins processing system messages. The user then presses a different button, which causes code to be executed, which in turn attempts to read data from the socket, and so on.

To resolve the general problems with blocking sockets, the Windows Sockets standard states that there may only be one outstanding blocked call per thread of execution. This means that applications that are re-entered will encounter errors whenever they try to take some action while a blocking function is already in progress. The creation of worker threads to perform blocking socket operations is a common approach to address this issue, although it does require the developer to understand how threading works, and it can introduce additional complexity into the application.

It should be noted that there are also advantages to using blocking sockets. In most cases, the application design and implementation is simpler, and raw throughput (the rate at which data is sent and received) is generally higher with blocking sockets because it does not have to go through an event mechanism to notify the application of a change in status. In general, if your application is designed as a client, and does not have the need to establish multiple simultaneous connections then blocking sockets may be appropriate. However, if your application functions as a server or needs to establish multiple connections then an asynchronous, event-driven design may be more appropriate.

In summary, there are three general approaches that can be taken when building an application with the control in regard to blocking or non-blocking sockets:

If you decide to use non-blocking sockets in your application, it’s important to keep in mind that you must check the return value from every read and write operation, since it is possible that you may not be able to send or receive all of the specified data. Frequently, developers encounter problems when they write a program that assumes a given number of bytes can always be written to, or read from, the socket. In many cases, the program works as expected when developed and tested on a local area network, but fails unpredictably when the program is released to a user that has a slower network connection (such as a serial dial-up connection to the Internet). By always checking the return values of these operations, you insure that your program will work correctly, regardless of the speed or configuration of the network.