Boost.Asio 서버 및 RAII

Oct 08 2020

Boost.Asio를 사용하여 C ++에서 네트워크 서버 응용 프로그램을 구현하려고합니다.

내가 충족하려는 요구 사항은 다음과 같습니다.

  • 응용 프로그램은 boost::io_context.
  • Single io_context은 run()공유 스레드 풀에 있습니다. 스레드 수가 정의되지 않았습니다.
  • 응용 프로그램은 여러 서버 개체를 인스턴스화 할 수 있습니다. 새 서버는 언제든지 생성 및 종료 할 수 있습니다.
  • 각 서버는 여러 클라이언트의 연결을 처리 할 수 ​​있습니다.

Server 클래스에 대해 RAII 패턴을 구현하려고합니다. 내가 보장하고 싶은 것은 서버가 할당 해제되면 모든 연결이 완전히 닫히는 것입니다. 각 연결을 닫을 수있는 세 가지 방법이 있습니다.

  1. 클라이언트가 응답하고 연결에서 더 이상 수행 할 작업이 없습니다.
  2. 서버 할당이 해제되고 모든 활성 연결이 닫힙니다.
  3. 연결은 stop()메서드 를 호출하여 수동으로 종료됩니다 .

위의 모든 기준을 충족하는 것처럼 보이는 솔루션에 도달했지만 Boost.Asio는 여전히 나에게 아주 새로운 기능이기 때문에 내가하는 일이 올바른지 확인하고 싶었습니다. 또한 제가 구체적으로 100 % 확신하지 못한 몇 가지 사항이 있습니다.

  • mutex서버 클래스 에서을 제거 하고 대신 strand모든 동기화에를 사용하려고했지만이를 수행하는 명확한 방법을 찾을 수 없었습니다.
  • 스레드 풀은 하나의 스레드로만 구성 될 수 있고이 스레드는 서버 소멸자를 호출 할 수 있기 때문에 io_context::poll_one()모든 보류중인 연결이 종료를 완료하고 잠재적 교착 상태를 방지 할 수있는 기회를 제공하기 위해 소멸자에서 호출해야했습니다 .
  • 당신이 생각할 수있는 개선을위한 다른 제안을 환영합니다.

어쨌든, 여기에 몇 가지 단위 테스트가있는 코드가 있습니다 (Coliru의 라이브 버전 : http://coliru.stacked-crooked.com/a/1afb0dc34dd09008 ) :

#include <boost/asio/io_context.hpp>
#include <boost/asio/io_context_strand.hpp>
#include <boost/asio/executor.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/asio/dispatch.hpp>
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <list>
using namespace std;
using namespace boost::asio;
using namespace std::placeholders;


class Connection;


class ConnectionDelegate
{
public:
    virtual ~ConnectionDelegate() { }
    
    virtual class executor executor() const = 0;
    virtual void didReceiveResponse(shared_ptr<Connection> connection) = 0;
};


class Connection: public enable_shared_from_this<Connection>
{
public:
    Connection(string name, io_context& ioContext)
    : _name(name)
    , _ioContext(ioContext)
    , _timer(ioContext)
    {
    }
    
    const string& name() const
    {
        return _name;
    }
    void setDelegate(ConnectionDelegate *delegate)
    {
        _delegate = delegate;
    }
    
    void start()
    {
        // Simulate a network request
        _timer.expires_from_now(boost::posix_time::seconds(3));
        _timer.async_wait(bind(&Connection::handleResponse, shared_from_this(), _1));
    }
    void stop()
    {
        _timer.cancel();
    }
    
private:
    string _name;
    io_context& _ioContext;
    boost::asio::deadline_timer _timer;
    ConnectionDelegate *_delegate;
    
    void handleResponse(const boost::system::error_code& errorCode)
    {
        if (errorCode == error::operation_aborted)
        {
            return;
        }
        dispatch(_delegate->executor(),
                 bind(&ConnectionDelegate::didReceiveResponse, _delegate, shared_from_this()));
    }
};


class Server: public ConnectionDelegate
{
public:
    Server(string name, io_context& ioContext)
    : _name(name)
    , _ioContext(ioContext)
    , _strand(_ioContext)
    {
    }
    ~Server()
    {
        stop();
        assert(_connections.empty());
        assert(_connectionIterators.empty());
    }
    weak_ptr<Connection> addConnection(string name)
    {
        auto connection = shared_ptr<Connection>(new Connection(name, _ioContext), bind(&Server::deleteConnection, this, _1));
        {
            lock_guard<mutex> lock(_mutex);
            _connectionIterators[connection.get()] = _connections.insert(_connections.end(), connection);
        }
        connection->setDelegate(this);
        connection->start();
        return connection;
    }
    
    vector<shared_ptr<Connection>> connections()
    {
        lock_guard<mutex> lock(_mutex);
        
        vector<shared_ptr<Connection>> connections;
        for (auto weakConnection: _connections)
        {
            if (auto connection = weakConnection.lock())
            {
                connections.push_back(connection);
            }
        }
        return connections;
    }
    void stop()
    {
        auto connectionsCount = 0;
        for (auto connection: connections())
        {
            ++connectionsCount;
            connection->stop();
        }
        
        while (connectionsCount != 0)
        {
            _ioContext.poll_one();
            connectionsCount = connections().size();
        }
    }
    
    // MARK: - ConnectionDelegate
    class executor executor() const override
    {
        return _strand;
    }
    void didReceiveResponse(shared_ptr<Connection> connection) override
    {
        // Strand to protect shared resourcess to be accessed by this method.
        assert(_strand.running_in_this_thread());
        
        // Here I plan to execute some business logic and I need both Server & Connection to be alive.
        std::cout << "didReceiveResponse - server: " << _name << ", connection: " << connection->name() << endl;
    }
    
private:
    typedef list<weak_ptr<Connection>> ConnectionsList;
    typedef unordered_map<Connection*, ConnectionsList::iterator> ConnectionsIteratorMap;
    
    string _name;
    io_context& _ioContext;
    io_context::strand _strand;
    ConnectionsList _connections;
    ConnectionsIteratorMap _connectionIterators;
    mutex _mutex;
    
    void deleteConnection(Connection *connection)
    {
        {
            lock_guard<mutex> lock(_mutex);
            auto iterator = _connectionIterators[connection];
            _connections.erase(iterator);
            _connectionIterators.erase(connection);
        }
        default_delete<Connection>()(connection);
    }
};


void testConnectionClosedByTheServer()
{
    io_context ioContext;
    auto server = make_unique<Server>("server1", ioContext);
    
    auto weakConnection = server->addConnection("connection1");
    assert(weakConnection.expired() == false);
    assert(server->connections().size() == 1);
    
    server.reset();
    assert(weakConnection.expired() == true);
}

void testConnectionClosedAfterResponse()
{
    io_context ioContext;
    auto server = make_unique<Server>("server1", ioContext);
    
    auto weakConnection = server->addConnection("connection1");
    assert(weakConnection.expired() == false);
    assert(server->connections().size() == 1);
    
    while (!weakConnection.expired())
    {
        ioContext.poll_one();
    }
    assert(server->connections().size() == 0);
}

void testConnectionClosedManually()
{
    io_context ioContext;
    auto server = make_unique<Server>("server1", ioContext);
    
    auto weakConnection = server->addConnection("connection1");
    assert(weakConnection.expired() == false);
    assert(server->connections().size() == 1);
    
    weakConnection.lock()->stop();
    ioContext.run();
    
    assert(weakConnection.expired() == true);
    assert(server->connections().size() == 0);
}

void testMultipleServers()
{
    io_context ioContext;
    auto server1 = make_unique<Server>("server1", ioContext);
    auto server2 = make_unique<Server>("server2", ioContext);

    auto weakConnection1 = server1->addConnection("connection1");
    auto weakConnection2 = server2->addConnection("connection2");

    server1.reset();
    assert(weakConnection1.expired() == true);
    assert(weakConnection2.expired() == false);
}

void testDeadLock()
{
    io_context ioContext;
    auto server = make_unique<Server>("server1", ioContext);
    
    auto weakConnection = server->addConnection("connection1");
    assert(weakConnection.expired() == false);
    assert(server->connections().size() == 1);
    
    auto connection = weakConnection.lock();
    server.reset(); // <-- deadlock, but that's OK, i will try to prevent it by other means
}


int main()
{
    testConnectionClosedByTheServer();
    testConnectionClosedAfterResponse();
    testConnectionClosedManually();
    // testDeadLock();
}

감사합니다, Marek

답변

2 Quuxplusone Oct 10 2020 at 05:16

원하는 피드백을 제공 할 Asio를 충분히 알지 못합니다.하지만 다음과 같은 몇 가지 사소한 정리를 수행 할 수 있습니다.

  • 하지 마십시오 using namespace std. using namespace명확성을 위해 다른 것도 피해야 합니다.

  • virtual ~ConnectionDelegate() { }virtual ~ConnectionDelegate() = default;대신 될 수 있습니다 . 이것은 당신의 의도를 조금 더 잘 나타냅니다.

  • ~Server()~Server() override가상 멤버 함수를 재정의 함을 나타내려면 이어야합니다 . 일반적으로 override언어가 물리적으로 허용 하는 모든 곳에서 사용해야 합니다. ( 소멸자를 제외한 모든 곳 에서 올바르게 수행한다고 생각합니다 .)

  • Connection(string name,그리고 Server(string name,모두 불필요의 복사본을 만듭니다 string name.

  • 모든 생성자는이어야합니다 explicit. 예를 들어 중괄호 쌍이 우연이 아니라 {"hello world", myIOContext}묵시적으로 Server객체 로 취급 (또는 묵시적으로 변환)되어서는 안된다는 것을 컴파일러에 알려줍니다 .

  • 개인적으로, 나는에 대한 형식 정의의 사용을 찾을 수 ConnectionsList및 ConnectionsIteratorMap간접 불필요한 층이 될 수 있습니다. 차라리 std::list<std::weak_ptr<Connection>> _connections;바로 거기에서 인라인으로 보고 싶습니다 . 해당 유형의 이름이 필요하면라고 말할 수 있습니다 decltype(_connections).

  • default_delete<Connection>()(connection)은 장황한 말입니다 delete connection. 직접하십시오.

  • class executor executor()혼란 스럽습니다. 클래스에 대한 올바른 이름이 아니거나이 메서드에 대한 올바른 이름이 아니라는 class경고가 있었어야 했다는 사실 . 예를 들어 메서드의 이름을로 변경하는 것을 고려하십시오 . 이 파일에 선언되어 있지 않기 때문에 이름을 변경할 수 없다고 가정 합니다. 그것은 당신이했던 일부 Boost 네임 스페이스에서 오는 것이 틀림 없다 . ( 네임 스페이스를 사용 하지 마십시오 !)executorexecutor()get_executor()class executorusingusing


참조를 통한 복사 및 / 또는 의미 이동을 피할 수있는 많은 기회를 건너 뜁니다. 예를 들어에서 다음과 같이 Server::connections()작성했을 것입니다.

std::vector<std::shared_ptr<Connection>> connections() {
    std::lock_guard<std::mutex> lock(_mutex);
    std::vector<std::shared_ptr<Connection>> result;
    for (const auto& weakConnection : _connections) {
        if (auto sptr = weakConnection.lock()) {
            result.push_back(std::move(sptr));
        }
    }
    return result;
}

이렇게하면 weakConnection복사본 대신 참조를 만들어서 약한 참조 개수를 범핑하는 것을 방지 한 다음 복사 대신 이동을 사용하여 강한 참조 개수를 범핑하는 것을 방지합니다 push_back. 4 개의 원자 작전이 저장되었습니다! (실생활에서 중요하지는 않지만 코드 리뷰에 오신 것을 환영합니다.)


dispatch(_delegate->executor(),
         bind(&ConnectionDelegate::didReceiveResponse, _delegate, shared_from_this()));

bind헷갈리는 사용법을 발견 했지만 확실하지 않습니다 (실제로 누군가가 저를 언급하고 계몽 해주기를 바랍니다). 여기 에 bind 필요 합니까? 확실히 읽기 쉽고, 컴파일 속도가 빠르며, 런타임에 쓰기 속도가 느리지 않습니다.

dispatch(
    _delegate->executor(),
    [self = shared_from_this(), d = _delegate]() {
        d->didReceiveResponse(self);
    }
);

이것은 그것을 실제로 복사 무슨 명확 비트 만들기 (하나를 것이다 shared_ptr유지 *this살아, 한 원시 포인터). 사실, 원시 포인터의 복사본을 숨길 필요가 있는지 궁금합니다. 대신 이것으로 벗어날 수 있습니까?

dispatch(
    _delegate->executor(),
    [self = shared_from_this()]() {
        self->_delegate->didReceiveResponse(self);
    }
);

아니면 때때로 그 람다의 본문에 들어갈 것이라고 예상 d != self->_delegate합니까? 그래서 추가 포인터가 필요합니까?


std::chrono::seconds대신 사용할 수 있는지 궁금합니다 boost::posix_time::seconds. Boost.Asio는 std::chrono요즘 C ++ 11 과 상호 운용 할 수 있습니까 ?


_connectionIterators[connection.get()] = _connections.insert(_connections.end(), connection);

여기에서 "영리함"이 등호의 잘못된쪽에있는 것 같습니다. _connections.insert(_connections.end(), connection)장황한 글쓰기 방식 인 것 같습니다 _connections.push_back(connection). 반대로, 나는 사람들 이 성능과 명확성 map[k] = v을 map.emplace(k, v)위해 대체하는 것을 보는 데 익숙합니다 . 그 기억 map[k] = v최초의 기본적 구조를 map[k] 다음 할당 그것으로 새로운 값을.

아, 반복자를 반환하고 그렇지 insert않기 때문에 사용해야 insert합니다 push_back.

그러나 그것은 의문을 제기합니다. 왜 두 가지 부작용을 한 줄로 묶으려고 하는가? 우리는 두 줄을 허용하는 경우, 우리는 그냥 할 push_back다음 설정 map.emplace(connection.get(), std::prev(_connections.end())). 아니면, 그 시점에서 나는 정말로 불평하지 않을 것입니다.

auto it = _connections.insert(_connections.end(), connection);
_connectionIterators.emplace(connection.get(), it);

붉은 깃발을 발견하고 더 깊이 파헤쳐 보자. 원 라이너와 더 명확한 투 라이너 의 차이점 은 무엇일까? 아하! 차이점은 _connections.insert(...)메모리가 부족하여 던지면 발생하는 것입니다. 2 라이너로 _connectionIterators손대지 않습니다. 한 - 라이너로, 당신은 처음 에 위험한 쓰레기를 기본적으로 구축 _connectionIterators[connection.get()]하고 다음 예외를 전파.

그래서 저는 일반적인 원칙에 따라 2 행을 찬성하는 합리적인 주장이 있다고 생각합니다.


다시 말하지만,이 답변은 RAII에 대한 귀하의 주요 관심사를 실제로 다루지는 않지만 어쨌든 생각할만한 음식을 제공하기를 바랍니다.