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

94.7% Lines (36/38) 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 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 4725 accept_awaitable(tcp_acceptor& acc, tcp_socket& peer) noexcept
77 4725 : acc_(acc)
78 4725 , peer_(peer)
79 {
80 4725 }
81
82 4725 bool await_ready() const noexcept
83 {
84 4725 return token_.stop_requested();
85 }
86
87 4725 capy::io_result<> await_resume() const noexcept
88 {
89
2/2
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 4719 times.
4725 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
5/6
✓ Branch 1 taken 4713 times.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 4713 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 4713 times.
✓ Branch 6 taken 6 times.
4719 if (!ec_ && peer_impl_)
95 {
96 4713 peer_.close();
97 4713 peer_.impl_ = peer_impl_;
98 }
99 4719 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 4725 auto await_suspend(
113 std::coroutine_handle<> h,
114 Ex const& ex,
115 std::stop_token token) -> std::coroutine_handle<>
116 {
117 4725 token_ = std::move(token);
118
1/1
✓ Branch 3 taken 4725 times.
4725 acc_.get().accept(h, ex, token_, &ec_, &peer_impl_);
119 4725 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 16 tcp_acceptor& operator=(tcp_acceptor&& other)
175 {
176
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 if (this != &other)
177 {
178
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (ctx_ != other.ctx_)
179 detail::throw_logic_error(
180 "cannot move tcp_acceptor across execution contexts");
181 16 close();
182 16 impl_ = other.impl_;
183 16 other.impl_ = nullptr;
184 }
185 16 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 4725 auto accept(tcp_socket& peer)
257 {
258
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4725 times.
4725 if (!impl_)
259 detail::throw_logic_error("accept: acceptor not listening");
260 4725 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 4741 inline acceptor_impl& get() const noexcept
309 {
310 4741 return *static_cast<acceptor_impl*>(impl_);
311 }
312 };
313
314 } // namespace boost::corosio
315
316 #endif
317