TLA Line data Source code
1 : //
2 : // Copyright (c) 2026 Michael Vandeberg
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/corosio
8 : //
9 :
10 : #ifndef BOOST_COROSIO_CONNECT_HPP
11 : #define BOOST_COROSIO_CONNECT_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 :
15 : #include <boost/capy/cond.hpp>
16 : #include <boost/capy/io_result.hpp>
17 : #include <boost/capy/task.hpp>
18 :
19 : #include <concepts>
20 : #include <iterator>
21 : #include <ranges>
22 : #include <system_error>
23 : #include <utility>
24 :
25 : /*
26 : Range-based composed connect operation.
27 :
28 : These free functions try each endpoint in a range (or iterator pair)
29 : in order, returning on the first successful connect. Between attempts
30 : the socket is closed so that the next attempt can auto-open with the
31 : correct address family (e.g. going from IPv4 to IPv6 candidates).
32 :
33 : The iteration semantics follow Boost.Asio's range/iterator async_connect:
34 : on success, the successful endpoint (or its iterator) is returned; on
35 : all-fail, the last attempt's error code is returned; on an empty range
36 : (or when a connect_condition rejects every candidate),
37 : std::errc::no_such_device_or_address is returned, matching the error
38 : the resolver uses for "no results" in posix_resolver_service.
39 :
40 : The operation is a plain coroutine; cancellation is propagated to the
41 : inner per-endpoint connect via the affine awaitable protocol on io_env.
42 : */
43 :
44 : namespace boost::corosio {
45 :
46 : namespace detail {
47 :
48 : /* Always-true connect condition used by the overloads that take no
49 : user-supplied predicate. Kept at namespace-detail scope so it has a
50 : stable linkage name across translation units. */
51 : struct default_connect_condition
52 : {
53 : template<class Endpoint>
54 HIT 20 : bool operator()(std::error_code const&, Endpoint const&) const noexcept
55 : {
56 20 : return true;
57 : }
58 : };
59 :
60 : } // namespace detail
61 :
62 : /* Forward declarations so the non-condition overloads can delegate
63 : to the condition overloads via qualified lookup (qualified calls
64 : bind to the overload set visible at definition, not instantiation). */
65 :
66 : template<class Socket, std::ranges::input_range Range, class ConnectCondition>
67 : requires std::convertible_to<
68 : std::ranges::range_reference_t<Range>,
69 : typename Socket::endpoint_type> &&
70 : std::predicate<
71 : ConnectCondition&,
72 : std::error_code const&,
73 : typename Socket::endpoint_type const&>
74 : capy::task<capy::io_result<typename Socket::endpoint_type>>
75 : connect(Socket& s, Range endpoints, ConnectCondition cond);
76 :
77 : template<class Socket, std::input_iterator Iter, class ConnectCondition>
78 : requires std::convertible_to<
79 : std::iter_reference_t<Iter>,
80 : typename Socket::endpoint_type> &&
81 : std::predicate<
82 : ConnectCondition&,
83 : std::error_code const&,
84 : typename Socket::endpoint_type const&>
85 : capy::task<capy::io_result<Iter>>
86 : connect(Socket& s, Iter begin, Iter end, ConnectCondition cond);
87 :
88 : /** Asynchronously connect a socket by trying each endpoint in a range.
89 :
90 : Each candidate is tried in order. Before each attempt the socket is
91 : closed (so the next `connect` auto-opens with the candidate's
92 : address family). On first successful connect, the operation
93 : completes with the connected endpoint.
94 :
95 : @par Cancellation
96 : Supports cancellation via the affine awaitable protocol. If a
97 : per-endpoint connect completes with `capy::cond::canceled` the
98 : operation completes immediately with that error and does not try
99 : further endpoints.
100 :
101 : @param s The socket to connect. Must have a `connect(endpoint)`
102 : member returning an awaitable, plus `close()` and `is_open()`.
103 : If the socket is already open, it will be closed before the
104 : first attempt.
105 : @param endpoints A range of candidate endpoints. Taken by value
106 : so temporaries (e.g. `resolver_results` returned from
107 : `resolver::resolve`) remain alive for the coroutine's lifetime.
108 : Because the range is owned by the coroutine, passing an lvalue
109 : copies it; since `resolver_results` is a
110 : `std::vector<resolver_entry>`, that is a deep copy of every entry.
111 : Pass an rvalue (`std::move(results)`) or use the iterator overload
112 : (`connect(s, results.begin(), results.end())`) to avoid the copy.
113 :
114 : @return An awaitable completing with
115 : `capy::io_result<typename Socket::endpoint_type>`:
116 : - on success: default error_code and the connected endpoint;
117 : - on failure of all attempts: the error from the last attempt
118 : and a default-constructed endpoint;
119 : - on empty range: `std::errc::no_such_device_or_address` and a
120 : default-constructed endpoint.
121 :
122 : @note The socket is closed and re-opened before each attempt, so
123 : any socket options set by the caller (e.g. `no_delay`,
124 : `reuse_address`) are lost. Apply options after this operation
125 : completes.
126 :
127 : If auto-opening the socket fails during an attempt, that attempt
128 : completes with the open error (inherits the contract of
129 : `Socket::connect`).
130 :
131 : @par Example
132 : @code
133 : resolver r(ioc);
134 : auto [rec, results] = co_await r.resolve("www.boost.org", "80");
135 : if (rec) co_return;
136 : tcp_socket s(ioc);
137 : auto [cec, ep] = co_await corosio::connect(s, results);
138 : @endcode
139 : */
140 : template<class Socket, std::ranges::input_range Range>
141 : requires std::convertible_to<
142 : std::ranges::range_reference_t<Range>,
143 : typename Socket::endpoint_type>
144 : capy::task<capy::io_result<typename Socket::endpoint_type>>
145 12 : connect(Socket& s, Range endpoints)
146 : {
147 MIS 0 : return corosio::connect(
148 HIT 12 : s, std::move(endpoints), detail::default_connect_condition{});
149 : }
150 :
151 : /** Asynchronously connect a socket by trying each endpoint in a range,
152 : filtered by a user-supplied condition.
153 :
154 : For each candidate the condition is invoked as
155 : `cond(last_ec, ep)` where `last_ec` is the error from the most
156 : recent attempt (default-constructed before the first attempt). If
157 : the condition returns `false` the candidate is skipped; otherwise a
158 : connect is attempted.
159 :
160 : @param s The socket to connect. See the non-condition overload for
161 : requirements.
162 : @param endpoints A range of candidate endpoints, taken by value. See
163 : the non-condition overload for the deep-copy caveat when passing
164 : an lvalue `resolver_results`.
165 : @param cond A predicate invocable with
166 : `(std::error_code const&, typename Socket::endpoint_type const&)`
167 : returning a value contextually convertible to `bool`.
168 :
169 : @return Same as the non-condition overload. If every candidate is
170 : rejected, completes with `std::errc::no_such_device_or_address`.
171 :
172 : If auto-opening the socket fails, the attempt completes with the
173 : open error.
174 : */
175 : template<class Socket, std::ranges::input_range Range, class ConnectCondition>
176 : requires std::convertible_to<
177 : std::ranges::range_reference_t<Range>,
178 : typename Socket::endpoint_type> &&
179 : std::predicate<
180 : ConnectCondition&,
181 : std::error_code const&,
182 : typename Socket::endpoint_type const&>
183 : capy::task<capy::io_result<typename Socket::endpoint_type>>
184 16 : connect(Socket& s, Range endpoints, ConnectCondition cond)
185 : {
186 : using endpoint_type = typename Socket::endpoint_type;
187 :
188 : std::error_code last_ec;
189 :
190 : for (auto&& e : endpoints)
191 : {
192 : endpoint_type ep = e;
193 :
194 : if (!cond(static_cast<std::error_code const&>(last_ec),
195 : static_cast<endpoint_type const&>(ep)))
196 : continue;
197 :
198 : if (s.is_open())
199 : s.close();
200 :
201 : auto [ec] = co_await s.connect(ep);
202 :
203 : if (!ec)
204 : co_return {std::error_code{}, std::move(ep)};
205 :
206 : if (ec == capy::cond::canceled)
207 : co_return {ec, endpoint_type{}};
208 :
209 : last_ec = ec;
210 : }
211 :
212 : if (!last_ec)
213 : last_ec = std::make_error_code(std::errc::no_such_device_or_address);
214 :
215 : co_return {last_ec, endpoint_type{}};
216 32 : }
217 :
218 : /** Asynchronously connect a socket by trying each endpoint in an
219 : iterator range.
220 :
221 : Behaves like the range overload, except the return value carries
222 : the iterator to the successfully connected endpoint on success, or
223 : `end` on failure. This mirrors Boost.Asio's iterator-based
224 : `async_connect`.
225 :
226 : @param s The socket to connect.
227 : @param begin The first candidate.
228 : @param end One past the last candidate.
229 :
230 : @return An awaitable completing with `capy::io_result<Iter>`:
231 : - on success: default error_code and the iterator of the
232 : successful endpoint;
233 : - on failure of all attempts: the error from the last attempt
234 : and `end`;
235 : - on empty range: `std::errc::no_such_device_or_address` and
236 : `end`.
237 :
238 : If auto-opening the socket fails, the attempt completes with the
239 : open error.
240 : */
241 : template<class Socket, std::input_iterator Iter>
242 : requires std::convertible_to<
243 : std::iter_reference_t<Iter>,
244 : typename Socket::endpoint_type>
245 : capy::task<capy::io_result<Iter>>
246 4 : connect(Socket& s, Iter begin, Iter end)
247 : {
248 : return corosio::connect(
249 : s,
250 4 : std::move(begin),
251 4 : std::move(end),
252 4 : detail::default_connect_condition{});
253 : }
254 :
255 : /** Asynchronously connect a socket by trying each endpoint in an
256 : iterator range, filtered by a user-supplied condition.
257 :
258 : @param s The socket to connect.
259 : @param begin The first candidate.
260 : @param end One past the last candidate.
261 : @param cond A predicate invocable with
262 : `(std::error_code const&, typename Socket::endpoint_type const&)`.
263 :
264 : @return Same as the plain iterator overload. If every candidate is
265 : rejected, completes with `std::errc::no_such_device_or_address`.
266 :
267 : If auto-opening the socket fails, the attempt completes with the
268 : open error.
269 : */
270 : template<class Socket, std::input_iterator Iter, class ConnectCondition>
271 : requires std::convertible_to<
272 : std::iter_reference_t<Iter>,
273 : typename Socket::endpoint_type> &&
274 : std::predicate<
275 : ConnectCondition&,
276 : std::error_code const&,
277 : typename Socket::endpoint_type const&>
278 : capy::task<capy::io_result<Iter>>
279 4 : connect(Socket& s, Iter begin, Iter end, ConnectCondition cond)
280 : {
281 : using endpoint_type = typename Socket::endpoint_type;
282 :
283 : std::error_code last_ec;
284 :
285 : for (Iter it = begin; it != end; ++it)
286 : {
287 : endpoint_type ep = *it;
288 :
289 : if (!cond(static_cast<std::error_code const&>(last_ec),
290 : static_cast<endpoint_type const&>(ep)))
291 : continue;
292 :
293 : if (s.is_open())
294 : s.close();
295 :
296 : auto [ec] = co_await s.connect(ep);
297 :
298 : if (!ec)
299 : co_return {std::error_code{}, std::move(it)};
300 :
301 : if (ec == capy::cond::canceled)
302 : co_return {ec, std::move(end)};
303 :
304 : last_ec = ec;
305 : }
306 :
307 : if (!last_ec)
308 : last_ec = std::make_error_code(std::errc::no_such_device_or_address);
309 :
310 : co_return {last_ec, std::move(end)};
311 8 : }
312 :
313 : } // namespace boost::corosio
314 :
315 : #endif
|