include/boost/corosio/tcp_socket.hpp

88.9% Lines (40/45) 100.0% Functions (22/22)
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 // Copyright (c) 2026 Steve Gerbino
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/cppalliance/corosio
9 //
10
11 #ifndef BOOST_COROSIO_TCP_SOCKET_HPP
12 #define BOOST_COROSIO_TCP_SOCKET_HPP
13
14 #include <boost/corosio/detail/config.hpp>
15 #include <boost/corosio/detail/platform.hpp>
16 #include <boost/corosio/detail/except.hpp>
17 #include <boost/corosio/io/io_stream.hpp>
18 #include <boost/capy/io_result.hpp>
19 #include <boost/corosio/detail/buffer_param.hpp>
20 #include <boost/corosio/endpoint.hpp>
21 #include <boost/corosio/tcp.hpp>
22 #include <boost/capy/ex/executor_ref.hpp>
23 #include <boost/capy/ex/execution_context.hpp>
24 #include <boost/capy/ex/io_env.hpp>
25 #include <boost/capy/concept/executor.hpp>
26
27 #include <system_error>
28
29 #include <concepts>
30 #include <coroutine>
31 #include <cstddef>
32 #include <stop_token>
33 #include <type_traits>
34
35 namespace boost::corosio {
36
37 /// Represent a platform-specific socket descriptor (`int` on POSIX, `SOCKET` on Windows).
38 #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
39 using native_handle_type = std::uintptr_t;
40 #else
41 using native_handle_type = int;
42 #endif
43
44 /** An asynchronous TCP socket for coroutine I/O.
45
46 This class provides asynchronous TCP socket operations that return
47 awaitable types. Each operation participates in the affine awaitable
48 protocol, ensuring coroutines resume on the correct executor.
49
50 The socket must be opened before performing I/O operations. Operations
51 support cancellation through `std::stop_token` via the affine protocol,
52 or explicitly through the `cancel()` member function.
53
54 @par Thread Safety
55 Distinct objects: Safe.@n
56 Shared objects: Unsafe. A socket must not have concurrent operations
57 of the same type (e.g., two simultaneous reads). One read and one
58 write may be in flight simultaneously.
59
60 @par Semantics
61 Wraps the platform TCP/IP stack. Operations dispatch to
62 OS socket APIs via the io_context reactor (epoll, IOCP,
63 kqueue). Satisfies @ref capy::Stream.
64
65 @par Example
66 @code
67 io_context ioc;
68 tcp_socket s(ioc);
69 s.open();
70
71 // Using structured bindings
72 auto [ec] = co_await s.connect(
73 endpoint(ipv4_address::loopback(), 8080));
74 if (ec)
75 co_return;
76
77 char buf[1024];
78 auto [read_ec, n] = co_await s.read_some(
79 capy::mutable_buffer(buf, sizeof(buf)));
80 @endcode
81 */
82 class BOOST_COROSIO_DECL tcp_socket : public io_stream
83 {
84 public:
85 /** Different ways a socket may be shutdown. */
86 enum shutdown_type
87 {
88 shutdown_receive,
89 shutdown_send,
90 shutdown_both
91 };
92
93 /** Define backend hooks for TCP socket operations.
94
95 Platform backends (epoll, IOCP, kqueue, select) derive from
96 this to implement socket I/O, connection, and option management.
97 */
98 struct implementation : io_stream::implementation
99 {
100 /** Initiate an asynchronous connect to the given endpoint.
101
102 @param h Coroutine handle to resume on completion.
103 @param ex Executor for dispatching the completion.
104 @param ep The remote endpoint to connect to.
105 @param token Stop token for cancellation.
106 @param ec Output error code.
107
108 @return Coroutine handle to resume immediately.
109 */
110 virtual std::coroutine_handle<> connect(
111 std::coroutine_handle<> h,
112 capy::executor_ref ex,
113 endpoint ep,
114 std::stop_token token,
115 std::error_code* ec) = 0;
116
117 /** Shut down the socket for the given direction(s).
118
119 @param what The shutdown direction.
120
121 @return Error code on failure, empty on success.
122 */
123 virtual std::error_code shutdown(shutdown_type what) noexcept = 0;
124
125 /// Return the platform socket descriptor.
126 virtual native_handle_type native_handle() const noexcept = 0;
127
128 /** Request cancellation of pending asynchronous operations.
129
130 All outstanding operations complete with operation_canceled error.
131 Check `ec == cond::canceled` for portable comparison.
132 */
133 virtual void cancel() noexcept = 0;
134
135 /** Set a socket option.
136
137 @param level The protocol level (e.g. `SOL_SOCKET`).
138 @param optname The option name (e.g. `SO_KEEPALIVE`).
139 @param data Pointer to the option value.
140 @param size Size of the option value in bytes.
141 @return Error code on failure, empty on success.
142 */
143 virtual std::error_code set_option(
144 int level,
145 int optname,
146 void const* data,
147 std::size_t size) noexcept = 0;
148
149 /** Get a socket option.
150
151 @param level The protocol level (e.g. `SOL_SOCKET`).
152 @param optname The option name (e.g. `SO_KEEPALIVE`).
153 @param data Pointer to receive the option value.
154 @param size On entry, the size of the buffer. On exit,
155 the size of the option value.
156 @return Error code on failure, empty on success.
157 */
158 virtual std::error_code
159 get_option(int level, int optname, void* data, std::size_t* size)
160 const noexcept = 0;
161
162 /// Return the cached local endpoint.
163 virtual endpoint local_endpoint() const noexcept = 0;
164
165 /// Return the cached remote endpoint.
166 virtual endpoint remote_endpoint() const noexcept = 0;
167 };
168
169 /// Represent the awaitable returned by @ref connect.
170 struct connect_awaitable
171 {
172 tcp_socket& s_;
173 endpoint endpoint_;
174 std::stop_token token_;
175 mutable std::error_code ec_;
176
177 6823x connect_awaitable(tcp_socket& s, endpoint ep) noexcept
178 6823x : s_(s)
179 6823x , endpoint_(ep)
180 {
181 6823x }
182
183 6823x bool await_ready() const noexcept
184 {
185 6823x return token_.stop_requested();
186 }
187
188 6823x capy::io_result<> await_resume() const noexcept
189 {
190 6823x if (token_.stop_requested())
191 return {make_error_code(std::errc::operation_canceled)};
192 6823x return {ec_};
193 }
194
195 6823x auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
196 -> std::coroutine_handle<>
197 {
198 6823x token_ = env->stop_token;
199 6823x return s_.get().connect(h, env->executor, endpoint_, token_, &ec_);
200 }
201 };
202
203 public:
204 /** Destructor.
205
206 Closes the socket if open, cancelling any pending operations.
207 */
208 ~tcp_socket() override;
209
210 /** Construct a socket from an execution context.
211
212 @param ctx The execution context that will own this socket.
213 */
214 explicit tcp_socket(capy::execution_context& ctx);
215
216 /** Construct a socket from an executor.
217
218 The socket is associated with the executor's context.
219
220 @param ex The executor whose context will own the socket.
221 */
222 template<class Ex>
223 requires(!std::same_as<std::remove_cvref_t<Ex>, tcp_socket>) &&
224 capy::Executor<Ex>
225 explicit tcp_socket(Ex const& ex) : tcp_socket(ex.context())
226 {
227 }
228
229 /** Move constructor.
230
231 Transfers ownership of the socket resources.
232
233 @param other The socket to move from.
234
235 @pre No awaitables returned by @p other's methods exist.
236 @pre @p other is not referenced as a peer in any outstanding
237 accept awaitable.
238 @pre The execution context associated with @p other must
239 outlive this socket.
240 */
241 176x tcp_socket(tcp_socket&& other) noexcept : io_object(std::move(other)) {}
242
243 /** Move assignment operator.
244
245 Closes any existing socket and transfers ownership.
246
247 @param other The socket to move from.
248
249 @pre No awaitables returned by either `*this` or @p other's
250 methods exist.
251 @pre Neither `*this` nor @p other is referenced as a peer in
252 any outstanding accept awaitable.
253 @pre The execution context associated with @p other must
254 outlive this socket.
255
256 @return Reference to this socket.
257 */
258 10x tcp_socket& operator=(tcp_socket&& other) noexcept
259 {
260 10x if (this != &other)
261 {
262 10x close();
263 10x h_ = std::move(other.h_);
264 }
265 10x return *this;
266 }
267
268 tcp_socket(tcp_socket const&) = delete;
269 tcp_socket& operator=(tcp_socket const&) = delete;
270
271 /** Open the socket.
272
273 Creates a TCP socket and associates it with the platform
274 reactor (IOCP on Windows). Calling @ref connect on a closed
275 socket opens it automatically with the endpoint's address family,
276 so explicit `open()` is only needed when socket options must be
277 set before connecting.
278
279 @param proto The protocol (IPv4 or IPv6). Defaults to
280 `tcp::v4()`.
281
282 @throws std::system_error on failure.
283 */
284 void open(tcp proto = tcp::v4());
285
286 /** Close the socket.
287
288 Releases socket resources. Any pending operations complete
289 with `errc::operation_canceled`.
290 */
291 void close();
292
293 /** Check if the socket is open.
294
295 @return `true` if the socket is open and ready for operations.
296 */
297 41937x bool is_open() const noexcept
298 {
299 #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
300 return h_ && get().native_handle() != ~native_handle_type(0);
301 #else
302 41937x return h_ && get().native_handle() >= 0;
303 #endif
304 }
305
306 /** Initiate an asynchronous connect operation.
307
308 If the socket is not already open, it is opened automatically
309 using the address family of @p ep (IPv4 or IPv6). If the socket
310 is already open, the existing file descriptor is used as-is.
311
312 The operation supports cancellation via `std::stop_token` through
313 the affine awaitable protocol. If the associated stop token is
314 triggered, the operation completes immediately with
315 `errc::operation_canceled`.
316
317 @param ep The remote endpoint to connect to.
318
319 @return An awaitable that completes with `io_result<>`.
320 Returns success (default error_code) on successful connection,
321 or an error code on failure including:
322 - connection_refused: No server listening at endpoint
323 - timed_out: Connection attempt timed out
324 - network_unreachable: No route to host
325 - operation_canceled: Cancelled via stop_token or cancel().
326 Check `ec == cond::canceled` for portable comparison.
327
328 @throws std::system_error if the socket needs to be opened
329 and the open fails.
330
331 @par Preconditions
332 This socket must outlive the returned awaitable.
333
334 @par Example
335 @code
336 // Socket opened automatically with correct address family:
337 auto [ec] = co_await s.connect(endpoint);
338 if (ec) { ... }
339 @endcode
340 */
341 6823x auto connect(endpoint ep)
342 {
343 6823x if (!is_open())
344 16x open(ep.is_v6() ? tcp::v6() : tcp::v4());
345 6823x return connect_awaitable(*this, ep);
346 }
347
348 /** Cancel any pending asynchronous operations.
349
350 All outstanding operations complete with `errc::operation_canceled`.
351 Check `ec == cond::canceled` for portable comparison.
352 */
353 void cancel();
354
355 /** Get the native socket handle.
356
357 Returns the underlying platform-specific socket descriptor.
358 On POSIX systems this is an `int` file descriptor.
359 On Windows this is a `SOCKET` handle.
360
361 @return The native socket handle, or -1/INVALID_SOCKET if not open.
362
363 @par Preconditions
364 None. May be called on closed sockets.
365 */
366 native_handle_type native_handle() const noexcept;
367
368 /** Disable sends or receives on the socket.
369
370 TCP connections are full-duplex: each direction (send and receive)
371 operates independently. This function allows you to close one or
372 both directions without destroying the socket.
373
374 @li @ref shutdown_send sends a TCP FIN packet to the peer,
375 signaling that you have no more data to send. You can still
376 receive data until the peer also closes their send direction.
377 This is the most common use case, typically called before
378 close() to ensure graceful connection termination.
379
380 @li @ref shutdown_receive disables reading on the socket. This
381 does NOT send anything to the peer - they are not informed
382 and may continue sending data. Subsequent reads will fail
383 or return end-of-file. Incoming data may be discarded or
384 buffered depending on the operating system.
385
386 @li @ref shutdown_both combines both effects: sends a FIN and
387 disables reading.
388
389 When the peer shuts down their send direction (sends a FIN),
390 subsequent read operations will complete with `capy::cond::eof`.
391 Use the portable condition test rather than comparing error
392 codes directly:
393
394 @code
395 auto [ec, n] = co_await sock.read_some(buffer);
396 if (ec == capy::cond::eof)
397 {
398 // Peer closed their send direction
399 }
400 @endcode
401
402 Any error from the underlying system call is silently discarded
403 because it is unlikely to be helpful.
404
405 @param what Determines what operations will no longer be allowed.
406 */
407 void shutdown(shutdown_type what);
408
409 /** Set a socket option.
410
411 Applies a type-safe socket option to the underlying socket.
412 The option type encodes the protocol level and option name.
413
414 @par Example
415 @code
416 sock.set_option( socket_option::no_delay( true ) );
417 sock.set_option( socket_option::receive_buffer_size( 65536 ) );
418 @endcode
419
420 @param opt The option to set.
421
422 @throws std::logic_error if the socket is not open.
423 @throws std::system_error on failure.
424 */
425 template<class Option>
426 60x void set_option(Option const& opt)
427 {
428 60x if (!is_open())
429 detail::throw_logic_error("set_option: socket not open");
430 60x std::error_code ec = get().set_option(
431 Option::level(), Option::name(), opt.data(), opt.size());
432 60x if (ec)
433 detail::throw_system_error(ec, "tcp_socket::set_option");
434 60x }
435
436 /** Get a socket option.
437
438 Retrieves the current value of a type-safe socket option.
439
440 @par Example
441 @code
442 auto nd = sock.get_option<socket_option::no_delay>();
443 if ( nd.value() )
444 // Nagle's algorithm is disabled
445 @endcode
446
447 @return The current option value.
448
449 @throws std::logic_error if the socket is not open.
450 @throws std::system_error on failure.
451 */
452 template<class Option>
453 62x Option get_option() const
454 {
455 62x if (!is_open())
456 detail::throw_logic_error("get_option: socket not open");
457 62x Option opt{};
458 62x std::size_t sz = opt.size();
459 std::error_code ec =
460 62x get().get_option(Option::level(), Option::name(), opt.data(), &sz);
461 62x if (ec)
462 detail::throw_system_error(ec, "tcp_socket::get_option");
463 62x opt.resize(sz);
464 62x return opt;
465 }
466
467 /** Get the local endpoint of the socket.
468
469 Returns the local address and port to which the socket is bound.
470 For a connected socket, this is the local side of the connection.
471 The endpoint is cached when the connection is established.
472
473 @return The local endpoint, or a default endpoint (0.0.0.0:0) if
474 the socket is not connected.
475
476 @par Thread Safety
477 The cached endpoint value is set during connect/accept completion
478 and cleared during close(). This function may be called concurrently
479 with I/O operations, but must not be called concurrently with
480 connect(), accept(), or close().
481 */
482 endpoint local_endpoint() const noexcept;
483
484 /** Get the remote endpoint of the socket.
485
486 Returns the remote address and port to which the socket is connected.
487 The endpoint is cached when the connection is established.
488
489 @return The remote endpoint, or a default endpoint (0.0.0.0:0) if
490 the socket is not connected.
491
492 @par Thread Safety
493 The cached endpoint value is set during connect/accept completion
494 and cleared during close(). This function may be called concurrently
495 with I/O operations, but must not be called concurrently with
496 connect(), accept(), or close().
497 */
498 endpoint remote_endpoint() const noexcept;
499
500 protected:
501 10x tcp_socket() noexcept = default;
502
503 explicit tcp_socket(handle h) noexcept : io_object(std::move(h)) {}
504
505 private:
506 friend class tcp_acceptor;
507
508 /// Open the socket for the given protocol triple.
509 void open_for_family(int family, int type, int protocol);
510
511 49130x inline implementation& get() const noexcept
512 {
513 49130x return *static_cast<implementation*>(h_.get());
514 }
515 };
516
517 } // namespace boost::corosio
518
519 #endif
520