Chapter 85: TCP Socket Programming Basics
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamentals of the TCP/IP protocol and its role in reliable, connection-oriented data transmission.
- Learn the concept of network sockets and the Berkeley Sockets API as implemented in ESP-IDF.
- Implement a TCP client on an ESP32 to connect to a remote server and exchange data.
- Develop a TCP server on an ESP32 capable of accepting connections from clients and processing data.
- Perform data sending (
send()
) and receiving (recv()
) operations over TCP sockets. - Understand basic error handling and resource management for socket programming.
Introduction
In previous chapters, we’ve explored how ESP32 devices can connect to networks, obtain IP addresses (Chapter 80: DHCP Client Implementation), and resolve human-readable domain names to IP addresses (Chapter 82: DNS Client Implementation). These are crucial foundational steps. Now, we delve into one of the most widely used protocols for actual data communication over these networks: the Transmission Control Protocol (TCP).
TCP provides a reliable, ordered, and error-checked stream of data between applications running on hosts communicating over an IP network. Whether your ESP32 needs to fetch data from a web server, send sensor readings to a cloud platform, or communicate with a custom application on a PC, TCP is often the protocol of choice when reliability is paramount.
This chapter will introduce you to socket programming, the standard way applications interact with network protocols like TCP. We’ll cover the basics of creating TCP clients and servers on your ESP32, enabling your projects to engage in robust, connection-oriented communication.
Theory
TCP/IP Protocol Suite Overview
The TCP/IP suite is a set of communication protocols that form the basis of the internet and most modern networks. It’s typically visualized as a layered model:
Layer | Primary Responsibility | Key Protocols/Examples | Relevance to TCP |
---|---|---|---|
Application Layer | Provides interfaces for applications to access network services. Defines protocols for specific applications. | HTTP, FTP, SMTP, MQTT, DNS | Applications use TCP (via sockets) to send/receive data for these protocols. |
Transport Layer | Provides end-to-end communication services, including reliability, flow control, and error checking. | TCP (Transmission Control Protocol), UDP (User Datagram Protocol) | This is the layer where TCP operates, providing its core services. |
Internet Layer (Network Layer) | Handles addressing, routing, and forwarding of data packets across networks. | IP (Internet Protocol – IPv4, IPv6), ICMP | TCP relies on IP to deliver its segments to the correct destination host. |
Link Layer (Network Interface Layer) | Manages communication with the physical network medium. Handles framing and MAC addressing. | Ethernet, Wi-Fi (IEEE 802.11), PPP | Transmits IP packets (which encapsulate TCP segments) over the physical network. |
- Application Layer: Protocols used by applications (e.g., HTTP, FTP, SMTP, MQTT). This is where your application logic resides.
- Transport Layer: Provides end-to-end communication services. Key protocols here are TCP (Transmission Control Protocol) and UDP (User Datagram Protocol). This chapter focuses on TCP.
- Internet Layer (or Network Layer): Responsible for addressing, routing, and packet forwarding. The main protocol is IP (Internet Protocol).
- Link Layer (or Network Interface Layer): Handles communication with the physical network hardware (e.g., Ethernet, Wi-Fi).
TCP operates at the Transport Layer, relying on IP at the Internet Layer to route packets.
TCP Characteristics
TCP is designed to provide reliable end-to-end byte stream communication. Its key characteristics include:
- Connection-Oriented: Before any data is exchanged, a connection must be established between the client and server. This involves a three-way handshake:
- SYN (Synchronize): The client sends a SYN segment to the server to initiate a connection, indicating its initial sequence number.SYN-ACK (Synchronize-Acknowledge): The server responds with a SYN-ACK segment, acknowledging the client’s SYN and including its own initial sequence number.ACK (Acknowledge): The client sends an ACK segment back to the server, acknowledging the server’s SYN. The connection is now established. A similar process (a four-way handshake) is used to terminate connections gracefully.
sequenceDiagram participant Client participant Server Client->>Server: SYN (SEQ=x) activate Server Note right of Server: Server receives SYN,<br>allocates resources. Server-->>Client: SYN-ACK (SEQ=y, ACK=x+1) deactivate Server activate Client Note left of Client: Client receives SYN-ACK,<br>allocates resources,<br>connection established from client side. Client->>Server: ACK (SEQ=x+1, ACK=y+1) deactivate Client activate Server Note right of Server: Server receives ACK,<br>connection established from server side. deactivate Server Note over Client,Server: Connection Established <br> Data can now be exchanged
- Reliable Delivery: TCP ensures that data sent by one end is received correctly by the other. It achieves this using:
- Sequence Numbers: Each byte of data is assigned a sequence number.
- Acknowledgments (ACKs): The receiver sends ACKs for data received. If the sender doesn’t receive an ACK within a certain time, it retransmits the data.
- Checksums: Used to detect corrupted segments.
- Ordered Data Delivery: TCP guarantees that data is delivered to the application layer in the same order it was sent. If segments arrive out of order, TCP buffers and reorders them before passing them to the application.
- Flow Control: TCP uses a sliding window mechanism to prevent a fast sender from overwhelming a slow receiver. The receiver advertises its available buffer space (receive window), and the sender adjusts its transmission rate accordingly.
- Congestion Control: TCP employs algorithms to detect network congestion and reduce its transmission rate to avoid overloading the network.
- Full-Duplex Communication: Once a connection is established, data can flow in both directions simultaneously.
- Stream-Oriented: TCP treats data as a continuous stream of bytes. It does not inherently preserve message boundaries. If an application sends two separate “messages” of 100 bytes each, the receiver might get them as a single 200-byte chunk, or two 100-byte chunks, or even a 50-byte chunk followed by a 150-byte chunk. The application layer is responsible for message framing if needed.
Characteristic | Description | Mechanism(s) |
---|---|---|
Connection-Oriented | A logical connection must be established before data transfer and torn down afterwards. | Three-way handshake (SYN, SYN-ACK, ACK) for setup; Four-way handshake (FIN, ACK) for teardown. |
Reliable Delivery | Ensures data arrives correctly and completely, or notifies the sender of failure. | Sequence numbers, acknowledgments (ACKs), retransmissions, checksums. |
Ordered Data Delivery | Guarantees that data segments are delivered to the receiving application in the order they were sent. | Sequence numbers, buffering at the receiver to reorder out-of-sequence segments. |
Flow Control | Prevents a fast sender from overwhelming a slow receiver. | Sliding window protocol; receiver advertises its available buffer space (receive window). |
Congestion Control | Detects network congestion and reduces transmission rate to avoid overloading the network. | Algorithms like slow start, congestion avoidance, fast retransmit, fast recovery. |
Full-Duplex Communication | Allows data to be sent and received simultaneously over a single connection. | Independent send and receive paths within the TCP connection. |
Stream-Oriented | Treats data as a continuous stream of bytes, without inherent message boundaries. | Application layer is responsible for message framing if distinct messages are needed. |
Sockets
A socket is a software abstraction that serves as an endpoint for communication between two programs across a network. The Berkeley Sockets API (or POSIX sockets) is the de facto standard for network programming and is the API provided by LwIP (the TCP/IP stack used in ESP-IDF).
flowchart TD A["Your ESP32 Application<br/><small>(e.g., HTTP Client, Sensor Data Sender)</small>"] B["Socket API<br/><small>(Endpoint)</small>"] C["LwIP TCP/IP Stack<br/><small>(Handles TCP, IP, Link Layer details)</small>"] D["Network"] A -->|"send()"| B B -.->|"recv()"| A B --> C C --> D D -.-> C style A fill:#DBEAFE,stroke:#2563EB,stroke-width:2px style B fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px style C fill:#E0E7FF,stroke:#4338CA,stroke-width:2px style D fill:#F3F4F6,stroke:#6B7280,stroke-width:2px
When creating a socket, you typically specify:
- Address Family (Domain): Specifies the protocol family.
AF_INET
: For IPv4 addresses.AF_INET6
: For IPv6 addresses.
- Socket Type: Determines the communication semantics.
SOCK_STREAM
: For connection-oriented protocols like TCP. Provides sequenced, reliable, two-way byte streams.SOCK_DGRAM
: For connectionless datagram protocols like UDP.
- Protocol: Usually set to 0 to let the system choose the appropriate protocol based on the address family and socket type. For TCP, it’s
IPPROTO_TCP
.
Parameter of socket() |
Description | Common Values for TCP/IP | Example |
---|---|---|---|
Domain (Address Family) | Specifies the communication domain or protocol family to be used by the socket. |
AF_INET : IPv4 Internet protocols.AF_INET6 : IPv6 Internet protocols.
|
AF_INET (for IPv4 TCP) |
Type (Socket Type) | Determines the semantics of communication (e.g., connection-oriented or connectionless, reliability). |
SOCK_STREAM : Provides sequenced, reliable, two-way, connection-based byte streams (used by TCP).SOCK_DGRAM : Supports datagrams, which are connectionless, unreliable messages of a fixed maximum length (used by UDP).
|
SOCK_STREAM (for TCP) |
Protocol | Specifies the particular protocol to be used with the socket. Usually set to 0 to allow the system to select the appropriate protocol based on the domain and type. |
0 : Default protocol for the given domain and type.IPPROTO_TCP : Explicitly TCP (used if domain is AF_INET/AF_INET6 and type is SOCK_STREAM).IPPROTO_UDP : Explicitly UDP (used if type is SOCK_DGRAM).
|
0 or IPPROTO_TCP (for TCP) |
TCP Socket Workflow
The steps involved in using TCP sockets differ slightly for the server and the client.
Server-Side Workflow:
socket(domain, type, protocol)
: Create a socket endpoint. For a TCP server using IPv4, this would besocket(AF_INET, SOCK_STREAM, 0)
.bind(sockfd, addr, addrlen)
: Assign a local IP address and port number to the created socket. This makes the server reachable at a specific network address.listen(sockfd, backlog)
: Mark the socket as a passive socket that will be used to accept incoming connection requests. Thebacklog
parameter defines the maximum length of the queue for pending connections.accept(sockfd, addr, addrlen)
: Wait (block) for an incoming connection request from a client. When a client attempts to connect,accept()
creates a new socket (the “connected socket”) specifically for communication with that client and returns its file descriptor. The original listening socket remains open to accept further connections.- Data Exchange (
recv()
,send()
): Use the connected socket returned byaccept()
to receive data from (recv()
orread()
) and send data to (send()
orwrite()
) the client. close(connected_sockfd)
: When communication with a specific client is finished, close the connected socket for that client.close(listening_sockfd)
: When the server is shutting down, close the original listening socket.
graph TD A["Start Server Application"] --> B["socket(): Create Listening Socket"]; style A fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 style B fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF B -- "Success" --> C["bind(): Assign IP Address & Port"]; style C fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF B -- "Failure" --> Z1["Error Handling: Socket Creation Failed"]; style Z1 fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B C -- "Success" --> D["listen(): Enable Accepting Connections"]; style D fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF C -- "Failure" --> Z2["Error Handling: Bind Failed"]; style Z2 fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B D -- "Success" --> E{"Wait for Client Connection"}; style E fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E D -- "Failure" --> Z3["Error Handling: Listen Failed"]; style Z3 fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B E -- "Connection Request" --> F["accept(): Create New Socket for Client"]; style F fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 F -- "Success" --> G["Communicate with Client"]; subgraph G direction LR G1["recv(): Receive Data"] G2["send(): Send Data"] end style G fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF F -- "Failure (e.g. resource issue)" --> E G --> H["close(): Close Client Socket"]; style H fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF H --> E E -- "Server Shutdown Signal" --> I["close(): Close Listening Socket"]; style I fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF I --> J["End Server Application"]; style J fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 Z1 --> J Z2 --> J Z3 --> J
Client-Side Workflow:
socket(domain, type, protocol)
: Create a socket endpoint (e.g.,socket(AF_INET, SOCK_STREAM, 0)
).connect(sockfd, server_addr, addrlen)
: Establish a connection to a specific server.server_addr
contains the server’s IP address and port number. This function initiates the TCP three-way handshake.- Data Exchange (
send()
,recv()
): Once connected, use the socket to send data to and receive data from the server. close(sockfd)
: When communication is finished, close the socket to terminate the connection.
graph TD A["Start Client Application"] --> B["socket(): Create Socket"]; style A fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 style B fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF B -- "Success" --> C["Prepare Server Address<br><i>(IP & Port)</i>"]; style C fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF B -- "Failure" --> Z1["Error Handling: Socket Creation Failed"]; style Z1 fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B C --> D["connect(): Establish Connection to Server"]; style D fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF D -- "Connection Established" --> E["Communicate with Server"]; subgraph E direction LR E1["send(): Send Data"] E2["recv(): Receive Data"] end style E fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46 D -- "Connection Failed" --> F{"Retry or Handle Error"}; style F fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E F -- "Retry" --> D F -- "Error Handled/Max Retries" --> Z2["Error Handling: Connection Failed"]; style Z2 fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B E -- "Communication Finished or Error" --> G["close(): Close Socket"]; style G fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF G --> H["End Client Application"]; style H fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 Z1 --> H Z2 --> H
IP Addresses and Port Numbers
To establish a TCP connection, you need the server’s IP address and the port number on which the server application is listening.
struct sockaddr_in
(for IPv4): This structure is used to specify IPv4 socket addresses. Key members include:sin_family
: Set toAF_INET
.sin_port
: The port number (must be in network byte order).sin_addr
: Astruct in_addr
containing the IPv4 address (must be in network byte order).
struct sockaddr_in6
(for IPv6): Used for IPv6 addresses.- Byte Order: Computers can store multi-byte numbers in different orders (Little Endian or Big Endian). Network protocols, however, use a standard Network Byte Order (which is Big Endian). Functions are provided to convert between host byte order and network byte order:
htons()
: Host To Network Short (16-bit, for port numbers).htonl()
: Host To Network Long (32-bit, for IPv4 addresses).ntohs()
: Network To Host Short.ntohl()
: Network To Host Long. It’s crucial to use these functions when populatingsockaddr_in
structures or interpreting values received from the network.
IPv4 Socket Address Structure: struct sockaddr_in |
||
---|---|---|
Member | Type | Description |
sin_family |
sa_family_t |
Address family. Must be set to AF_INET for IPv4. |
sin_port |
in_port_t |
Port number (16-bit). Must be in Network Byte Order. Use htons() . |
sin_addr |
struct in_addr |
IPv4 address (32-bit, contains s_addr member). Must be in Network Byte Order. Use htonl() for raw addresses or functions like inet_aton() / inet_pton() for string conversions. |
sin_zero[8] |
char |
Padding to make the structure the same size as struct sockaddr . Should be set to all zeros. |
Byte Order Conversion Functions | |
---|---|
Function | Purpose |
htons() |
“Host to Network Short”: Converts a 16-bit unsigned integer (e.g., port number) from host byte order to network byte order (Big Endian). |
htonl() |
“Host to Network Long”: Converts a 32-bit unsigned integer (e.g., IPv4 address) from host byte order to network byte order. |
ntohs() |
“Network to Host Short”: Converts a 16-bit unsigned integer from network byte order to host byte order. |
ntohl() |
“Network to Host Long”: Converts a 32-bit unsigned integer from network byte order to host byte order. |
Practical Examples
This section provides practical examples of implementing TCP clients and servers on an ESP32. We’ll use the LwIP Berkeley Sockets API.
Prerequisites
- ESP-IDF v5.x installed and configured with VS Code.
- An ESP32 development board.
- A Wi-Fi network for the ESP32 to connect to (for client examples, this network should have internet access; for server examples, clients will connect to the ESP32 on this network).
- For testing:
- A public TCP echo server (e.g.,
tcpbin.com
on port4242
) or a simple TCP server running on your PC (using tools likenetcat
or a Python script). - A TCP client tool on your PC (e.g.,
netcat
,telnet
, or a Python script) to connect to the ESP32 server.
- A public TCP echo server (e.g.,
Common Wi-Fi Station Setup
For brevity, assume the connect_wifi_sta()
function from Chapter 84 (or a similar function that initializes Wi-Fi, connects to an AP, and signals connection status via an event group s_wifi_event_group
with WIFI_CONNECTED_BIT
) is available and has been called in app_main
.
Project Setup
- Create a new ESP-IDF project (e.g.,
esp32_tcp_socket_example
). - Include necessary headers in your
main.c
:
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h" // If using common Wi-Fi setup
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "lwip/err.h"
#include "lwip/sockets.h" // For socket functions
#include "lwip/sys.h"
#include <lwip/netdb.h> // For getaddrinfo
Example 1: TCP Client (Connects to a Public Echo Server)
This example shows the ESP32 acting as a TCP client connecting to tcpbin.com:4242
, which echoes back any data sent to it.
// In main.c
static const char *TAG_TCP_CLIENT = "tcp_client";
// Define HOST_IP_ADDR and PORT based on your target server
// For tcpbin.com, first resolve "tcpbin.com" to an IP.
// As of writing, one IP for tcpbin.com is 52.203.83.192 (this can change!)
// It's better to use getaddrinfo for hostname resolution.
#define HOST_NAME "tcpbin.com"
#define PORT 4242
void tcp_client_task(void *pvParameters) {
char rx_buffer[128];
char tx_buffer[128];
struct sockaddr_in dest_addr;
int sock = -1;
// Wait for Wi-Fi connection (assuming s_wifi_event_group and WIFI_CONNECTED_BIT are set up)
// xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
// For standalone example, ensure Wi-Fi is connected before this task runs.
ESP_LOGI(TAG_TCP_CLIENT, "Attempting to resolve hostname: %s", HOST_NAME);
struct addrinfo hints = { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM };
struct addrinfo *res;
int err = getaddrinfo(HOST_NAME, NULL, &hints, &res); // Port will be set later
if(err != 0 || res == NULL) {
ESP_LOGE(TAG_TCP_CLIENT, "DNS lookup failed err=%d res=%p", err, res);
vTaskDelete(NULL);
return;
}
// res contains a linked list of addresses. Use the first one.
struct in_addr *addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
ESP_LOGI(TAG_TCP_CLIENT, "DNS lookup succeeded. IP=%s", inet_ntoa(*addr));
dest_addr.sin_addr.s_addr = addr->s_addr; // Use resolved IP
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(PORT);
freeaddrinfo(res); // Free the resources allocated by getaddrinfo
while (1) { // Retry loop for connection
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (sock < 0) {
ESP_LOGE(TAG_TCP_CLIENT, "Unable to create socket: errno %d", errno);
vTaskDelay(pdMS_TO_TICKS(1000));
continue;
}
ESP_LOGI(TAG_TCP_CLIENT, "Socket created, attempting to connect to %s:%d", inet_ntoa(dest_addr.sin_addr), PORT);
err = connect(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0) {
ESP_LOGE(TAG_TCP_CLIENT, "Socket connect failed: errno %d", errno);
close(sock);
sock = -1;
vTaskDelay(pdMS_TO_TICKS(4000));
continue; // Retry connection
}
ESP_LOGI(TAG_TCP_CLIENT, "Successfully connected to %s:%d", inet_ntoa(dest_addr.sin_addr), PORT);
// Connection successful, send and receive data
for (int i = 0; i < 3; i++) {
sprintf(tx_buffer, "Hello from ESP32! Message #%d\r\n", i);
int err = send(sock, tx_buffer, strlen(tx_buffer), 0);
if (err < 0) {
ESP_LOGE(TAG_TCP_CLIENT, "Error occurred during sending: errno %d", errno);
break; // Break inner loop, will lead to closing socket and retrying connection
}
ESP_LOGI(TAG_TCP_CLIENT, "Sent: %s", tx_buffer);
int len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0) {
ESP_LOGE(TAG_TCP_CLIENT, "recv failed: errno %d", errno);
break; // Break inner loop
} else if (len == 0) {
ESP_LOGW(TAG_TCP_CLIENT, "Connection closed by server");
break; // Break inner loop
} else {
rx_buffer[len] = 0; // Null-terminate whatever we received
ESP_LOGI(TAG_TCP_CLIENT, "Received %d bytes: %s", len, rx_buffer);
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
if (sock != -1) {
ESP_LOGI(TAG_TCP_CLIENT, "Shutting down socket and restarting...");
shutdown(sock, SHUT_RDWR); // Graceful shutdown
close(sock);
sock = -1;
}
vTaskDelay(pdMS_TO_TICKS(5000)); // Wait before trying to connect again
}
vTaskDelete(NULL); // Should not reach here in this loop
}
// In app_main:
// void app_main(void) {
// // NVS, Wi-Fi init (connect_wifi_sta()) ...
// EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
// if (bits & WIFI_CONNECTED_BIT) {
// xTaskCreate(tcp_client_task, "tcp_client", 4096, NULL, 5, NULL);
// }
// }
Explanation (TCP Client):
- Resolves hostname using
getaddrinfo
. - Creates a socket using
socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
. - Populates
dest_addr
(astruct sockaddr_in
) with the server’s IP and port.htons(PORT)
converts the port to network byte order. connect()
attempts to establish a connection.send()
transmits data to the server.recv()
waits to receive data from the server. It can return:> 0
: Number of bytes received.0
: Connection closed gracefully by the peer.< 0
: An error occurred.
shutdown(sock, SHUT_RDWR)
gracefully closes both send and receive paths.close(sock)
releases the socket descriptor.- The outer
while(1)
loop with delays implements a retry mechanism for connection.
Example 2: TCP Server (ESP32 as a Simple Echo Server)
This example shows the ESP32 acting as a TCP server, listening on a specific port, and echoing back any data it receives from a client.
// In main.c
static const char *TAG_TCP_SERVER = "tcp_server";
#define TCP_SERVER_PORT 3333
#define KEEPALIVE_IDLE 5 // Keepalive idle time (seconds)
#define KEEPALIVE_INTERVAL 5 // Keepalive interval time (seconds)
#define KEEPALIVE_COUNT 3 // Keepalive packet retry count
void tcp_server_task(void *pvParameters) {
char rx_buffer[128];
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int listen_sock = -1;
// Create listening socket
listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (listen_sock < 0) {
ESP_LOGE(TAG_TCP_SERVER, "Unable to create socket: errno %d", errno);
goto server_cleanup;
}
ESP_LOGI(TAG_TCP_SERVER, "Listening socket created");
// Bind to local IP address and port
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Bind to all available interfaces
server_addr.sin_port = htons(TCP_SERVER_PORT);
int err = bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (err != 0) {
ESP_LOGE(TAG_TCP_SERVER, "Socket bind failed: errno %d", errno);
goto server_cleanup;
}
ESP_LOGI(TAG_TCP_SERVER, "Socket bound to port %d", TCP_SERVER_PORT);
// Listen for incoming connections
err = listen(listen_sock, 1); // Listen with a backlog of 1
if (err != 0) {
ESP_LOGE(TAG_TCP_SERVER, "Error occurred during listen: errno %d", errno);
goto server_cleanup;
}
ESP_LOGI(TAG_TCP_SERVER, "Socket listening...");
while (1) {
int connected_sock = accept(listen_sock, (struct sockaddr *)&client_addr, &client_addr_len);
if (connected_sock < 0) {
ESP_LOGE(TAG_TCP_SERVER, "Unable to accept connection: errno %d", errno);
// If accept fails, the listen_sock is still valid, so continue listening.
vTaskDelay(pdMS_TO_TICKS(100)); // Small delay before retrying accept
continue;
}
ESP_LOGI(TAG_TCP_SERVER, "Connection accepted from %s:%d",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// Configure TCP Keep-Alive
int keepalive = 1;
int idle = KEEPALIVE_IDLE;
int interval = KEEPALIVE_INTERVAL;
int count = KEEPALIVE_COUNT;
setsockopt(connected_sock, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(int));
setsockopt(connected_sock, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(int));
setsockopt(connected_sock, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(int));
setsockopt(connected_sock, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(int));
ESP_LOGI(TAG_TCP_SERVER, "TCP Keep-Alive enabled for accepted socket.");
// Communication loop with the connected client
int len;
do {
len = recv(connected_sock, rx_buffer, sizeof(rx_buffer) - 1, 0);
if (len < 0) {
ESP_LOGE(TAG_TCP_SERVER, "recv failed: errno %d", errno);
} else if (len == 0) {
ESP_LOGW(TAG_TCP_SERVER, "Connection closed by client");
} else {
rx_buffer[len] = 0; // Null-terminate
ESP_LOGI(TAG_TCP_SERVER, "Received %d bytes from %s:", len, inet_ntoa(client_addr.sin_addr));
ESP_LOGI(TAG_TCP_SERVER, "%s", rx_buffer);
// Echo back
int written = send(connected_sock, rx_buffer, len, 0);
if (written < 0) {
ESP_LOGE(TAG_TCP_SERVER, "Error occurred during sending: errno %d", errno);
// This error might mean the client disconnected abruptly.
// Break from the loop to close this client's socket.
len = -1; // Force loop exit
} else {
ESP_LOGI(TAG_TCP_SERVER, "Echoed %d bytes.", written);
}
}
} while (len > 0); // Continue while data is received and no send error
ESP_LOGI(TAG_TCP_SERVER, "Closing connection with %s:%d",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
shutdown(connected_sock, SHUT_RDWR);
close(connected_sock);
}
server_cleanup:
if (listen_sock != -1) {
close(listen_sock);
}
ESP_LOGE(TAG_TCP_SERVER, "TCP server task exiting due to error.");
vTaskDelete(NULL);
}
// In app_main:
// void app_main(void) {
// // NVS, Wi-Fi init (connect_wifi_sta()) ...
// // Ensure Wi-Fi is connected and ESP32 has an IP before starting the server.
// EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
// if (bits & WIFI_CONNECTED_BIT) {
// xTaskCreate(tcp_server_task, "tcp_server", 4096, NULL, 5, NULL);
// }
// }
Explanation (TCP Server):
- Creates a listening socket
listen_sock
. bind()
associates the socket with all available IP addresses of the ESP32 (INADDR_ANY
) on portTCP_SERVER_PORT
.listen()
enables the socket to accept incoming connections.backlog=1
means one pending connection can be queued.- The
while(1)
loop continuously callsaccept()
. accept()
blocks until a client connects. It returns a new socket descriptorconnected_sock
for this specific client.- TCP Keep-Alive:
setsockopt()
is used to enable TCP keep-alive packets. This helps detect if a client has disconnected ungracefully (e.g., power loss, network cable unplugged) without sending a FIN packet. - The inner
do-while
loop handles communication with the connected client:recv()
receives data.send()
echoes the data back.
- If
recv()
returns 0 (client closed connection) or an error occurs, the inner loop terminates. shutdown()
andclose()
are called onconnected_sock
to terminate the specific client connection. The server then goes back toaccept()
another client.- The
server_cleanup
label andgoto
are used for error handling during setup to ensure the listening socket is closed if an unrecoverable error occurs.
Build Instructions
- Ensure Wi-Fi credentials in the common Wi-Fi setup are correct.
- In
main/main.c
, include the chosen example code (client or server) and call its task creation function fromapp_main
after ensuring Wi-Fi is connected. - Build the project using the ESP-IDF build system (VS Code:
ESP-IDF: Build your project
).
Run/Flash/Observe Steps
- Flash the project to your ESP32 (
ESP-IDF: Flash your project
). - Open the ESP-IDF Monitor (
ESP-IDF: Monitor your device
).
For TCP Client Example:
- The ESP32 will attempt to connect to
tcpbin.com:4242
(or your configured server). - Observe the logs for connection status, data sent, and data received.
- If
tcpbin.com
is down or its IP has changed significantly, you might need to find an alternative public TCP echo server or set up your own.
For TCP Server Example:
- The ESP32 will start a TCP server on port 3333. The log will show “Socket listening…”.
- Note the IP address assigned to your ESP32 from the Wi-Fi connection logs.
- On your PC (connected to the same network as the ESP32):
- Use
netcat
(Linux/macOS):nc <ESP32_IP_ADDRESS> 3333
- Or
telnet
(Windows/Linux/macOS):telnet <ESP32_IP_ADDRESS> 3333
- Once connected, type some text and press Enter. The ESP32 should echo it back.
- Observe the ESP32’s serial monitor for logs about accepted connections and data exchange.
- Close the
netcat
/telnet
session to see the ESP32 close the client connection.
- Use
Variant Notes
- API Consistency: The Berkeley Sockets API provided by LwIP is consistent across all ESP32 variants (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2) when using ESP-IDF v5.x. The C code for socket operations will generally be portable.
- Performance and Resources:
- Concurrent Connections: The maximum number of concurrent TCP connections a server can handle depends on available RAM (for socket buffers, task stacks if each client is handled in a new task) and CPU processing power. Dual-core variants (ESP32, ESP32-S3) might offer better performance for handling multiple clients than single-core variants (ESP32-S2, ESP32-C3, ESP32-C6, ESP32-H2).
- Throughput: Data throughput over TCP can be affected by Wi-Fi/Ethernet performance, CPU speed (for data processing and LwIP stack execution), and overall system load.
- RAM: Each active socket consumes memory for transmit and receive buffers. LwIP’s memory pools (
MEMP_NUM_TCP_PCB
,MEMP_NUM_TCP_PCB_LISTEN
, etc. inlwipopts.h
) define hard limits. Default configurations are usually sufficient for several connections, but may need tuning for applications with many concurrent connections.
- Network Interfaces: The examples primarily use Wi-Fi. If your ESP32 variant has Ethernet, TCP sockets work identically over an initialized Ethernet interface. Ensure
INADDR_ANY
is used inbind()
for servers to listen on all configured network interfaces.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Forgetting Error Checks | – Unexpected behavior, crashes, or silent failures. – Functions like socket() , connect() , send() , recv() returning -1. |
Fix:
|
Byte Order Issues | – Connection refused or fails silently. – Server receives garbled port numbers or client connects to wrong port. |
Fix:
|
Blocking Operations Freezing Tasks | – ESP32 becomes unresponsive, watchdog timer may trigger. |
Fix:
|
Server: Mishandling accept() or Client Disconnects |
– Server stops accepting new connections after an error or client disconnect. |
Fix:
|
Client: Server Unreachable / Firewall | – connect() fails with ETIMEDOUT , ECONNREFUSED , or EHOSTUNREACH . |
Fix:
|
Forgetting close() / Socket Leaks |
– socket() or accept() eventually fail with ENFILE (too many open files) or EMFILE , or out of memory errors. |
Fix:
|
Partial recv() Reads |
– Application receives incomplete messages or misinterprets data. |
Fix:
|
Incorrect use of getaddrinfo() |
– DNS lookup fails (EAI_NONAME , etc.).– connect() fails due to incorrect address. |
Fix:
|
Exercises
- HTTP GET Request Client:
- Modify the TCP client example to connect to
httpbin.org
on port 80. - After connecting, send the following HTTP GET request:
GET /ip HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n
- Receive and print the full HTTP response from the server. Note the “Connection: close” header, which tells the server to close the connection after sending the response.
- Modify the TCP client example to connect to
- Server: Client Information Logging:
- Enhance the TCP echo server example. When a new client connects, log the client’s IP address and port number obtained from the
client_addr
structure filled byaccept()
. - Also, log when that specific client disconnects.
- Enhance the TCP echo server example. When a new client connects, log the client’s IP address and port number obtained from the
- Simple Command Server:
- Modify the TCP server to accept simple string commands from a client.
- If the client sends “GET_TEMP”, the server responds with a fictional temperature (e.g., “Temperature: 25.5 C”).
- If the client sends “GET_HUMIDITY”, the server responds with a fictional humidity (e.g., “Humidity: 45.2 %”).
- If any other command is received, respond with “Unknown command”.
- Serial-to-TCP Bridge (Client):
- Write an ESP32 application that acts as a TCP client.
- It should connect to a specified server IP and port.
- Any data typed into the ESP32’s serial monitor (and Enter pressed) should be sent over the TCP connection.
- Any data received from the TCP server should be printed to the serial monitor.
- Client Connection Retry with Backoff:
- Modify the TCP client example. If
connect()
fails, implement a retry mechanism. - The client should try to connect up to 5 times.
- Implement an exponential backoff for retries: wait 1 second before the first retry, 2 seconds before the second, 4 before the third, and so on.
- Log each attempt and the delay. If all retries fail, log a final error and terminate the task.
- Modify the TCP client example. If
Summary
- TCP is a connection-oriented, reliable, stream-based protocol in the TCP/IP suite, ensuring ordered and error-checked data delivery.
- Sockets are the programming interface for network communication. The Berkeley Sockets API is used in ESP-IDF via LwIP.
- TCP Server Workflow:
socket()
->bind()
->listen()
->accept()
(creates new socket for client) ->recv()
/send()
->close()
client socket. - TCP Client Workflow:
socket()
->connect()
->send()
/recv()
->close()
. - Key socket functions include
socket()
,bind()
,listen()
,connect()
,accept()
,send()
,recv()
, andclose()
. - IP addresses and port numbers are specified using
struct sockaddr_in
(IPv4), with values converted to network byte order (htons
,htonl
). - Proper error checking for all socket operations and resource management (closing sockets) are crucial for robust applications.
- TCP is a stream protocol; applications must handle message framing and potentially partial
recv()
calls.
Further Reading
- ESP-IDF LwIP Sockets Documentation: Check the ESP-IDF Programming Guide for sections on LwIP and BSD Sockets API usage. (Often found under Networking APIs).
- Beej’s Guide to Network Programming: An excellent and widely recommended resource for learning socket programming.
- RFC 793 – Transmission Control Protocol (TCP): The official specification for TCP.
- LwIP Project Documentation: For details on the Lightweight IP stack used by ESP-IDF.
- LwIP Wiki & Documentation (The specific API docs are often included with the source code or summarized by Espressif).
- ESP-IDF Protocol Examples: The
protocols
directory in ESP-IDF examples often contains socket-based examples (e.g.,sockets/tcp_client
,sockets/tcp_server
).
