include/boost/corosio/tcp_acceptor.hpp

97.1% Lines (34/35) 100.0% Functions (11/11)
include/boost/corosio/tcp_acceptor.hpp
Line 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_socket.hpp>
20 #include <boost/capy/ex/executor_ref.hpp>
21 #include <boost/capy/ex/execution_context.hpp>
22 #include <boost/capy/ex/io_env.hpp>
23 #include <boost/capy/concept/executor.hpp>
24
25 #include <system_error>
26
27 #include <concepts>
28 #include <coroutine>
29 #include <cstddef>
30 #include <memory>
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 io_context ioc;
57 tcp_acceptor acc(ioc);
58 if (auto ec = acc.listen(endpoint(8080))) // Bind to port 8080
59 return ec;
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 class BOOST_COROSIO_DECL tcp_acceptor : public io_object
70 {
71 struct accept_awaitable
72 {
73 tcp_acceptor& acc_;
74 tcp_socket& peer_;
75 std::stop_token token_;
76 mutable std::error_code ec_;
77 mutable io_object::implementation* peer_impl_ = nullptr;
78
79 7262 accept_awaitable(tcp_acceptor& acc, tcp_socket& peer) noexcept
80 7262 : acc_(acc)
81 7262 , peer_(peer)
82 {
83 7262 }
84
85 7262 bool await_ready() const noexcept
86 {
87 7262 return token_.stop_requested();
88 }
89
90 7262 capy::io_result<> await_resume() const noexcept
91 {
92 7262 if (token_.stop_requested())
93 6 return {make_error_code(std::errc::operation_canceled)};
94
95 7256 if (!ec_ && peer_impl_)
96 7250 peer_.h_.reset(peer_impl_);
97 7256 return {ec_};
98 }
99
100 7262 auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
101 -> std::coroutine_handle<>
102 {
103 7262 token_ = env->stop_token;
104 21786 return acc_.get().accept(
105 21786 h, env->executor, token_, &ec_, &peer_impl_);
106 }
107 };
108
109 public:
110 /** Destructor.
111
112 Closes the acceptor if open, cancelling any pending operations.
113 */
114 ~tcp_acceptor() override;
115
116 /** Construct an acceptor from an execution context.
117
118 @param ctx The execution context that will own this acceptor.
119 */
120 explicit tcp_acceptor(capy::execution_context& ctx);
121
122 /** Construct an acceptor from an executor.
123
124 The acceptor is associated with the executor's context.
125
126 @param ex The executor whose context will own the acceptor.
127 */
128 template<class Ex>
129 requires(!std::same_as<std::remove_cvref_t<Ex>, tcp_acceptor>) &&
130 capy::Executor<Ex>
131 explicit tcp_acceptor(Ex const& ex) : tcp_acceptor(ex.context())
132 {
133 }
134
135 /** Move constructor.
136
137 Transfers ownership of the acceptor resources.
138
139 @param other The acceptor to move from.
140
141 @pre No awaitables returned by @p other's methods exist.
142 @pre The execution context associated with @p other must
143 outlive this acceptor.
144 */
145 2 tcp_acceptor(tcp_acceptor&& other) noexcept : io_object(std::move(other)) {}
146
147 /** Move assignment operator.
148
149 Closes any existing acceptor and transfers ownership.
150
151 @param other The acceptor to move from.
152
153 @pre No awaitables returned by either `*this` or @p other's
154 methods exist.
155 @pre The execution context associated with @p other must
156 outlive this acceptor.
157
158 @return Reference to this acceptor.
159 */
160 2 tcp_acceptor& operator=(tcp_acceptor&& other) noexcept
161 {
162 2 if (this != &other)
163 {
164 2 close();
165 2 h_ = std::move(other.h_);
166 }
167 2 return *this;
168 }
169
170 tcp_acceptor(tcp_acceptor const&) = delete;
171 tcp_acceptor& operator=(tcp_acceptor const&) = delete;
172
173 /** Open, bind, and listen on an endpoint.
174
175 Creates an IPv4 TCP socket, binds it to the specified endpoint,
176 and begins listening for incoming connections. This must be
177 called before initiating accept operations.
178
179 @param ep The local endpoint to bind to. Use `endpoint(port)` to
180 bind to all interfaces on a specific port.
181
182 @param backlog The maximum length of the queue of pending
183 connections. Defaults to 128.
184
185 @return An error code indicating success or the reason for failure.
186 A default-constructed error code indicates success.
187
188 @par Error Conditions
189 @li `errc::address_in_use`: The endpoint is already in use.
190 @li `errc::address_not_available`: The address is not available
191 on any local interface.
192 @li `errc::permission_denied`: Insufficient privileges to bind
193 to the endpoint (e.g., privileged port).
194 @li `errc::operation_not_supported`: The acceptor service is
195 unavailable in the context (POSIX only).
196
197 @throws Nothing.
198 */
199 [[nodiscard]] std::error_code listen(endpoint ep, int backlog = 128);
200
201 /** Close the acceptor.
202
203 Releases acceptor resources. Any pending operations complete
204 with `errc::operation_canceled`.
205 */
206 void close();
207
208 /** Check if the acceptor is listening.
209
210 @return `true` if the acceptor is open and listening.
211 */
212 7707 bool is_open() const noexcept
213 {
214 7707 return h_ && get().is_open();
215 }
216
217 /** Initiate an asynchronous accept operation.
218
219 Accepts an incoming connection and initializes the provided
220 socket with the new connection. The acceptor must be listening
221 before calling this function.
222
223 The operation supports cancellation via `std::stop_token` through
224 the affine awaitable protocol. If the associated stop token is
225 triggered, the operation completes immediately with
226 `errc::operation_canceled`.
227
228 @param peer The socket to receive the accepted connection. Any
229 existing connection on this socket will be closed.
230
231 @return An awaitable that completes with `io_result<>`.
232 Returns success on successful accept, or an error code on
233 failure including:
234 - operation_canceled: Cancelled via stop_token or cancel().
235 Check `ec == cond::canceled` for portable comparison.
236
237 @par Preconditions
238 The acceptor must be listening (`is_open() == true`).
239 The peer socket must be associated with the same execution context.
240
241 Both this acceptor and @p peer must outlive the returned
242 awaitable.
243
244 @par Example
245 @code
246 tcp_socket peer(ioc);
247 auto [ec] = co_await acc.accept(peer);
248 if (!ec) {
249 // Use peer socket
250 }
251 @endcode
252 */
253 7262 auto accept(tcp_socket& peer)
254 {
255 7262 if (!is_open())
256 detail::throw_logic_error("accept: acceptor not listening");
257 7262 return accept_awaitable(*this, peer);
258 }
259
260 /** Cancel any pending asynchronous operations.
261
262 All outstanding operations complete with `errc::operation_canceled`.
263 Check `ec == cond::canceled` for portable comparison.
264 */
265 void cancel();
266
267 /** Get the local endpoint of the acceptor.
268
269 Returns the local address and port to which the acceptor is bound.
270 This is useful when binding to port 0 (ephemeral port) to discover
271 the OS-assigned port number. The endpoint is cached when listen()
272 is called.
273
274 @return The local endpoint, or a default endpoint (0.0.0.0:0) if
275 the acceptor is not listening.
276
277 @par Thread Safety
278 The cached endpoint value is set during listen() and cleared
279 during close(). This function may be called concurrently with
280 accept operations, but must not be called concurrently with
281 listen() or close().
282 */
283 endpoint local_endpoint() const noexcept;
284
285 struct implementation : io_object::implementation
286 {
287 virtual std::coroutine_handle<> accept(
288 std::coroutine_handle<>,
289 capy::executor_ref,
290 std::stop_token,
291 std::error_code*,
292 io_object::implementation**) = 0;
293
294 /// Returns the cached local endpoint.
295 virtual endpoint local_endpoint() const noexcept = 0;
296
297 /// Return true if the acceptor has a kernel resource open.
298 virtual bool is_open() const noexcept = 0;
299
300 /** Cancel any pending asynchronous operations.
301
302 All outstanding operations complete with operation_canceled error.
303 */
304 virtual void cancel() noexcept = 0;
305 };
306
307 protected:
308 4 explicit tcp_acceptor(handle h) noexcept : io_object(std::move(h)) {}
309
310 /// Transfer accepted peer impl to the peer socket.
311 static void
312 4 reset_peer_impl(tcp_socket& peer, io_object::implementation* impl) noexcept
313 {
314 4 if (impl)
315 4 peer.h_.reset(impl);
316 4 }
317
318 private:
319 15053 inline implementation& get() const noexcept
320 {
321 15053 return *static_cast<implementation*>(h_.get());
322 }
323 };
324
325 } // namespace boost::corosio
326
327 #endif
328