DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
client.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file client.cpp
6 * @brief Layer 4 outbound TCP client transport (pooled). See client.h.
7 *
8 * Mirrors the server transport's cross-thread rule: every raw lwIP call runs in
9 * tcpip_thread via tcpip_api_call(). Each slot owns its pcb and an SPSC wire ring
10 * (producer = the lwIP recv callback in tcpip_thread; consumer = the caller's
11 * loop/blocking task). The rings use `volatile` indices, matching the shipped
12 * per-client implementations this consolidates.
13 */
14
15#include "client.h"
16
17// Compiles only on Arduino AND only when a client transport is actually enabled
18// (HTTP client / MQTT / WS client). A server-only build leaves DNS_RESOLVER off,
19// so the resolver symbols this unit calls would not be declared - see
20// DETWS_NEED_DET_CLIENT in ServerConfig.h.
21#if defined(ARDUINO) && DETWS_NEED_DET_CLIENT
22
23#include "lwip/priv/tcpip_priv.h"
24#include "lwip/tcp.h"
25#include "services/clock.h" // detws_millis()
26#include "services/dns_resolver/dns_resolver.h" // shared host->IP resolve (one DNS owner)
27#include "shared_primitives/ring.h" // shared DetAtomic + SPSC ring drain (same primitive as the server)
28#include <Arduino.h> // delay()
29#include <string.h>
30
31struct ClientConn
32{
33 struct tcp_pcb *pcb;
34 volatile bool in_use;
35 volatile bool connected;
36 volatile bool closed; // peer FIN or error
37 uint8_t rx[DETWS_CLIENT_RX_BUF];
38 DetAtomic<size_t> head; // producer (lwIP recv cb); acquire/release SPSC, same as the server ring
39 DetAtomic<size_t> tail; // consumer (caller)
40};
41
42// Outbound client connection pool, owned by one instance (internal linkage): the per-slot
43// ClientConn state. One named owner, unreachable from any other translation unit.
44struct DetClientCtx
45{
46 ClientConn cc[DETWS_CLIENT_CONNS];
47};
48static DetClientCtx s_client;
49
50// Hostname resolution is delegated to the shared DNS resolver (detws_dns_resolve,
51// services/dns_resolver) so there is one owner of the gethostbyname-marshal +
52// deadline-poll pattern instead of a private copy here.
53
54// --- lwIP callbacks (tcpip_thread); arg = the owning ClientConn* -------------
55
56static err_t cc_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
57{
58 (void)err;
59 ClientConn *c = (ClientConn *)arg;
60 if (!c)
61 return ERR_OK;
62 if (p == nullptr)
63 {
64 c->closed = true; // peer closed
65 return ERR_OK;
66 }
67 // Wire bytes -> ring via the shared producer primitive (same as the server): if
68 // the whole segment will not fit, refuse it (lwIP retains + redelivers); else
69 // bulk-memcpy each pbuf span and publish head once.
70 (void)tpcb;
71 if (p->tot_len > det_ring_free(c->head, c->tail, DETWS_CLIENT_RX_BUF))
72 return ERR_MEM;
73 size_t h = c->head; // sole producer of head; advance a local and publish once
74 for (struct pbuf *q = p; q; q = q->next)
75 h = det_ring_write_span(c->rx, DETWS_CLIENT_RX_BUF, h, (const uint8_t *)q->payload, q->len);
76 c->head = h; // single release store publishes the whole segment
77 // Do NOT tcp_recved() here. The window is reopened by det_client_read() as the
78 // caller drains (ack-on-consume), so it tracks ring occupancy and the peer can
79 // never overflow the ring - same model as the server transport. ACKing on copy
80 // would decouple the window from drainage and deadlock a large inbound transfer
81 // once DETWS_CLIENT_RX_BUF < TCP_WND.
82 pbuf_free(p);
83 return ERR_OK;
84}
85
86static err_t cc_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
87{
88 (void)tpcb;
89 ClientConn *c = (ClientConn *)arg;
90 if (c)
91 {
92 if (err == ERR_OK)
93 c->connected = true;
94 else
95 c->closed = true;
96 }
97 return ERR_OK;
98}
99
100static void cc_err(void *arg, err_t err)
101{
102 (void)err;
103 ClientConn *c = (ClientConn *)arg;
104 if (c)
105 {
106 c->pcb = nullptr; // lwIP already freed it
107 c->closed = true;
108 }
109}
110
111// --- tcpip_thread-marshaled ops ---------------------------------------------
112
113struct CcConnCall
114{
115 struct tcpip_api_call_data base;
116 ClientConn *c;
117 ip_addr_t addr;
118 uint16_t port;
119 err_t result;
120};
121struct CcSendCall
122{
123 struct tcpip_api_call_data base;
124 ClientConn *c;
125 const void *data;
126 u16_t len;
127 err_t result;
128};
129struct CcRecvedCall
130{
131 struct tcpip_api_call_data base;
132 ClientConn *c;
133 u16_t len;
134};
135
136static err_t cc_do_connect(struct tcpip_api_call_data *cd)
137{
138 CcConnCall *k = (CcConnCall *)cd;
139 ClientConn *c = k->c;
140 c->pcb = tcp_new_ip_type(IPADDR_TYPE_V4);
141 if (!c->pcb)
142 {
143 k->result = ERR_MEM;
144 return ERR_OK;
145 }
146 tcp_arg(c->pcb, c);
147 tcp_recv(c->pcb, cc_recv);
148 tcp_err(c->pcb, cc_err);
149 k->result = tcp_connect(c->pcb, &k->addr, k->port, cc_connected);
150 return ERR_OK;
151}
152
153static err_t cc_do_send(struct tcpip_api_call_data *cd)
154{
155 CcSendCall *k = (CcSendCall *)cd;
156 ClientConn *c = k->c;
157 if (!c->pcb)
158 {
159 k->result = ERR_CONN;
160 return ERR_OK;
161 }
162 k->result = tcp_write(c->pcb, k->data, k->len, TCP_WRITE_FLAG_COPY);
163 if (k->result == ERR_OK)
164 tcp_output(c->pcb);
165 return ERR_OK;
166}
167
168static err_t cc_do_close(struct tcpip_api_call_data *cd)
169{
170 CcSendCall *k = (CcSendCall *)cd;
171 ClientConn *c = k->c;
172 if (c->pcb)
173 {
174 tcp_arg(c->pcb, nullptr);
175 tcp_recv(c->pcb, nullptr);
176 tcp_err(c->pcb, nullptr);
177 if (tcp_close(c->pcb) != ERR_OK)
178 tcp_abort(c->pcb);
179 c->pcb = nullptr;
180 }
181 return ERR_OK;
182}
183
184static err_t cc_do_recved(struct tcpip_api_call_data *cd)
185{
186 CcRecvedCall *k = (CcRecvedCall *)cd;
187 if (k->c->pcb)
188 tcp_recved(k->c->pcb, k->len); // reopen the window by the consumed bytes
189 return ERR_OK;
190}
191
192// --- public API --------------------------------------------------------------
193
194int det_client_open(const char *host, uint16_t port, uint32_t timeout_ms)
195{
196 int cid = -1;
197 for (int i = 0; i < DETWS_CLIENT_CONNS; i++)
198 if (!s_client.cc[i].in_use)
199 {
200 cid = i;
201 break;
202 }
203 if (cid < 0)
204 return -1; // pool full
205
206 ClientConn *c = &s_client.cc[cid];
207 c->pcb = nullptr;
208 c->connected = false;
209 c->closed = false;
210 c->head = 0;
211 c->tail = 0;
212 c->in_use = true;
213
214 // Resolve through the shared DNS owner (its own DETWS_DNS_TIMEOUT_MS budget),
215 // then give the connect its full timeout_ms.
216 uint32_t ip = 0;
217 if (!detws_dns_resolve(host, &ip))
218 {
219 c->in_use = false;
220 return -2; // DNS failure
221 }
222 uint32_t deadline = detws_millis() + timeout_ms;
223
224 CcConnCall k;
225 memset(&k, 0, sizeof(k));
226 k.c = c;
227 IP_ADDR4(&k.addr, (uint8_t)(ip >> 24), (uint8_t)(ip >> 16), (uint8_t)(ip >> 8), (uint8_t)ip);
228 k.port = port;
229 tcpip_api_call(cc_do_connect, &k.base);
230 if (k.result != ERR_OK)
231 {
232 det_client_close(cid);
233 return -3; // connect issue
234 }
235 while (!c->connected && !c->closed && (int32_t)(deadline - detws_millis()) > 0)
236 delay(5);
237 if (!c->connected)
238 {
239 det_client_close(cid);
240 return -4; // connect timeout / refused
241 }
242 return cid;
243}
244
245bool det_client_connected(int cid)
246{
247 return cid >= 0 && cid < DETWS_CLIENT_CONNS && s_client.cc[cid].in_use && s_client.cc[cid].connected &&
248 !s_client.cc[cid].closed;
249}
250
251bool det_client_is_closed(int cid)
252{
253 if (cid < 0 || cid >= DETWS_CLIENT_CONNS)
254 return true;
255 return s_client.cc[cid].closed;
256}
257
258bool det_client_send(int cid, const void *data, size_t len)
259{
260 if (cid < 0 || cid >= DETWS_CLIENT_CONNS || !s_client.cc[cid].in_use)
261 return false;
262 CcSendCall k;
263 memset(&k, 0, sizeof(k));
264 k.c = &s_client.cc[cid];
265 k.data = data;
266 k.len = (u16_t)(len > 0xFFFF ? 0xFFFF : len);
267 tcpip_api_call(cc_do_send, &k.base);
268 return k.result == ERR_OK;
269}
270
271size_t det_client_available(int cid)
272{
273 if (cid < 0 || cid >= DETWS_CLIENT_CONNS)
274 return 0;
275 ClientConn *c = &s_client.cc[cid];
276 return det_ring_available(c->head, c->tail, DETWS_CLIENT_RX_BUF);
277}
278
279size_t det_client_read(int cid, uint8_t *buf, size_t cap)
280{
281 if (cid < 0 || cid >= DETWS_CLIENT_CONNS)
282 return 0;
283 ClientConn *c = &s_client.cc[cid];
284 size_t n = det_ring_read(c->rx, DETWS_CLIENT_RX_BUF, c->head, c->tail, buf, cap);
285 if (n > 0 && c->pcb)
286 {
287 // Ack-on-consume: reopen the receive window by exactly what we just drained.
288 CcRecvedCall k;
289 memset(&k, 0, sizeof(k));
290 k.c = c;
291 k.len = (u16_t)n;
292 tcpip_api_call(cc_do_recved, &k.base);
293 }
294 return n;
295}
296
297void det_client_close(int cid)
298{
299 if (cid < 0 || cid >= DETWS_CLIENT_CONNS || !s_client.cc[cid].in_use)
300 return;
301 CcSendCall k;
302 memset(&k, 0, sizeof(k));
303 k.c = &s_client.cc[cid];
304 tcpip_api_call(cc_do_close, &k.base);
305 s_client.cc[cid].in_use = false;
306}
307
308#else // !ARDUINO - host stub (the clients are ARDUINO-only; host builds no-op)
309
310int det_client_open(const char *, uint16_t, uint32_t)
311{
312 return -1;
313}
315{
316 return false;
317}
319{
320 return true;
321}
322bool det_client_send(int, const void *, size_t)
323{
324 return false;
325}
327{
328 return 0;
329}
330size_t det_client_read(int, uint8_t *, size_t)
331{
332 return 0;
333}
335{
336}
337
338#endif // ARDUINO
#define DETWS_CLIENT_CONNS
Number of simultaneous outbound client connections (BSS pool size).
#define DETWS_CLIENT_RX_BUF
Per-connection wire receive ring size (bytes).
bool det_client_connected(int)
True once the TCP handshake has completed for cid.
Definition client.cpp:314
size_t det_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:326
bool det_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:318
int det_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:310
size_t det_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:330
void det_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:334
bool det_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:322
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
uint32_t detws_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
DNS resolver with answer verification (DETWS_ENABLE_DNS_RESOLVER).
Shared single-producer / single-consumer byte-ring primitive.