I'm trying to implement two-way asynchronous communication in C++. I'd like to be able to specify the IP address and port number on two machines and be able to get the machines to communicate with each other.
I've looked at Boost::asio and have implemented the following so far:
#include <cstdlib>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
class session{
public:
session(boost::asio::io_service& io_service) : socket_(io_service){
}
tcp::socket& socket(){
return socket_;
}
void start(){
socket_.async_read_some(boost::asio::buffer(data_, max_length),boost::bind(&session::handle_read, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred));
onConnect();
}
void handle_read(const boost::system::error_code& error, size_t bytes_transferred){
if (!error){
char* buf = boost::asio::buffer_cast<char*>(boost::asio::buffer(data_, bytes_transferred));
char buf2[bytes_transferred];
int n;
n=sprintf(buf2,"%.*s",bytes_transferred,buf);
onData(buf2);
boost::asio::async_write(socket_, boost::asio::buffer("\0",0), boost::bind(&session::handle_write, this, boost::asio::placeholders::error));
}else{
delete this;
}
}
void handle_write(const boost::system::error_code& error){
if (!error){
socket_.async_read_some(boost::asio::buffer(data_, max_length),boost::bind(&session::handle_read, this,boost::asio::placeholders::error,boost::asio::placeholders::bytes_transferred));
}else{
delete this;
}
}
void onConnect(){
printf("Connected\n");
}
void onData(char* buf){
printf("%s",buf);
}
void write(const char* data){
//boost::asio::async_write(socket_, boost::asio::buffer(data, strlen(data)), boost::bind(&session::handle_write, this, boost::asio::placeholders::error));
}
private:
tcp::socket socket_;
enum { max_length = 1500 };
char data_[max_length];
};
class server{
public:
server(boost::asio::io_service& io_service, short port) : io_service_(io_service), acceptor_(io_service, tcp::endpoint(tcp::v4(), port)){
session* new_session = new session(io_service_);
acceptor_.async_accept(new_session->socket(), boost::bind(&server::handle_accept, this, new_session, boost::asio::placeholders::error));
}
void handle_accept(session* new_session, const boost::system::error_code& error){
if (!error){
new_session->start();
new_session = new session(io_service_);
acceptor_.async_accept(new_session->socket(), boost::bind(&server::handle_accept, this, new_session, boost::asio::placeholders::error));
}else{
delete new_session;
}
}
private:
boost::asio::io_service& io_service_;
tcp::acceptor acceptor_;
};
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;
using namespace std; // For atoi.
server s(io_service, atoi(argv[1]));
io_service.run();
}catch (std::exception& e){
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
I can telnet into this server and send messages to it, but how to access this server programatically from a remote machine? I don't seem to be able to specify an ip address from this code!
I hope someone might have some pointers.
I've not used Boost.ASIO, but searching for "boost asio ip address" and "boost asio gethostbyname" yielded this stuff:
http://www.boost.org/doc/libs/1_45_0/doc/html/boost_asio/reference/ip__address.html
http://www.boost.org/doc/libs/1_42_0/doc/html/boost_asio/reference/ip__tcp/resolver.html
The resolver has a resolve method that lets you do things like:
boost::shared_ptr< boost::asio::io_service > io_service(
new boost::asio::io_service
);
boost::asio::ip::tcp::resolver resolver( *io_service );
boost::asio::ip::tcp::resolver::query query(
"www.google.com", // host string
boost::lexical_cast< std::string >( 80 ) // port #
);
boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query );
boost::asio::ip::tcp::endpoint endpoint = *iterator;
So that'll get you to having a boost::asio::tcp::endpoint which you can use in your socket connection of your client code. The site where I grabbed this is here if you want more details:
http://www.gamedev.net/blog/950/entry-2249317-a-guide-to-getting-started-with-boostasio?pg=8
There are plenty of good examples on the Boost.Asio pages to guide you in the right direction.
Simply put, you need to write a server and a client. The former will create the endpoint to connect to and the latter connects. Take a look at the examples. They're very straight-forward and easy to adapt to what your needs.
Related
I took the example code from boost website and modified it to return responses 100k+ bytes long.
message_ = "HTTP/1.1 200 OK\r\nContent-Length: 100000\r\n\r\n";
message_ += std::string(100000, 'X');
When run and I curl the endpoint, it sometimes returns the correct response but more often it fails after received exactly 65536 bytes with curl: (56) Recv failure: Connection reset by peer.
I've tried adding different socket/acceptor flags, closing the socket, shutdown and writing response in small chunks, but all failed to resolve this. I've also checked that the socket is open on the application side after writing the response, and it is. Application does not report any error code and indicates the entire message has been written (checked the bytes_transferred). Any advice on what I'm doing wrong here?
Full listing below
#include <ctime>
#include <iostream>
#include <string>
#include <boost/bind/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
class tcp_connection
: public boost::enable_shared_from_this<tcp_connection>
{
public:
typedef boost::shared_ptr<tcp_connection> pointer;
static pointer create(boost::asio::io_context& io_context)
{
return pointer(new tcp_connection(io_context));
}
tcp::socket& socket()
{
return socket_;
}
void start()
{
message_ = "HTTP/1.1 200 OK\r\nContent-Length: 100000\r\n\r\n";
message_ += std::string(100000, 'X');
boost::asio::async_write(socket_, boost::asio::buffer(message_),
boost::bind(&tcp_connection::handle_write, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
private:
tcp_connection(boost::asio::io_context& io_context)
: socket_(io_context)
{
}
void handle_write(const boost::system::error_code& /*error*/,
size_t /*bytes_transferred*/)
{
}
tcp::socket socket_;
std::string message_;
};
class tcp_server
{
public:
tcp_server(boost::asio::io_context& io_context)
: io_context_(io_context),
acceptor_(io_context, tcp::endpoint(tcp::v4(), 9000))
{
start_accept();
}
private:
void start_accept()
{
tcp_connection::pointer new_connection =
tcp_connection::create(io_context_);
acceptor_.async_accept(new_connection->socket(),
boost::bind(&tcp_server::handle_accept, this, new_connection,
boost::asio::placeholders::error));
}
void handle_accept(tcp_connection::pointer new_connection,
const boost::system::error_code& error)
{
if (!error)
{
new_connection->start();
}
start_accept();
}
boost::asio::io_context& io_context_;
tcp::acceptor acceptor_;
};
int main()
{
try
{
boost::asio::io_context io_context;
tcp_server server(io_context);
io_context.run();
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
The reason this was not working is because the boost example is not prepared to serve HTTP traffic properly. I've found the answer to this from another SO question boost::asio fails to close TCP connection cleanly
In essence it's necessary to read the HTTP request (even if it's not useful) before attempting to send a response. Why the example code from my question works for responses up to 65536 bytes remains a mystery to me, but it's probably because of curl implementation specifics.
So my tcp_connection::start() should look like the following:
void start() {
boost::asio::async_read_until(socket_, input_buffer_, "\r\n\r\n",
boost::bind(&tcp_connection::handle_read,
shared_from_this(),
boost::asio::placeholders::error);
}
With input_buffer_ being boost::asio::streambuf instance defined in tcp_connection. Only after that I can call boost::asio::async_write from within the handle_read.
In this example(async_tcp_echo_server.cpp),
#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();
}
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;
}
multiple sessions are using the same boost::ip::tcp::socket object. Inside do_accept() function, upon every incoming connection we are creating a new session object and passing socket_ to it by rvalue reference i.e. by std::move().
Let's say we have two connections (S1 and S2) which are active. The structure will roughly look like this
Server
|
|_ socket_
/\
/ \
/ \
S1 S2
So both S1 and S2 will be using the same socket_ to read/write messages from/to the network.
I have two questions about this:
For the first connection everything is fine, but why is the second connection supposed to work all right? Haven't we already transferred the ownership of socket_ to the first session?
How is it ensured that reply is being sent to the correct client? Whenever something arrives on the socket, can't any one of the two async_read_some get triggered?
For the first connection everything is fine, but why is the second connection supposed to work all right? Haven't we already transferred the ownership of socket_ to the first session?
After moving from the socket, the socket happens to be essentially "empty" or "newly created". That's why this works.
And no, the socket object does not "magically" share identity with the moved instances. In fact, the identity is little more than the underlying socket handle, which is obviously not shared.
But what happens in case of a second connection, as we are using the same socket in all connections?
You're not. You're not using the same socket handle. Neither are you using the same asio::ip::tcp::socket object instance.
I am using the code provided in the Boost example.
The server only accepts 1 connection at a time. This means, no new connections until the current one is closed.
How to make the above code accept unlimited connections at the same time?
#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)
{
boost::this_thread::sleep(boost::posix_time::milliseconds(10000));//sleep some time
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();
}
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;
}
As you see, the program waits for the sleep and it doesn't grab a second connection in the meantime.
You're doing a synchronous wait inside the handler which runs on the only thread that serves your io_service. This makes Asio wait with invoking the handlers for any new requests.
Use a deadline_time with wait_async, or,
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) {
timer_.expires_from_now(boost::posix_time::seconds(1));
timer_.async_wait([this, self, length](boost::system::error_code ec) {
if (!ec)
do_write(length);
});
}
});
}
Where the timer_ field is a boost::asio::deadline_timer member of session
as a poor-man's solution add more threads (this simply means that if more requests arrive at the same time than there are threads to handle them, it will still block until the first thread becomes available to pick up the new request)
boost::thread_group tg;
for (int i=0; i < 10; ++i)
tg.create_thread([&]{ io_service.run(); });
tg.join_all();
Both the original code and the modified code are asynchronous and accept multiple connections. As can be seen in the following snippet, the async_accept operation's AcceptHandler initiates another async_accept operation, forming an asynchronous loop:
.-----------------------------------.
V |
void server::do_accept() |
{ |
acceptor_.async_accept(..., |
[this](boost::system::error_code ec) |
{ |
// ... |
do_accept(); ----------------------'
});
}
The sleep() within the session's ReadHandler causes the one thread running the io_service to block until the sleep completes. Hence, the program will be doing nothing. However, this does not cause any outstanding operations to be cancelled. For a better understanding of asynchronous operations and io_service, consider reading this answer.
Here is an example demonstrating the server handling multiple connections. It spawns off a thread that creates 5 client sockets and connects them to the server.
#include <cstdlib>
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
using boost::asio::ip::tcp;
class session
: public std::enable_shared_from_this<session>
{
public:
session(tcp::socket socket)
: socket_(std::move(socket))
{
}
~session()
{
std::cout << "session ended" << std::endl;
}
void start()
{
std::cout << "session started" << std::endl;
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();
}
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;
auto port = std::atoi(argv[1]);
server s(io_service, port);
boost::thread client_main(
[&io_service, port]
{
tcp::endpoint server_endpoint(
boost::asio::ip::address_v4::loopback(), port);
// Create and connect 5 clients to the server.
std::vector<std::shared_ptr<tcp::socket>> clients;
for (auto i = 0; i < 5; ++i)
{
auto client = std::make_shared<tcp::socket>(
std::ref(io_service));
client->connect(server_endpoint);
clients.push_back(client);
}
// Wait 2 seconds before destroying all clients.
boost::this_thread::sleep(boost::posix_time::seconds(2));
});
io_service.run();
client_main.join();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
The output:
session started
session started
session started
session started
session started
session ended
session ended
session ended
session ended
session ended
Come across the following codes (from user368831) which is what I am looking for. I have modified a little to make it a threaded TCP session that listen and read for connection and data while the main loop can do other tasks.
class CSession
{
public:
CSession(boost::asio::io_service& io_service) : m_Socket(io_service)
{}
tcp::socket& socket() return m_Socket;
void start()
{
boost::asio::async_read_until(m_Socket, m_Buffer, " ",
boost::bind(&CSession::handle_read, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
void handle_read(const boost::system::error_code& error,
size_t bytes_transferred)
{
if (!error)
{
ostringstream ss;
ss << &m_Buffer;
m_RecvMsg = ss.str();
std::cout << "handle_read():" << m_RecvMsg << std::endl;
}
else
delete this;
}
private:
boost::asio::streambuf m_Buffer;
tcp::socket m_Socket;
string m_RecvMsg;
};
class CTcpServer
{
public:
CTcpServer(short port)
: m_Acceptor(m_IOService, tcp::endpoint(tcp::v4(), port)),
m_Thread(boost::bind(&boost::asio::io_service::run, &m_IOService))
{
CSession* new_session = new CSession(m_IOService);
m_Acceptor.async_accept(new_session->socket(),
boost::bind(&CTcpServer::handle_accept, this, new_session,
boost::asio::placeholders::error));
};
void handle_accept(CSession* new_session, const boost::system::error_code& error)
{
if (!error)
{
new_session->start();
new_session = new CSession(m_IOService);
m_Acceptor.async_accept(new_session->socket(),
boost::bind(&CTcpServer::handle_accept, this, new_session,
boost::asio::placeholders::error));
}
else
delete new_session;
}
private:
boost::asio::io_service m_IOService;
tcp::acceptor m_Acceptor;
boost::thread m_Thread;
};
void main()
{
:
CTcpServer *server = new CTcpServer(6002); // tcp port 6002
/* How to get the incoming data sent from the client here?? */
// string message;
// while(1)
// {
// if ( server->incomingData(message) )
// {
// std::cout << "Data recv: " << message.data() << std::endl;
// }
// :
// : // other tasks
// :
// }
}
However, how do I code incomingData() in the main loop such that it will monitor the data from the client and return true whenever handle_read() is called?
Can use Boost::signals library in this case?
This code is frankly horrible. There are memory leaks galore because of the way you are using raw pointers. Asio works best with shared_ptrs, it needs guarantees about the lifetimes of objects. I suggest you throw this code away, and start by looking at asio's simple enough to follow examples.
As for the method you want to code - that's not the way it works, you should put that logic in handle_read. handle_read will get called when you have a full message according to your protocol, you should put the logic you want to happen in this method - not in the main while loop. In your main thread, you should simply call io_service::run().
I'm trying to modify the echo server example from boost asio and I'm running into problem when I try to use boost::asio::async_read_until. Here's the code:
#include <cstdlib>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;
class session
{
public:
session(boost::asio::io_service& io_service)
: socket_(io_service)
{
}
tcp::socket& socket()
{
return socket_;
}
void start()
{
std::cout<<"starting"<<std::endl;
boost::asio::async_read_until(socket_, boost::asio::buffer(data_, max_length), ' ',
boost::bind(&session::handle_read, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
void handle_read(const boost::system::error_code& error,
size_t bytes_transferred)
{
std::cout<<"handling read"<<std::endl;
if (!error)
{
boost::asio::async_write(socket_,
boost::asio::buffer(data_, bytes_transferred),
boost::bind(&session::handle_write, this,
boost::asio::placeholders::error));
}
else
{
delete this;
}
}
void handle_write(const boost::system::error_code& error)
{
if (!error)
{
/*
socket_.async_read_some(boost::asio::buffer(data_, max_length),
boost::bind(&session::handle_read, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
*/ }
else
{
delete this;
}
}
private:
tcp::socket socket_;
enum { max_length = 1024 };
char data_[max_length];
};
class server
{
public:
server(boost::asio::io_service& io_service, short port)
: io_service_(io_service),
acceptor_(io_service, tcp::endpoint(tcp::v4(), port))
{
session* new_session = new session(io_service_);
acceptor_.async_accept(new_session->socket(),
boost::bind(&server::handle_accept, this, new_session,
boost::asio::placeholders::error));
}
void handle_accept(session* new_session,
const boost::system::error_code& error)
{
if (!error)
{
new_session->start();
new_session = new session(io_service_);
acceptor_.async_accept(new_session->socket(),
boost::bind(&server::handle_accept, this, new_session,
boost::asio::placeholders::error));
}
else
{
delete new_session;
}
}
private:
boost::asio::io_service& io_service_;
tcp::acceptor acceptor_;
};
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;
using namespace std; // For atoi.
server s(io_service, atoi(argv[1]));
io_service.run();
}
catch (std::exception& e)
{
std::cerr << "Exception: " << e.what() << "\n";
}
return 0;
}
The problem is when I try to compile I get this weird error:
server.cpp: In member function ‘void session::start()’:
server.cpp:27: error: no matching function for call to ‘async_read_until(boost::asio::basic_stream_socket >&, boost::asio::mutable_buffers_1, char, boost::_bi::bind_t, boost::_bi::list3, boost::arg<1> ()(), boost::arg<2> ()()> >)’
Can someone please explain what's going on? From what I can tell the arguments to async_read_until are correct.
Thanks!
The second argument of async_read_until should be a streambuf object into which the data will be read. To put it simple, you need to pass a boost::asio::streambuf by reference, not a boost::asio::buffer by value.
There is no need to use streambufs. There are overloads that accept dynamic buffers. Dynamic is key since read_until implies the need for a buffer that can increase its size at the callee's discretion. Therefore, all you need is to replace the call to boost::asio::buffer with boost::asio::dynamic_buffer and call it on either std::string or std::vector<char>.
...
void start()
{
std::cout<<"starting"<<std::endl;
boost::asio::async_read_until(
socket_, boost::asio::dynamic_buffer(data_), ' ',
boost::bind(&session::handle_read, this,
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred));
}
...
private:
tcp::socket socket_;
std::string data_;
// or
//std::vector<char> data_;
};
I would also get rid of boost::bind since it's an overkill and use a lambda (use them as much as you can)
[this](const boost::system::error_code& error, size_t bytes_transferred) {
handle_read(error, bytes_transferred);
}
const (in)correctness may also cause this type of error message.