Пример разработки простого многопоточного сетевого сервера

Часть 2. Полноценный разбор параметров командной строки

Comments

Серия контента:

Этот контент является частью # из серии # статей: Пример разработки простого многопоточного сетевого сервера

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Пример разработки простого многопоточного сетевого сервера

Следите за выходом новых статей этой серии.

В первой части цикла мы вкратце рассмотрели работу с параметрами командной строки для передачи в программу запроса на вывод справки. Описанный там вариант обработки этого запроса практически бесполезен, и для реальной жизни его нужно доработать. Если вам хочется потренироваться в разборе параметров командной строки, то вы можете сделать это самостоятельно, мы же задерживаться на этом не будем и воспользуемся готовым решением – семейством функций getopt(3).

Реализация

Традиционно, подробности можно посмотреть в руководстве (man 3 getopt). Допишем/перепишем наш код:

...
#include <getopt.h>

extern void printUsage(FILE *stream, int exitStatus);
struct globalParams {
    char confFile[_confFilePathLength]; /* путь до конфигурационного файла */
    int verbosity; /* уровень подробностей отладочных сообщений */
    char **modulesList; /* массив указателей на строки с именами модулей */
};

int main(int argc, void **argv) {
    char *error, *result;
    void *libHandle;
    int i = 0, mCount = 0, len = 0, next_option;
    float (*sum) (float, float);
    pid_t pid;
    struct globalParams gp;
    strncpy(gp.confFile, "main.conf\0", 10);
    gp.verbosity = 0;
    gp.modulesList = NULL;

    char* const short_options = "hc:v:m:";
    struct option long_options[] = {
        { "help",         0,  NULL,   'h'},
        { "config",       1,  NULL,   'c'},
        { "verbosity",    1,  NULL,   'v'},
        { "vmodules",     1,  NULL,   'm'},
        { NULL,           0,  NULL,   0 }
    };
    do {
        next_option = getopt_long(argc, (char**)argv, short_options, long_options, NULL);
        switch(next_option) {
            case 'h':
                printUsage(stdout, EXIT_SUCCESS);
            case 'c':
                len = strlen((char*)optarg);
                if (_confFilePathLength > len) {
                    memset(&gp.confFile, '\0', len + 1);
    	            strncpy(gp.confFile, optarg, len);
    	        }
                break;
            case 'v':
                if ((0 <= atoi(optarg)) && (9 >= atoi(optarg))) {
                    gp.verbosity = atoi(optarg);
                }
                break;
            case 'm':
                result = strtok(optarg, ",");
                do {
                    if (NULL != result) {
                        len = strlen(result);
                        if (_moduleNameLength >= len) {
                            if (NULL == gp.modulesList) {
                                gp.modulesList = (char**) calloc(1, sizeof(char*));
                            }
                            gp.modulesList[mCount] = (char*) calloc(1, len + 1);
                            strncpy(gp.modulesList[mCount], result, len);
                            i++; mCount++;
                            while (1) {
                                i++; result = strtok(NULL, ",");
                                gp.modulesList = (char**) realloc (
                                    (void*)gp.modulesList, 
                                    (mCount + 1) * sizeof(char*)
                                );
                                if (_modulesCount == (i - 1)) {
                                    gp.modulesList[mCount] = NULL;
                                    break;
                                }
                                else if (NULL == result) {
                                    gp.modulesList[mCount] = NULL;
                                    break;
                                }
                                else if (NULL != result) {
                                    len = strlen(result);
                                    if (_moduleNameLength >= len) {
                                        gp.modulesList[mCount] = (char*)calloc(1, len+1);
                                        strncpy(gp.modulesList[mCount], result, len);
                                        mCount++;
                                    }
                                }
                            }
                        }
                        else {
                            result = strtok(NULL, ",");
                        }
                    }
                    i++;
                } while ((NULL != result) && (i < _modulesCount - 1));
                break;
            case '?':
                printUsage(stdout, EXIT_SUCCESS);
            case -1 :
                break;
            default :
                exit(EXIT_SUCCESS);
        }

    } while(-1 != next_option);

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

Чтобы подчеркнуть отсутствие в примере тела printUsage(), она объявлена как extern.

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

#define _confFilePathLength 32
#define _moduleNameLength 32
#define _modulesCount 64

Они ограничивают длину пути до конфигурационного файла, длину имени модуля и количество имён модулей (всех, а не только распознанных программой) в строке параметров соответственно. Такие ограничители нужны нам для защиты от действий злоумышленников, так как в отсутствие ограничений и при наличии ошибок с нашей стороны у злоумышленников появится возможность влиять на работу нашей программы в своих интересах, например, через "традиционное" переполнение буфера или через очень длинное имя модуля.

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

В GNU-библиотеке языка C функция, отвечающая за порождение потока, позволяет передать ему только один параметр – указатель на что-либо типа void. За счёт операции приведения типов мы можем передать таким образом что угодно, но только в единственном экземпляре, поэтому все параметры сведены в одну структуру globalParams, указатель на которую мы и будем впоследствии передавать потоку.

Итак, после объявления и инициализации всех переменных мы переходим к объявлению и инициализации структур данных для параметров командной строки, которые будет понимать наша программа – кратких (односимвольных, строка short_options) и длинных (словесных, это содержимое структуры option). В строке с короткими параметрами одно двоеточие после буквы означает, что для обозначенного буквой параметра необходимо (обязательно) указать значение. Два двоеточия указывали бы на допустимость, но необязательность значения. То же назначение у второго поля структуры option: 0 обозначает отсутствие значения, 1 – его обязательность, 2 – допустимость, но необязательность. Поскольку параметров нам нужно несколько, мы объявляем и инициализируем массив структур option, по одной на каждый параметр. Дальше идёт собственно разбор переданных параметров – цикл выполнится столько раз, сколько параметров будет распознано. Переключение выполняется по символьным ("коротким") именам параметров, а при обнаружении параметра выполняется соответствующий блок кода.

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

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

Содержащие отдельные имена подстроки мы получаем при помощи функции strtok(), однако нам надо обработать несколько граничных ситуаций вроде превышения допустимой длины имени модуля и количества указанных имён – эти параметры заданы выше как _moduleNameLength и _modulesCount соответственно. При этом мы создаём программу "терпимой" – встреча слишком длинного имени модуля не отменяет поиск имён других модулей, хотя такая отмена упростила бы код. Это хороший подход, в идеале программа должна делать всё, что она в состоянии сделать в рамках поставленной задачи, а не только то, что посчитал для себя удобным реализовать её автор. Кроме того, программа должна обрабатывать как можно больше ошибок и уметь "откатываться" к последнему либо исходному стабильным состояниям (которые необходимо определять ещё на этапе проектирования). С обработкой ошибок у нас пока не всё хорошо, например, на текущем этапе ни одна операция выделения памяти не проверяется на успешность.

К сожалению, объём и цели статьи не позволяют описать все возможные варианты, однако я об этом упомянул, и если вас интересует разработка надёжных программ, то вам необходимо обо всём этом позаботиться в своих проектах.

После кода обработки параметров командной строки и перед вызовом fork() можно разместить код, выводящий полученные значения на экран, например такой:

    fprintf(stdout, "Работаем со следующими параметрами:\n");
    fprintf(stdout, "- путь до конфигурационного файла 
			(gp.confFile): [%s]\n", gp.confFile);
    fprintf(stdout, "- уровень подробностей (gp.verbosity): [%d]\n", gp.verbosity);
    i = 0;
    if (NULL != gp.modulesList) {
    	fprintf(stdout, "Распознанные имена модулей:\n");
        while (NULL != gp.modulesList[i]) {
            fprintf(stdout, "[%s]\n", gp.modulesList[i]);
            i++;
        }
        i--;
        while (0 <= i) {
            free(gp.modulesList[i]);
            i--;
        }
        free(gp.modulesList);
    }

В этом блоке для иллюстрации работы с динамически выделяемой памятью также присутствует код для освобождения этой памяти, выделенной при разборе строки со списком модулей (в массив указателей на строки gp.modulesList). Разумеется, в реальной работе этот код нужно будет вызывать в других местах, а не перед тем, как распознанные значения могут понадобиться. Повторюсь, этот блок приведён только как пример того, как можно посмотреть распознанное и как освободить память.

Теперь выполняем make для пересборки программы, запускаем её и смотрим на результат:

$ ./main -v 5 -m module1,modulemodulemodulemodulemodulemodule2,module3

Работаем со следующими параметрами:

  • путь до конфигурационного файла (gp.confFile): [main.conf]
  • уровень подробностей (gp.verbosity): [5]

Распознанные имена модулей:

[module1]
[module3]

РОДИТЕЛЬ: потомок порождён и родитель завершается.

В отношении имён модулей тут есть тонкий момент, связанный с безопасностью. Имена модулей вместе с путём до рабочего каталога программы понадобятся нам в будущем для загрузки этих самых модулей в виде файлов динамических библиотек (*.so). Подсовывая нашей программе имя модуля, содержащее спецсимволы (например, "..", "/"), можно заставить её открыть файл с тем же именем, но из другого каталога, например, домашнего каталога злоумышленника и выполнить содержащийся в файле код. Нам это не нужно, поэтому для исключения имен модулей, содержащих недопустимые символы, стоит добавить в программу какой-нибудь фильтр, например такой:

{
    char badSymbols[] = "/*?\'\"{}<>\0";
    int bsLen = strlen(badSymbols);
    int flag = 1;
    int j = 0;
    while (j < bsLen) {
        if (NULL != strchr(result, badSymbols[j])) {
            flag = 0;
            break;
        }
        j++;
    }
    if (flag) {
        if (NULL == gp.modulesList) {
            gp.modulesList = (char**) calloc(1, sizeof(char*));
            gp.modulesList[mCount] = (char*) calloc(1, len + 1);
        }
        strncpy(gp.modulesList[mCount], result, len);
        mCount++;
    }
}

Собственно проверка выполняется внутри цикла while(){} перебором символов из массива badSymbols[] и поиском каждого из них в очередном распознанном имени модуля. Переменная bsLen введена для того, чтобы не вычислять длину массива badSymbols[] при каждом выполнении цикла (хотя современные компиляторы, скорее всего, в состоянии распознать и обработать такую ситуацию, на мой взгляд, разумнее позаботиться обо всем самому).

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

Заключение

Рассмотренная выше обработка параметров командной строки является хорошим кандидатом на включение в "промышленные" программы. Тем не менее перед реальным использованием её нужно дополнить проверкой значений, возвращаемых функциями, работающими с динамически выделяемой памятью, и продумать поведение программы в тех случаях, когда в выделении памяти будет отказано. Для поиска же своих ошибок, допущенных при работе с "динамической" памятью, существует ряд инструментов разного уровня способностей, из которых я предпочитаю valgrind.

В следующей статье цикла речь пойдет о работе с сетевыми запросами к нашему серверу.


Ресурсы для скачивания


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=467912
ArticleTitle=Пример разработки простого многопоточного сетевого сервера: Часть 2. Полноценный разбор параметров командной строки
publish-date=02112010