Boost.Asio 서버 및 RAII
Boost.Asio를 사용하여 C ++에서 네트워크 서버 응용 프로그램을 구현하려고합니다.
내가 충족하려는 요구 사항은 다음과 같습니다.
- 응용 프로그램은
boost::io_context. - Single
io_context은run()공유 스레드 풀에 있습니다. 스레드 수가 정의되지 않았습니다. - 응용 프로그램은 여러 서버 개체를 인스턴스화 할 수 있습니다. 새 서버는 언제든지 생성 및 종료 할 수 있습니다.
- 각 서버는 여러 클라이언트의 연결을 처리 할 수 있습니다.
Server 클래스에 대해 RAII 패턴을 구현하려고합니다. 내가 보장하고 싶은 것은 서버가 할당 해제되면 모든 연결이 완전히 닫히는 것입니다. 각 연결을 닫을 수있는 세 가지 방법이 있습니다.
- 클라이언트가 응답하고 연결에서 더 이상 수행 할 작업이 없습니다.
- 서버 할당이 해제되고 모든 활성 연결이 닫힙니다.
- 연결은
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
답변
원하는 피드백을 제공 할 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에 대한 귀하의 주요 관심사를 실제로 다루지는 않지만 어쨌든 생각할만한 음식을 제공하기를 바랍니다.