include/boost/corosio/tcp_socket.hpp

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