libs/corosio/include/boost/corosio/tcp_acceptor.hpp

94.6% Lines (35/37) 100.0% Functions (9/9) 73.3% Branches (11/15)
libs/corosio/include/boost/corosio/tcp_acceptor.hpp
Line Branch Hits 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
2/2
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 8507 times.
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
5/6
✓ Branch 1 taken 8501 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 8501 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 8501 times.
✓ Branch 6 taken 6 times.
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
1/1
✓ Branch 3 taken 8513 times.
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
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (this != &other)
167 {
168
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (ctx_ != other.ctx_)
169 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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8513 times.
8513 if (!impl_)
261 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
319