Skip to content
Go back

Creating UDP Client and Server in Go

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

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:

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:

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:

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:

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:

References


Share this post on:

Previous Post
Why I Migrated from Next.js to Astro for My Personal Blog
Next Post
Learn Go in 60 Seconds