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