I am exploring using boost::asio along with C++11 features. In particular, I am focusing on an example called "async_tcp_echo_server.cpp", located here (code is also shown at the end of my question):
My question involves the tcp::socket
member socket_
of the server
class. In the do_accept()
method of the server
class, socket_
is passed to async_accept()
. (According to the asio documentation, async_accept()
requires, as its first parameter, the socket
to accept the connection into.) So far, so good.
The next parameter, the callback for the asynchronous accept operation, is a lambda function. The body of the lambda constructs a new session
object, whose constructor also needs the same socket
. Interestingly, socket
objects cannot be copied; so in the example, the socket_
object, which is a member of the server
object, is passed using std::move()
.
I understand that the "one and only" socket_
object (which is a "permanent" member of the server
object) is "moved" into the session
object. Fine -- socket
object is not copied, but moved -- everybody's happy.
But what happens on the next call to async_accept()
? Is the same socket_
(member of server
), that was previously moved, passed in again? When we "move" a member, what is left behind? Is there a magical fountain of unlimited socket
objects?
Or is something really less-than-obvious happening here? When the socket
is moved into the session
, is the contents of the "left behind/moved from" object (socket_
member of server
) swapped with the contents of the "new" session
object's own "not-yet-constructed" socket_
member? Am I even making sense?
Code is below. Program flow is fairly simple. main()
constructs a single server
object. The server
makes repeated calls to async_accept()
. Each async_accept()
callback creates a new session
object, each constructed with a (fresh?) socket
. Where do all the "fresh" socket
objects come from, if they are simply (repeatedly) "moved" from the same socket_
member in the (single) server
?
#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
class session
: public std::enable_shared_from_this<session>
{
public:
session( tcp::socket socket )
: socket_( std::move( socket ) )
{}
void start() {
do_read();
}
private:
void do_read() {
auto self( shared_from_this() );
socket_.async_read_some(
boost::asio::buffer( data_, max_length ),
[this, self]( boost::system::error_code ec, std::size_t length )
{
if( !ec ) {
do_write( length );
}
}
);
}
void do_write( std::size_t length ) {
auto self( shared_from_this() );
boost::asio::async_write(
socket_,
boost::asio::buffer( data_, length ),
[this, self]( boost::system::error_code ec, std::size_t /*length*/ )
{
if( !ec ) {
do_read();
}
}
);
}
tcp::socket socket_;
enum { max_length = 1024 };
char data_[max_length];
};
class server {
public:
server( boost::asio::io_service& io_service, short port )
: acceptor_( io_service, tcp::endpoint( tcp::v4(), port ) )
, socket_( io_service )
{
do_accept();
}
private:
void do_accept() {
acceptor_.async_accept(
socket_,
[this]( boost::system::error_code ec )
{
if( !ec ) {
std::make_shared<session>( std::move( socket_ ) )->start(); // is this a *swap* of socket_ ???
}
do_accept();
}
);
}
tcp::acceptor acceptor_;
tcp::socket socket_;
};
int main( int argc, char* argv[] ) {
try {
if( argc != 2 ) {
std::cerr << "Usage: async_tcp_echo_server <port>\n";
return 1;
}
boost::asio::io_service io_service;
server s( io_service, std::atoi( argv[1] ) );
io_service.run();
} catch( std::exception& e ) {
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
Move semantics can be thought of as passing ownership of resources. Resource Acquisition Is Instantiation (RAII) is the concept of assigning ownership of resources at the time of object construction and the releasing of those resources at destruction. Move semantics allow for the transfer of ownership of resources at other times besides construction and destruction.
In this case, the object (server::socket_
) is the recipient of a transfer of ownership of the OS socket resource from server::acceptor_
. That transfer occurs at some point after async_accept()
returns, when a client connects. The newly connected socket resources are moved into socket_
, and the callback lambda function is called. During the lambda, the socket resources are moved into session::socket_
. Server::socket_ only owned the resource for a fraction of a microsecond.
Move semantics allow RAII classes to exist in the twilight state of not owning any resources. Think of a unique_ptr
after a call to release (it refers to no memory). The server::socket_ after the move out still has space to hold a resource, but for the moment it owns nothing.
The last thing the lambda function does is call do_accept
, which calls async_accept()
again. A reference to socket_
is passed in. When another client connects at some point in the future, async_accept()
will transfer ownership of a newly connected OS socket there.