Boost TCP server not able to receive string messages with \0 in the end

Viewed 35

The server works fine and receives all the messages when there are not prepended with \0.

Suppose if i send a message like "Message" , and send it 100 times to the server all the messages are received by the server but if i add \0 in the end like "Message\0" , than the server only receives couple messages.

This is the code for the TCP server.

#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/write.hpp>
#include <iostream>
#include <global.h>
#include <memory>
#include <fstream>


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)
            {
                if (length > 0) {
                
                    std::string strData(data_, length);
                    std::cout << strData;
                    
        
                    if (strData.find_first_not_of(' ') != std::string::npos)
                    {
                        // There's a non-space.
                        commandsQueue.push(strData);
                    }
                    
                }
                
                    do_write(length);
                
            }
        });
        
    }

    void do_write(std::size_t length)
    {
        
        if (stdqueAnswers.size() > 0) {
            auto self(shared_from_this());
            std::string stdstrAnswer = stdqueAnswers.front();
            boost::asio::async_write(socket_, boost::asio::buffer(stdstrAnswer.c_str(), stdstrAnswer.length() ),
                [this, self](boost::system::error_code ec, std::size_t /*length*/)
            {
                if (!ec)
                {
                    do_read();
                }
            });

            stdqueAnswers.pop();
        }
        else {

            auto self(shared_from_this());
        
            

             do_read();
        }
    }

    tcp::socket socket_;
    enum { max_length = 8096 };
    char data_[max_length];
};

class server {
public:
    server(boost::asio::io_context& io_context, short port)
        : acceptor_(io_context, tcp::endpoint(tcp::v4(), port))
    {
        acceptor_.listen();
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(
            make_strand(acceptor_.get_executor()),
            [this](boost::system::error_code ec, tcp::socket socket) {
            
                if (!ec) {
                    std::make_shared<session>(std::move(socket))->start();
                    do_accept();
                }
            });
    }

    tcp::acceptor acceptor_;
};
1 Answers

At first I thought what's likely happening is that you are breaking the output stream by writing \0 to it.

However, reading on, I spotted Undefined Behavior:

std::string stdstrAnswer = stdqueAnswers.front();
async_write(socket_, boost::asio::buffer(stdstrAnswer),
            [this, self](error_code ec, size_t /*length*/) {
                if (!ec) {
                    do_read();
                }
            });
stdqueAnswers.pop();

You're passing a reference to stdstrAnswer, a local variable, which goes out of scope before async_write completes. That's a problem regardless of whether you included stdqueAnswers.pop(). Instead consider:

auto self(shared_from_this());
async_write(socket_, boost::asio::buffer(stdqueAnswers.front()),
            [this, self](error_code ec, size_t /*length*/) {
                if (!ec) {
                    stdqueAnswers.pop();
                    do_read();
                }
            });

Of course, there's

  • the code smell of unused length parameter to do_write (what was it intended for?)
  • the fact that stdqueAnswers is used, but never pushed to
  • the fact that stdqueAnswers queues answers, but the queue is never drained: instead, after a single async_write there is always just do_read, so the queue won't function as a queue.

It feels like you're trying to have full-duplex IO. In that case you need to change from the half-duplex model that you currently still use:

enter image description here

To something conceptually like

enter image description here

There's some catches how to start the write loop when the first message is pushed to the out queue. See enqueueAnswer in the code below, which shows what I expect you were after:

Live On Coliru

#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/write.hpp>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <queue>

using boost::asio::ip::tcp;
using boost::system::error_code;

class session : public std::enable_shared_from_this<session>
{
public:
    session(tcp::socket socket)
        : socket_(std::move(socket))
    {
    }

    void start()
    {
        auto self(shared_from_this());
        // dispatch not strictly necessary for single-threaded contexts
        dispatch(
            socket_.get_executor(),
            [this, self]
            {
                do_read();
            });
    }

private:
    // all private member functions assumed to be on-strand
    std::queue<std::string> stdqueAnswers;

    void handleCommand(std::string&& cmd)
    {
        std::cout << std::quoted(cmd);

        if(cmd.find_first_not_of(' ') != std::string::npos)
        {
            // There's a non-space.
            enqueueAnswer("Handled command: '" + cmd + "'\n");
        }
    }

    void enqueueAnswer(std::string&& answer)
    {
        stdqueAnswers.push(std::move(answer));

        if(stdqueAnswers.size() == 1)
        {
            do_write();
        }
    }

    void do_read()
    {
        auto self(shared_from_this());
        socket_.async_read_some( //
            boost::asio::buffer(data_),
            [this, self](error_code ec, size_t length)
            {
                if(! ec)
                {
                    handleCommand({data_, length});
                    do_read();
                }
            });
    }

    void do_write()
    {
        if(stdqueAnswers.empty())
            return;
        auto self(shared_from_this());

        async_write(
            socket_,
            boost::asio::buffer(stdqueAnswers.front()),
            [this, self](error_code ec, size_t)
            {
                if(! ec)
                {
                    stdqueAnswers.pop();
                    do_write();
                }
            });
    }

    tcp::socket socket_;
    char data_[8096];
};

class server
{
public:
    server(boost::asio::io_context& io_context, std::uint16_t port)
        : acceptor_{io_context, tcp::endpoint(tcp::v4(), port)}
    {
        acceptor_.listen();
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(
            make_strand(acceptor_.get_executor()),
            [this](error_code ec, tcp::socket socket)
            {
                if(! ec)
                {
                    std::make_shared<session>(std::move(socket))->start();
                    do_accept();
                }
            });
    }

    tcp::acceptor acceptor_;
};

int main()
{
    boost::asio::io_context ioc;
    server s(ioc, 8989);

    ioc.run();
}

With a demo client:

sleep 1; printf 'hello\nworld\nnull chars \0 are the best\nbye' | netcat 127.0.0.1 8989 | xxd

Seems to handle \0 characters just fine:

enter image description here

Related