So far, our journey into socket programming has focused exclusively on TCP (Transmission Control Protocol). TCP, with its reliability, ordered delivery, and connection-oriented nature, is fantastic for applications like web Browse, file transfers, and our chat application. However, it comes with overhead due to its guarantees.
But what if you need speed more than absolute reliability? What if small data loss is acceptable, or you want to send small, independent packets without the handshake overhead? Enter UDP (User Datagram Protocol).
In this article, we will explore UDP's unique characteristics and implement a practical application: a simple time synchronization server and client.
let us reiterate the core characteristics of UDP and discuss scenarios where It is the protocol of choice:
Connectionless: Unlike TCP, UDP does not establish a persistent connection. Each message (datagram) is an independent unit of data sent from one point to another, much like sending a postcard. THere is no handshake, no session management.
Unreliable: UDP offers no guarantees that a datagram will reach its destination, or that if multiple datagrams are sent, they will arrive in the order they were sent. THere is no automatic retransmission for lost packets.
Faster / Lower Overhead: Because it lacks the mechanisms for reliability, ordering, flow control, and congestion control, UDP has significantly less overhead than TCP. This means faster data transfer and less resource consumption.
Datagram-Oriented: When you send data via UDP, It is sent as distinct packets (datagrams). If you send two 100-byte UDP packets, the receiver will get two distinct 100-byte packets. With TCP (stream-oriented), these might arrive as a single 200-byte chunk.
Choose UDP for:
DNS (Domain Name System): Quick, single request-response queries where a small delay or re-query is acceptable.
VoIP (Voice over IP) / Video Streaming: Real-time applications where a slight loss of audio/video frames is less noticeable (or preferable) than significant delay caused by retransmissions.
Online Gaming (Fast-Paced): Latency is critical. Small, frequent updates (like player positions) can be sent via UDP. If a packet is lost, a new update will likely arrive shortly, making retransmission unnecessary.
Network Management (SNMP): Simple query-response models.
Discovery Services: Broadcasting messages to find other devices on a local network.
Choose TCP for:
Web Browse (HTTP/HTTPS): You need to ensure all parts of a webpage arrive correctly and in order.
File Transfer (FTP): Data integrity is paramount; you cannot afford lost bytes in a file.
Email (SMTP/POP3/IMAP): Emails must be delivered reliably and completely.
Secure Shell (SSH): Remote terminal access requires reliable, ordered communication.
Our goal is to build a simple application where:
A UDP Server runs, listens for requests, and when it receives one, sends back the current system time.
A UDP Client sends a small request to the server and prints the time it receives back.
This demonstrates UDP's simple request-response pattern and its connectionless nature, where the server doesn't need to establish a dedicated link for each client.
The UDP server will primarily use `socket()`, `bind()`, `recvfrom()`, and `sendto()`. Noticeably absent are `listen()` and `accept()` because tHere is no "connection" to establish.
socket()
with `SOCK_DGRAM`: We create a datagram socket.
bind()
: The server still needs to `bind()` to a specific local IP address and port so clients know where to send their requests.
recvfrom()
: This function is used to receive data from any client. Crucially, it also populates a `sockaddr` structure with the sender's address and port, allowing the server to know where to send the response.
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
The `src_addr` and `addrlen` arguments are where the sender's address information is returned.
sendto()
: This function sends data to a *specific* destination address, identified by a `sockaddr` structure.
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
You will use the `src_addr` obtained from `recvfrom()` as the `dest_addr` for `sendto()`.
Retrieving Current Time: We will use standard C library functions like `time()` (from
) to get the current time as a `time_t` value, and `ctime()` (also from
) to convert it into a human-readable string.
The UDP client is also straightforward:
socket()
with `SOCK_DGRAM`: Similar to the server, it creates a datagram socket.
No `connect()`: For simple UDP, clients typically do not call `connect()`. Instead, they directly use `sendto()` to send a datagram to the server's known address. This is the essence of connectionless communication. (Note: `connect()` can be used with UDP sockets to "lock" the socket to a single destination, meaning subsequent `send()` calls don't need the address, but It is less common for basic request/response).
sendto()
: Sends the request (e.g., an empty string or a simple command like "GET_TIME") to the server's pre-configured IP and port.
recvfrom()
: Waits for and receives the time response from the server. It will also capture the server's address, though for a simple client that only talks to one server, this might not be strictly necessary for sending, but useful for verification.
As You will see in the code, the most significant differences are:
No `listen()` or `accept()`: These functions are entirely absent in UDP. UDP sockets are always ready to send or receive datagrams.
`sendto()` and `recvfrom()`: These replace `send()` and `recv()` (though `send()` and `recv()` can be used on UDP sockets if `connect()` was used, but that defeats some of the flexibility of UDP). They include explicit arguments for the source/destination address for each datagram.
Datagram-Oriented: You send a complete message at once, and It is received as a complete message. There is no concept of a continuous "stream" of bytes.
No Automatic Guarantees: You won't see code for retransmissions or acknowledgements. If a datagram is lost, It is up to the application layer to detect and handle it if reliability is desired.
Here are the complete code listings for our UDP time server and client.
udp_time_server.c
#include
#include
#include
#include // For time() and ctime()
// --- Platform-specific includes and definitions ---
#ifdef _WIN32
#include
#include // For inet_ntop
#pragma comment(lib, "ws2_32.lib")
#define CLOSE_SOCKET closesocket
#define GET_LAST_ERROR WSAGetLastError
// Define ssize_t for Windows compatibility if not already defined
#ifndef _SSIZE_T_DEFINED
typedef SSIZE_T ssize_t;
#define _SSIZE_T_DEFINED
#endif
#else
#include
#include
#include
#include // For close
#include // For errno
#define CLOSE_SOCKET close
#define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------
#define PORT 8888 // Choose a different port for UDP
#define BUFFER_SIZE 1024
int main() {
// --- Windows-specific Winsock initialization ---
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
return 1;
}
#endif
// ----------------------------------------------------
int sockfd;
struct sockaddr_in servaddr, cliaddr;
char buffer[BUFFER_SIZE];
time_t current_time;
char time_str[26]; // ctime returns 25 chars + null terminator
int n;
socklen_t len;
// 1. Create UDP socket
// AF_INET: IPv4 Internet protocols
// SOCK_DGRAM: Datagram-oriented socket (UDP)
// 0: Default protocol
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
-1
#endif
) {
fprintf(stderr, "socket creation failed with error: %d\n", GET_LAST_ERROR());
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Set server address structure
memset(&servaddr, 0, sizeof(servaddr)); // Clear the structure
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY; // Listen on all available interfaces
servaddr.sin_port = htons(PORT); // Convert port to network byte order
// 2. Bind the socket to the specified IP and port
// This makes the server listen for UDP datagrams on this address/port.
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "bind failed with error: %d\n", GET_LAST_ERROR());
CLOSE_SOCKET(sockfd);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("UDP Time Server listening on port %d...\n", PORT);
// Server main loop: continuously receive requests and send responses
while (1) {
len = sizeof(cliaddr); // Must be initialized before recvfrom call
memset(buffer, 0, BUFFER_SIZE); // Clear buffer for incoming data
// 3. Receive datagram from a client
// recvfrom stores the sender's address in cliaddr
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
0, (struct sockaddr *)&cliaddr, &len);
if (n ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "recvfrom failed with error: %d\n", GET_LAST_ERROR());
continue; // Keep server running for next request
}
buffer[n] = '\0'; // Null-terminate the received data
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cliaddr.sin_addr, client_ip, INET_ADDRSTRLEN);
printf("Received request '%s' from client: %s:%d\n",
buffer, client_ip, ntohs(cliaddr.sin_port));
// Get current time
current_time = time(NULL);
// ctime returns a string with a newline at the end; remove it for cleaner output
strncpy(time_str, ctime(¤t_time), sizeof(time_str) - 1);
time_str[sizeof(time_str) - 1] = '\0'; // Ensure null termination
time_str[strcspn(time_str, "\n")] = 0; // Remove newline
printf("Sending time: %s\n", time_str);
// 4. Send the time back to the requesting client
// sendto uses the address obtained from recvfrom to send the response.
sendto(sockfd, (const char *)time_str, strlen(time_str),
0, (const struct sockaddr *)&cliaddr, len);
}
// Close socket (this part won't be reached in the infinite loop)
CLOSE_SOCKET(sockfd);
printf("Server socket closed.\n");
// --- Windows-specific Winsock cleanup ---
#ifdef _WIN32
WSACleanup();
#endif
// -----------------------------------------
return 0;
}
udp_time_client.c
#include
#include
#include
// --- Platform-specific includes and definitions ---
#ifdef _WIN32
#include
#include // For inet_pton
#pragma comment(lib, "ws2_32.lib")
#define CLOSE_SOCKET closesocket
#define GET_LAST_ERROR WSAGetLastError
// Define ssize_t for Windows compatibility if not already defined
#ifndef _SSIZE_T_DEFINED
typedef SSIZE_T ssize_t;
#define _SSIZE_T_DEFINED
#endif
#else
#include
#include
#include
#include // For close
#include // For errno
#define CLOSE_SOCKET close
#define GET_LAST_ERROR() errno
#endif
// ----------------------------------------------------
#define PORT 8888
#define SERVER_IP "127.0.0.1" // Server's IP address
#define BUFFER_SIZE 1024
int main() {
// --- Windows-specific Winsock initialization ---
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed with error: %d\n", GET_LAST_ERROR());
return 1;
}
#endif
// ----------------------------------------------------
int sockfd;
struct sockaddr_in servaddr;
char buffer[BUFFER_SIZE];
const char *request_message = "GET_TIME";
socklen_t len;
int n;
// 1. Create UDP socket
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) ==
#ifdef _WIN32
INVALID_SOCKET
#else
-1
#endif
) {
fprintf(stderr, "socket creation failed with error: %d\n", GET_LAST_ERROR());
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
// Set server address structure for sending requests
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
// Convert IP address from text to binary form
if (inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr) <= 0) {
fprintf(stderr, "Invalid address/ Address not supported\n");
CLOSE_SOCKET(sockfd);
#ifdef _WIN32
WSACleanup();
#endif
exit(EXIT_FAILURE);
}
printf("Sending request '%s' to server %s:%d\n", request_message, SERVER_IP, PORT);
// 2. Send request to the server
// No connect() needed for UDP unless you want to fix the remote address
sendto(sockfd, (const char *)request_message, strlen(request_message),
0, (const struct sockaddr *)&servaddr, sizeof(servaddr));
// 3. Receive response from the server
// It is good practice to pass a cliaddr (source address) even if not strictly needed
// so you can verify who sent the response.
len = sizeof(servaddr); // reuse servaddr for receiving source address, or declare new one
memset(buffer, 0, BUFFER_SIZE);
n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE,
0, (struct sockaddr *)&servaddr, &len);
if (n ==
#ifdef _WIN32
SOCKET_ERROR
#else
-1
#endif
) {
fprintf(stderr, "recvfrom failed with error: %d\n", GET_LAST_ERROR());
} else {
buffer[n] = '\0'; // Null-terminate the received data
char server_ip_recv[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &servaddr.sin_addr, server_ip_recv, INET_ADDRSTRLEN);
printf("Received time from server %s:%d: %s\n", server_ip_recv, ntohs(servaddr.sin_port), buffer);
}
// Close socket
CLOSE_SOCKET(sockfd);
printf("Client socket closed.\n");
// --- Windows-specific Winsock cleanup ---
#ifdef _WIN32
WSACleanup();
#endif
// -----------------------------------------
return 0;
}
To run your UDP time application:
Compile both programs:
Linux/WSL:
gcc udp_time_server.c -o udp_time_server
gcc udp_time_client.c -o udp_time_client
Windows (MinGW-w64):
gcc udp_time_server.c -o udp_time_server.exe -lws2_32
gcc udp_time_client.c -o udp_time_client.exe -lws2_32
Open terminal windows:
In one terminal, run the server:
# Linux/WSL
./udp_time_server
# Windows
udp_time_server.exe
You should see: `UDP Time Server listening on port 8888...`
In one or more separate terminals, run the client:
# Linux/WSL
./udp_time_client
# Windows
udp_time_client.exe
You should see: `Sending request 'GET_TIME' to server 127.0.0.1:8888` followed by the received time.
Observe the interaction:
Notice how the UDP server, unlike the TCP server, does not block after handling one client. It immediately loops back to `recvfrom()` and is ready to serve the next client request without needing to `accept()` a new connection. You can run multiple client instances concurrently, and the server will respond to each one as their requests arrive.
Potential Packet Loss: While testing on your local machine (`127.0.0.1`), packet loss is extremely rare. However, over a real network, UDP packets can be lost or arrive out of order. Our simple client doesn't handle this, but real-world UDP applications would implement their own retransmission or error recovery logic if needed (e.g., DNS re-queries if no response within a timeout).
Example output (similar for both platforms):
Server Terminal:
UDP Time Server listening on port 8888...
Received request 'GET_TIME' from client: 127.0.0.1:XXXXX
Sending time: Wed Jun 18 12:31:40 2025
Received request 'GET_TIME' from client: 127.0.0.1:YYYYY
Sending time: Wed Jun 18 12:31:41 2025
... (continues to serve requests)
Client Terminal:
Sending request 'GET_TIME' to server 127.0.0.1:8888
Received time from server 127.0.0.1:8888: Wed Jun 18 12:31:40 2025
Client socket closed.
You have now successfully implemented a UDP client-server application and experienced the connectionless nature of UDP firsthand. This provides a crucial contrast to TCP and expands your understanding of network communication paradigms.
While UDP offers speed, our previous TCP chat server could only handle one client at a time. In the next article, we will tackle one of the most important concepts in server development: concurrency. You will learn how to design a TCP server that can gracefully handle multiple clients simultaneously, a critical skill for building any scalable network application.
How to move your Email accounts from one hosting provider to another without losing any mails?
How to resolve the issue of receiving same email message multiple times when using Outlook?
Self Referential Data Structure in C - create a singly linked list
Mosquito Demystified - interesting facts about mosquitoes
Elements of the C Language - Identifiers, Keywords, Data types and Data objects
How to pass Structure as a parameter to a function in C?
Rajeev Kumar is the primary author of How2Lab. He is a B.Tech. from IIT Kanpur with several years of experience in IT education and Software development. He has taught a wide spectrum of people including fresh young talents, students of premier engineering colleges & management institutes, and IT professionals.
Rajeev has founded Computer Solutions & Web Services Worldwide. He has hands-on experience of building variety of websites and business applications, that include - SaaS based erp & e-commerce systems, and cloud deployed operations management software for health-care, manufacturing and other industries.