include/boost/corosio/tcp_socket.hpp

88.9% Lines (40/45) 100.0% Functions (22/22)
include/boost/corosio/tcp_socket.hpp
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/io_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 <memory>
33 #include <stop_token>
34 #include <type_traits>
35
36 namespace boost::corosio {
37
38 #if BOOST_COROSIO_HAS_IOCP
39 using native_handle_type = std::uintptr_t; // SOCKET
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 struct implementation : io_stream::implementation
94 {
95 virtual std::coroutine_handle<> connect(
96 std::coroutine_handle<>,
97 capy::executor_ref,
98 endpoint,
99 std::stop_token,
100 std::error_code*) = 0;
101
102 virtual std::error_code shutdown(shutdown_type) noexcept = 0;
103
104 virtual native_handle_type native_handle() const noexcept = 0;
105
106 /** Request cancellation of pending asynchronous operations.
107
108 All outstanding operations complete with operation_canceled error.
109 Check `ec == cond::canceled` for portable comparison.
110 */
111 virtual void cancel() noexcept = 0;
112
113 /** Set a socket option.
114
115 @param level The protocol level (e.g. `SOL_SOCKET`).
116 @param optname The option name (e.g. `SO_KEEPALIVE`).
117 @param data Pointer to the option value.
118 @param size Size of the option value in bytes.
119 @return Error code on failure, empty on success.
120 */
121 virtual std::error_code set_option(
122 int level, int optname,
123 void const* data, std::size_t size) noexcept = 0;
124
125 /** Get a socket option.
126
127 @param level The protocol level (e.g. `SOL_SOCKET`).
128 @param optname The option name (e.g. `SO_KEEPALIVE`).
129 @param data Pointer to receive the option value.
130 @param size On entry, the size of the buffer. On exit,
131 the size of the option value.
132 @return Error code on failure, empty on success.
133 */
134 virtual std::error_code get_option(
135 int level, int optname,
136 void* data, std::size_t* size) const noexcept = 0;
137
138 /// Returns the cached local endpoint.
139 virtual endpoint local_endpoint() const noexcept = 0;
140
141 /// Returns the cached remote endpoint.
142 virtual endpoint remote_endpoint() const noexcept = 0;
143 };
144
145 struct connect_awaitable
146 {
147 tcp_socket& s_;
148 endpoint endpoint_;
149 std::stop_token token_;
150 mutable std::error_code ec_;
151
152 7688 connect_awaitable(tcp_socket& s, endpoint ep) noexcept
153 7688 : s_(s)
154 7688 , endpoint_(ep)
155 {
156 7688 }
157
158 7688 bool await_ready() const noexcept
159 {
160 7688 return token_.stop_requested();
161 }
162
163 7688 capy::io_result<> await_resume() const noexcept
164 {
165 7688 if (token_.stop_requested())
166 return {make_error_code(std::errc::operation_canceled)};
167 7688 return {ec_};
168 }
169
170 7688 auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
171 -> std::coroutine_handle<>
172 {
173 7688 token_ = env->stop_token;
174 7688 return s_.get().connect(h, env->executor, endpoint_, token_, &ec_);
175 }
176 };
177
178 public:
179 /** Destructor.
180
181 Closes the socket if open, cancelling any pending operations.
182 */
183 ~tcp_socket() override;
184
185 /** Construct a socket from an execution context.
186
187 @param ctx The execution context that will own this socket.
188 */
189 explicit tcp_socket(capy::execution_context& ctx);
190
191 /** Construct a socket from an executor.
192
193 The socket is associated with the executor's context.
194
195 @param ex The executor whose context will own the socket.
196 */
197 template<class Ex>
198 requires(!std::same_as<std::remove_cvref_t<Ex>, tcp_socket>) &&
199 capy::Executor<Ex>
200 explicit tcp_socket(Ex const& ex) : tcp_socket(ex.context())
201 {
202 }
203
204 /** Move constructor.
205
206 Transfers ownership of the socket resources.
207
208 @param other The socket to move from.
209
210 @pre No awaitables returned by @p other's methods exist.
211 @pre @p other is not referenced as a peer in any outstanding
212 accept awaitable.
213 @pre The execution context associated with @p other must
214 outlive this socket.
215 */
216 188 tcp_socket(tcp_socket&& other) noexcept : io_object(std::move(other)) {}
217
218 /** Move assignment operator.
219
220 Closes any existing socket and transfers ownership.
221
222 @param other The socket to move from.
223
224 @pre No awaitables returned by either `*this` or @p other's
225 methods exist.
226 @pre Neither `*this` nor @p other is referenced as a peer in
227 any outstanding accept awaitable.
228 @pre The execution context associated with @p other must
229 outlive this socket.
230
231 @return Reference to this socket.
232 */
233 10 tcp_socket& operator=(tcp_socket&& other) noexcept
234 {
235 10 if (this != &other)
236 {
237 10 close();
238 10 h_ = std::move(other.h_);
239 }
240 10 return *this;
241 }
242
243 tcp_socket(tcp_socket const&) = delete;
244 tcp_socket& operator=(tcp_socket const&) = delete;
245
246 /** Open the socket.
247
248 Creates a TCP socket and associates it with the platform
249 reactor (IOCP on Windows). Calling @ref connect on a closed
250 socket opens it automatically with the endpoint's address family,
251 so explicit `open()` is only needed when socket options must be
252 set before connecting.
253
254 @param proto The protocol (IPv4 or IPv6). Defaults to
255 `tcp::v4()`.
256
257 @throws std::system_error on failure.
258 */
259 void open( tcp proto = tcp::v4() );
260
261 /** Close the socket.
262
263 Releases socket resources. Any pending operations complete
264 with `errc::operation_canceled`.
265 */
266 void close();
267
268 /** Check if the socket is open.
269
270 @return `true` if the socket is open and ready for operations.
271 */
272 47137 bool is_open() const noexcept
273 {
274 #if BOOST_COROSIO_HAS_IOCP
275 return h_ && get().native_handle() != ~native_handle_type(0);
276 #else
277 47137 return h_ && get().native_handle() >= 0;
278 #endif
279 }
280
281 /** Initiate an asynchronous connect operation.
282
283 If the socket is not already open, it is opened automatically
284 using the address family of @p ep (IPv4 or IPv6). If the socket
285 is already open, the existing file descriptor is used as-is.
286
287 The operation supports cancellation via `std::stop_token` through
288 the affine awaitable protocol. If the associated stop token is
289 triggered, the operation completes immediately with
290 `errc::operation_canceled`.
291
292 @param ep The remote endpoint to connect to.
293
294 @return An awaitable that completes with `io_result<>`.
295 Returns success (default error_code) on successful connection,
296 or an error code on failure including:
297 - connection_refused: No server listening at endpoint
298 - timed_out: Connection attempt timed out
299 - network_unreachable: No route to host
300 - operation_canceled: Cancelled via stop_token or cancel().
301 Check `ec == cond::canceled` for portable comparison.
302
303 @throws std::system_error if the socket needs to be opened
304 and the open fails.
305
306 @par Preconditions
307 This socket must outlive the returned awaitable.
308
309 @par Example
310 @code
311 // Socket opened automatically with correct address family:
312 auto [ec] = co_await s.connect(endpoint);
313 if (ec) { ... }
314 @endcode
315 */
316 7688 auto connect(endpoint ep)
317 {
318 7688 if (!is_open())
319 16 open(ep.is_v6() ? tcp::v6() : tcp::v4());
320 7688 return connect_awaitable(*this, ep);
321 }
322
323 /** Cancel any pending asynchronous operations.
324
325 All outstanding operations complete with `errc::operation_canceled`.
326 Check `ec == cond::canceled` for portable comparison.
327 */
328 void cancel();
329
330 /** Get the native socket handle.
331
332 Returns the underlying platform-specific socket descriptor.
333 On POSIX systems this is an `int` file descriptor.
334 On Windows this is a `SOCKET` handle.
335
336 @return The native socket handle, or -1/INVALID_SOCKET if not open.
337
338 @par Preconditions
339 None. May be called on closed sockets.
340 */
341 native_handle_type native_handle() const noexcept;
342
343 /** Disable sends or receives on the socket.
344
345 TCP connections are full-duplex: each direction (send and receive)
346 operates independently. This function allows you to close one or
347 both directions without destroying the socket.
348
349 @li @ref shutdown_send sends a TCP FIN packet to the peer,
350 signaling that you have no more data to send. You can still
351 receive data until the peer also closes their send direction.
352 This is the most common use case, typically called before
353 close() to ensure graceful connection termination.
354
355 @li @ref shutdown_receive disables reading on the socket. This
356 does NOT send anything to the peer - they are not informed
357 and may continue sending data. Subsequent reads will fail
358 or return end-of-file. Incoming data may be discarded or
359 buffered depending on the operating system.
360
361 @li @ref shutdown_both combines both effects: sends a FIN and
362 disables reading.
363
364 When the peer shuts down their send direction (sends a FIN),
365 subsequent read operations will complete with `capy::cond::eof`.
366 Use the portable condition test rather than comparing error
367 codes directly:
368
369 @code
370 auto [ec, n] = co_await sock.read_some(buffer);
371 if (ec == capy::cond::eof)
372 {
373 // Peer closed their send direction
374 }
375 @endcode
376
377 Any error from the underlying system call is silently discarded
378 because it is unlikely to be helpful.
379
380 @param what Determines what operations will no longer be allowed.
381 */
382 void shutdown(shutdown_type what);
383
384 /** Set a socket option.
385
386 Applies a type-safe socket option to the underlying socket.
387 The option type encodes the protocol level and option name.
388
389 @par Example
390 @code
391 sock.set_option( socket_option::no_delay( true ) );
392 sock.set_option( socket_option::receive_buffer_size( 65536 ) );
393 @endcode
394
395 @param opt The option to set.
396
397 @throws std::logic_error if the socket is not open.
398 @throws std::system_error on failure.
399 */
400 template<class Option>
401 60 void set_option( Option const& opt )
402 {
403 60 if (!is_open())
404 detail::throw_logic_error( "set_option: socket not open" );
405 60 std::error_code ec = get().set_option(
406 Option::level(), Option::name(), opt.data(), opt.size() );
407 60 if (ec)
408 detail::throw_system_error( ec, "tcp_socket::set_option" );
409 60 }
410
411 /** Get a socket option.
412
413 Retrieves the current value of a type-safe socket option.
414
415 @par Example
416 @code
417 auto nd = sock.get_option<socket_option::no_delay>();
418 if ( nd.value() )
419 // Nagle's algorithm is disabled
420 @endcode
421
422 @return The current option value.
423
424 @throws std::logic_error if the socket is not open.
425 @throws std::system_error on failure.
426 */
427 template<class Option>
428 62 Option get_option() const
429 {
430 62 if (!is_open())
431 detail::throw_logic_error( "get_option: socket not open" );
432 62 Option opt{};
433 62 std::size_t sz = opt.size();
434 62 std::error_code ec = get().get_option(
435 Option::level(), Option::name(), opt.data(), &sz );
436 62 if (ec)
437 detail::throw_system_error( ec, "tcp_socket::get_option" );
438 62 opt.resize( sz );
439 62 return opt;
440 }
441
442 /** Get the local endpoint of the socket.
443
444 Returns the local address and port to which the socket is bound.
445 For a connected socket, this is the local side of the connection.
446 The endpoint is cached when the connection is established.
447
448 @return The local endpoint, or a default endpoint (0.0.0.0:0) if
449 the socket is not connected.
450
451 @par Thread Safety
452 The cached endpoint value is set during connect/accept completion
453 and cleared during close(). This function may be called concurrently
454 with I/O operations, but must not be called concurrently with
455 connect(), accept(), or close().
456 */
457 endpoint local_endpoint() const noexcept;
458
459 /** Get the remote endpoint of the socket.
460
461 Returns the remote address and port to which the socket is connected.
462 The endpoint is cached when the connection is established.
463
464 @return The remote endpoint, or a default endpoint (0.0.0.0:0) if
465 the socket is not connected.
466
467 @par Thread Safety
468 The cached endpoint value is set during connect/accept completion
469 and cleared during close(). This function may be called concurrently
470 with I/O operations, but must not be called concurrently with
471 connect(), accept(), or close().
472 */
473 endpoint remote_endpoint() const noexcept;
474
475 protected:
476 10 tcp_socket() noexcept = default;
477
478 explicit tcp_socket(handle h) noexcept : io_object(std::move(h)) {}
479
480 private:
481 friend class tcp_acceptor;
482
483 /// Open the socket for the given protocol triple.
484 void open_for_family(int family, int type, int protocol);
485
486 55183 inline implementation& get() const noexcept
487 {
488 55183 return *static_cast<implementation*>(h_.get());
489 }
490 };
491
492 } // namespace boost::corosio
493
494 #endif
495