libs/corosio/include/boost/corosio/tcp_acceptor.hpp

94.7% Lines (36/38) 100.0% Functions (9/9) 73.3% Branches (11/15)
libs/corosio/include/boost/corosio/tcp_acceptor.hpp
Line Branch Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
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_TCP_ACCEPTOR_HPP
11 #define BOOST_COROSIO_TCP_ACCEPTOR_HPP
12
13 #include <boost/corosio/detail/config.hpp>
14 #include <boost/corosio/detail/except.hpp>
15 #include <boost/corosio/io_object.hpp>
16 #include <boost/capy/io_result.hpp>
17 #include <boost/corosio/endpoint.hpp>
18 #include <boost/corosio/tcp_socket.hpp>
19 #include <boost/capy/ex/executor_ref.hpp>
20 #include <boost/capy/ex/execution_context.hpp>
21 #include <boost/capy/concept/executor.hpp>
22
23 #include <system_error>
24
25 #include <concepts>
26 #include <coroutine>
27 #include <cstddef>
28 #include <memory>
29 #include <stop_token>
30 #include <type_traits>
31
32 namespace boost::corosio {
33
34 /** An asynchronous TCP acceptor for coroutine I/O.
35
36 This class provides asynchronous TCP accept operations that return
37 awaitable types. The acceptor binds to a local endpoint and listens
38 for incoming connections.
39
40 Each accept operation participates in the affine awaitable protocol,
41 ensuring coroutines resume on the correct executor.
42
43 @par Thread Safety
44 Distinct objects: Safe.@n
45 Shared objects: Unsafe. An acceptor must not have concurrent accept
46 operations.
47
48 @par Semantics
49 Wraps the platform TCP listener. Operations dispatch to
50 OS accept APIs via the io_context reactor.
51
52 @par Example
53 @code
54 io_context ioc;
55 tcp_acceptor acc(ioc);
56 if (auto ec = acc.listen(endpoint(8080))) // Bind to port 8080
57 return ec;
58
59 tcp_socket peer(ioc);
60 auto [ec] = co_await acc.accept(peer);
61 if (!ec) {
62 // peer is now a connected socket
63 auto [ec2, n] = co_await peer.read_some(buf);
64 }
65 @endcode
66 */
67 class BOOST_COROSIO_DECL tcp_acceptor : public io_object
68 {
69 struct accept_awaitable
70 {
71 tcp_acceptor& acc_;
72 tcp_socket& peer_;
73 std::stop_token token_;
74 mutable std::error_code ec_;
75 mutable io_object::io_object_impl* peer_impl_ = nullptr;
76
77 4725 accept_awaitable(tcp_acceptor& acc, tcp_socket& peer) noexcept
78 4725 : acc_(acc)
79 4725 , peer_(peer)
80 {
81 4725 }
82
83 4725 bool await_ready() const noexcept
84 {
85 4725 return token_.stop_requested();
86 }
87
88 4725 capy::io_result<> await_resume() const noexcept
89 {
90
2/2
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 4719 times.
4725 if (token_.stop_requested())
91 6 return {make_error_code(std::errc::operation_canceled)};
92
93 // Transfer the accepted impl to the peer socket
94 // (acceptor is a friend of socket, so we can access impl_)
95
5/6
✓ Branch 1 taken 4713 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 4713 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4713 times.
✓ Branch 6 taken 6 times.
4719 if (!ec_ && peer_impl_)
96 {
97 4713 peer_.close();
98 4713 peer_.impl_ = peer_impl_;
99 }
100 4719 return {ec_};
101 }
102
103 template<typename Ex>
104 auto await_suspend(
105 std::coroutine_handle<> h,
106 Ex const& ex) -> std::coroutine_handle<>
107 {
108 acc_.get().accept(h, ex, token_, &ec_, &peer_impl_);
109 return std::noop_coroutine();
110 }
111
112 template<typename Ex>
113 4725 auto await_suspend(
114 std::coroutine_handle<> h,
115 Ex const& ex,
116 std::stop_token token) -> std::coroutine_handle<>
117 {
118 4725 token_ = std::move(token);
119
1/1
✓ Branch 3 taken 4725 times.
4725 acc_.get().accept(h, ex, token_, &ec_, &peer_impl_);
120 4725 return std::noop_coroutine();
121 }
122 };
123
124 public:
125 /** Destructor.
126
127 Closes the acceptor if open, cancelling any pending operations.
128 */
129 ~tcp_acceptor();
130
131 /** Construct an acceptor from an execution context.
132
133 @param ctx The execution context that will own this acceptor.
134 */
135 explicit tcp_acceptor(capy::execution_context& ctx);
136
137 /** Construct an acceptor from an executor.
138
139 The acceptor is associated with the executor's context.
140
141 @param ex The executor whose context will own the acceptor.
142 */
143 template<class Ex>
144 requires (!std::same_as<std::remove_cvref_t<Ex>, tcp_acceptor>) &&
145 capy::Executor<Ex>
146 explicit tcp_acceptor(Ex const& ex)
147 : tcp_acceptor(ex.context())
148 {
149 }
150
151 /** Move constructor.
152
153 Transfers ownership of the acceptor resources.
154
155 @param other The acceptor to move from.
156 */
157 2 tcp_acceptor(tcp_acceptor&& other) noexcept
158 2 : io_object(other.context())
159 {
160 2 impl_ = other.impl_;
161 2 other.impl_ = nullptr;
162 2 }
163
164 /** Move assignment operator.
165
166 Closes any existing acceptor and transfers ownership.
167 The source and destination must share the same execution context.
168
169 @param other The acceptor to move from.
170
171 @return Reference to this acceptor.
172
173 @throws std::logic_error if the acceptors have different execution contexts.
174 */
175 9 tcp_acceptor& operator=(tcp_acceptor&& other)
176 {
177
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 if (this != &other)
178 {
179
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (ctx_ != other.ctx_)
180 detail::throw_logic_error(
181 "cannot move tcp_acceptor across execution contexts");
182 9 close();
183 9 impl_ = other.impl_;
184 9 other.impl_ = nullptr;
185 }
186 9 return *this;
187 }
188
189 tcp_acceptor(tcp_acceptor const&) = delete;
190 tcp_acceptor& operator=(tcp_acceptor const&) = delete;
191
192 /** Open, bind, and listen on an endpoint.
193
194 Creates an IPv4 TCP socket, binds it to the specified endpoint,
195 and begins listening for incoming connections. This must be
196 called before initiating accept operations.
197
198 @param ep The local endpoint to bind to. Use `endpoint(port)` to
199 bind to all interfaces on a specific port.
200
201 @param backlog The maximum length of the queue of pending
202 connections. Defaults to 128.
203
204 @return An error code indicating success or the reason for failure.
205 A default-constructed error code indicates success.
206
207 @par Error Conditions
208 @li `errc::address_in_use`: The endpoint is already in use.
209 @li `errc::address_not_available`: The address is not available
210 on any local interface.
211 @li `errc::permission_denied`: Insufficient privileges to bind
212 to the endpoint (e.g., privileged port).
213 @li `errc::operation_not_supported`: The acceptor service is
214 unavailable in the context (POSIX only).
215
216 @throws Nothing.
217 */
218 [[nodiscard]] std::error_code listen(endpoint ep, int backlog = 128);
219
220 /** Close the acceptor.
221
222 Releases acceptor resources. Any pending operations complete
223 with `errc::operation_canceled`.
224 */
225 void close();
226
227 /** Check if the acceptor is listening.
228
229 @return `true` if the acceptor is open and listening.
230 */
231 25 bool is_open() const noexcept
232 {
233 25 return impl_ != nullptr;
234 }
235
236 /** Initiate an asynchronous accept operation.
237
238 Accepts an incoming connection and initializes the provided
239 socket with the new connection. The acceptor must be listening
240 before calling this function.
241
242 The operation supports cancellation via `std::stop_token` through
243 the affine awaitable protocol. If the associated stop token is
244 triggered, the operation completes immediately with
245 `errc::operation_canceled`.
246
247 @param peer The socket to receive the accepted connection. Any
248 existing connection on this socket will be closed.
249
250 @return An awaitable that completes with `io_result<>`.
251 Returns success on successful accept, or an error code on
252 failure including:
253 - operation_canceled: Cancelled via stop_token or cancel().
254 Check `ec == cond::canceled` for portable comparison.
255
256 @par Preconditions
257 The acceptor must be listening (`is_open() == true`).
258 The peer socket must be associated with the same execution context.
259
260 @par Example
261 @code
262 tcp_socket peer(ioc);
263 auto [ec] = co_await acc.accept(peer);
264 if (!ec) {
265 // Use peer socket
266 }
267 @endcode
268 */
269 4725 auto accept(tcp_socket& peer)
270 {
271
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4725 times.
4725 if (!impl_)
272 detail::throw_logic_error("accept: acceptor not listening");
273 4725 return accept_awaitable(*this, peer);
274 }
275
276 /** Cancel any pending asynchronous operations.
277
278 All outstanding operations complete with `errc::operation_canceled`.
279 Check `ec == cond::canceled` for portable comparison.
280 */
281 void cancel();
282
283 /** Get the local endpoint of the acceptor.
284
285 Returns the local address and port to which the acceptor is bound.
286 This is useful when binding to port 0 (ephemeral port) to discover
287 the OS-assigned port number. The endpoint is cached when listen()
288 is called.
289
290 @return The local endpoint, or a default endpoint (0.0.0.0:0) if
291 the acceptor is not listening.
292
293 @par Thread Safety
294 The cached endpoint value is set during listen() and cleared
295 during close(). This function may be called concurrently with
296 accept operations, but must not be called concurrently with
297 listen() or close().
298 */
299 endpoint local_endpoint() const noexcept;
300
301 struct acceptor_impl : io_object_impl
302 {
303 virtual void accept(
304 std::coroutine_handle<>,
305 capy::executor_ref,
306 std::stop_token,
307 std::error_code*,
308 io_object_impl**) = 0;
309
310 /// Returns the cached local endpoint.
311 virtual endpoint local_endpoint() const noexcept = 0;
312
313 /** Cancel any pending asynchronous operations.
314
315 All outstanding operations complete with operation_canceled error.
316 */
317 virtual void cancel() noexcept = 0;
318 };
319
320 private:
321 4743 inline acceptor_impl& get() const noexcept
322 {
323 4743 return *static_cast<acceptor_impl*>(impl_);
324 }
325 };
326
327 } // namespace boost::corosio
328
329 #endif
330