I'm trying to learn something about networking and I decided to try to make a p2p terminal program, but I can't get the idea of how to do this correctly. The idea behind this code is that you make an object of class other then call connect and if the other end already did the same you get connected, else you wait for the other device to try to call connect. It just plainly does not work. What am I doing wrong?
class other
{
public:
other(asio::io_context& _context, asio::error_code _ec, std::string _IP, int _port);
void Connect();
asio::ip::tcp::endpoint endpoint;
asio::ip::tcp::socket socket;
asio::ip::tcp::acceptor acceptor;
bool isConnected();
private:
void AwaitConnection();
asio::io_context& context;
asio::error_code& ec;
bool connected;
};
other::other(asio::io_context& _context, asio::error_code _ec, std::string _IP, int _port) :
context(_context),
ec(_ec),
endpoint(asio::ip::address::from_string(_IP), _port),
socket(_context),
acceptor(_context),
connected(false)
{
}
void other::Connect()
{
std::cout << "Looking for connection..." << std::endl;
socket.connect(endpoint, ec);
if (!ec)
{
std::cout << "Connected!" << std::endl;
connected = true;
}
else
{
std::cout << "Connection not found: " << ec.message() << std::endl;
acceptor.open(endpoint.protocol(), ec);
acceptor.bind(endpoint, ec);
acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true), ec);
if (ec)
std::cout << ec.message() << std::endl;
socket.close();
if (ec)
std::cout << "Binding socket failed: " << ec.message() << std::endl;
std::cout << "Waiting for connection..." << std::endl;
AwaitConnection();
}
}
void other::AwaitConnection()
{
static std::function<void(const asio::error_code&)> acceptHandler =
[this](const asio::error_code& error)
{
if (!error)
{
std::cout << "Connection has been found!" << std::endl;
connected = true;
}
else
{
std::cout << "Connection failed: " << error.message() << std::endl;
//AwaitConnection();
}
};
acceptor.async_accept(socket, acceptHandler);
}```
If you enable compiler warnings, the compiler will tell you some of the things you are doing wrong (like binding ec
reference to stack-local):
, ec(_ec)
That's just UB.
You don't actually show the code actually using this, but it seems that you want to do the error-handling centrally. I'd use exceptions then.
On the high level, TCP is client/server by nature. One side listens, the other one connects. Of course you can hide this by making both sides capable of acting in both roles, but the fact remains, one side is the server (listening, accepting) the other is the client (connecting).
It's probably best if you make the distinction in your code. Otherwise you will easily end up in the situation where both ends are listening (e.g. because they start at the same time, and both tried to connect to an "other" instance listening, but failed, so they both start listening for incoming connections instead.
If you want to make that "automatic" you might just always start a listener, and treat failure to listen as non-fatal (e.g. because the "other" instance that was already listening is running on the same machine).
One key reason to make the distinction explicit is because you're now creating confusion with "hostnames". You use the same "_IP"
value both to connect
or to listen
to. That will rarely make sense, unless you do already know ahead of time which role will be taken. So, why conflate the two roles?
Finally, you don't show that/where you are running the io_context
. Since you don't seem to require asynchronous IO, you might simplify.
This would work:
class Object {
public:
void Connect(std::string const& ip_address, uint16_t port) {
std::cout << "Looking for connection..." << std::endl;
socket_.connect({asio::ip::address::from_string(ip_address), port});
std::cout << "Connected!" << std::endl;
connected_ = true;
}
void Listen(uint16_t port) {
acceptor_.open(tcp::v4());
acceptor_.bind(tcp::endpoint{{}, port});
acceptor_.set_option(tcp::acceptor::reuse_address(true));
acceptor_.listen();
socket_ = acceptor_.accept();
std::cout << "Connection has been found!" << std::endl;
connected_ = true;
}
bool isConnected() const { return connected_; }
private:
tcp::socket socket_{asio::system_executor{}};
tcp::acceptor acceptor_{asio::system_executor{}};
bool connected_ = false;
};
Adding minimal code to actually send/receive some messages:
#include <boost/asio.hpp>
#include <iomanip>
#include <iostream>
namespace asio = boost::asio;
using tcp = asio::ip::tcp;
class Object {
public:
void Connect(std::string const& ip_address, uint16_t port) {
std::cout << "Looking for connection..." << std::endl;
socket_.connect({asio::ip::address::from_string(ip_address), port});
std::cout << "Connected!" << std::endl;
connected_ = true;
}
void Listen(uint16_t port) {
acceptor_.open(tcp::v4());
acceptor_.set_option(tcp::acceptor::reuse_address(true));
acceptor_.bind(tcp::endpoint{{}, port});
acceptor_.listen();
socket_ = acceptor_.accept();
std::cout << "Connection has been found!" << std::endl;
connected_ = true;
}
void Send(std::string const& msg) {
assert(isConnected());
asio::write(socket_, asio::buffer(msg + "\n"));
}
std::string Receive() {
assert(isConnected());
asio::read_until(socket_, buf_, "\n");
std::string msg;
getline(std::istream(&buf_), msg);
return msg;
}
bool isConnected() const { return connected_; }
private:
tcp::socket socket_{asio::system_executor{}};
tcp::acceptor acceptor_{asio::system_executor{}};
bool connected_ = false;
asio::streambuf buf_;
};
int main(int argc, char**) try {
if (argc > 1) { // server
Object one;
one.Listen(7878);
std::cout << "Received message " << quoted(one.Receive()) << std::endl;
one.Send("Response from server");
} else { // client
Object two;
two.Connect("127.0.0.1", 7878);
two.Send("Hello from client");
std::cout << "Peer responded with " << quoted(two.Receive()) << std::endl;
}
} catch (boost::system::system_error const& se) {
std::cout << "Failed: " << se.code().message() << std::endl;
}
Demo: