include/boost/corosio/tcp_acceptor.hpp

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