вторник, 30 апреля 2013 г.

Двунаправленный обмен в режиме датаграмм на Unix сокетах

Для взаимодействия с приложением, которое было запущенно в фоновом режиме, есть несколько вариантов. К примеру это именованные каналы, или очереди сообщений. Одним из часто применяемых способов является использование локальных сокетов, которые так же иногда называют UNIX сокетами. Это средство предоставляет возможность осуществлять двусторонний обмен данными между управляемым и управляющим приложениями. При этом управляемое приложение выступает в роли сервера, а управляющее — в роли клиента.

Меня заинтересовала возможность использовать данный тип в режиме датаграмм. Большая часть руководств по применению UNIX сокетов ориентирована на использование режима SOCK_STREAM, который подразумевает создание потока обмена данными и его поддержание в процессе работы. В отличие от этого режима, режим датаграм (SOCK_DGRAM) позволяет относительно простым способом обрабатывать входящее соединение на стороне сервера. Данный вопрос особенно актуален, если не требуется никаких дополнительных действий, подтверждающих прием сообщения, или, если такое подтверждение требуется для сравнительно небольшого количества сообщений.

В случае UNIX сокетов возникает потребность в определении двух имен для осуществления обмена через один сокет. Эта особенность поведения связана с тем, что каждый из сокетов закрепляется за определенным именем в файловой системе и для приема использует именно это имя. Попытка использовать одно и тоже имя для нескольких сокетов, ожидающих приема сообщения, приводит к рациональном отказу от этого действия со стороны операционной системы.


Схема взаимодействия клиент-сервер

Итак, по порядку: на стороне сервера открывается сокет. Затем сокет связывается с именем в файловой системе. Следует обратить внимание, что если такой файл уже существует, это делает невозможным выполнения команды связывания сокета — команды bind. Затем сервер переходит в режим ожидания. Так как по сути сокет — это то же, что файловый дескриптор, то и операции над ним позволены такие же, как над файловым дескриптором и нам достаточно выполнить read. Однако, так как разговор идет о двунаправленном обмене, то необходимо использовать recvfrom — функцию позволяющую получить информацию об отправителе, которую мы сможем использовать в дальнейшем при ответе на полученный запрос.

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/unistd.h>

int server_loop()
{
    int sockfd;
    struct sockaddr_un self, client;
    socklen_t len;
    int   exit=0, answer=0;
    char  message[MAXMESGSZ];

    
    sockfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        ErrorF("Can't open socket fd");
    }

    self.sun_family = AF_UNIX;
    strcpy(self.sun_path, srv_sock_path);
    

    if (bind(sockfd, (struct sockaddr*)&self, SUN_LEN(&self)))
    {
        ErrorF("Bind to socket %s failed!", self.sun_path);
    }

    while(!exit)
    {
        int rc;
        rc = recvfrom(sockfd, message, MAXMSGSZ, 0, 
                      (struct sockaddr*) &client, &len);
        if (rc < 0)
        {
            ErrorF("Failed to read socket");
        }

        // Здесь мы производим нашу логику по взаимодействию с
        // cообщением
        rc = do_some_logic(message, rc, &exit, &answer);
        
        if (answer)
        {
            rc = sendto(sockfd, message, rc, 0, 
                        (struct sockaddr*) &client, len);
            if (rc < 0)
            {
                ErrorF("Failed to send answer");
            }
        }
    }
    close(sockfd);
    return 0;
}

В коде сервера открывается дескриптор, который в дальнейшем и будет использоваться во всех обменах. Обратите внимание, что именно он, а не имя файла сокета, и является инструментом двунаправленного обмена.

Затем сокет связывается с именем файла. В момент связывания в файловой системе создается файл с таким именем. Хотя по этому пути и будет создан файл, но по факту данный файл является лишь адресом для сокета. Этот адрес должен быть уникальным для того, чтобы можно было получать по нему сообщения. Таким образом, использовать одно и то-же имя для клиента и сервера не получиться. Также следует отметить, что в случае если файл с запрошенным именем уже существует, то вызов bind для такого имени вызывает ошибку и связывание не происходит. Именно по этому рекомендуется осуществлять удаление файла сокета по имени перед его связыванием (bind).

При разработке нужно обратить внимание, что если имя сокета задано относительно, то и доступен это сокет будет лишь для приложений запущенных из того-же каталога. По крайней мере такой эффект я наблюдал на моей операционной системе (Debian 7).

Далее необходимо получить данные, пришедшие в сокет. В принципе, так как сокет является почти обычным файловым дескриптором, то чтение можно выполнить и при помощи вызова read. Однако такое решение не дает нам ни малейшего представления об отправителе. Для чтения из сокета при двунаправленном обменен применяется вызов recvfrom, первые три параметра аналогичны первым трем параметрам read, а далее начинается специфика, связанная с сокетами. Четвертый параметр работы с сокетами — флаг взаимодействия, пятый и шестой - данные об отправителе. В нашем примере эти данные заносятся в переменные client и len.

Ответ осуществляется вызовом sendto. Синтаксически эта функция подобна функции recvfrom, однако пятый и шестой параметры теперь не выходные, а входные данные функции.

int 
client_action(char *dgram, int dlen, 
              int need_answer, int (*parse_answer)(char*, int))
{
    int sockfd, len;
    struct sockaddr_un self, server;
    char message[MAXMESGSZ];
    char sockpath[UNIX_PATH_MAX];
    
    sprintf(sockpath,"%s.%d", server_sockpath, getpid());
    
    self.sun_family = AF_UNIX;
    strcpy(self.sun_path, sockpath);
    server.sun_family = AF_UNIX;
    strcpy(server.sun_path, server_sockpath);
    
    sockfd = socket(PF_UNIX, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        ErrorF("Can't open client socket");
    }
    
    // Присвоем своему сокету адрес
    if ( 0 > bind(sockfd, self, SUN_LEN(&self)))
    {
        ErrorF("Bind socket to clientname %s failed", sockpath);
    }

    if (0 > sendto(sockfd, dgram, dlen, 0, &server, SUN_LEN(&server)))
    {
        ErrorF("Send dgram to serber failed");
    }
    
    if (need_answer)
    {
        if( 0 > (len = recv(sockfd, message, MAXMESGSZ)))
        {
            ErrorF("Read from socket failed");
        }
        parse_answer(message, len);
    }
    close(sockfd);
    unlink(sockpath);
    return 0;
}

Теперь рассмотрим код клиента. Во многом он повторяет код сервера, за исключением небольшой детали - нас не интересует адрес отправителя ответа. Не скажу что это решение идеально. Существует значительный изъян в безопасности, так как всякий процесс может прислать нам ответ как наш сервер, но в случае протокола датаграмм сложно вообще говорить о безопасности.

Итак, мы открываем сокет и связываем его с именем. В качестве имени мы берем имя сокета сервера, добавляя к нему свой идентификатор процесса. В данном случае можно было с тем-же успехом воспользоваться mktemp для генерации уникального имении временного сокета.

Основное отличие в получении через recv, так как нас не интересует кто послал ответ.

О чем было забыто

Первое, о чем я забыл при написании примера — это то, что сам по себе файл сокета не будет уничтожен по завершении приложения. Это означаете, что нам требуется явным образом уничтожить файл сокета. В клиенте это сделано явно через вызов unlink. Для сервера все несколько сложнее. Дело в том, что сервер должен завершаться по какой-либо команде (впрочем это совершенно не обязательно) и по получении сигнала SIGTERM. Так что освобождение ресурсов, к которому относится и удаление более ненужного сокета, требуется сделать либо в обработчике прерывания, либо в каком-то общем для всех случаев механизме завершения. Причем удаление является более критичной операцией, чем закрытие дескриптора сокета.

Второе — это использование блокирующего чтения. Такое решение далеко не всегда верно, в особенности на стороне клиента, когда у нас есть некий таймаут на получение ответа. Такую временную задержку легко организовать с использованием функции poll.

Третье, о чем уже было сказано — это безопасность. Здесь безопасность обеспечивается лишь правами доступа на файл сокета и потому необходимо использовать различные комбинации chown/chmod, для обеспечения безопасности или обезопасить протокол управления, к примеру использованием сессионных токенов или шифрования.

P.S.

Этот материал является крайне поверхностным введение в UNIX сокеты и датаграммы.

С радостью приму любые замечания и предложения по поводу материала.

четверг, 1 марта 2012 г.

Подсветка синтаксиса в blogger с помощью SyntaxHighlighter 3

Хочу поделиться тем как реализовал поддержку подсветки синтаксиса в этом блоге.

У всех рекомендаций, найденных мной в сети, были недостатки - они требовали либо достаточно крупного редактирования шаблона, либо отдельного хостинга. К сожалению сервис blogger не особо щедр на место, по крайней мере мне найти такую возможность не удалось. Однако решение для тех кто ютится на чужом сервисе описано на самом сайте производителя в разделе описание хостинг версии. Для меня это решение оказалось удачным.

Перейдем к делу - открываем шаблон через редактор шаблонов и добавляем в конец секции head следующие строки:
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shThemeDefault.css' rel='stylesheet' type='text/css'/>
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shCore.css' rel='stylesheet' type='text/css'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shCore.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shAutoloader.js' type='text/javascript'/>
<script src='http://alexgorbatchev.com/pub/sh/current/scripts/shBrushCpp.js' type='text/javascript'/>
<link href='http://alexgorbatchev.com/pub/sh/current/styles/shThemeEmacs.css' rel='Stylesheet' type='text/css'/>
В данном случае из текущей версии Hightligtera подключаются файлы shAutoloader.js, shCore.css и shCore.js, а также подключается наиболее подходящий под оформление блога стиль оформления shThemeEmacs.css. Для каждого из требуемых синтаксисов необходимо добавить файл с поддерживающим его скриптом. В моем случае это shBrushCpp.js. Полный список входящих в комплект поставки кистей можно найти на страницы syntaxes сайта разработчика.

После подключения всех необходимых кистей и стилей нам необходимо произвести настройку на blogger. Для этого необходимо следом за подключением добавить небольшой скрипт следующего содержания:
<script language='javascript'>
 SyntaxHighlighter.config.bloggerMode = true;
 SyntaxHighlighter.all();
</script>

Первая строка этого скрипта настраивает SyntaxHighlighter на работу с сервисом blogger. Вторая отвечает за применение настроек ко всем элементам.

Подробнее о настройках можно опять-же почитать на сайте разработчика.

Теперь мы можем раскрашивать свой код. Для этого мы можем использовать тег pre приблизительно следующим образом:
<pre class='brush: cpp;'>
static int __init
proctest_init(void)
{
    proc_entry = create_proc_entry(proc_file_name, 0644, NULL);
    if (proc_entry != NULL)
    {
        return 0;
    }
    return -ENOMEM;
}
</pre>

В результате получаем следующий внешний вид:

static int __init
proctest_init(void)
{
    proc_entry = create_proc_entry(proc_file_name, 0644, NULL);
    if (proc_entry != NULL)
    {
        return 0;
    }
    return -ENOMEM;
}

Отдельно следует упомянуть о возможности подсвечивать определённые строки. Это делается с помощью изменения имени класса тега:
<pre class='brush: cpp; highlight: [1, 4]'>
static int __init
proctest_init(void)
{
    proc_entry = create_proc_entry(proc_file_name, 0644, NULL);
    if (proc_entry != NULL)
    {
        return 0;
    }
    return -ENOMEM;
}
</pre>
В результате получаем оформление кода в следующем виде:
static int __init
proctest_init(void)
{
    proc_entry = create_proc_entry(proc_file_name, 0644, NULL);
    if (proc_entry != NULL)
    {
        return 0;
    }
    return -ENOMEM;
}

И в качестве заключения хочу напомнить, что символы < и > необходимо заменять на &lt; и &gt; соответственно.

понедельник, 19 января 2009 г.

Первый ляп

Обидно начинать блог с такого сообщения, но что поделаешь.

Если кто то в жизни своей встретил сообщение
LIBC.lib(crt0dat.obj) : error LNK2005 _exit alredy defined in libcmt.lib(crt0dat.obj)
Знайте что выпытаетесь прилинковать к многопоточному приложению однопоточную библиотеку - решение простоле - пересобрать библиотеку (для VC6 с ключем /MT ) и попробовать снова.