libs/corosio/include/boost/corosio/tcp_socket.hpp

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