3

I have written a basic client-server application in C++ using asio library. The client sends messages from the console to the server.

If I run it on localhost on either linux or windows, it works great. However, when I run it on my actual server, I get a strange behavior. Each time I send a message, then immediately after another message is sent that contains garbage or is empty. This sometimes happens, sometimes doesn't. But it does most of the times. I tried using a different port.

For example if I send messages 1, 2, and 3 this is what I see in the server's console:

1

What could I be doing wrong ?

server.cpp - Almost same code as seen here

#define ASIO_STANDALONE
#include <iostream>
#include <asio.hpp>

using asio::ip::tcp;

const std::size_t    max_length = 2048;
const unsigned short PORT       = 15562;


class Session
    : public std::enable_shared_from_this<Session>
{
public:
    Session(tcp::socket server_socket)
        : _session_socket(std::move(server_socket))
    {
    }

    void start()
    {
        do_read();
    }

private:
    void do_read()
    {
        auto self(shared_from_this()); // shared_ptr instance to this

        // Start an asynchronous read.
        // This function is used to asynchronously read data from the stream socket.
        _session_socket.async_read_some(asio::buffer(_data, max_length),
                                        [this, self](std::error_code error, std::size_t length)
                                        {
                                            if (!error)
                                            {
                                                std::cout << "Data RECEIVED: " << std::endl;
                                                std::cout << _data << std::endl;
                                                do_write(length);
                                            }
                                        });
    }

    void do_write(std::size_t length)
    {
        auto self(shared_from_this()); // shared_ptr instance to this

        // Start an asynchronous write.
        // This function is used to asynchronously write data to the stream socket.
        strncpy(_data, "Hi, from the server", max_length);
        asio::async_write(_session_socket, asio::buffer(_data, length),
                          [this, self](std::error_code error, std::size_t /*length*/)
                          {
                              if (!error)
                              {
                                  do_read();
                              }
                          });
    }

    tcp::socket _session_socket;
    char        _data[max_length];
};


class server
{
public:
    server(asio::io_service &io_service, const tcp::endpoint &endpoint)
        : _server_socket(io_service),
          _server_acceptor(io_service, endpoint)
    {
    }

    void do_accept()
    {
        // Start an asynchronous accept.
        // This function is used to asynchronously accept a new connection into a socket.
        _server_acceptor.async_accept(_server_socket,
                                      [this](std::error_code error)
                                      {
                                          // Accept succeeded
                                          if (!error)
                                          {
                                              // Create a session
                                              auto session = std::make_shared<Session>(
                                                  std::move(_server_socket));
                                              session->start();
                                          }

                                          // Continue to accept more connections
                                          do_accept();
                                      });
    }

private:
    tcp::acceptor _server_acceptor;
    tcp::socket   _server_socket;
};


int main()
{
    try
    {
        asio::io_service io_service;                   // io_service provides functionality for sockets, connectors, etc
        tcp::endpoint    endpoint(tcp::v4(), PORT);    // create an endpoint using a IP='any' and the specified PORT
        server           server(io_service, endpoint); // create server on PORT
        server.do_accept();
        std::cout << "Server started on port: " << PORT << std::endl;
        io_service.run();
    }
    catch (std::exception &e)
    {
        std::cerr << "Exception: " << e.what() << "\n"; // Print error
    }

    return 0;
}

client.cpp - Almost same code as seen here

#define ASIO_STANDALONE  
#include <iostream>
#include <asio.hpp>

using asio::ip::tcp;


int main(int argc, char *argv[])
{
    asio::io_service io_service;
    tcp::socket      socket(io_service);
    tcp::resolver    resolver(io_service);
    // Connect
    asio::connect(socket, resolver.resolve({"localhost", "15562"}));

    for (int i = 0; i < 10; ++i)
    {
        std::cout << "Enter message to sent to server:" << std::endl;
        char client_message[2048];
        std::cin.getline(client_message, 2048);
        // Send message to server
        asio::write(socket, asio::buffer(client_message, 2048));

        char server_message[2048];
        // Read message from server
        asio::read(socket, asio::buffer(server_message, 2048));
        std::cout << "Reply is: " << std::endl;
        std::cout << server_message << std::endl;
    }

    return 0;
}
dimitris93
  • 4,155
  • 11
  • 50
  • 86
  • It is very possible that the server is not receiving the entire message in one shot and as a result `_data` is not null terminated. Same for `server_message` in the client. – user4581301 Apr 11 '16 at 22:17
  • @user4581301 What is the proper way of handling this situation in code ? How can I know when I have received the entire message ? I am simply printing out `_data`. If you have knowledge of websockets please take a look at my [other question](http://stackoverflow.com/questions/36557865/websockets-using-asio-c-library-for-the-server-and-javascript-as-client) too if you can – dimitris93 Apr 11 '16 at 22:24

1 Answers1

1
std::cin.getline(client_message, 2048);    

Gets a line of input from the user. In this case "1". This will be politely NULL terminated, but without looking you have no idea how much data was actually provided by the user.

asio::write(socket, asio::buffer(client_message, 2048))

Writes the entire 2048 bytes of client_message into the socket. So in goes '1', a NULL, and 2046 more bytes of unknown contents. All of this will be read by the server.

How this causes at least some of the OP's deviant behaviour:

Some of that 2048 bytes of data wind up in one packet. The rest winds up in another packet. The server reads the first packet and processes it. A few milliseconds later the second packet arrives. The first packet as a 1 and null in it, so cout prints 1 and discards the rest because that's what cout does with char *. The second packet has god-knows-what in it. cout will try to interpret it the way it would any other null terminated string. It will print random garbage until it finds a null, the cows come home, or the program crashes.

This needs to be fixed. Quick hack fix:

std::cin.getline(client_message, 2048);    
size_t len = strlen(client_message)
asio::write(socket, asio::buffer(client_message, len+1))

Now only the user's input string and a null will be sent. Consider using std::string and std::getline instead of the char array and iostream::getline

But because many messages may be put into the same packet by the TCP stack, you need to know when a message begins and ends. You can't count on one message one packet.

Typical solutions are

  1. read-a-byte read-a-byte read-a-byte-byte-byte until a protpcol-defined terminator is reached. Slow and painful, but sometimes the best solution. Buffering packets in a std::stringstream while waiting for a terminator that may not have arrived yet can ease this pain.

  2. I prefer prepending the length of the message to the message in a fixed size data type. Receiver reads for a the size of the length, then reads length bytes. Say you send an unsigned 32 bit length field. Receiver reads 32 bits to get the length, then reads length bytes for the message. When sending binary numbers over a network watch out for different endian among receivers. To avoid differing endians, make sure your protocol specifies what endian to use. Industry standard is to always send in big endian, but most processors you are likely to encounter these days are little endian. You make the call.

I'm fuzzy on the specifics of asio::buffer. You want to get the length (as a uint32_t) and the message (as a std::string) into the output stream. This might be as simple as

std::getline(cin, client_message);    
uint32_t len = client_message.length();
asio::write(socket, asio::buffer(len, sizeof(len)))
asio::write(socket, asio::buffer(client_message.c_str(), len+1))

There may be a better way built into asio, and the above may be total craptastic nonsense. Please consult an asio expert on how to optimize this.

The receiver reads the message something like:

uint32_t len;
asio::read(socket, asio::buffer(len, sizeof(len)));
asio::read(socket, asio::buffer(server_message, len));
std::cout << "Reply is: " << std::endl;
std::cout << server_message << std::endl;

The asynch version should be somewhat similar.

user4581301
  • 33,082
  • 7
  • 33
  • 54