Skip to content

Instantly share code, notes, and snippets.

@ramntry
Created October 3, 2012 15:07
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ramntry/3827445 to your computer and use it in GitHub Desktop.
Save ramntry/3827445 to your computer and use it in GitHub Desktop.
Simple Boost.Asio based synchronous multithreaded server
#include <ctime>
#include <string>
#include <iostream>
#include <boost/bind.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/format.hpp>
#include <boost/smart_ptr.hpp>
using boost::asio::ip::tcp;
class client_session
{
public:
typedef boost::shared_ptr<client_session> pointer;
static pointer create(boost::asio::io_service &io) {
return pointer(new client_session(io));
}
~client_session() {
log("Connection closed");
}
tcp::socket &socket() { return socket_; }
void start() {
log("Connection established");
std::string time_string;
for (int i = 0; i < 5; ++i) {
time_string = make_time_string();
boost::asio::write(socket_, boost::asio::buffer(time_string));
sleep(1);
}
}
protected:
client_session(boost::asio::io_service &io)
: socket_(io) {
}
std::string make_time_string() {
char buf[32];
time_t now = time(0);
return ctime_r(&now, buf);
}
void log(std::string const &message) {
std::clog << boost::format("%|-25| [client address: %|15|]\n")
% message % socket_.remote_endpoint().address().to_string();
}
private:
tcp::socket socket_;
};
int main()
{
boost::asio::io_service io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 13));
for (;;) {
client_session::pointer new_client = client_session::create(io);
acceptor.accept(new_client->socket());
boost::thread(boost::bind(&client_session::start, new_client)).detach();
}
}
CONFIG -= qt
CONFIG += debug
CONFIG += thread
boost_path = /home/ramntry/boost/boost_1_50_0
INCLUDEPATH += \
$${boost_path} \
LIBS += \
$${boost_path}/stage/lib/libboost_system.a \
$${boost_path}/stage/lib/libboost_thread.a \
SOURCES = \
simple_daytime_server.cpp \
@ramntry
Copy link
Author

ramntry commented Oct 3, 2012

13: Класс client_session - это своеобразный хранитель (существует одноименный паттерн, хотя, надо признать, наш случай лишь общеидеологически с ним перекликается) части внутреннего состояния сервера, индивидуальной для каждого клиента. Экземпляр этого класса создается на каждого клиента в количестве одной штуки, живет столько же, сколько соединение, его породившее. Парадигма не меняется при переходе к асинхронной модели, скорее даже напротив, становится очевиднее ее значение: сервер, выполняясь в одном (для простоты) потоке постоянно "бегает" по установившим соединение клиетам, меж делом устанавливает новые или завершает существующие соединения, и в каждом случае ему нужно "вспомнить", что уже было сделано для клиента, что еще нет - нужно уметь восстанавливать контекст работы. Класс client_session и является таким контекстом.

16: Даже для очень простых серверов актуальна проблема - не совсем очевидно, когда, как и кто будет уничтожать экземпляр класса client_session. Некоторая специфика работы классов Boost.Thread позволяет удобно использовать тот факт, что поток (thread), закончив всю полезную работу, уничтожается самостоятельно - а с уничтожением своего стека вызовет деструкторы всех объектов, созданных на нем. Маленькая трудность состоит в том, что экземпляр класса client_session намного удобнее создавать еще в родительском потоке, тогда, когда стека дочернего потока, как и самого потока, еще не существует. Перенос объекта со стека на стек - нетривиальная задача, адекватно решаемая только по схеме копирование - уничтожение оригинала, которая не всегда подходит (например, копирование сокета как уникального системного идентификатора (дескриптора) может не пройти гладко) потому проще поступить иначе - создать экземпляр класса client_session в динамической памяти, общей для всех потоков, а на стек дочернего потока скопировать умный указатель на созданный экземпляр. На стеке родительского потока этот указатель уничтожается, так что счетчик ссылок этого указателя меняется так: 1 - 2 - 1 (очевидно). Объект остается жить ровно столько, сколько живет поток - со смертью потока умирает умный указатель (см. выше), счетчик ссылок обнуляется и экземпляр client_session уничтожается. Это нам и нужно. typedef же крайне удобен и улучшает инкапсуляцию - клиентскому коду не нужно знать, какова реализация умного указателя (да и умен ли он вовсе :-))

18: Удобно защитить самого себя от ошибок: согласно описанной выше идеоме корректен лишь один способ создания экземпляра - на куче с обязательным оборачиванием в умный указатель. Потому все "альтернативные" способы, по возможности, следует запрещать (см. 40: - конструктор защищен и не может быть использован вне класса). Для простоты не запрещались копирующий конструктор и оператор присваивания, что стоит, вообще говоря, сделать (не обязательно "руками", к примеру в boost есть маленький класс-примесь noncopyable, достаточно от него отнаследоваться)

26: Сокет на момент создания экземпляра не ассоциирован ни с каким соединением - это должен сделать главный цикл сервера в методе accept - потому необходимо дать доступ к сокету.

28: Главный метод класса, осуществляющий всю работу с клиентом - код именно этого метода будет выполняться в отдельном потоке, стек именно этого метода - корневой стек дочернего потока. С завершением этого метода разворачиваются события, описанные в 16:

32: Сервер пятикратно отправляет клиенту актуальные дату и время с задержкой в секунду. Так не нужно, так удобно - можно успеть запустить одновременно несколько клиентов и убедиться, что сервер действительно обслуживает их одновременно.

34: Глобальная функция boost::asio::write осуществляет блокирующую запись в сокет всего содержимого переданного буфера (в отличие от методов сокетов вида "write_some", способных завершиться не закончив передачу всех данных). Концепция "буфера" нетривиальна, и следует посмотреть документацию, чтобы прояснить ее себе.

61: Все потенциально асинхронные сервисы в Boost.Asio создаются поверх экземпляра io_service. Подробности в документации Boost.Asio.

63: acceptor - всего лишь специализация сокета (прослушивающая), в данном случае он связывается (bind) с любым адресом IPv4 и портом 13 (порт сервиса даты и времени)

64: Режим работы большинства реальных серверов - бесконечный. Большая часть серверов запускается как демоны (daemon) в терминах Unix или службы - в терминах Windows. Останов хорошо спроектированного сервера выполняется подачей ему сигнала (в linux традиционно SIGUSR1), для которого существует специальный обработчик. Регистрация обработчика осуществляется системным вызовом, а управление ему впоследствии передается по аппаратному прерыванию ядром ОС, потому главный поток сервера действительно выйдет из этого якобы неразрывного цикла.

65: "Правильно" (см. 16:) создаем экземпляр client_session для будущего клиента.
64: Осуществляем блокирующее ожидание нового подключения в основном потоке на сокете заранее подготовленного контекста (см. 13:) подключения (потому сервер и синхронный - все операции в рамках одного конкретно взятого потока блокирующие)
65: Создаем новый поток, захватываем с помощью boost::bind умный указатель на client_session в функтор (теперь он "удерживает" счетчик ссылок от падения в ноль), попутно превращая метод start класса client_session в нульарную функцию, которую и требует конструктор boost::thread, запускаем в этом потоке таким полученную функцию, а сам поток отсоединяем (detach) от основного - теперь со смертью экземпляра thread (а она наступает незамедлительно) описываемый им поток продолжает жить самостоятельно, уже не имея в основном потоке никаких средств обращения к себе.

@ramntry
Copy link
Author

ramntry commented Oct 3, 2012

13: Класс client_session - это своеобразный хранитель (существует одноименный паттерн, хотя, надо признать, наш случай лишь общеидеологически с ним перекликается) части внутреннего состояния сервера, индивидуальной для каждого клиента. Экземпляр этого класса создается на каждого клиента в количестве одной штуки, живет столько же, сколько соединение, его породившее. Парадигма не меняется при переходе к асинхронной модели, скорее даже напротив, становится очевиднее ее значение: сервер, выполняясь в одном (для простоты) потоке постоянно "бегает" по установившим соединение клиетам, меж делом устанавливает новые или завершает существующие соединения, и в каждом случае ему нужно "вспомнить", что уже было сделано для клиента, что еще нет - нужно уметь восстанавливать контекст работы. Класс client_session и является таким контекстом.

16: Даже для очень простых серверов актуальна проблема - не совсем очевидно, когда, как и кто будет уничтожать экземпляр класса client_session. Некоторая специфика работы классов Boost.Thread позволяет удобно использовать тот факт, что поток (thread), закончив всю полезную работу, уничтожается самостоятельно - а с уничтожением своего стека вызовет деструкторы всех объектов, созданных на нем. Маленькая трудность состоит в том, что экземпляр класса client_session намного удобнее создавать еще в родительском потоке, тогда, когда стека дочернего потока, как и самого потока, еще не существует. Перенос объекта со стека на стек - нетривиальная задача, адекватно решаемая только по схеме копирование - уничтожение оригинала, которая не всегда подходит (например, копирование сокета как уникального системного идентификатора (дескриптора) может не пройти гладко) потому проще поступить иначе - создать экземпляр класса client_session в динамической памяти, общей для всех потоков, а на стек дочернего потока скопировать умный указатель на созданный экземпляр. На стеке родительского потока этот указатель уничтожается, так что счетчик ссылок этого указателя меняется так: 1 - 2 - 1 (очевидно). Объект остается жить ровно столько, сколько живет поток - со смертью потока умирает умный указатель (см. выше), счетчик ссылок обнуляется и экземпляр client_session уничтожается. Это нам и нужно. typedef же крайне удобен и улучшает инкапсуляцию - клиентскому коду не нужно знать, какова реализация умного указателя (да и умен ли он вовсе :-))

18: Удобно защитить самого себя от ошибок: согласно описанной выше идеоме корректен лишь один способ создания экземпляра - на куче с обязательным оборачиванием в умный указатель. Потому все "альтернативные" способы, по возможности, следует запрещать (см. 40: - конструктор защищен и не может быть использован вне класса). Для простоты не запрещались копирующий конструктор и оператор присваивания, что стоит, вообще говоря, сделать (не обязательно "руками", к примеру в boost есть маленький класс-примесь noncopyable, достаточно от него отнаследоваться)

26: Сокет на момент создания экземпляра не ассоциирован ни с каким соединением - это должен сделать главный цикл сервера в методе accept - потому необходимо дать доступ к сокету.

28: Главный метод класса, осуществляющий всю работу с клиентом - код именно этого метода будет выполняться в отдельном потоке, стек именно этого метода - корневой стек дочернего потока. С завершением этого метода разворачиваются события, описанные в 16:

32: Сервер пятикратно отправляет клиенту актуальные дату и время с задержкой в секунду. Так не нужно, так удобно - можно успеть запустить одновременно несколько клиентов и убедиться, что сервер действительно обслуживает их одновременно.

34: Глобальная функция boost::asio::write осуществляет блокирующую запись в сокет всего содержимого переданного буфера (в отличие от методов сокетов вида "write_some", способных завершиться не закончив передачу всех данных). Концепция "буфера" нетривиальна, и следует посмотреть документацию, чтобы прояснить ее себе.

61: Все потенциально асинхронные сервисы в Boost.Asio создаются поверх экземпляра io_service. Подробности в документации Boost.Asio.

63: acceptor - всего лишь специализация сокета (прослушивающая), в данном случае он связывается (bind) с любым адресом IPv4 и портом 13 (порт сервиса даты и времени)

64: Режим работы большинства реальных серверов - бесконечный. Большая часть серверов запускается как демоны (daemon) в терминах Unix или службы - в терминах Windows. Останов хорошо спроектированного сервера выполняется подачей ему сигнала (в linux традиционно SIGUSR1), для которого существует специальный обработчик. Регистрация обработчика осуществляется системным вызовом, а управление ему впоследствии передается по аппаратному прерыванию ядром ОС, потому главный поток сервера действительно выйдет из этого якобы неразрывного цикла.

65: "Правильно" (см. 16:) создаем экземпляр client_session для будущего клиента.
64: Осуществляем блокирующее ожидание нового подключения в основном потоке на сокете заранее подготовленного контекста (см. 13:) подключения (потому сервер и синхронный - все операции в рамках одного конкретно взятого потока блокирующие)
65: Создаем новый поток, захватываем с помощью boost::bind умный указатель на client_session в функтор (теперь он "удерживает" счетчик ссылок от падения в ноль), попутно превращая метод start класса client_session в нульарную функцию, которую и требует конструктор boost::thread, запускаем в этом потоке таким полученную функцию, а сам поток отсоединяем (detach) от основного - теперь со смертью экземпляра thread (а она наступает незамедлительно) описываемый им поток продолжает жить самостоятельно, уже не имея в основном потоке никаких средств обращения к себе.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment