This tutorial demonstrates how to create UDP (User Datagram Protocol) client and server applications in Go using the standard net
package.
Table of contents
Open Table of contents
What is UDP?
UDP, or User Datagram Protocol, is a connectionless transport protocol that operates at the transport layer of the Internet Protocol Suite. Unlike TCP, UDP prioritizes speed and simplicity over reliability by sending data as unacknowledged datagrams without establishing a connection or guaranteeing delivery order.
Key Characteristics of UDP
- Connectionless: No handshake process required before data transmission
- Unreliable: No guarantee of packet delivery, order, or duplicate protection
- Low overhead: Minimal header size (8 bytes) compared to TCP (20+ bytes)
- Fast: No connection establishment, acknowledgments, or flow control mechanisms
- Stateless: Each packet is independent and self-contained
Project Setup
Create a new directory for the project and initialize a Go module:
mkdir udp-tutorial
cd udp-tutorial
go mod init github.com/edgar-montano/udp
This tutorial will create two separate programs: a UDP server and a UDP client. Both programs will use the following standard library packages:
import (
"bufio" // For buffered I/O operations
"fmt" // For formatted I/O operations
"log" // For logging errors and messages
"net" // For network operations
"os" // For operating system interface
"strings" // For string manipulation
)
Implementing the UDP Server
The UDP server listens for incoming messages from clients and responds to each request. Create a new file called server.go
:
func main() {
// Resolve the server listening address
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
log.Fatal("Error resolving UDP address:", err)
}
// Start listening on UDP port
conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Fatal("Error listening on UDP:", err)
}
defer conn.Close()
fmt.Println("UDP Server listening on :8080")
// Buffer to receive incoming data
buffer := make([]byte, 1024)
// Main server loop
for {
// Read incoming message
n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
log.Printf("Error reading UDP message: %v", err)
continue
}
message := string(buffer[:n])
fmt.Printf("Received from %s: %s\n", clientAddr, message)
// Send response back to client
response := fmt.Sprintf("Server received: %s", message)
_, err = conn.WriteToUDP([]byte(response), clientAddr)
if err != nil {
log.Printf("Error sending response: %v", err)
}
}
}
Server Code Explanation
Address Resolution and Binding
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
log.Fatal("Error resolving UDP address:", err)
}
The server resolves its listening address. The empty string before the colon (:8080
) indicates the server should listen on all available network interfaces. You can provide a host name as the address parameter, but this is discouraged since it will resolve to only a single IP address from that host.
After resolving the listening address we should check to see if that operation succeeded. Normally ResolveUDPAddr
doesn’t bind the port — it just parses. So it won’t fail because the port is already in use, however it can fail if it has trouble resolving
the host (for instance in a restricted sandboxed environment) or if there is a typo in one of the parameters.
Socket Creation and Listening
conn, err := net.ListenUDP("udp", addr)
The net.ListenUDP
function creates a UDP socket bound to the specified address and begins listening for incoming packets.
Message Reception
n, clientAddr, err := conn.ReadFromUDP(buffer)
The ReadFromUDP
method reads incoming data and returns:
n
: Number of bytes readclientAddr
: Address of the client that sent the messageerr
: Any error that occurred during the read operation
Response Transmission
_, err = conn.WriteToUDP([]byte(response), clientAddr)
The WriteToUDP
method sends a response back to the specific client address that sent the original message.
Implementing the UDP Client
The UDP client establishes communication with the server by sending messages and receiving responses. Create a new file called client.go
:
func main() {
// Resolve the server address
serverAddr, err := net.ResolveUDPAddr("udp", "localhost:8080")
if err != nil {
log.Fatal("Error resolving server address:", err)
}
// Establish UDP connection to server
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
log.Fatal("Error connecting to server:", err)
}
defer conn.Close()
fmt.Println("Connected to UDP server. Type messages (or 'quit' to exit):")
// Initialize input scanner and response buffer
scanner := bufio.NewScanner(os.Stdin)
buffer := make([]byte, 1024)
// Main communication loop
for {
fmt.Print("> ")
// Read user input
if !scanner.Scan() {
break
}
message := strings.TrimSpace(scanner.Text())
if message == "quit" {
break
}
// Send message to server
_, err := conn.Write([]byte(message))
if err != nil {
log.Printf("Error sending message: %v", err)
continue
}
// Read server response
n, err := conn.Read(buffer)
if err != nil {
log.Printf("Error reading response: %v", err)
continue
}
fmt.Printf("Server response: %s\n", string(buffer[:n]))
}
fmt.Println("Client disconnected.")
}
Client Code Explanation
Address Resolution
serverAddr, err := net.ResolveUDPAddr("udp", "localhost:8080")
The net.ResolveUDPAddr
function converts a string address into a *UDPAddr
structure. The first parameter specifies the network type (“udp”, “udp4”, or “udp6”), and the second parameter is the address in the format “host:port”.
Connection Establishment
conn, err := net.DialUDP("udp", nil, serverAddr)
The net.DialUDP
function creates a UDP connection to the specified server address. The second parameter (local address) is set to nil
, allowing the system to choose an available local port automatically.
Data Transmission
_, err := conn.Write([]byte(message))
The Write
method sends data to the connected server. UDP packets have a maximum size limit (typically 65,507 bytes), but practical limits are often much smaller to avoid fragmentation.
Data Reception
n, err := conn.Read(buffer)
The Read
method receives data from the server into the provided buffer. The method returns the number of bytes read and any error encountered.
Testing the Implementation
Step 1: Build the Applications
Compile both programs using the Go compiler:
# Build the server
go build -o server server.go
# Build the client
go build -o client client.go
Step 2: Start the Server
Run the server in one terminal window:
./server
Expected output:
UDP Server listening on :8080
Step 3: Run the Client
Open a new terminal window and run the client:
./client
Expected output:
Connected to UDP server. Type messages (or 'quit' to exit):
>
Step 4: Test Communication
Type messages in the client terminal and observe the server responses:
> Hello, UDP Server!
Server response: Server received: Hello, UDP Server!
> How are you?
Server response: Server received: How are you?
> quit
Client disconnected.
The server terminal will display received messages:
UDP Server listening on :8080
Received from 127.0.0.1:54321: Hello, UDP Server!
Received from 127.0.0.1:54321: How are you?
Error Handling Considerations
Network Errors
UDP applications should handle various network-related errors:
- Connection timeouts: Set read/write timeouts using
SetReadDeadline()
andSetWriteDeadline()
- Network unreachability: Handle cases where the destination is unreachable
- Buffer overflow: Ensure receive buffers are appropriately sized
Example with Timeout Handling
import "time"
// Set a 5-second timeout for read operations
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
Packet Size Limitations
UDP has practical size limitations:
- Maximum UDP payload: 65,507 bytes (65,535 - 8 byte UDP header - 20 byte IP header)
- Ethernet MTU: 1,500 bytes (recommended to avoid fragmentation)
- Safe UDP payload: 1,472 bytes (1,500 - 20 IP - 8 UDP headers)
Best Practices
1. Buffer Management
Always allocate appropriately sized buffers and handle partial reads:
const MaxUDPSize = 65507
buffer := make([]byte, MaxUDPSize)
2. Concurrent Server Handling
For production servers, handle multiple clients concurrently:
func handleClient(conn *net.UDPConn, addr *net.UDPAddr, message []byte) {
// Process message in separate goroutine
response := processMessage(message)
conn.WriteToUDP(response, addr)
}
// In main loop:
go handleClient(conn, clientAddr, buffer[:n])
3. Graceful Shutdown
Implement proper cleanup mechanisms:
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("\nShutting down server...")
conn.Close()
os.Exit(0)
}()
Use Cases for UDP
UDP is suitable for applications where speed is prioritized over reliability:
- Real-time gaming: Low-latency multiplayer games
- Live streaming: Video and audio streaming applications
- DNS queries: Domain name resolution
- DHCP: Dynamic host configuration
- IoT sensors: Lightweight sensor data transmission
- Network discovery: Broadcasting service announcements
Conclusion
This tutorial demonstrated how to implement UDP client and server applications in Go using the standard net
package. The examples show basic message exchange functionality, but production applications should include additional error handling, security measures, and performance optimizations based on specific requirements.
Key takeaways:
- UDP provides fast, connectionless communication
- Go’s
net
package offers straightforward UDP implementation - Proper error handling and buffer management are essential
- Consider packet size limitations and network reliability
- UDP is ideal for applications requiring low latency over guaranteed delivery