Skip to content

Commit 91ce497

Browse files
committed
socket::shutdown with tests and documentation
1 parent 004fdd9 commit 91ce497

6 files changed

Lines changed: 208 additions & 6 deletions

File tree

include/boost/corosio/io_stream.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ class BOOST_COROSIO_DECL io_stream : public io_object
4343
@return An awaitable that completes with a pair of
4444
`{error_code, bytes_transferred}`. Returns success with the
4545
number of bytes read, or an error code on failure including:
46-
- capy::error::eof: End of stream reached.
47-
Check `ec == cond::eof` for portable comparison.
48-
- operation_canceled: Cancelled via stop_token or cancel().
49-
Check `ec == cond::canceled` for portable comparison.
46+
- cond::eof: The peer closed their send direction by calling
47+
shutdown or close, sending a TCP FIN. Use the portable
48+
condition test: `if (ec == capy::cond::eof)`
49+
- cond::canceled: Cancelled via stop_token or cancel().
50+
Use the portable condition test:
51+
`if (ec == capy::cond::canceled)`
5052
5153
@par Preconditions
5254
The socket must be open and connected.

include/boost/corosio/socket.hpp

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ namespace corosio {
7474
class BOOST_COROSIO_DECL socket : public io_stream
7575
{
7676
public:
77+
/** Different ways a socket may be shutdown. */
78+
enum shutdown_type
79+
{
80+
shutdown_receive,
81+
shutdown_send,
82+
shutdown_both
83+
};
84+
7785
struct socket_impl : io_stream_impl
7886
{
7987
virtual void connect(
@@ -82,6 +90,8 @@ class BOOST_COROSIO_DECL socket : public io_stream
8290
endpoint,
8391
std::stop_token,
8492
system::error_code*) = 0;
93+
94+
virtual system::error_code shutdown(shutdown_type) noexcept = 0;
8595
};
8696

8797
struct connect_awaitable
@@ -271,6 +281,47 @@ class BOOST_COROSIO_DECL socket : public io_stream
271281
*/
272282
void cancel();
273283

284+
/** Disable sends or receives on the socket.
285+
286+
TCP connections are full-duplex: each direction (send and receive)
287+
operates independently. This function allows you to close one or
288+
both directions without destroying the socket.
289+
290+
@li @ref shutdown_send sends a TCP FIN packet to the peer,
291+
signaling that you have no more data to send. You can still
292+
receive data until the peer also closes their send direction.
293+
This is the most common use case, typically called before
294+
close() to ensure graceful connection termination.
295+
296+
@li @ref shutdown_receive disables reading on the socket. This
297+
does NOT send anything to the peer - they are not informed
298+
and may continue sending data. Subsequent reads will fail
299+
or return end-of-file. Incoming data may be discarded or
300+
buffered depending on the operating system.
301+
302+
@li @ref shutdown_both combines both effects: sends a FIN and
303+
disables reading.
304+
305+
When the peer shuts down their send direction (sends a FIN),
306+
subsequent read operations will complete with `capy::cond::eof`.
307+
Use the portable condition test rather than comparing error
308+
codes directly:
309+
310+
@code
311+
auto [ec, n] = co_await sock.read_some(buffer);
312+
if (ec == capy::cond::eof)
313+
{
314+
// Peer closed their send direction
315+
}
316+
@endcode
317+
318+
Any error from the underlying system call is silently discarded
319+
because it is unlikely to be helpful.
320+
321+
@param what Determines what operations will no longer be allowed.
322+
*/
323+
void shutdown(shutdown_type what);
324+
274325
private:
275326
friend class acceptor;
276327

src/corosio/src/detail/epoll/sockets.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ class epoll_socket_impl
137137
system::error_code*,
138138
std::size_t*) override;
139139

140+
system::error_code shutdown(socket::shutdown_type what) noexcept override
141+
{
142+
int how;
143+
switch (what)
144+
{
145+
case socket::shutdown_receive: how = SHUT_RD; break;
146+
case socket::shutdown_send: how = SHUT_WR; break;
147+
case socket::shutdown_both: how = SHUT_RDWR; break;
148+
default:
149+
return make_err(EINVAL);
150+
}
151+
if (::shutdown(fd_, how) != 0)
152+
return make_err(errno);
153+
return {};
154+
}
155+
140156
int native_handle() const noexcept { return fd_; }
141157
bool is_open() const noexcept { return fd_ >= 0; }
142158
void cancel() noexcept;

src/corosio/src/detail/iocp/sockets.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,22 @@ class win_socket_impl
225225
internal_->write_some(h, d, buf, token, ec, bytes);
226226
}
227227

228+
system::error_code shutdown(socket::shutdown_type what) noexcept override
229+
{
230+
int how;
231+
switch (what)
232+
{
233+
case socket::shutdown_receive: how = SD_RECEIVE; break;
234+
case socket::shutdown_send: how = SD_SEND; break;
235+
case socket::shutdown_both: how = SD_BOTH; break;
236+
default:
237+
return make_err(WSAEINVAL);
238+
}
239+
if (::shutdown(internal_->native_handle(), how) != 0)
240+
return make_err(WSAGetLastError());
241+
return {};
242+
}
243+
228244
win_socket_impl_internal* get_internal() const noexcept { return internal_.get(); }
229245
};
230246

src/corosio/src/socket.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ socket::
5151
open()
5252
{
5353
if (impl_)
54-
return; // Already open
54+
return;
5555

5656
auto& svc = ctx_->use_service<socket_service>();
5757
auto& wrapper = svc.create_impl();
@@ -75,7 +75,7 @@ socket::
7575
close()
7676
{
7777
if (!impl_)
78-
return; // Already closed
78+
return;
7979

8080
auto* wrapper = static_cast<socket_impl_type*>(impl_);
8181
wrapper->release();
@@ -94,5 +94,13 @@ cancel()
9494
#endif
9595
}
9696

97+
void
98+
socket::
99+
shutdown(shutdown_type what)
100+
{
101+
if (impl_)
102+
get().shutdown(what);
103+
}
104+
97105
} // namespace corosio
98106
} // namespace boost

test/unit/socket.cpp

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,109 @@ struct socket_test
679679
s2.close();
680680
}
681681

682+
// Shutdown
683+
684+
void
685+
testShutdownSend()
686+
{
687+
io_context ioc;
688+
auto [s1, s2] = test::make_socket_pair(ioc);
689+
690+
auto task = [](socket& a, socket& b) -> capy::task<>
691+
{
692+
// Write data then shutdown send
693+
co_await a.write_some(capy::const_buffer("hello", 5));
694+
a.shutdown(socket::shutdown_send);
695+
696+
// Read the data
697+
char buf[32] = {};
698+
auto [ec1, n1] = co_await b.read_some(
699+
capy::mutable_buffer(buf, sizeof(buf)));
700+
BOOST_TEST(!ec1);
701+
BOOST_TEST_EQ(std::string_view(buf, n1), "hello");
702+
703+
// Next read should get EOF
704+
auto [ec2, n2] = co_await b.read_some(
705+
capy::mutable_buffer(buf, sizeof(buf)));
706+
BOOST_TEST(ec2 == capy::cond::eof);
707+
};
708+
capy::run_async(ioc.get_executor())(task(s1, s2));
709+
710+
ioc.run();
711+
s1.close();
712+
s2.close();
713+
}
714+
715+
void
716+
testShutdownReceive()
717+
{
718+
io_context ioc;
719+
auto [s1, s2] = test::make_socket_pair(ioc);
720+
721+
auto task = [](socket& a, socket& b) -> capy::task<>
722+
{
723+
// Shutdown receive on b
724+
b.shutdown(socket::shutdown_receive);
725+
726+
// b can still send
727+
co_await b.write_some(capy::const_buffer("from_b", 6));
728+
729+
char buf[32] = {};
730+
auto [ec, n] = co_await a.read_some(
731+
capy::mutable_buffer(buf, sizeof(buf)));
732+
BOOST_TEST(!ec);
733+
BOOST_TEST_EQ(std::string_view(buf, n), "from_b");
734+
};
735+
capy::run_async(ioc.get_executor())(task(s1, s2));
736+
737+
ioc.run();
738+
s1.close();
739+
s2.close();
740+
}
741+
742+
void
743+
testShutdownOnClosedSocket()
744+
{
745+
io_context ioc;
746+
socket sock(ioc);
747+
748+
// Shutdown on closed socket should not crash
749+
sock.shutdown(socket::shutdown_send);
750+
sock.shutdown(socket::shutdown_receive);
751+
sock.shutdown(socket::shutdown_both);
752+
}
753+
754+
void
755+
testShutdownBothSendDirection()
756+
{
757+
io_context ioc;
758+
auto [s1, s2] = test::make_socket_pair(ioc);
759+
760+
auto task = [](socket& a, socket& b) -> capy::task<>
761+
{
762+
// Write data then shutdown both
763+
co_await a.write_some(capy::const_buffer("goodbye", 7));
764+
a.shutdown(socket::shutdown_both);
765+
766+
// Peer should receive the data
767+
char buf[32] = {};
768+
auto [ec1, n1] = co_await b.read_some(
769+
capy::mutable_buffer(buf, sizeof(buf)));
770+
BOOST_TEST(!ec1);
771+
BOOST_TEST_EQ(std::string_view(buf, n1), "goodbye");
772+
773+
// Next read should get EOF
774+
auto [ec2, n2] = co_await b.read_some(
775+
capy::mutable_buffer(buf, sizeof(buf)));
776+
BOOST_TEST(ec2 == capy::cond::eof);
777+
};
778+
capy::run_async(ioc.get_executor())(task(s1, s2));
779+
780+
ioc.run();
781+
s1.close();
782+
s2.close();
783+
}
784+
682785
// Data Integrity
683786

684787
void
@@ -770,6 +873,12 @@ struct socket_test
770873
testReadAfterPeerClose();
771874
testWriteAfterPeerClose();
772875

876+
// Shutdown
877+
testShutdownSend();
878+
testShutdownReceive();
879+
testShutdownOnClosedSocket();
880+
testShutdownBothSendDirection();
881+
773882
// Cancellation
774883
testCancelRead();
775884
testCloseWhileReading();

0 commit comments

Comments
 (0)