User Datagram Protocol (UDP)

Subsections 8.3.1 and 8.3.2 respectively introduce TCP and HTTP, both of which are characterised by reliable transmission. This subsection will introduce another protocol at the transport layer, UDP. Unlike TCP, UDP is an unreliable transmission protocol. Common application protocols based on UDP include DNS, TFTP, and SNMP.

1. Introduction to UDP

UDP is a simple datagram-oriented communication protocol, which is located at the transport layer like TCP. UDP was designed by David P. Reed in 1980 and defined in RFC 768 (excerpted from Wikipedia). UDP is an unreliable transmission protocol. After data is sent through UDP, the underlying layer does not retain the data to prevent loss during transmission. UDP itself does not support error correction, queue management, or congestion control, but supports checksums.

UDP is a connectionless protocol. It does not need to establish a connection before sending data, unlike TCP. Data can be sent directly to the peer without establishing a connection. Because no connection needs to be established during data transmission, there is no need to maintain connection status, including sending and receiving status.

UDP is only responsible for transmission, so applications that use this protocol need to do more control over how data is sent and processed, such as how to ensure that peer's applications receive the data correctly and in order.

Compared with TCP, UDP cannot guarantee the safe and reliable transmission of data. You may wonder why the UDP protocol is still used. The connectionless nature of UDP results in less network and time overhead than TCP. The unreliable transmission of UDP (mainly the inability to guarantee retransmission after packet loss) is more suitable for applications such as streaming media, real-time multiplayer games, and IP voice, where losing a few packets will not affect the application. On the other hand, if TCP is used for retransmission, it will greatly increase network latency.

2. Creating a UDP server using socket

📝 Source code

For the source code of the function esp_create_udp_server(), please refer to book-esp32c3-iot-projects/test_case/udp_socket.

Creating a UDP server using socket is similar to creating a multicast group receiver as introduced in subsection 8.2.2. Both involve creating a UDP socket, configuring the bound port, and receiving and sending data. The function esp_create_udp_server() sets the SO_REUSEADDR option, allowing the server to bind the address of the already established connection. The code is as below:

esp_err_t esp_create_udp_server(void)
{
    char rx_buffer[128];
    char addr_str[32];
    esp_err_t err = ESP_FAIL;
    struct sockaddr_in server_addr;
    //Create a UDP socket
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        ESP_LOGE(TAG, "create socket error");
        return err;
    }
    ESP_LOGI(TAG, "create socket success, sock : %d", sock);
    //Enable SO_REUSEADDR, allowing the server to bind connected address
    int opt = 1;
    int ret = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    if (ret < 0) {
        ESP_LOGE(TAG, "Failed to set SO_REUSEADDR. Error %d", errno);
        goto exit;
    }

    //Bind the server to an interface with all-zero IP address and port number 3333
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
    ret = bind(sock, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (ret < 0) {
        ESP_LOGE(TAG, "bind socket failed, socketfd: %d, errno : %d", sock, errno);
        goto exit;
    }
    ESP_LOGI(TAG, "bind socket success");
    while (1) {
        struct sockaddr_in source_addr;
        socklen_t addr_len = sizeof(source_addr);
        memset(rx_buffer, 0, sizeof(rx_buffer));
        int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0,
                            (struct sockaddr *)&source_addr, &addr_len);
        // Reception error
        if (len < 0) {
            ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
            break;
        } else { //Data is received
            if (source_addr. sin_family == PF_INET) {
                inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr,
                            addr_str, sizeof(addr_str) - 1);
            }
            //String ends with NULL
            rx_buffer[len] = 0;
            ESP_LOGI(TAG, "Received %d bytes from %s:" , len, addr_str);
            ESP_LOGI(TAG, "%s", rx_buffer);
        }
    }
exit:
    close(sock);
    return err;
}

3. Creating a UDP client using socket

📝 Source code

For the source code of the function esp_create_udp_client(), please refer to book-esp32c3-iot-projects/test_case/udp_socket.

With the function esp_create_udp_client(), the UDP client can send data, including creating UDP sockets, configuring destination addresses and ports, calling socket interface sendto() to send data. The code is as below:

esp_err_t esp_create_udp_client(void)
{
    esp_err_t err = ESP_FAIL;
    char *payload = "Open the light";
    struct sockaddr_in dest_addr;
    dest_addr.sin_addr.s_addr = inet_addr(HOST_IP);
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(PORT);

    //Create a UDP socket
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
        return err;
    }
     
    //Send data
    int ret = sendto(sock, payload, strlen(payload), 0,
                    (struct sockaddr *)&dest_addr, sizeof(dest_addr));
    if (ret < 0) {
        ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
        goto exit;
    }
    ESP_LOGI(TAG, "Message send successfully");
    err = ESP_OK;
exit:
    close(sock);
    return err;
}

UDP clients do not need to establish a connection with the server, and can directly send data to the server. Since UDP creates unreliable connections, the data sent, such as "Open the light," may be lost, causing the peer to fail to receive it. Therefore, when writing code for the client and server, some logic should be added to the application layer code to ensure that data is not lost. For example, when the client sends "Open the light" to the server, the server returns "Open the light OK" after receiving it successfully. If the client receives the data within 1 second, it means that the data has been sent to the server correctly. If the client does not receive it within 1 second, it needs to send the data "Open the light" again.