C++
Winsock
non-blocking
async
UDP socket

C Winsock non-blocking/async UDP socket

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Winsock provides the networking API for Windows applications, and UDP is the protocol of choice when low latency matters more than delivery guarantees. By default, Winsock socket calls are blocking, meaning recvfrom and sendto will halt your thread until data arrives or is sent. For real-time applications like games, live audio, or telemetry, this is unacceptable. This article shows how to create non-blocking and asynchronous UDP sockets using Winsock in C++.

Winsock Initialization

Before creating any socket, you must initialize the Winsock library with WSAStartup and clean up with WSACleanup when finished:

cpp
1#include <WinSock2.h>
2#include <WS2tcpip.h>
3#include <cstdio>
4
5#pragma comment(lib, "ws2_32.lib")
6
7bool InitWinsock() {
8    WSADATA wsaData;
9    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
10    if (result != 0) {
11        printf("WSAStartup failed: %d\n", result);
12        return false;
13    }
14    return true;
15}

Always call WSACleanup() before your program exits to release Winsock resources.

Creating a UDP Socket

Create a datagram socket and bind it to a local port:

cpp
1SOCKET CreateUDPSocket(unsigned short port) {
2    SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
3    if (sock == INVALID_SOCKET) {
4        printf("socket() failed: %d\n", WSAGetLastError());
5        return INVALID_SOCKET;
6    }
7
8    sockaddr_in localAddr = {};
9    localAddr.sin_family = AF_INET;
10    localAddr.sin_port = htons(port);
11    localAddr.sin_addr.s_addr = INADDR_ANY;
12
13    if (bind(sock, (sockaddr*)&localAddr, sizeof(localAddr)) == SOCKET_ERROR) {
14        printf("bind() failed: %d\n", WSAGetLastError());
15        closesocket(sock);
16        return INVALID_SOCKET;
17    }
18
19    return sock;
20}

Method 1: Non-Blocking with ioctlsocket

The simplest way to make a socket non-blocking is to use ioctlsocket with the FIONBIO command. After this call, recvfrom returns immediately with WSAEWOULDBLOCK if no data is available:

cpp
1bool SetNonBlocking(SOCKET sock) {
2    u_long mode = 1;  // 1 = non-blocking, 0 = blocking
3    if (ioctlsocket(sock, FIONBIO, &mode) == SOCKET_ERROR) {
4        printf("ioctlsocket failed: %d\n", WSAGetLastError());
5        return false;
6    }
7    return true;
8}
9
10void ReceiveLoop(SOCKET sock) {
11    char buffer[1024];
12    sockaddr_in senderAddr;
13    int senderLen = sizeof(senderAddr);
14
15    while (true) {
16        int bytesReceived = recvfrom(
17            sock, buffer, sizeof(buffer), 0,
18            (sockaddr*)&senderAddr, &senderLen
19        );
20
21        if (bytesReceived > 0) {
22            buffer[bytesReceived] = '\0';
23            printf("Received %d bytes: %s\n", bytesReceived, buffer);
24        } else if (WSAGetLastError() == WSAEWOULDBLOCK) {
25            // No data available, do other work
26            Sleep(1);  // Prevent busy-waiting
27        } else {
28            printf("recvfrom error: %d\n", WSAGetLastError());
29            break;
30        }
31    }
32}

This approach is simple but has a downside: you must poll the socket repeatedly, which wastes CPU cycles even with a Sleep call.

Method 2: select() for Multiplexing

The select function lets you wait for data on one or more sockets with a timeout. This is more efficient than busy-polling:

cpp
1void SelectReceiveLoop(SOCKET sock) {
2    char buffer[1024];
3    sockaddr_in senderAddr;
4    int senderLen = sizeof(senderAddr);
5
6    while (true) {
7        fd_set readSet;
8        FD_ZERO(&readSet);
9        FD_SET(sock, &readSet);
10
11        timeval timeout;
12        timeout.tv_sec = 0;
13        timeout.tv_usec = 100000;  // 100ms timeout
14
15        int ready = select(0, &readSet, nullptr, nullptr, &timeout);
16        if (ready > 0 && FD_ISSET(sock, &readSet)) {
17            int bytesReceived = recvfrom(
18                sock, buffer, sizeof(buffer), 0,
19                (sockaddr*)&senderAddr, &senderLen
20            );
21            if (bytesReceived > 0) {
22                buffer[bytesReceived] = '\0';
23                printf("Received: %s\n", buffer);
24            }
25        } else if (ready == 0) {
26            // Timeout, no data. Do other work here.
27        } else {
28            printf("select error: %d\n", WSAGetLastError());
29            break;
30        }
31    }
32}

The first argument to select is ignored on Windows (it exists for BSD compatibility). The timeval struct controls how long select will wait before returning with zero if no data arrives.

Method 3: Overlapped I/O with WSARecvFrom

For high-performance servers, overlapped I/O provides true asynchronous operation. The system performs the I/O in the background and notifies you when it completes:

cpp
1void AsyncReceive(SOCKET sock) {
2    char buffer[1024];
3    WSABUF dataBuf;
4    dataBuf.buf = buffer;
5    dataBuf.len = sizeof(buffer);
6
7    DWORD bytesReceived = 0;
8    DWORD flags = 0;
9    sockaddr_in senderAddr;
10    int senderLen = sizeof(senderAddr);
11
12    WSAOVERLAPPED overlapped = {};
13    overlapped.hEvent = WSACreateEvent();
14
15    int result = WSARecvFrom(
16        sock, &dataBuf, 1, &bytesReceived, &flags,
17        (sockaddr*)&senderAddr, &senderLen,
18        &overlapped, nullptr
19    );
20
21    if (result == SOCKET_ERROR && WSAGetLastError() == WSA_IO_PENDING) {
22        // Wait for the operation to complete
23        WSAWaitForMultipleEvents(1, &overlapped.hEvent, FALSE, 5000, FALSE);
24
25        DWORD transferred = 0;
26        WSAGetOverlappedResult(sock, &overlapped, &transferred, FALSE, &flags);
27        if (transferred > 0) {
28            buffer[transferred] = '\0';
29            printf("Async received: %s\n", buffer);
30        }
31    }
32
33    WSACloseEvent(overlapped.hEvent);
34}

Overlapped I/O scales much better than select because it does not require polling. For the highest performance, combine overlapped I/O with I/O Completion Ports (IOCP) to service thousands of sockets with a small thread pool.

Common Pitfalls

Forgetting to link ws2_32.lib. Without the linker pragma or project setting, you will get unresolved external symbol errors for every Winsock function. Add #pragma comment(lib, "ws2_32.lib") at the top of your file or add it to your project's linker input.

Not checking WSAEWOULDBLOCK. When using non-blocking sockets, recvfrom returning SOCKET_ERROR with WSAEWOULDBLOCK is normal. It means no data is available yet. Treating it as a fatal error will cause your receive loop to exit prematurely.

Busy-waiting without Sleep or select. A tight loop calling recvfrom on a non-blocking socket without any wait mechanism will consume 100% of a CPU core. Always use select, an event, or at minimum a small Sleep call.

Not calling WSACleanup. Failing to call WSACleanup can cause resource leaks. In long-running applications, this leads to degraded network performance over time.

Summary

For simple non-blocking UDP on Windows, use ioctlsocket with FIONBIO and poll with select. For GUI applications, WSAAsyncSelect ties socket events to the message loop. For high-performance servers, use overlapped I/O with WSARecvFrom and consider I/O Completion Ports. Always initialize Winsock with WSAStartup, handle WSAEWOULDBLOCK gracefully in non-blocking mode, and clean up with WSACleanup.


Course illustration
Course illustration

All Rights Reserved.