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 <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 8236 accept_awaitable(tcp_acceptor& acc, tcp_socket& peer) noexcept
92 8236 : acc_(acc)
93 8236 , peer_(peer)
94 {
95 8236 }
96
97 8236 bool await_ready() const noexcept
98 {
99 8236 return token_.stop_requested();
100 }
101
102 8236 capy::io_result<> await_resume() const noexcept
103 {
104 8236 if (token_.stop_requested())
105 6 return {make_error_code(std::errc::operation_canceled)};
106
107 8230 if (!ec_ && peer_impl_)
108 8224 peer_.h_.reset(peer_impl_);
109 8230 return {ec_};
110 }
111
112 8236 auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
113 -> std::coroutine_handle<>
114 {
115 8236 token_ = env->stop_token;
116 24708 return acc_.get().accept(
117 24708 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 2 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 2 tcp_acceptor& operator=(tcp_acceptor&& other) noexcept
201 {
202 2 if (this != &other)
203 {
204 2 close();
205 2 h_ = std::move(other.h_);
206 }
207 2 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 9232 bool is_open() const noexcept
287 {
288 9232 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 8236 auto accept(tcp_socket& peer)
328 {
329 8236 if (!is_open())
330 detail::throw_logic_error("accept: acceptor not listening");
331 8236 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 137 void set_option(Option const& opt)
381 {
382 137 if (!is_open())
383 detail::throw_logic_error("set_option: acceptor not open");
384 137 std::error_code ec = get().set_option(
385 Option::level(), Option::name(), opt.data(), opt.size());
386 137 if (ec)
387 detail::throw_system_error(ec, "tcp_acceptor::set_option");
388 137 }
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 struct implementation : io_object::implementation
420 {
421 virtual std::coroutine_handle<> accept(
422 std::coroutine_handle<>,
423 capy::executor_ref,
424 std::stop_token,
425 std::error_code*,
426 io_object::implementation**) = 0;
427
428 /// Returns the cached local endpoint.
429 virtual endpoint local_endpoint() const noexcept = 0;
430
431 /// Return true if the acceptor has a kernel resource open.
432 virtual bool is_open() const noexcept = 0;
433
434 /** Cancel any pending asynchronous operations.
435
436 All outstanding operations complete with operation_canceled error.
437 */
438 virtual void cancel() noexcept = 0;
439
440 /** Set a socket option.
441
442 @param level The protocol level.
443 @param optname The option name.
444 @param data Pointer to the option value.
445 @param size Size of the option value in bytes.
446 @return Error code on failure, empty on success.
447 */
448 virtual std::error_code set_option(
449 int level,
450 int optname,
451 void const* data,
452 std::size_t size) noexcept = 0;
453
454 /** Get a socket option.
455
456 @param level The protocol level.
457 @param optname The option name.
458 @param data Pointer to receive the option value.
459 @param size On entry, the size of the buffer. On exit,
460 the size of the option value.
461 @return Error code on failure, empty on success.
462 */
463 virtual std::error_code
464 get_option(int level, int optname, void* data, std::size_t* size)
465 const noexcept = 0;
466 };
467
468 protected:
469 4 explicit tcp_acceptor(handle h) noexcept : io_object(std::move(h)) {}
470
471 /// Transfer accepted peer impl to the peer socket.
472 static void
473 4 reset_peer_impl(tcp_socket& peer, io_object::implementation* impl) noexcept
474 {
475 4 if (impl)
476 4 peer.h_.reset(impl);
477 4 }
478
479 private:
480 17725 inline implementation& get() const noexcept
481 {
482 17725 return *static_cast<implementation*>(h_.get());
483 }
484 };
485
486 } // namespace boost::corosio
487
488 #endif
489