include/boost/corosio/tcp_socket.hpp

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