0

I am trying to test my boost asio socket listener using boost unit test. The purpose of the listener is simply listening on a port and read what ever comes in and save it into a queue and send back a http response header.

As a first step, I have created a client to send messages to a listener and read the response message comes from the listener. I have also created a thread to start the listener. Main thread will send and receive messages to and from the Listener. I could able to send and receive messages between client and listner. But when I try to join it does not join and keeps on waiting to join(I guess).

Please help me what went wrong.

Thanks in advance.

Listener.hpp

#ifndef Listener_hpp
#define Listener_hpp
#include <iostream>
#include <stdio.h>
#include <atomic>
#include <boost/asio.hpp>
#include "sharedQueue.hpp"

using namespace std;
using namespace boost;
using boost::asio::ip::tcp;

class Listener{

private:
int port;
std::atomic<bool> m_stop;
boost::system::error_code error;
boost::asio::io_service io_service;
std::shared_ptr<sharedQueue> queue_ptr;
void saveMessageToQueue(std::string,std::string);
Listener(int portToListen, std::shared_ptr<sharedQueue>  queue_unique_ptr);
Listener(const Listener&);
Listener& operator=(const Listener&);
public:
static Listener& getInstance(int portToListenOn,         std::shared_ptr<sharedQueue>   queue_unique_ptr);
void listen();
};
#endif /* Listener_hpp */

Listener.cpp

 #include "Listener.hpp"
 Listener::Listener(int portToListen, std::shared_ptr<sharedQueue>   queue_unique_ptr):port(portToListen), queue_ptr(queue_unique_ptr),m_stop(false)
 {;}
 Listener& Listener::getInstance(int portToListenOn, std::shared_ptr<sharedQueue>   queue_unique_ptr)
 {
  static Listener instance(portToListenOn,  queue_unique_ptr);
  return instance;
 }
 void Listener::stopListen(){
 m_stop = true;
 }
 void Listener::listen()
 {
  try{
    tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), port));
    while(!m_stop)
    {
        std::cout<<"Server is listening on port "<<port<<std::endl;
        boost::asio::ip::tcp::socket socket(io_service);
        acceptor.accept(socket);
        std::cout<<"Connection Request Accepted "<<std::endl;
        std::array<char, 512> buf;
        socket.read_some(boost::asio::buffer(buf), error);
        std::string responseBack =" HTTP:300";
        boost::asio::write(socket, boost::asio::buffer(responseBack, responseBack.size()), error);
    }
  }
  catch (std::exception& e)
  {
    std::cerr <<"Listener Exception : "<< e.what() << std::endl;
    exit(0);
  }
  }

sharedQueue.hpp

  #ifndef SharedQueue_hpp
  #define SharedQueue_hpp
  #include <stdio.h>
  #include <iostream>
  class sharedQueue{
  public:
  sharedQueue(){};
  void pushToQueue(std::string str){};
  };
  #endif /* SharedQueue_hpp */

ClientTestHelper.hpp

  #ifndef ClientTestHelper_hpp
  #define ClientTestHelper_hpp

  #include <iostream>
  #include <boost/asio.hpp>
  using namespace std;
  class ClientTestHelper
  {
    string address = "";
    public:
    ClientTestHelper(std::string addressToPush){
    this->address = addressToPush;
   };
  ~ClientTestHelper(){};
  std::string sendMessage(string message);
  };

  #endif /* ClientTestHelper_hpp */

ClientTestHelper.cpp

#include "ClientTestHelper.hpp"
std::string ClientTestHelper::sendMessage(string message){
boost::asio::io_service io_service;
boost::asio::ip::tcp::resolver resolver(io_service);
boost::asio::ip::tcp::resolver::query query(address, boost::asio::ip::resolver_query_base::numeric_service);
boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
boost::asio::ip::tcp::socket socket(io_service);
boost::asio::connect(socket, endpoint_iterator);
boost::asio::streambuf writeBuffer;
boost::asio::streambuf readBuffer;
std::string str = message;
boost::system::error_code error;
boost::asio::write(socket, boost::asio::buffer(str, str.size()), error);
std::array<char,1024> buf;
socket.read_some(boost::asio::buffer(buf), error);
std::string respo = buf.data();
cout<<respo<<std::endl;
return respo;
}

ListenerTest.hpp

#ifndef ListenerTest_hpp
#define ListenerTest_hpp

#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MAIN

#include <iostream>
#include <stdio.h>
#include <thread>
#include <boost/test/unit_test.hpp>
#include "Listener.hpp"
#include "ClientTestHelper.hpp"
#include "SharedQueue.hpp"

struct ListenerTestFixture{
public:
void startListen(Listener &receiver){
    receiver.listen();
}
std::shared_ptr<sharedQueue> myQueue  = std::make_shared<sharedQueue>();
Listener &receiver = Listener::getInstance(8282, myQueue);
void doRun(){
    receiver.listen();
}
ClientTestHelper *client = new ClientTestHelper("8282");
ListenerTestFixture(){};
~ListenerTestFixture(){};
};
#endif /* ListenerTest_hpp */

ListenerTest.cpp

#include "ListenerTest.hpp"

BOOST_FIXTURE_TEST_CASE(connectionTest, ListenerTestFixture)
{
std::thread t1(&ListenerTestFixture::doRun, *this);
std::string response = "";
response   = client->sendMessage("SomeRandom Messages \r\n\r\n\r\n");
//receiver.~Listener();
receiver.stopListen();
std::cout<<"Response Received "<<response;
t1.join();
std::cout<<"Finished Testing "<<endl;
}

Console Output For Clarification:

Running 1 test case...
Server is listening on port 8585
Connection Request Accepted 
Server is listening on port 8585
  HTTP:300

After this execution did not stopped instead waiting to join.

EDIT: Modified Listener class added member m_stop and stopListen(), Changed for(;;) into while(!m_stop). Header file included.

Kid
  • 169
  • 1
  • 19
  • 3
    Why do you manually call the destructor of an object ? – Blacktempel Aug 18 '17 at 11:22
  • 1
    In order to join, the thread function must exit. Does `listen` ever exit? – n. m. could be an AI Aug 18 '17 at 11:30
  • Please make the sample code SSCCE. This doesn't compile for simple reasons as typos – sehe Aug 18 '17 at 11:39
  • hi @n.m . The method listens never exit as it needs to listen on for next incoming messages. To stop this process I manually called destructor `receiver.~listener();` – Kid Aug 18 '17 at 11:54
  • Hi @sehe, since the listener class had 300 LOC I dint pasted whole class. And also I dont know SSCCE abbreviation. – Kid Aug 18 '17 at 11:55
  • 1
    It's not just that. The first thing that caught my eye was `#include "listnener.h"`. Help us help you: make a SSCCE. See also http://kera.name/articles/2013/10/nobody-writes-testcases-any-more/ – sehe Aug 18 '17 at 11:56
  • Yeah I see that now. The problem as I changed the names of the files I am actually using. Thats causes typo. l will give you compilable files then. – Kid Aug 18 '17 at 12:01
  • Thanks for the article @sehe. I am trying to create a SSCCE. – Kid Aug 18 '17 at 13:09
  • @Blacktemple because listen() will not exit instead it will be listening for a new connection. For the sack of stopping this, I called the destructor manually. – Kid Aug 18 '17 at 14:09
  • @sehe I have included SSCCE. – Kid Aug 18 '17 at 14:09
  • 1
    The destructor of Listener cannot magically stop the listen method. Regardless, calling the Listener destructor while listen is running leads to undefined behaviour as soon as listen accesses the Listener, which is not a good idea. – PaulR Aug 18 '17 at 15:03
  • Hi @PaulR, Thanks, I don't know about this until now. Since the listen method runs forever, how can I stop this manually?. – Kid Aug 18 '17 at 15:07
  • Just a comment to the question, but the namespace `::` is eye-boggling. Too many god damn colons. I love C++ as a powerful language, but some of its traits are just crazily overly complicated. – sivabudh Aug 18 '17 at 15:25
  • 1
    @Kid: Well the basic idea is to add a condition to the `for(;;)` loop as I wanted to write in an answer, but if the socket and acceptor methods are blocking this will not be sufficient, as the thread may still be stuck waiting for input. I am not quite familiar with those, but maybe there are versions which have a timeout. – PaulR Aug 18 '17 at 15:36
  • @PaulR I have put in an answer on those lines. If the members are blocking then they need to be replaced with non-block equivalents. That's either `try` versions or versions with time-outs. I don't know what is available in Boost. There may be a way of checking if the socket and acceptor members are 'ready'. – Persixty Aug 18 '17 at 15:57
  • @PaulR a quick look at the docco says `acceptor` can be set to a non-blocking mode. I'm sure socket will be the same but haven't looked. http://www.boost.org/doc/libs/1_62_0/doc/html/boost_asio/reference/basic_socket_acceptor/non_blocking.html – Persixty Aug 18 '17 at 16:00
  • @PaulR Better still there appears to be a `cancel()` member that forces the blocking members to return immediately. As mentioned I'm no expert on Boost. It appears calling that during my proposed `stop()` member would do it. http://www.boost.org/doc/libs/1_62_0/doc/html/boost_asio/reference/basic_socket_acceptor/cancel.html – Persixty Aug 18 '17 at 17:12
  • You cannot touch a regular object like `m_stop` in one thread and look at it in another thread. You need at least an [atomic](http://en.cppreference.com/w/cpp/atomic/atomic). – n. m. could be an AI Aug 19 '17 at 11:07
  • @n.m m_stop is an atomic variable. – Kid Aug 19 '17 at 11:10
  • @Kid sorry missed that – n. m. could be an AI Aug 19 '17 at 12:04

2 Answers2

2

receiver.~Listener(); doesn't do what you think it does. That line of code can't work and has to come out.

What it does is cause the destructors to be called on all the members of receiver. It won't (directly) interrupt the call to listen(). The likely outcome is that the thread executing listen() will perform undefined behaviour and almost certainly that means fail to end in a way that notifies join().

In short your main thread will wait forever because you've "pulled the rug out" from under receiver().

Even if that somehow worked the compiler will have added an implicit call of receiver.~Listener() at the end of main() and likely crash there if by chance it makes it that far.

The normal solution is to add a member to Listener such as std::atomic<bool> m_stop; (or if you prefer Boost equivalents if you're using Boost) initialise it to false in the constructor (not listen()) and then call a member called stop() that sets it to true immediately before calling join().

You then replace the non-terminating loop in listen() with while(!stop).

That way you main thread can notify the listening the to come to an orderly conclusion at the end of a loop.

There is no general way of just making a thread "just" or "suddenly" stop cleanly. You can't anticipate what point it is at in a process or what resources it was using. It might be in a library call and corrupt the heap, the operating system, the file system, anything.

As an aside Java has a thread method called stop() it was pretty much deprecated immediately after it was added. There is no safe way to implement it or use it.


It is rare that you should ever call a destructor directly. The only use case I know is where you've used placement new and don't want to return the space back to the heap. It's even rarer in modern C because things like recycling space in containers is all handed over to std containers like std::vector<>.

Most implementations of std::vector<> call destructors because if you resize the vector down it needs to destruct the object but not reallocate the internal storage.

You should pretty much never call a destructor on a statically allocated object.

Persixty
  • 8,165
  • 2
  • 13
  • 35
  • Hi @Persixty, Thanks for the response. I have implemented your logic. But the problem still persists. As far as I understand from the console output, the second thread executing `listen()` method is waiting to accept new connection, `ie already inside the loop`. After executing those codes only this thread checks the value of `m_stop`. That's why the loop did not terminate by setting `m_stop` to `false`. – Kid Aug 19 '17 at 11:04
  • @kid Did you look at my comments on the OP. There are apparently ways in the Boost library to force 'waiting' activities to stop using `cancel()`. Did you try that? It also appears there are ways to making the objects non-blocking. As I said I'm not a Boost expert. – Persixty Aug 19 '17 at 11:43
  • sorry for the very late reply, I have got an idea for the sake of testing (due to the time constraints). ` BOOST_FIXTURE_TEST_CASE(connectionTest, pushMessageReceiverFixture) { std::thread t1(&pushMessageReceiverFixture::doRun, *this); std::string response = ""; response = pushServer->pushEmptyMessage("SomeRandom Messages \r\n\r\n\r\n"); receiver.~pushMessageReceiver(); std::cout<<"Response Received "<pushEmptyMessage("stop listening \r\n\r\n\r\n"); t1.join(); std::cout<<"Finished Testing "< – Kid Aug 28 '17 at 11:46
  • it does work. but I am not sure weather it is acceptable or not as a proper test. – Kid Aug 28 '17 at 11:46
  • @kid it looks like you're injecting some kind of stop message that will unblock a blocked thread. That is valid though it looks like the `cancel()` member is designed to do that in a formal way. Remember testing can only prove a program is wrong. Not right. That goes 10 fold for concurrency. It's difficult or impossible to cause all the concurrent states you'd like to test. Cautious static-checking is the only way to establish concurrent program correctness. Testing just adds confidence. – Persixty Aug 29 '17 at 09:48
0

if you are running asio functions like

acceptor.accept(socket);

they will block unless they have finished or return with an error or exception (except their underlying socket has been set to non-blocking mode!). Thus, you do not reach your test if m_stop has been set or not.

you'd need to cancel any running operation inside the stop() function, i.e.:

void Listener::stopListen(){
    std::error_code _ignore;
    m_stop = true;
    m_listener.cancel(_ignore); // pass in an ec, it may throw otherwise
    m_sock.cancel(_ignore);
}

Of course, you'd need to embedd the io objects (socket/acceptor) inside your class, to make them reachable inside stopListen().

However, to be honest, I'd highly recommand to writing that code with the asynchroneous pattern, and use

m_acceptor.async_accept(m_sock, [&](const std::error_code &ec) {
    if (ec) {
        return handle_error(ec, "accept"); // bail out
    }

    handle_new_client(std::move(m_sock)); // move out socket
    // restart listen here
});

The threaded approach of course pulls in additional potential race conditions, which are sometimes hard to handle properly.

Chris Kohlhoff has some echo server examples with different acceptor styles from blocking to async, showing working patterns.

Cheers, A.

S: read_some() will most likely not do what you want. It just returns the first few bytes that already have reached your computers TCP stack. There's however no guarantee that the full request is available, so you'd need to invoke async_read multiple times. If the protocol is http, asio::async_read_until would be better, and use a "\r\n" separator.

argonaut6x
  • 115
  • 1
  • 8