include/boost/corosio/tcp_acceptor.hpp

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