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