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