include/boost/corosio/tcp_acceptor.hpp

92.9% Lines (39/42) 100.0% Functions (13/13)
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 // Copyright (c) 2026 Steve Gerbino
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_TCP_ACCEPTOR_HPP
12 #define BOOST_COROSIO_TCP_ACCEPTOR_HPP
13
14 #include <boost/corosio/detail/config.hpp>
15 #include <boost/corosio/detail/except.hpp>
16 #include <boost/corosio/io/io_object.hpp>
17 #include <boost/capy/io_result.hpp>
18 #include <boost/corosio/endpoint.hpp>
19 #include <boost/corosio/tcp.hpp>
20 #include <boost/corosio/tcp_socket.hpp>
21 #include <boost/capy/ex/executor_ref.hpp>
22 #include <boost/capy/ex/execution_context.hpp>
23 #include <boost/capy/ex/io_env.hpp>
24 #include <boost/capy/concept/executor.hpp>
25
26 #include <system_error>
27
28 #include <concepts>
29 #include <coroutine>
30 #include <cstddef>
31 #include <stop_token>
32 #include <type_traits>
33
34 namespace boost::corosio {
35
36 /** An asynchronous TCP acceptor for coroutine I/O.
37
38 This class provides asynchronous TCP accept operations that return
39 awaitable types. The acceptor binds to a local endpoint and listens
40 for incoming connections.
41
42 Each accept operation participates in the affine awaitable protocol,
43 ensuring coroutines resume on the correct executor.
44
45 @par Thread Safety
46 Distinct objects: Safe.@n
47 Shared objects: Unsafe. An acceptor must not have concurrent accept
48 operations.
49
50 @par Semantics
51 Wraps the platform TCP listener. Operations dispatch to
52 OS accept APIs via the io_context reactor.
53
54 @par Example
55 @code
56 // Convenience constructor: open + SO_REUSEADDR + bind + listen
57 io_context ioc;
58 tcp_acceptor acc( ioc, endpoint( 8080 ) );
59
60 tcp_socket peer( ioc );
61 auto [ec] = co_await acc.accept( peer );
62 if ( !ec ) {
63 // peer is now a connected socket
64 auto [ec2, n] = co_await peer.read_some( buf );
65 }
66 @endcode
67
68 @par Example
69 @code
70 // Fine-grained setup
71 tcp_acceptor acc( ioc );
72 acc.open( tcp::v6() );
73 acc.set_option( socket_option::reuse_address( true ) );
74 acc.set_option( socket_option::v6_only( true ) );
75 if ( auto ec = acc.bind( endpoint( ipv6_address::any(), 8080 ) ) )
76 return ec;
77 if ( auto ec = acc.listen() )
78 return ec;
79 @endcode
80 */
81 class BOOST_COROSIO_DECL tcp_acceptor : public io_object
82 {
83 struct accept_awaitable
84 {
85 tcp_acceptor& acc_;
86 tcp_socket& peer_;
87 std::stop_token token_;
88 mutable std::error_code ec_;
89 mutable io_object::implementation* peer_impl_ = nullptr;
90
91 6831x accept_awaitable(tcp_acceptor& acc, tcp_socket& peer) noexcept
92 6831x : acc_(acc)
93 6831x , peer_(peer)
94 {
95 6831x }
96
97 6831x bool await_ready() const noexcept
98 {
99 6831x return token_.stop_requested();
100 }
101
102 6831x capy::io_result<> await_resume() const noexcept
103 {
104 6831x if (token_.stop_requested())
105 6x return {make_error_code(std::errc::operation_canceled)};
106
107 6825x if (!ec_ && peer_impl_)
108 6819x peer_.h_.reset(peer_impl_);
109 6825x return {ec_};
110 }
111
112 6831x auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
113 -> std::coroutine_handle<>
114 {
115 6831x token_ = env->stop_token;
116 20493x return acc_.get().accept(
117 20493x h, env->executor, token_, &ec_, &peer_impl_);
118 }
119 };
120
121 public:
122 /** Destructor.
123
124 Closes the acceptor if open, cancelling any pending operations.
125 */
126 ~tcp_acceptor() override;
127
128 /** Construct an acceptor from an execution context.
129
130 @param ctx The execution context that will own this acceptor.
131 */
132 explicit tcp_acceptor(capy::execution_context& ctx);
133
134 /** Convenience constructor: open + SO_REUSEADDR + bind + listen.
135
136 Creates a fully-bound listening acceptor in a single
137 expression. The address family is deduced from @p ep.
138
139 @param ctx The execution context that will own this acceptor.
140 @param ep The local endpoint to bind to.
141 @param backlog The maximum pending connection queue length.
142
143 @throws std::system_error on bind or listen failure.
144 */
145 tcp_acceptor(capy::execution_context& ctx, endpoint ep, int backlog = 128);
146
147 /** Construct an acceptor from an executor.
148
149 The acceptor is associated with the executor's context.
150
151 @param ex The executor whose context will own the acceptor.
152 */
153 template<class Ex>
154 requires(!std::same_as<std::remove_cvref_t<Ex>, tcp_acceptor>) &&
155 capy::Executor<Ex>
156 explicit tcp_acceptor(Ex const& ex) : tcp_acceptor(ex.context())
157 {
158 }
159
160 /** Convenience constructor from an executor.
161
162 @param ex The executor whose context will own the acceptor.
163 @param ep The local endpoint to bind to.
164 @param backlog The maximum pending connection queue length.
165
166 @throws std::system_error on bind or listen failure.
167 */
168 template<class Ex>
169 requires capy::Executor<Ex>
170 tcp_acceptor(Ex const& ex, endpoint ep, int backlog = 128)
171 : tcp_acceptor(ex.context(), ep, backlog)
172 {
173 }
174
175 /** Move constructor.
176
177 Transfers ownership of the acceptor resources.
178
179 @param other The acceptor to move from.
180
181 @pre No awaitables returned by @p other's methods exist.
182 @pre The execution context associated with @p other must
183 outlive this acceptor.
184 */
185 2x tcp_acceptor(tcp_acceptor&& other) noexcept : io_object(std::move(other)) {}
186
187 /** Move assignment operator.
188
189 Closes any existing acceptor and transfers ownership.
190
191 @param other The acceptor to move from.
192
193 @pre No awaitables returned by either `*this` or @p other's
194 methods exist.
195 @pre The execution context associated with @p other must
196 outlive this acceptor.
197
198 @return Reference to this acceptor.
199 */
200 2x tcp_acceptor& operator=(tcp_acceptor&& other) noexcept
201 {
202 2x if (this != &other)
203 {
204 2x close();
205 2x h_ = std::move(other.h_);
206 }
207 2x return *this;
208 }
209
210 tcp_acceptor(tcp_acceptor const&) = delete;
211 tcp_acceptor& operator=(tcp_acceptor const&) = delete;
212
213 /** Create the acceptor socket without binding or listening.
214
215 Creates a TCP socket with dual-stack enabled for IPv6.
216 Does not set SO_REUSEADDR — call `set_option` explicitly
217 if needed.
218
219 If the acceptor is already open, this function is a no-op.
220
221 @param proto The protocol (IPv4 or IPv6). Defaults to
222 `tcp::v4()`.
223
224 @throws std::system_error on failure.
225
226 @par Example
227 @code
228 acc.open( tcp::v6() );
229 acc.set_option( socket_option::reuse_address( true ) );
230 acc.bind( endpoint( ipv6_address::any(), 8080 ) );
231 acc.listen();
232 @endcode
233
234 @see bind, listen
235 */
236 void open(tcp proto = tcp::v4());
237
238 /** Bind to a local endpoint.
239
240 The acceptor must be open. Binds the socket to @p ep and
241 caches the resolved local endpoint (useful when port 0 is
242 used to request an ephemeral port).
243
244 @param ep The local endpoint to bind to.
245
246 @return An error code indicating success or the reason for
247 failure.
248
249 @par Error Conditions
250 @li `errc::address_in_use`: The endpoint is already in use.
251 @li `errc::address_not_available`: The address is not available
252 on any local interface.
253 @li `errc::permission_denied`: Insufficient privileges to bind
254 to the endpoint (e.g., privileged port).
255
256 @throws std::logic_error if the acceptor is not open.
257 */
258 [[nodiscard]] std::error_code bind(endpoint ep);
259
260 /** Start listening for incoming connections.
261
262 The acceptor must be open and bound. Registers the acceptor
263 with the platform reactor.
264
265 @param backlog The maximum length of the queue of pending
266 connections. Defaults to 128.
267
268 @return An error code indicating success or the reason for
269 failure.
270
271 @throws std::logic_error if the acceptor is not open.
272 */
273 [[nodiscard]] std::error_code listen(int backlog = 128);
274
275 /** Close the acceptor.
276
277 Releases acceptor resources. Any pending operations complete
278 with `errc::operation_canceled`.
279 */
280 void close();
281
282 /** Check if the acceptor is listening.
283
284 @return `true` if the acceptor is open and listening.
285 */
286 7827x bool is_open() const noexcept
287 {
288 7827x return h_ && get().is_open();
289 }
290
291 /** Initiate an asynchronous accept operation.
292
293 Accepts an incoming connection and initializes the provided
294 socket with the new connection. The acceptor must be listening
295 before calling this function.
296
297 The operation supports cancellation via `std::stop_token` through
298 the affine awaitable protocol. If the associated stop token is
299 triggered, the operation completes immediately with
300 `errc::operation_canceled`.
301
302 @param peer The socket to receive the accepted connection. Any
303 existing connection on this socket will be closed.
304
305 @return An awaitable that completes with `io_result<>`.
306 Returns success on successful accept, or an error code on
307 failure including:
308 - operation_canceled: Cancelled via stop_token or cancel().
309 Check `ec == cond::canceled` for portable comparison.
310
311 @par Preconditions
312 The acceptor must be listening (`is_open() == true`).
313 The peer socket must be associated with the same execution context.
314
315 Both this acceptor and @p peer must outlive the returned
316 awaitable.
317
318 @par Example
319 @code
320 tcp_socket peer(ioc);
321 auto [ec] = co_await acc.accept(peer);
322 if (!ec) {
323 // Use peer socket
324 }
325 @endcode
326 */
327 6831x auto accept(tcp_socket& peer)
328 {
329 6831x if (!is_open())
330 detail::throw_logic_error("accept: acceptor not listening");
331 6831x return accept_awaitable(*this, peer);
332 }
333
334 /** Cancel any pending asynchronous operations.
335
336 All outstanding operations complete with `errc::operation_canceled`.
337 Check `ec == cond::canceled` for portable comparison.
338 */
339 void cancel();
340
341 /** Get the local endpoint of the acceptor.
342
343 Returns the local address and port to which the acceptor is bound.
344 This is useful when binding to port 0 (ephemeral port) to discover
345 the OS-assigned port number. The endpoint is cached when listen()
346 is called.
347
348 @return The local endpoint, or a default endpoint (0.0.0.0:0) if
349 the acceptor is not listening.
350
351 @par Thread Safety
352 The cached endpoint value is set during listen() and cleared
353 during close(). This function may be called concurrently with
354 accept operations, but must not be called concurrently with
355 listen() or close().
356 */
357 endpoint local_endpoint() const noexcept;
358
359 /** Set a socket option on the acceptor.
360
361 Applies a type-safe socket option to the underlying listening
362 socket. The socket must be open (via `open()` or `listen()`).
363 This is useful for setting options between `open()` and
364 `listen()`, such as `socket_option::reuse_port`.
365
366 @par Example
367 @code
368 acc.open( tcp::v6() );
369 acc.set_option( socket_option::reuse_port( true ) );
370 acc.bind( endpoint( ipv6_address::any(), 8080 ) );
371 acc.listen();
372 @endcode
373
374 @param opt The option to set.
375
376 @throws std::logic_error if the acceptor is not open.
377 @throws std::system_error on failure.
378 */
379 template<class Option>
380 137x void set_option(Option const& opt)
381 {
382 137x if (!is_open())
383 detail::throw_logic_error("set_option: acceptor not open");
384 137x std::error_code ec = get().set_option(
385 Option::level(), Option::name(), opt.data(), opt.size());
386 137x if (ec)
387 detail::throw_system_error(ec, "tcp_acceptor::set_option");
388 137x }
389
390 /** Get a socket option from the acceptor.
391
392 Retrieves the current value of a type-safe socket option.
393
394 @par Example
395 @code
396 auto opt = acc.get_option<socket_option::reuse_address>();
397 @endcode
398
399 @return The current option value.
400
401 @throws std::logic_error if the acceptor is not open.
402 @throws std::system_error on failure.
403 */
404 template<class Option>
405 Option get_option() const
406 {
407 if (!is_open())
408 detail::throw_logic_error("get_option: acceptor not open");
409 Option opt{};
410 std::size_t sz = opt.size();
411 std::error_code ec =
412 get().get_option(Option::level(), Option::name(), opt.data(), &sz);
413 if (ec)
414 detail::throw_system_error(ec, "tcp_acceptor::get_option");
415 opt.resize(sz);
416 return opt;
417 }
418
419 /** Define backend hooks for TCP acceptor operations.
420
421 Platform backends derive from this to implement
422 accept, endpoint query, open-state checks, cancellation,
423 and socket-option management.
424 */
425 struct implementation : io_object::implementation
426 {
427 /// Initiate an asynchronous accept operation.
428 virtual std::coroutine_handle<> accept(
429 std::coroutine_handle<>,
430 capy::executor_ref,
431 std::stop_token,
432 std::error_code*,
433 io_object::implementation**) = 0;
434
435 /// Returns the cached local endpoint.
436 virtual endpoint local_endpoint() const noexcept = 0;
437
438 /// Return true if the acceptor has a kernel resource open.
439 virtual bool is_open() const noexcept = 0;
440
441 /** Cancel any pending asynchronous operations.
442
443 All outstanding operations complete with operation_canceled error.
444 */
445 virtual void cancel() noexcept = 0;
446
447 /** Set a socket option.
448
449 @param level The protocol level.
450 @param optname The option name.
451 @param data Pointer to the option value.
452 @param size Size of the option value in bytes.
453 @return Error code on failure, empty on success.
454 */
455 virtual std::error_code set_option(
456 int level,
457 int optname,
458 void const* data,
459 std::size_t size) noexcept = 0;
460
461 /** Get a socket option.
462
463 @param level The protocol level.
464 @param optname The option name.
465 @param data Pointer to receive the option value.
466 @param size On entry, the size of the buffer. On exit,
467 the size of the option value.
468 @return Error code on failure, empty on success.
469 */
470 virtual std::error_code
471 get_option(int level, int optname, void* data, std::size_t* size)
472 const noexcept = 0;
473 };
474
475 protected:
476 4x explicit tcp_acceptor(handle h) noexcept : io_object(std::move(h)) {}
477
478 /// Transfer accepted peer impl to the peer socket.
479 static void
480 4x reset_peer_impl(tcp_socket& peer, io_object::implementation* impl) noexcept
481 {
482 4x if (impl)
483 4x peer.h_.reset(impl);
484 4x }
485
486 private:
487 14915x inline implementation& get() const noexcept
488 {
489 14915x return *static_cast<implementation*>(h_.get());
490 }
491 };
492
493 } // namespace boost::corosio
494
495 #endif
496