I'm trying to connect to multiple servers at once using the cpp boost library.
I wrote a Socket class, but for some reason, when i have multiple instances of one class, and try to connect, the other instance will connect aswell.
socket.h
class Socket{
private:
boost::asio::io_service io_service_;
#if USE_SSL
boost::asio::ssl::context context;
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssocket;
#else
tcp::socket ssocket;
#endif
int headerLength;
public:
Socket(int = HEADERLENGTH);
bool connect(std::string, std::string);
};
socket.cpp
#if USE_SSL
Socket::Socket(int h) : context(boost::asio::ssl::context::sslv23), ssocket(io_service_, context){
headerLength = h;
}
#else
Socket::Socket(int h) : ssocket(io_service_){
headerLength = h;
}
#endif
bool Socket::connect(std::string host, std::string port){
tcp::resolver resolver(io_service_);
tcp::resolver::query query(tcp::v4(), host.c_str(), port.c_str());
tcp::resolver::iterator iterator;
std::cout << "resolving " << host << "\n";
try {
iterator = resolver.resolve(query);
}
catch (...){
return false;
}
//ssocket.lowest_layer().set_option(tcp::no_delay(true));
std::cout << "connecting to " << host << "\n";
boost::system::error_code ec;
#if USE_SSL
try {
boost::asio::connect(ssocket.lowest_layer(), iterator, ec);
if (ec){ // throw error
return false;
}
}
catch (...){
return false;
}
//ssocket.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert);
ssocket.set_verify_mode(boost::asio::ssl::context::verify_none);
ssocket.handshake(boost::asio::ssl::stream<tcp::socket>::client);
std::cout << "connected \n";
return true;
#else
boost::asio::connect(ssocket, iterator, ec);
return !ec;
#endif
}
I feel like it's having to do something with the way the constructor is called, but afaik are neither context nor ssocket static variables, or am I wrong?
USE_SSL is 1.
I would appreciate your help.
I think the problem is at the way you are using your Socket class. Here is a simplified version of your code, showing it works fine on a synchronous approach:
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <iostream>
class SecureSocket{
private:
boost::asio::io_service io_service_;
boost::asio::ssl::context context;
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssocket;
public:
SecureSocket():
context(boost::asio::ssl::context::sslv23),
ssocket(io_service_, context)
{
context.set_verify_mode(boost::asio::ssl::context::verify_none);
}
void connect(std::string host, std::string port){
boost::asio::ip::tcp::resolver resolver(io_service_);
boost::asio::ip::tcp::resolver::query query( boost::asio::ip::tcp::v4(), host.c_str(), port.c_str() );
boost::asio::ip::tcp::resolver::iterator iterator;
std::cout << "resolving " << host << "\n";
iterator = resolver.resolve(query);
std::cout << "connecting to " << host << "\n";
boost::asio::connect(ssocket.lowest_layer(), iterator);
ssocket.handshake(boost::asio::ssl::stream<boost::asio::ip::tcp::socket>::client);
std::cout << "connected \n";
}
};
int main(int argc, char** argv) {
SecureSocket s, s2;
try{
s.connect("echo.websocket.org","https");
std::cout << "---------\n";
s2.connect("stackoverflow.com","https");
}catch(std::exception& e){
std::cout << "Error: " << e.what();
return 1;
}
return 0;
}
Maybe you are calling each instance on its own thread and that is messing things up. Show us that part of the code, please.
Related
I've tried to separate my server socket in a singleton. Here's the code:
ServerSocket.h
#pragma once
#include <asio.hpp>
#include <iostream>
using asio::ip::tcp;
class ServerSocket
{
public:
ServerSocket(ServerSocket& otherSingleton) = delete;
void operator=(const ServerSocket& copySingleton) = delete;
tcp::acceptor* InitAcceptor();
tcp::socket* InitSocket();
void StartServerSocket();
void SendData(std::string);
std::array<char, 5000> RecieveData();
static ServerSocket* GetInstance();
private:
static ServerSocket* instance;
tcp::acceptor* acceptor;
tcp::socket* socket;
asio::io_context io_context;
ServerSocket() {
acceptor = InitAcceptor();
socket = InitSocket();
}
~ServerSocket()
{
std::cout << "Server closed";
}
};
ServerSocket.cpp
#include "ServerSocket.h"
tcp::acceptor* ServerSocket::InitAcceptor()
{
try
{
tcp::acceptor* acceptor = new tcp::acceptor(io_context, tcp::endpoint(tcp::v4(), 27015));
return acceptor;
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
}
tcp::socket* ServerSocket::InitSocket()
{
try
{
tcp::socket* socket = new tcp::socket(io_context);
return socket;
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
}
void ServerSocket::StartServerSocket()
{
try
{
std::cout << "Server started";
for (;;)
{
acceptor->accept(*socket);
};
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
}
std::array<char, 5000> ServerSocket::RecieveData()
{
try {
std::array<char, 5000> buf;
asio::error_code error;
size_t len = socket->read_some(asio::buffer(buf), error);
buf[len] = '\0';
return buf;
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
}
ServerSocket* ServerSocket::instance(nullptr);
ServerSocket* ServerSocket::GetInstance()
{
if (instance == nullptr)
{
instance = new ServerSocket();
}
return instance;
}
Server socket starts, I get:
Server started
when a client connects, I get:
accept: Already open
and the server stops.
I think the error comes from the acceptor being in a for function. But according to the docs, it should work this way. (or at least that's how I understand - https://think-async.com/Asio/asio-1.20.0/doc/asio/tutorial/tutdaytime2.html)
I tried deleting the for loop, like this:
try
{
std::cout << "Server started";
acceptor->accept(*socket);
}
and now there is no problem. But the connection isn't kept open by the server. The client connects once, sends data, and the server stops running.
As far as I understand from the docs, if I set the acceptor in a for(;;), it should be running - but it doesn't work in my case.
So, how can I keep my socket open in my implementation? I want it to be running for more than one SendData - I want it to be able to communicate with the client as long as the client is connected.
Thanks.
//Edit:
Here's the client code:
#include <iostream>
#include <asio.hpp>
#include "../../cereal/archives/json.hpp"
using asio::ip::tcp;
int main(int argc, char* argv[])
{
try
{
if (argc != 2)
{
std::cerr << "Usage: client <host>" << std::endl;
return 1;
}
// Socket Parameters
const unsigned port = 27015;
auto ip_address = asio::ip::make_address_v4(argv[1]);
auto endpoint = tcp::endpoint{ ip_address, port };
// Creating and Connecting the Socket
asio::io_context io_context;
auto resolver = tcp::resolver{ io_context };
auto endpoints = resolver.resolve(endpoint);
auto socket = tcp::socket{ io_context };
asio::connect(socket, endpoints);
std::array<char, 5000> buf;
std::cout << "Message to server: ";
asio::error_code ignored_error;
std::string username = "test", password = "mihai";
std::stringstream os;
{
cereal::JSONOutputArchive archive_out(os);
archive_out(
CEREAL_NVP(username),
CEREAL_NVP(password)
);
}
asio::write(socket, asio::buffer(os.str()), ignored_error);
return 0;
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
return 1;
}
And Communication.h which is responsible to catching the operation from the client and sending it to the server
#pragma once
#include <iostream>
#include "DBUser.h"
#include "DBPost.h"
class Communication
{
public:
enum class Operations {
eLogin,
eRegister
};
void ExecuteOperation(Operations operation,const std::array<char, 5000>& buffer);
};
.cpp
#include "Communication.h"
void Communication::ExecuteOperation(Operations operation,const std::array<char, 5000>& buffer)
{
DBUser* user= DBUser::getInstance();
switch (operation)
{
case Communication::Operations::eLogin:
{
std::string username, password;
std::stringstream is(buffer.data());
{
cereal::JSONInputArchive archive_in(is);
archive_in(username,password);
}
try
{
user->LoginUser(username, password);
}
catch (const std::exception& e)
{
std::cout << e.what();
}
break;
}
case Communication::Operations::eRegister:
{
std::string username, password;
std::stringstream is(buffer.data());
{
cereal::JSONInputArchive archive_in(is);
archive_in(username, password);
}
try
{
user->CreateUser(username, password);
}
catch (const std::exception& e)
{
std::cout << e.what();
}
break;
}
}
}
Main
#include <iostream>
#include <pqxx/pqxx>
#include "DBLink.h"
#include "DBUser.h"
#include "DBPost.h"
#include "../Logging/Logging.h"
#include <iostream>
#include <string>
#include <asio.hpp>
#include "ServerSocket.h"
#include "Communication.h"
int main()
{
ServerSocket* test = ServerSocket::GetInstance();
test->StartServerSocket();
std::array<char, 5000> buf = test->RecieveData();
Communication communicationInterface;
communicationInterface.ExecuteOperation(Communication::Operations::eRegister, buf);
system("pause");
}
There's a lot of antipattern going on.
Overuse of pointers.
Overuse of new (without any delete, a guaranteed leak)
The destructor claims that "Server closed" but it doesn't actually do a single thing to achieve that.
Two-step initialization (InitXXXX functions). Firstly, you should obviously favor initializer lists
ServerSocket()
: acceptor_(InitAcceptor()), socket_(InitSocket())
{ }
And you need to makeInitAcceptor/InitSocket private to the implementation.
I'll forget the Singleton which is anti-pattern 99% of the time, but I guess that's almost debatable.
In your StartServerSocket you have a loop that reuses the same socket all the time. Of course, it will already be connected. You need separate socket instances:
for (;;) {
acceptor_->accept(*socket_);
};
Simplify/Fix
#include <boost/asio.hpp>
#include <iostream>
namespace asio = boost::asio;
using asio::ip::tcp;
struct Listener {
void Start()
{
std::cout << "Server started";
for (;;) {
auto socket = acceptor_.accept();
std::cout << "Accepted connection from " << socket.remote_endpoint()
<< std::endl;
};
}
static Listener& GetInstance() {
static Listener s_instance{27015}; // or use weak_ptr for finite lifetime
return s_instance;
}
private:
asio::io_context ioc_; // order of declaration is order of init!
tcp::acceptor acceptor_;
Listener(uint16_t port) : acceptor_{ioc_, tcp::endpoint{tcp::v4(), port}} {}
};
int main() {
try {
Listener::GetInstance().Start();
} catch (std::exception const& e) {
std::cerr << e.what() << std::endl;
}
}
Now you could hand the socket instances to a thread. I concur with the other commenters that thread-per-request is likely also an anti-pattern, and you should consider using async IO with Asio (hence the name).
Live Demo
EDIT complete and working example based on the server code from the question:
// main.cxx
#include "ServerSocket.hxx"
#include <boost/asio.hpp>
#include <iostream>
#include <string>
using boost::asio::ip::tcp;
int
main ()
{
ServerSocket *test = ServerSocket::GetInstance ();
test->StartServerSocket ();
std::cout << std::endl;
while (auto msg = test->RecieveData ())
{
std::cout << msg.value ();
}
}
// ServerSocket.hxx
#pragma once
#include <boost/asio.hpp>
#include <iostream>
#include <optional>
using boost::asio::ip::tcp;
class ServerSocket
{
public:
ServerSocket (ServerSocket &otherSingleton) = delete;
void operator= (const ServerSocket ©Singleton) = delete;
tcp::acceptor *InitAcceptor ();
tcp::socket *InitSocket ();
void StartServerSocket ();
void SendData (std::string);
std::optional<std::string> RecieveData ();
static ServerSocket *GetInstance ();
private:
static ServerSocket *instance;
tcp::acceptor *acceptor;
tcp::socket *socket;
boost::asio::io_context io_context;
ServerSocket ()
{
acceptor = InitAcceptor ();
socket = InitSocket ();
}
~ServerSocket () {
delete socket;
delete acceptor;
std::cout << "Server closed"; }
};
// ServerSocket.cxx
#include "ServerSocket.hxx"
#include <optional>
tcp::acceptor *
ServerSocket::InitAcceptor ()
{
try
{
return new tcp::acceptor (io_context, tcp::endpoint (tcp::v4 (), 27015));
}
catch (std::exception &e)
{
std::cerr << e.what () << std::endl;
}
return nullptr;
}
tcp::socket *
ServerSocket::InitSocket ()
{
try
{
return new tcp::socket (io_context);
}
catch (std::exception &e)
{
std::cerr << e.what () << std::endl;
}
return nullptr;
}
void
ServerSocket::StartServerSocket ()
{
try
{
std::cout << "Server started";
acceptor->accept (*socket);
}
catch (std::exception &e)
{
std::cerr << e.what () << std::endl;
}
}
std::optional<std::string>
ServerSocket::RecieveData ()
{
try
{
char data[5000];
for (;;)
{
boost::system::error_code error;
size_t length = socket->read_some (boost::asio::buffer (data), error);
if (error == boost::asio::error::eof) return std::nullopt; // Connection closed cleanly by peer.
else if (error)
throw boost::system::system_error (error); // Some other error.
return std::string{ data, length };
}
}
catch (std::exception &e)
{
std::cerr << e.what () << std::endl;
}
return {};
}
ServerSocket *ServerSocket::instance (nullptr);
ServerSocket *
ServerSocket::GetInstance ()
{
if (instance == nullptr)
{
instance = new ServerSocket ();
}
return instance;
}
Note that there are still some problems with the server:
Error handling
More than one connection
The server does not send a message if the operation was successful
If you disconnect the client the server shuts down
We could replace some pointers with optional no need to write "new"
Just make a normal class do not write it as singleton.
If you like to test the server you can run
telnet localhost 27015
and then write some text and press enter
I am trying to connect to a secure websocket using asio.
This example will work for an ip address:
#include <iostream>
#include <asio.hpp>
int main() {
asio::error_code ec;
asio::io_context context;
asio::io_context::work idleWork(context);
asio::ip::tcp::endpoint endpoint(asio::ip::make_address("51.38.81.49", ec), 80);
asio::ip::tcp::socket socket(context);
socket.connect(endpoint, ec);
if (!ec) {
std::cout << "Connected!" << std::endl;
} else {
std::cout << "Failed to connect to address: \n" << ec.message() << std::endl;
}
return 0;
}
but how would I change it so I connect to "wss://api2.example.com"?
EDIT:
Thanks for your answer karastojko - it seems to get me some of the way. I would though like to know if I am actually connected to the server, so I have updated my example with your input, added a working WSS which I know will answer and created read and write.
#include <asio.hpp>
#include <asio/ts/buffer.hpp>
std::vector<char> vBuffer(1 * 1024);
// This should output the received data
void GrabSomeData(asio::ip::tcp::socket& socket) {
socket.async_read_some(asio::buffer(vBuffer.data(), vBuffer.size()),
[&](std::error_code ec, std::size_t lenght) {
if (!ec) {
std::cout << "\n\nRead " << lenght << " bytes\n\n";
for (int i = 0; i < lenght; i++)
std::cout << vBuffer[i];
GrabSomeData(socket);
}
}
);
}
int main() {
asio::error_code ec;
asio::io_context context;
asio::io_context::work idleWork(context);
std::thread thrContext = std::thread([&]() { context.run(); });
// I hope this is what you meant
asio::ip::tcp::resolver res(context);
asio::ip::tcp::socket socket(context);
asio::connect(socket, res.resolve("echo.websocket.org", std::to_string(443)));
// Check the socket is open
if (socket.is_open()) {
// Start to output incoming data
GrabSomeData(socket);
// Send data to the websocket, which should be sent back
std::string sRequest = "Echo";
socket.write_some(asio::buffer(sRequest.data(), sRequest.size()), ec);
// Wait some time, so the data is received
using namespace std::chrono_literals;
std::this_thread::sleep_for(20000ms);
context.stop();
if (thrContext.joinable()) thrContext.join();
}
return 0;
}
For that purpose use the resolver class:
tcp::resolver res(context);
asio::ip::tcp::socket socket(context);
boost::asio::connect(socket, res.resolve("api2.example.com", 80));
I want to send a struct from client to server using boost::asio. I followed boost tutorial link https://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/examples.html#boost_asio.examples.serialization. I slighty modified the code in server.cpp and client.cpp. With the new code, after a connection is established, client.cpp writes the struct stock to server and reads stock information at server side. (in the tutorial version, after a connection established, the server writes stock struct to client and client reads them. This version works for me.)
My problem is that after a connection is established, the async_write in client.cpp causes error
Error in write: An existing connection was forcibly closed by the remote host
and the async_read in server.cpp causes error
Error in read:The network connection was aborted by the local system.
As suggested by some forum answers, I changed this pointers in function handlers of async_write and async_read to shared_from_this. Still the problem exists.
I am not able to identify whether the client or the server side is causing problem. Please help.
server.cpp
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <boost/lexical_cast.hpp>
#include <iostream>
#include <vector>
#include "connection.h" // Must come before boost/serialization headers.
#include <boost/serialization/vector.hpp>
#include <boost/enable_shared_from_this.hpp>
#include "stock.h"
namespace s11n_example
{
/// Serves stock quote information to any client that connects to it.
class server : public boost::enable_shared_from_this<server>
{
private:
/// The acceptor object used to accept incoming socket connections.
boost::asio::ip::tcp::acceptor acceptor_;
/// The data to be sent to each client.
std::vector<stock> stocks_;
public:
/// Constructor opens the acceptor and starts waiting for the first incoming
/// connection.
server(boost::asio::io_service& io_service, unsigned short port):
acceptor_(io_service, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port))
{
// Start an accept operation for a new connection.
connection_ptr new_conn(new connection(acceptor_.get_io_service()));
acceptor_.async_accept(new_conn->socket(),
boost::bind(&server::handle_accept, this,boost::asio::placeholders::error, new_conn));
}
/// Handle completion of a accept operation.
void handle_accept(const boost::system::error_code& e, connection_ptr conn)
{
if (!e)
{
std::cout << "Received a connection" <<std::endl;
conn->async_read(stocks_,
boost::bind(&server::handle_read, shared_from_this(),boost::asio::placeholders::error));
}
// Start an accept operation for a new connection.
connection_ptr new_conn(new connection(acceptor_.get_io_service()));
acceptor_.async_accept(new_conn->socket(),
boost::bind(&server::handle_accept, this,boost::asio::placeholders::error, new_conn));
}
/// Handle completion of a read operation.
void handle_read(const boost::system::error_code& e)
{
if (!e)
{
// Print out the data that was received.
for (std::size_t i = 0; i < stocks_.size(); ++i)
{
std::cout << "Stock number " << i << "\n";
std::cout << " code: " << stocks_[i].code << "\n";
std::cout << " name: " << stocks_[i].name << "\n";
std::cout << " open_price: " << stocks_[i].open_price << "\n";
std::cout << " high_price: " << stocks_[i].high_price << "\n";
std::cout << " low_price: " << stocks_[i].low_price << "\n";
std::cout << " last_price: " << stocks_[i].last_price << "\n";
std::cout << " buy_price: " << stocks_[i].buy_price << "\n";
std::cout << " buy_quantity: " << stocks_[i].buy_quantity << "\n";
std::cout << " sell_price: " << stocks_[i].sell_price << "\n";
std::cout << " sell_quantity: " << stocks_[i].sell_quantity << "\n";
}
}
else
{
// An error occurred.
std::cerr << "Error in read:" << e.message() << std::endl;
}
}
};
} // namespace s11n_example
int main(int argc, char* argv[])
{
try
{
// Check command line arguments.
if (argc != 2)
{
std::cerr << "Usage: server <port>" << std::endl;
return 1;
}
unsigned short port = boost::lexical_cast<unsigned short>(argv[1]);
boost::asio::io_service io_service;
boost::shared_ptr<s11n_example::server> server(new s11n_example::server(io_service, port));
io_service.run();
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
client.cpp
#include <boost/asio.hpp>
#include <boost/bind.hpp>
#include <iostream>
#include <vector>
#include "connection.h" // Must come before boost/serialization headers.
#include <boost/serialization/vector.hpp>
#include <boost/enable_shared_from_this.hpp>
#include "stock.h"
namespace s11n_example {
/// Downloads stock quote information from a server.
class client : public boost::enable_shared_from_this<client>
{
private:
/// The connection to the server.
connection connection_;
/// The data received from the server.
std::vector<stock> stocks_;
public:
/// Constructor starts the asynchronous connect operation.
client(boost::asio::io_service& io_service, const std::string& host, const std::string& service)
: connection_(io_service)
{
// Resolve the host name into an IP address.
boost::asio::ip::tcp::resolver resolver(io_service);
boost::asio::ip::tcp::resolver::query query(host, service);
boost::asio::ip::tcp::resolver::iterator endpoint_iterator =
resolver.resolve(query);
// Start an asynchronous connect operation.
boost::asio::async_connect(connection_.socket(), endpoint_iterator,
boost::bind(&client::handle_connect, this,boost::asio::placeholders::error));
}
/// Handle completion of a connect operation.
void handle_connect(const boost::system::error_code& e) //, connection_ptr conn
{
if (!e)
{
std::cout << "Connected to server!" << std::endl;
// Create the data to be sent to each client.
stock s;
s.code = "ABC";
s.name = "A Big Company";
s.open_price = 4.56;
s.high_price = 5.12;
s.low_price = 4.33;
s.last_price = 4.98;
s.buy_price = 4.96;
s.buy_quantity = 1000;
s.sell_price = 4.99;
s.sell_quantity = 2000;
stocks_.push_back(s);
s.code = "DEF";
s.name = "Developer Entertainment Firm";
s.open_price = 20.24;
s.high_price = 22.88;
s.low_price = 19.50;
s.last_price = 19.76;
s.buy_price = 19.72;
s.buy_quantity = 34000;
s.sell_price = 19.85;
s.sell_quantity = 45000;
stocks_.push_back(s);
// Successfully established connection. Start operation to write the list
// of stocks.
connection_.async_write(stocks_,
boost::bind(&client::handle_write, shared_from_this(),boost::asio::placeholders::error)); //,&conn )
}
else
{
// An error occurred. Log it and return.
std::cerr << "Error in connecting to server" << e.message() << std::endl;
}
}
/// Handle completion of a write operation.
void handle_write(const boost::system::error_code& e)//, connection* conn
{
if (!e)
{
std::cout << "Finished writing to server" << std::endl;
}
else
{
// An error occurred. Log it and return. Since we are not starting a new
// operation the io_service will run out of work to do and the client will
// exit.
std::cerr << "Error in write: " << e.message() << std::endl;
}
// Nothing to do. The socket will be closed automatically when the last
// reference to the connection object goes away.
}
};
} // namespace s11n_example
int main(int argc, char* argv[])
{
try
{
// Check command line arguments.
if (argc != 3)
{
std::cerr << "Usage: client <host> <port>" << std::endl;
return 1;
}
boost::asio::io_service io_service;
//s11n_example::client client(io_service, argv[1], argv[2]);
boost::shared_ptr<s11n_example::client> client(new s11n_example::client(io_service, argv[1], argv[2]));
io_service.run();
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Thanks.
You need to pass conn to handle read otherwise it will be destructed at the end of the handle_accept method. When it's destructed the socket it contains will also be destructed and the connection will close.
conn->async_read(stocks_,
boost::bind(&server::handle_read, shared_from_this(), conn, boost::asio::placeholders::error));
Lambdas make this easier to read than using bind:
auto self = shared_from_this();
conn->async_read(stocks_,
[self, this, conn] (boost::system::error_code ec) { handle_read(ec); });
The variables listed in the capture list will be copied so the shared pointers will be kept alive.
I have the following RESTServer implemented using boost::beast. The way the server is started is using
void http_server(tcp::acceptor& acceptor, tcp::socket& socket).
Logically acceptor and socket should logically belong to http_connection class,instead of as a separate function outside. What is the reason it is implemented like this?
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio.hpp>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <memory>
#include <string>
namespace beast = boost::beast; // from <boost/beast.hpp>
namespace http = beast::http; // from <boost/beast/http.hpp>
namespace net = boost::asio; // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
namespace my_program_state
{
std::size_t
request_count()
{
static std::size_t count = 0;
return ++count;
}
std::time_t
now()
{
return std::time(0);
}
}
class http_connection : public std::enable_shared_from_this<http_connection>
{
public:
http_connection(tcp::socket socket)
: socket_(std::move(socket))
{
}
// Initiate the asynchronous operations associated with the connection.
void
start()
{
read_request();
check_deadline();
}
private:
// The socket for the currently connected client.
tcp::socket socket_;
// The buffer for performing reads.
beast::flat_buffer buffer_{8192};
// The request message.
http::request<http::dynamic_body> request_;
// The response message.
http::response<http::dynamic_body> response_;
// The timer for putting a deadline on connection processing.
net::steady_timer deadline_{
socket_.get_executor(), std::chrono::seconds(60)};
// Asynchronously receive a complete request message.
void
read_request()
{
auto self = shared_from_this();
http::async_read(
socket_,
buffer_,
request_,
[self](beast::error_code ec,
std::size_t bytes_transferred)
{
boost::ignore_unused(bytes_transferred);
if(!ec)
self->process_request();
});
}
// Determine what needs to be done with the request message.
void
process_request()
{
response_.version(request_.version());
response_.keep_alive(false);
switch(request_.method())
{
case http::verb::get:
response_.result(http::status::ok);
response_.set(http::field::server, "Beast");
create_response();
break;
default:
// We return responses indicating an error if
// we do not recognize the request method.
response_.result(http::status::bad_request);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body())
<< "Invalid request-method '"
<< std::string(request_.method_string())
<< "'";
break;
}
write_response();
}
// Construct a response message based on the program state.
void
create_response()
{
if(request_.target() == "/count")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Request count</title></head>\n"
<< "<body>\n"
<< "<h1>Request count</h1>\n"
<< "<p>There have been "
<< my_program_state::request_count()
<< " requests so far.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else if(request_.target() == "/time")
{
response_.set(http::field::content_type, "text/html");
beast::ostream(response_.body())
<< "<html>\n"
<< "<head><title>Current time</title></head>\n"
<< "<body>\n"
<< "<h1>Current time</h1>\n"
<< "<p>The current time is "
<< my_program_state::now()
<< " seconds since the epoch.</p>\n"
<< "</body>\n"
<< "</html>\n";
}
else
{
response_.result(http::status::not_found);
response_.set(http::field::content_type, "text/plain");
beast::ostream(response_.body()) << "File not found\r\n";
}
}
// Asynchronously transmit the response message.
void
write_response()
{
auto self = shared_from_this();
response_.set(http::field::content_length, response_.body().size());
http::async_write(
socket_,
response_,
[self](beast::error_code ec, std::size_t)
{
self->socket_.shutdown(tcp::socket::shutdown_send, ec);
self->deadline_.cancel();
});
}
// Check whether we have spent enough time on this connection.
void
check_deadline()
{
auto self = shared_from_this();
deadline_.async_wait(
[self](beast::error_code ec)
{
if(!ec)
{
// Close socket to cancel any outstanding operation.
self->socket_.close(ec);
}
});
}
};
// "Loop" forever accepting new connections.
void
http_server(tcp::acceptor& acceptor, tcp::socket& socket)
{
acceptor.async_accept(socket,
[&](beast::error_code ec)
{
if(!ec)
std::make_shared<http_connection>(std::move(socket))->start();
http_server(acceptor, socket);
});
}
int
main(int argc, char* argv[])
{
try
{
// Check command line arguments.
if(argc != 3)
{
std::cerr << "Usage: " << argv[0] << " <address> <port>\n";
std::cerr << " For IPv4, try:\n";
std::cerr << " receiver 0.0.0.0 80\n";
std::cerr << " For IPv6, try:\n";
std::cerr << " receiver 0::0 80\n";
return EXIT_FAILURE;
}
auto const address = net::ip::make_address(argv[1]);
unsigned short port = static_cast<unsigned short>(std::atoi(argv[2]));
net::io_context ioc{1};
tcp::acceptor acceptor{ioc, {address, port}};
tcp::socket socket{ioc};
http_server(acceptor, socket);
ioc.run();
}
catch(std::exception const& e)
{
std::cerr << "Error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
}
One reason would be the author wanted to separate the logic.
Moreover i think it would complicate the procedure to create new sessions if you would move the listener code into the client session.
The acceptor and socket object should stay independent, you may reference it in your client and use it if you want but since these a more "global" and unique objects, it should stay outside of the session. Instead it can be also put into a separate class.
Roughly speaking the acceptor should just listen for incoming connection attempts from remote hosts and create the sessions accordingly.
My problem is related to moving Boost.Asio sockets using std::move.
I'm working on a server in C++ with switchable implementations and libraries (Boost.Asio, POSIX sockets, Winsock sockets). To achieve this, I have HttpServer and HttpClient interfaces which are implemented by classes like PosixHttpServer, PosixHttpClient, BoostHttpServer, BoostHttpClient. After binding and listening on the server, the HttpClient would be created by HttpServer::accept(). I made sure to disable copying on both HttpServer and HttpClient. I'm not planning to use async operations right now.
// main.cpp
using HttpServerPtr = std::unique_ptr<HttpServer>;
using HttpClientPtr = std::unique_ptr<HttpClient>;
...
int main(int argc, char* argv[])
{
...
//HttpServerPtr server = std::make_unique<PosixHttpServer>();
HttpServerPtr server = std::make_unique<BoostHttpServer>();
if (!server->bind(server_port)
{
...
}
if (!server->listen())
{
...
}
std::cout << "Listening to client connections at "
<< server->getAddress()
<< " on port " << std::to_string(server_port)
<< std::endl;
while (1)
{
HttpClientPtr client = server->accept();
if (!client)
{
std::cerr << "failed to accept" << std::endl;
continue;
}
std::cout << "Got connection from "
<< client->getAddress() << std::endl;
// TODO: Write HTTP response
if (!client->write("Hello World!"))
{
std::cerr << "failed to write" << std::endl;
}
}
return EXIT_SUCCESS;
}
To decouple the client socket from the HttpServer, the client boost::asio::ip::tcp::socket would be stored as a member of HttpClient. In BoostHttpServer::accept(), I am able to pass the Boost.Asio socket to the HttpClient if I use std::shared_ptr (_io_service and _acceptor are private members of HttpServer).
// boosthttpserver.cpp
using tcp = boost::asio::ip::tcp;
bool BoostHttpServer::bind(const uint16_t port)
{
try
{
tcp::resolver resolver(_io_service);
tcp::resolver::query query(tcp::v4(), boost::asio::ip::host_name(), std::to_string(port));
tcp::resolver::iterator available_endpoint = resolver.resolve(query);
tcp::resolver::iterator end;
for (; available_endpoint != end; ++available_endpoint)
{
tcp::endpoint localhost = *available_endpoint;
_acceptor.open(localhost.protocol());
_acceptor.bind(localhost);
_address = localhost.address().to_string();
break;
}
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
return false;
}
return true;
}
bool BoostHttpServer::listen()
{
try
{
_acceptor.listen();
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
return false;
}
return true;
}
HttpClientPtr BoostHttpServer::accept()
{
try
{
auto client_socket = std::make_shared<tcp::socket>(_io_service);
_acceptor.accept(*client_socket);
return std::make_unique<BoostHttpClient>(client_socket);
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
return nullptr;
}
}
// boosthttpclient.hpp
class BoostHttpClient : public HttpClient
{
public:
BoostHttpClient(std::shared_ptr<boost::asio::ip::tcp::socket> socket);
BoostHttpClient(const BoostHttpClient&) = delete;
BoostHttpClient& operator=(const BoostHttpClient&) = delete;
virtual bool write(const std::string& message) override;
virtual std::string getAddress() const override;
private:
boost::asio::ip::tcp::socket _socket;
const std::string _address;
};
// boosthttpclient.cpp
BoostHttpClient::BoostHttpClient(
std::shared_ptr<boost::asio::ip::tcp::socket> socket
)
: _socket(socket)
, _address(socket->remote_endpoint().address().to_string())
{
}
This is the application output when I start the server and use telnet localhost 8080 for example:
Listening to connections at 127.0.0.1 on port 8080
Got connection from 127.0.0.1
However, I want to spare the heap allocation, create client_socket on the stack and transfer the ownership of it to the BoostHttpClient with std::move.
// boosthttpserver.cpp
using tcp = boost::asio::ip::tcp;
...
HttpClientPtr BoostHttpServer::accept()
{
try
{
tcp::socket client_socket(_io_service);
_acceptor.accept(client_socket);
return std::make_unique<BoostHttpClient>(std::move(client_socket));
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
return nullptr;
}
}
// boosthttpclient.hpp
class BoostHttpClient : public HttpClient
{
public:
BoostHttpClient(boost::asio::ip::tcp::socket&& socket);
BoostHttpClient(const BoostHttpClient&) = delete;
BoostHttpClient& operator=(const BoostHttpClient&) = delete;
virtual bool write(const std::string& message) override;
virtual std::string getAddress() const override;
private:
boost::asio::ip::tcp::socket _socket;
const std::string _address;
};
// boosthttpclient.cpp
BoostHttpClient::BoostHttpClient(boost::asio::ip::tcp::socket&& socket)
: _socket(std::move(socket))
, _address(socket.remote_endpoint().address().to_string())
{
}
However, the socket is gone after the move. As if its destructor got called and got closed.
Listening to connections at 127.0.0.1 on port 8080
remote_endpoint: Bad file descriptor
failed to accept
I thought that it could be as easy as passing other non-copyable objects like std::thread or std::mutex as constructor parameters, but cleary there's something I don't understand what happens at std::move(client_socket). Why doesn't it get transferred and why does it closes itself?
I use Boost 1.64.0, gcc version 7.3.1 20180712 (Red Hat 7.3.1-6) and C++17 if that matters.