29 января 2011

Двадцать бит от Джесси Фармера. Ерланг: Базовый TCP сервер

Данный документ является переводом статьи Джесси Фармера: Базовый TCP сервер.

В последних моих статьях об Ерланге мы рассмотрели основы сетевого программирования с использованием модуля gen_tcp и Ерланг/OTP (Открытая телекоммуникационная платформа) gen_server. Давайте объединим эти два понятия. Прим. переводчика: ссылки на эти статьи не работали.
В умах большинства людей "сервер" означает сетевой сервер, но Ерланг использует этот термин наиболее абстрактно. gen_server действительно сервер, который использует механизм передачи сообщений Ерланга в качестве протокола. Мы можем привить TCP сервер к поведению gen_server, но для этого нужно немного потрудиться.

Структура Сетевого Сервера

Большинство серверов сети имеют одинаковую архитектуру. Сначала они создают сокет, который прослушивает некоторый порт на некотором интерфейсе. Потом они принимают соединения, и делают это в цикле, пока их кто-нибудь не завершит.

Чтобы увидеть это в действии. Напомню простой эхо-сервер из моей статьи о программировании для сети.


-module(echo).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).
-export([listen/1]).

-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).


% Call echo:listen(Port) to start the service.
% Вызовите echo:listen(Port) для запуска процесса.
listen(Port) ->
    {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
    accept(LSocket).

% Wait for incoming connections and spawn the echo loop when we get one.
% Ожидание входящего содеинения и запуск цикла как отдельного процесса при соединении.
accept(LSocket) ->
    {ok, Socket} = gen_tcp:accept(LSocket),
    spawn(fun() -> loop(Socket) end),
    accept(LSocket).

% Echo back whatever data we receive on Socket.
% Отправляем обратно все то, что получили из сокета
loop(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            gen_tcp:send(Socket, Data),
            loop(Socket);
        {error, closed} ->
            ok
    end.


Как Вы заметили, функция listen создает сокет и немедленно вызывает функцию accept. Последняя ждет входящего подключения, порождает новый процесс с «рабочей» функцией (loop), которая и делает всю работу по приему/передаче, и далее вновь ожидает входящего подключения.

В этом коде родительский процесс является владельцем как слушающего сокета, так и принимающего подключения цикла. Вы узнаете, что не работает не так хорошо, когда мы попытаемся интегрировать accept/listen цикл с gen_server.

Абстрагирование Сетевого Сервера

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

-module(my_server).
start(Port) ->
    connection_handler:start(my_server, Port, business_logic).

business_logic(Socket) ->
% Read data from the network socket and do our thang!
% Чтение данных из сетевого сокета и их обработка

Давайте пойдем дальше и сделаем именно это.

Реализация Обычного Сетевого Сервера

Проблема при реализации сетевого сервера с помощью поведения gen_server заключается в том, что функция gen_tcp:accept является блокирующей. Например, если мы вызовем ее в инициализационной процедуре, то весь механизм поведения gen_server будет заблокирован пока хоть один клиент не подключиться.

Есть два способа обойти эту проблему. Один предполагает низкоуровневый механизм соединения, который поддерживает неблокирующий (или асинхронный) вызов accept. Существует целое семейство функций, в первую очередь gen_tcp:controlling_process, которые помогают Вам управлять процессом: «Кто и какие сообщения получает при подключении клиента».

Более простое, и на мой взгляд, изящное решение — это использовать один процесс, которому принадлежит сокет. Этот процесс делает две вещи: порождает новых «слушателей» и ожидает сообщения «соединение установлено». Когда он получает сообщение, он знает, что нужно создать нового «слушателя».
Прим. переводчика: данный механизм применен в postgresql сервере, для процессного (не-потокового) распараллеливания подключений. Процессным распараллеливанием, но уже пула клиентских сокетов, занимаются для google chrome.

«Слушатель» волен применять блокирующую gen_tcp:accept, поскольку он работает в своем собственном процессе. Когда он принимает соединение, он отсылает асинхронное сообщений родительскому процессе и тотчас вызывает функцию бизнес-логики.

Вот код. Где уместно я его прокомментировал, так что надеюсь, что анализирование не составит труда.


-module(socket_server).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).
-behavior(gen_server).

-export([init/1, code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-export([accept_loop/1]).
-export([start/3]).

-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).

-record(server_state, {
        port,
        loop,
        ip=any,
        lsocket=null}).

start(Name, Port, Loop) ->
    State = #server_state{port = Port, loop = Loop},
    gen_server:start_link({local, Name}, ?MODULE, State, []).

init(State = #server_state{port=Port}) ->
    case gen_tcp:listen(Port, ?TCP_OPTIONS) of
        {ok, LSocket} ->
            NewState = State#server_state{lsocket = LSocket},
            {ok, accept(NewState)};
        {error, Reason} ->
            {stop, Reason}
    end.

handle_cast({accepted, _Pid}, State=#server_state{}) ->
    {noreply, accept(State)}.

accept_loop({Server, LSocket, {M, F}}) ->
    {ok, Socket} = gen_tcp:accept(LSocket),
    % Let the server spawn a new process and replace this loop
    % with the echo loop, to avoid blocking
    % Просим главный процесс создать нового "слушателя" и вызываем цикл обработки данных,
    % чтобы избежать блокировки  
    gen_server:cast(Server, {accepted, self()}),
    M:F(Socket).
    
% To be more robust we should be using spawn_link and trapping exits
% Для большей безопасности мы должны использовать spawn_link и обрабатывать ошибки
accept(State = #server_state{lsocket=LSocket, loop = Loop}) ->
    proc_lib:spawn(?MODULE, accept_loop, [{self(), LSocket, Loop}]),
    State.

% These are just here to suppress warnings.
% Это здесь только для того чтобы скрыть предупреждения
handle_call(_Msg, _Caller, State) -> {noreply, State}.
handle_info(_Msg, Library) -> {noreply, Library}.
terminate(_Reason, _Library) -> ok.
code_change(_OldVersion, Library, _Extra) -> {ok, Library}.


Мы используем gen_server:cast для передачи асинхронных сообщений в главный процесс. Когда главный процесс получает сообщение «соединение установлено» он создает нового «слушателя».

На данный момент этот сервер не очень надежный, потому что если текущий «слушатель» перестанет работать по какой-либо причине, сервер перестанет принимать входящие подключения. Чтобы сделать его более OTP'нутым мы должны обрабатывать ошибки «слушателя», создавая нового в случае поломки предыдущего.

«Базовый» Эхо-Сервер
Эхо-Сервер является простейшим для написания сервером, так что давайте сделаем это с помощью нашего нового абстрактного сокет-сервера.

-module(echo_server).
-author(‘Jesse E.I. Farmer <jesse@20bits.com>’).

-export([start/0, loop/1]).

% echo_server specific code
% Специфичный код эхо-сервера
start() ->
    socket_server:start(?MODULE, 7000, {?MODULE, loop}).
loop(Socket) ->
    case gen_tcp:recv(Socket, 0) of
        {ok, Data} ->
            gen_tcp:send(Socket, Data),
            loop(Socket);
        {error, closed} ->
            ok
    end.


Как Вы уже заметили, «сервер» не содержит ничего лишнего, кроме бизнес-логики. Работа с соединениями была обобщена и реализована в socket_server'е. Цикл в нашем базовом сервере фактически также совпадает с циклом в нашей оригинальном Эхо-Сервере.

Надеюсь, вы смогли понять все то, что я сделал. Я, наконец, чувствую, что я начинаю понимать Ерланг.

Кроме того, не стесняйтесь оставлять комментарии, особенно если у вас есть какие-то мысли о том, как я могу улучшить свой код. Здоровья Вам.

Статьи по теме
Erlang: A Generic Server Tutorial
Learning Erlang
Erlang: An Introduction to Records
An EventMachine Tutorial
Network Programming in Erlang
Создание неблокирующего TCP сервера с использованием принципов OTP 

23 января 2011

Let it fail

Главная мантра при написании кода на Erlang — «Let it fail» («Пусть падает»).
Как гром среди ясного неба. А если быть точнее: как солнце среди миллиардов туч. Неужели кто-то думает так же как и я? Неужели не один я вижу конструкцию try/catch неудобной?

Все очень просто: разработчики языка Erlang взяли на себя ответсвенность за наши ошибки.
Вы чувствуете? Гора с плеч спала. Огромная такая гора вопросов:
Показывать или не показывать message box?
На каком уровне его показывать (вы же понимаете что вызывающая функция тоже может ловить ваши ошибки)?
А какие вообще исключения ловить, а что делать в catch(...)? А может стоит попробовать восстановить работоспособность, а что нужно тогда реинициализировать?
В серверном приложении пороще конечно, все просто пишем в лог. А если забыли какую-то функцию  обработать?

А тут ра-а-а-з, и всё. Тишина, спокойствие. Пишешь программу, как будто она будет жить вечно, и так и происходит. Принцип прост: программа аварийно завершилась, erlang ее перезапустил.

Вот ещё, нет глобальных переменных. Если функция завершилась неудачно, это значит неправильные параметры. И всё. Не надо искать какие-то там еще объекты, к которым обращается функция. Она ни к чему не обращается. Только к параметрам.

19 января 2011

Common Lisp. Начало

Не очень хочеться делать контрольные касающиеся экономики, бухгалтерии и прочей управленческой деятельности. Видимо будут хвосты. Но зато у меня есть линукс (пусть даже в виде ubuntu), и теперь у меня есть желание дотронуться до самых святых, common lisp'a.

Инсталляция
sudo apt-get install -y emacs slime sbcl cl-asdf
Настраиваем emacs. Для этого необходимо самим emacs'ом открыть файл настроек по умолчанию:
emacs $HOME/.emacs
Все, что следует после знака ; "точка с запятой" считается комментарием. Вы чувствуете где бы вы могли обойтись без двух наклонных черт. Нет не в url.
В конец файла добавьте следующие строки:
;; Set up the Common Lisp environment
;; Настройка Common Lisp окружения
(add-to-list 'load-path "/usr/share/common-lisp/source/slime/")
(setq inferior-lisp-program "/usr/bin/sbcl")
(require 'slime)
(slime-setup)
Поздравляю это первый hello world. Только он звучит по-другому. Вы с помощью emacs диалекта lisp рассказали, что нужно делать текстовому редактору когда он запускается. Первое слово за открывающей скобой интерпретируется как команда/функция, остальные элементы разделенные пробелом - параметры. Первые две строки - установка переменных. Третья строка загрузка пакета. Четвертая строка вызов функции из загруженного пакета. Я могу быть немного неточным, зато объяснение не сложное.
Вам необходим sbcl. Однако Вы его уже установили с помощью "коровьей суперсилы".

Дополнительная настройка текстового редактора
Многое написано о текстовом редакторе emacs. Прочитайте о нем поподробнее.... и через, хм... две недели возвращайтесь к данной заметке. Или пока не задумывайтесь.
;; Text and the such
;; Use colors to highlight commands, etc.
(global-font-lock-mode t) 
;; Disable the welcome message
(setq inhibit-startup-message t)
;; Format the title-bar to always include the buffer name
(setq frame-title-format "emacs - %b")
;; Display time
(display-time)
;; Make the mouse wheel scroll Emacs
(mouse-wheel-mode t)
;; Always end a file with a newline
(setq require-final-newline t)
;; Stop emacs from arbitrarily adding lines to the end of a file when the
;; cursor is moved past the end of it:
(setq next-line-add-newlines nil)
;; Flash instead of that annoying bell
(setq visible-bell t)
;; Remove icons toolbar
(if (> emacs-major-version 20)
(tool-bar-mode -1))
;; Use y or n instead of yes or not
(fset 'yes-or-no-p 'y-or-n-p) 

Теперь нажмите ctrl-x-s. Файл сохранится. Закройте emacs.

Запуск
Запустите emacs. Нажмите alt-x, напишите slime в появившемся мини-буфере. Нажмите enter.
Вы получите строку-приглашение для ввода команд
CL-USER>
В действительности был запущен процесс sbcl и потоки ввода/вывода перенаправлены прямо в буфер emacs.

Первые шаги
Введите 10, нажмите ввод.
CL-USER>10
10
CL-USER>

Ну вот первое программирование в стиле REPL (чтение, вычисление, вывод, и снова). Интерпретатор прочел 10. Вычислил 10. Получил 10, вывел его. И снова ждет вашей команды. Кто-бы мог подумать три поколения назад, что будет устройство целиком и полностью преданное Вам.
Давайте что-нибудь посложнее.

CL-USER> (+ 5 8)
13
CL-USER>

Ну вот опять. Здесь правда немного сложнее. Вы ввели список. lisp список обработал, т.е. вычислил. Вычисление происходит по правилу: первой элемент списка - команда, остальные разделенные пробелами - параметры. Хм, кажется это уже где-то было. Ах да, emacs конфигурируется подобным образом. В данном случае команда + "плюс". Параметров два: 5 и 8. Результат команды число 13. Попробуйте сложить большее количество чисел.
- А теперь,- спросите Вы,- где же мой любимый char[]/std::string/QString/gstring/что-то_там_из_winapi,
CL-USER> "Здраствуй, мир"
"Здраствуй, мир"
CL-USER>

Ничего у Вас не должно получиться, потому что slime ничего не знает о русском языке. Для того, чтобы исправить ситуацию, откройте файл настроек .emacs. Нажмите ctrl-x-f, затем введите .emacs и нажмите ввод. В конец файла добавьте две строки:
(set-language-environment 'utf-8)
(setq slime-net-coding-system 'utf-8-unix)

Перезапустите emacs. Опять введите строку "Здравствуй, мир". Как и с числом произошло вычисление объекта. Все просто: строка вычисляется в строку.
Теперь давайте вызовем какую-нибудь функцию. Какая там в си самая популярная? Наверно, printf. Итак пробуем:
CL-USER>(format t "Здраствуй, мир")
"Здраствуй, мир"
NIL
CL-USER>
Произошла буква E из REPL. Функция format вывела строку в stdout. А далее вернула объект NIL. И буква P из REPL вывела нам результат выполнение format, т.е. NIL.

На этом пока все.

Список использованных источников
How to set up Emacs + SLIME + SBCL under GNU/Linux
Русский перевод Practical Common Lisp

16 января 2011

QSqlDatabase pattern

Не храните указатель на объект данного типа. Если вы в каком-то месте работаете с БД, просто получите объект с помощью статического метода QSqlDatabase::database, поиспользуйте его и пускай он удалиться в закрывающей фигурной скобке. Наблюдаю код, в котором QSqlDatabase отнаследован, а потом еще и передаётся через QSharedPointer между различными объектами.

Управление памятью в Qt.

Сначала я думал написать целый анализ на данную тему, и даже написал, но потом понял, что в этом нет смысла, все и так все знают. Лучше просто дам пару советов.

Совет №1.
Value-based, точнее сказать копируемые объекты, такие как QString, QSqlDatabase создавайте в стеке. Не используйте указатели для работы с этими объектами.

Совет №2.
QObject-based объекты создавайте только в куче и только с указанием родителя. К этому принципу также относяться QStandardItem, QGraphicsItem.
Исключения: модальные диалоги лучше создавать в стеке и с родителем. Первое избавит Вас от ненужной строки delete dialog, второе условие расположит диалог посередине родителя.

Совет №3 следует из двух предыдущих.
У вас нет необходимости удалять объекты, 1-й тип объектов удаляется средствами c++, 2-й тип объектов удаляется средствами Qt QObject-иерархии.

Совет №4.
Если вы используете указатель на объект в области видимости функции или в качестве члена класса, оберните его с помощью QScopedPointer. Это избавит Вас от использования оператора delete.

P.S. А вообще вот читаю сейчас о лиспе и понимаю, что эти советы ну просто мысли трех-летнего ребенка, в других инструментах можно просто не задумываться о распределении памяти.

10 января 2011

Прямо в точку

Необходимость наличия db разработчика в команде при любом проекте. Просто и понятно. И я даже не почуствовал целью поста сравнение sql и no-sql. Основная и ужасающе правильная главная мысль - это шаблон проектирования проектирования и разработки приложения. Повторяющееся слово "проектирования" не ошибка. Философия SQL или причина популярности NoSQL.

08 января 2011

Монтирование samba ресурсов в linux

Монтировать smb ресурсы можно через утилиту smbmount, которая входит в состав пакета smbfs, установим его

sudo apt-get install smbfs

Пример команды монтирования

sudo smbmount //srv-windows/base/project /home/user/this -o rw,iocharset=utf8,username="domain\user",pass=mypassword,uid=usernamelinux
username - имя домена и пользователя
pass - пароль
uid - имя пользователя в linux, он будет владельцем монтированного ресурса
Источник: сообщение

06 января 2011

Рекурсивно перечислить все исходные файлы проекта

Когда надо в CMakeLists.txt весь список файлов с исходными кодами проекта можно воспользоваться следующей командой:

find -type f -iname "*.cpp"

01 января 2011

Асинхронное соединение с postgresql

Механизм неблокирующего создания соединения бывает полезен при создании списка подключений, а также при медленном соединении.
To begin a nonblocking connection request, call conn = PQconnectStart("connection_info_string"). If conn is null, then libpq has been unable to allocate a new PGconn structure. Otherwise, a valid PGconn pointer is returned (though not yet representing a valid connection to the database). On return from PQconnectStart, call status = PQstatus(conn). If status equals CONNECTION_BAD, PQconnectStart has failed. If PQconnectStart succeeds, the next stage is to poll libpq so that it can proceed with the connection sequence. Use PQsocket(conn) to obtain the descriptor of the socket underlying the database connection. Loop thus: If PQconnectPoll(conn) last returned PGRES_POLLING_READING, wait until the socket is ready to read (as indicated by select(), poll(), or similar system function). Then call PQconnectPoll(conn) again. Conversely, if PQconnectPoll(conn) last returned PGRES_POLLING_WRITING, wait until the socket is ready to write, then call PQconnectPoll(conn) again. If you have yet to call PQconnectPoll, i.e., just after the call to PQconnectStart, behave as if it last returned PGRES_POLLING_WRITING. Continue this loop until PQconnectPoll(conn) returns PGRES_POLLING_FAILED, indicating the connection procedure has failed, or PGRES_POLLING_OK, indicating the connection has been successfully made.
Вольный и неточный перевод:
Для создания неблокирующего запроса на подключение вызовите PQconnectStart("conn_info-string"). Если объект соединения нулевой, значит libpq не смогла выделить память для этого объекта. Иначе объект соединения корректен (однако само подключение к базе еще некорректно). После вызова PQconnectStart, узнайте статус с помощью функции PQstatus(conn). Если полученное значение CONNECTION_BAD, значит подключение к базе данных не удалось. Иначе необходимо подождать цикл подключения. Прежде всего получите дескриптор сокета с помощью функции PQsocket. Далее выполняйте в цикле:
Если PQconenctPoll(conn) вернула PGRES_POLLING_READING, подождите пока сокет будет готов к приему данных (используйте для этого фукнцию select(), poll(), или другую подходящую).  
Если PQconnectPoll(conn) вернула PGRES_POLLING_WRITING, подождите пока сокет будет готов к передаче данных. Далее снова вызовите PQconnectPoll(conn).
Повторяйте данные действия пока PQconnectPoll(conn) не вернет одно из двух значений: PGRES_POLLING_FAILED или PGRES_POLLING_OK. Значения отображают неуспешность или успешность установления соединения соответсвенно.
Сразу после вызова PQconnectStart можете без вызова PQconnectPoll считать что сокет подготавливается к записи (PGRES_POLLING_WRITING).
Код на языке python.

#
#    Waiting while libpq process connect
#
def wait_connect(conn):
    while 1:
        status = conn.status
        if status == CONNECTION_STARTED:
            print >> sys.stderr, 'Connecting...'
        elif status == CONNECTION_MADE:
            print >> sys.stderr, 'Connected to server...'
        elif status == CONNECTION_AWAITING_RESPONSE:
            print >> sys.stderr, 'Awaiting responce...'
        elif status == CONNECTION_AUTH_OK:
            print >> sys.stderr, 'Authentificated...'
        elif status == CONNECTION_SSL_STARTUP:
            print >> sys.stderr, 'SSL startup...'
        elif status == CONNECTION_SETENV:
            print >> sys.stderr, 'Negotiating environment-driven parameter settings.'
        else:
            print >> sys.stderr, 'Connecting error'
            break

        state = conn.poll()
        if state == PGRES_POLLING_OK:
            print >> sys.stderr, 'PGRES_POLLING_OK'
            break
        elif state == PGRES_POLLING_WRITING:
            print >> sys.stderr, 'PGRES_POLLING_WRITING'
            select.select([], [conn.socket], [])
        elif state == PGRES_POLLING_READING:
            print >> sys.stderr, 'PGRES_POLLING_READING'
            select.select([conn.socket], [], [])
        elif state == PGRES_POLLING_FAILED:
            print >> sys.stderr, 'PGRES_POLLING_FAILED'
            break
        else:
            print >> sys.stderr, state
            break
conn = PGConnection('dbname=postgres', 1)
if conn.status == CONNECTION_BAD:
    print >> sys.stderr, 'Connection to database failed: %s' % conn.error_message
else:
    wait_connect(conn)
    if conn.status != CONNECTION_BAD:
        print >> sys.stderr, 'Connection created'