LINUX.ORG.RU

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

 ,


3

1

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

>>> Подробности

★★★

Проверено: Shaman007 ()
Последнее исправление: anonymous_incognito (всего исправлений: 1)

Ответ на: комментарий от pimiento

ну не 10, всего лишь 2 :) Ничего особо нового, но напомнить полезно, мне кажется ...

sur02111976
()
Ответ на: комментарий от pimiento

просто разработка простого многопоточного сервера с поддержкой сессий у специалиста уровня Александра Андреева занимает 2 года.

heisenberg ★★
()

Ну просто фиерическая статья. Содержит столько «полезной» информации что даже студент наверно выдаст «на гора» побольше.

TheMixa ★★★
()
Ответ на: комментарий от pimiento

>Лет десять назад IBM_dW разве уже не писал об этом?

Так можно ж отметить!

slackwarrior ★★★★★
()
Ответ на: комментарий от TheMixa

>студент наверно выдаст «на гора» побольше.

Не выдаст. Мне дали в помощнички вчерашнего студиоза - ничего абсолютно доверить нельзя.

Deleted
()

Статью не читал, но осуждаю. По мотивам предыдущей части. Уверен, что выдана куча банальностей. Я за бан IBM_dW ;)

Casus ★★★★★
()
Ответ на: комментарий от TheMixa

>Ну просто фиерическая статья. Содержит столько «полезной» информации что даже студент наверно выдаст «на гора» побольше.
Зато очень полезный цикл статей для тех, кто ещё не продвинутый студент, а только начинающий.

fractaler ★★★★★
()

Прочитав заголовок, почему-то думал, что будет про Erlang.

naryl ★★★★★
()

>Но в нашей задаче — организации надежного и безопасного взаимодействия клиента и сервера — есть нюансы, которые надо учитывать.

Не читал. Статья про эрланг?

yaws
()
Ответ на: комментарий от Casus

> Разработка окружена! Родина в опастносте!

IBM всех съест.

bbk123 ★★★★★
()

Я джва года ждал такую статью про разработку простого многопоточного сетевого сервера!

ei-grad ★★★★★
()

> Собственно, такой алгоритм и является планировщиком заданий (scheduler – «шедулер»).

Да и вообще статья - полный шедулер, ага.

Из первой части «цикла статей»:

/* ТОТ САМЫЙ ПОЧТИ БЕСКОНЕЧНЫЙ ЦИКЛ */

И тот самый Мюнхгаузен в авторах.

northerner ★★★
()

Не нужно. Уже есть Erlang.

anonymous
()
Ответ на: комментарий от anonymous

сделаю на питоне c твистедом за 5-10 минут.

включая unit-тесты

Divius ★★
()

Мне кажется или каждая новая статья ibm-dw действительно становится посмешищем на ЛОРе?

anonymous_ultimate
()

Обратил внимание на то, что в коде «демонизации» вызывается только fork() и закрываются 3 файловых дескриптора. Но не вызывается функция setsid() для процесса-потомка, то есть, фактической отвязки процесса от управляющего терминала, как было обещано, не происходит.

Процесс-потомок просто становится осиротевшим, но не демоном. Терминал остается при нем.

Это что, новая мода в написании сверхнадежных демонов?

anonymous
()
Ответ на: комментарий от anonymous

> закрываются 3 файловых дескриптора

Кстати, этого делать не следует. Заменять их на /dev/null (с помощью dup2) - разумно. А вот закрывать - нет. Дело в том, что если их закрыть, то потом на их месте наш демон или используемые им библиотечные функции могут открыть что-то еще (файлы, сокеты и т.п.) В то же время, некоторые библиотечные функции могут выдавать сообщения об ошибках («Assertion failed ...», «*** glibc detected *** double free or corruption ...» и т.п.) в stderr или stdout. Если на соответствующих fd открыто что-то новое, то эти сообщения «промахнутся» (могут уйти подключившемуся по TCP клиенту, могут попасть в какой-нибудь файл вроде БД SQLite, что-то там затерев, и т.п.)

solardiz
()
Ответ на: комментарий от solardiz

Чувствую, что не зря даже читать статью не стал. Скажите мне, родитель там дохнет сразу после форка или ждёт, пока потомок setsid сделает? Ах, блин, и не делает setsid даже... Это ваще абзац, что за демонизация?

Casus ★★★★★
()
Ответ на: комментарий от Casus

> Скажите мне, родитель там дохнет сразу после форка ...

Раз вопрос задан мне, отвечаю: сразу.

Но я этот цикл статей целиком не читал - незачем - обратил внимание лишь на «Часть 6» (там, где про пароли), которую считаю вредной/опасной - поэтому и частично прокомментировал ее (там). Одно дело просто грязный/недоделанный/заумный код, а другое - когда частично ошибочные советы и такой код подаются в контексте «безопасности».

solardiz
()
Ответ на: комментарий от solardiz

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

Casus ★★★★★
()
Ответ на: комментарий от Casus

> Статью не читал, но осуждаю.

Лучше так все же не делать. Я вот прочел и стал публично осуждать лишь «Часть 6», которая достаточно самостоятельная, а не весь цикл в целом. Про остальные «Части» (за исключением отдельных прочтенных мест из «Части 1») я могу лишь подозревать о недостаточном качестве по аналогии с прочтенной «Частью 6», но прям так осуждать их все целиком не читая не стану.

(Понимаю, что цитата «не читал, но осуждаю» подразумевает иронию и ее не следует воспринимать буквально, но все же решил развить тему серьезно.)

Уверен, что выдана куча банальностей.

«Куча банальностей» тоже может быть нужна - например, «начинающему студенту», как написал здесь fractaler. Если эти банальности подаются хорошим специалистом, умеющим также и понятно подавать материал, и статье уделено должное время, то такие статьи полезны. (Нужны ли они конкретно на LOR - отдельный вопрос.) Здесь я пишу о статьях на такие темы в целом, не конкретно о данном цикле статей. При хорошем качестве подачи «банальностей» - почему бы и нет, зачем осуждать лишь за «банальности»?

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

solardiz
()
Ответ на: комментарий от Casus

> solardiz, ты подбиваешь меня на прочитывание этой статьи вместо просмотра ужастиков ;)

И не думал. Лучше посмотреть The Nightmare Before Christmas - вот это качество, включая русскую озвучку - хотя уже чуть-чуть поздновато, да и offtopic. ;-)

Лан, пойду заценю твои камменты штоли ;)

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

Там еще много недостатков, на которые я не указал. Боюсь, что это лишь отвлечет от основного «ляпа», который автору надо осознать сначала (про salt'ы). Дискуссия про str* уже отвлекла - вероятно, ее следовало отложить на потом. Не говоря уже о том, что время жалко.

solardiz
()
Ответ на: комментарий от solardiz

> Лучше так все же не делать. Я вот прочел и стал публично осуждать ...

SolarDiz, я прочёл камменты. Автор явно молод, чтобы так задираться ;)

«Куча банальностей» тоже может быть нужна - например, «начинающему студенту»

У. Р. Стивенс «UNIX: Разработка сетевых приложений», «UNIX: Взаимодействие процессов», доступны в любом книжном магазине, на странице автора все примеры кода есть. Писать для студентов надо хотя бы после прочтения этих книжек. Реальной «изюминкой» статьи «Часть 1» мог бы быть код демонизации, учитывающий, что родитель может отработать раньше, чем потомок успеет сделать setsid. Классический fork() && exit(); setsid(); это не учитывает совсем. Но автор, хоть и намеревался, но совсем забыл даже setsid сделать. Подозреваю, что там весь уровень статьи такой. Это очень печально, поскольку претендует на публикацию, которую прочитают множество ещё менее грамотных людей, который потом скопируют оттуда куски кода.

Casus ★★★★★
()
Ответ на: комментарий от Casus

> Писать для студентов надо хотя бы после прочтения этих книжек.

Согласен. Или других хороших книжек и/или примеров кода (хотя признаю, даже из хорошего кода не все потенциальные проблемы видны).

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

Да, именно это побудило меня комментировать «Часть 6».

solardiz
()
Ответ на: комментарий от Casus

> У. Р. Стивенс «UNIX: Разработка сетевых приложений», «UNIX: Взаимодействие процессов», доступны в любом книжном магазине, на странице автора все примеры кода есть.

Сейчас глянул. Там в unpv22e/lib/daemon_init.c есть та же проблема с закрытием fd 0-2, что и у «нашего» автора. Вернее, там циклом закрываются fd с 0 по MAXFD (64). А надо - начиная с 3, либо этого вообще не делать, либо делать иначе (тот же dup2 на 0-2). Вероятно, этот код просто был написан до того как проблема с fd 0-2 стала обсуждаться в конце 1990-х в контексте безопасности (возможность закрыть fd 0-2 до запуска SUID/SGID-программ, что иногда приводило к их некорректному поведению), что уже несложно было распространить и на демоны. Дата на файле - Oct 2, 1997 - и исправить его, выпустить новое издание Стивенс уже не может... (Хорошо, что веб-сайт еще поддерживают.)

Так что конкретно от этой ошибки лишь чтение, увы, уже слегка устаревающих книг не помогло бы. Нужны еще и более свежие знания, опыт (вероятно, у автора какой-то есть, но такая проблема может и не проявиться), peer review.

А еще в unpv22e/lib/daemon_init.c делается umask(0). Ой.

(Если кому интересно: проблему с fd 0-2 для SUID/SGID с тех пор «решили» в glibc (стартап-код сам проверяет не закрыт ли один из этих fd и сам их открывает до начала серьезной работы, чтобы они не достались под что-то важное). До того я держал аналогичный workaround (авторства Pavel Kankovsky) в -ow патчах к ядру Linux 2.0 и 2.2. ...А если кто-то сейчас соберет SUID/SGID-программу без glibc (например, напишет ее на ассемблере), он получит эту проблему (потенциально - уязвимость) снова. Так что workaround в ядре по-моему был правильнее.)

solardiz
()
Ответ на: комментарий от solardiz

> Вероятно, этот код просто был написан до того как проблема с fd 0-2 стала обсуждаться в конце 1990-х

Очень даже вероятно. Я сильно подозреваю, что когда-от давно не пользовались glib и прочими библиотеками, которые могли что-то в fd 0-2 писать. Навскидку я помню только glib, которая может в stderr накакать, хотя, наверняка таких много. Направление fd 0-2 >/dev/null описано много где, конкретно у Стивенса я просто не стал сейчас проверять, в моём шаблоне демонизации есть безусловно. Вообще, когда я начинал писать своих демонов, сам код демонизации мне в итоге нигде не понравился. В частности, я довольно оперативно нарвался на проблему, что не запускается демон, когда setsid в потомке просто не успевает выполниться. Редко, но такое бывает, в литературе такие случаи ни разу не видел. Следующая засада была в генераторе псевдослучайных чисел, он при форке дублируется и во всех потомках имеет одинаковое состояние, что часто очень плохо. Ещё интересная тема маскировки сигналов. Умереть по SIGPIPE вместо корректной обработки тоже «интересно».

Casus ★★★★★
()
Ответ на: комментарий от Casus

> Я сильно подозреваю, что когда-от давно не пользовались glib и прочими библиотеками, которые могли что-то в fd 0-2 писать.

assert() существует очень давно. Возможно, когда-то его использования специально избегали в библиотечном коде, но думаю не все и не всегда. Конечно, failed assertions не должно быть, но наличие на fd 2 чего-то «не положенного» превращает «защитный» код (собственно, выдача сообщения и завершение программы) в потенциально «вредительный».

... в моём шаблоне демонизации ...

А можно его посмотреть?

solardiz
()
Ответ на: комментарий от Casus

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

Я эту проблему не понял. Можешь рассказать подробнее - на каких системах проявляется, какие точно последствия имеет и почему (или то из этого, что помнишь)? У меня на Linux setsid отрабатывает одинаково успешно - насколько я могу судить по его коду возврата и выдаче getpid(), getsid(0), getpgid(0) - и при уже завершившемся родителе и при еще живом (переставлял sleep в исходнике).

... в литературе такие случаи ни разу не видел.

Да, я не вижу никакой попытки решить эту проблему (если она есть) ни у Стивенса, ни в daemon(3) в glibc (код пришел из BSD).

solardiz
()
Ответ на: комментарий от solardiz

assert() существует очень давно

Точно. Я, кажется, сам ни разу его не использовал ;)

А можно его посмотреть?

        int fd, pipedes[2];
        if( pipe(pipedes) < 0 )
                _exit(1);
        fd = fork();
        if( fd == 0 ) {
                int w = 0;
                setsid();
                close(pipedes[0]);
                write(pipedes[1],&w,sizeof(w));
                close(pipedes[1]);
        } else if( fd > 0 ) {
                int r;
                close(pipedes[1]);
                read(pipedes[0],&r,sizeof(r));
                close(pipedes[0]);
                _exit(0);
        } else 
                _exit(1);

Да, я не вижу никакой попытки решить эту проблему (если она есть) ни у Стивенса, ни в daemon(3) в glibc (код пришел из BSD).

Ну да, она как буд-то бы не существует. Однако, я её наблюдал. При этом, правда, на сервере было > 2000 процессов, нагрузка была высокая. Если считать, что системные демоны запускаются только при старте сервера, когда нагрузки нет, то и проблемы, конечно, как бы нет.

Casus ★★★★★
()
Ответ на: комментарий от Casus

> Ну да, она как буд-то бы не существует. Однако, я её наблюдал. При этом, правда, на сервере было > 2000 процессов, нагрузка была высокая.

Думаю, это был баг в конкретной версии ядра (интересно, в какой?) Думаю, его быстро исправили - так как я сам такого не встречал, хотя количество процессов бывало и побольше (8000+ лишь в одном OpenVZ-контейнере, но это считая и clone()'овые полу-thread'ы - много отдельных экземпляров mysqld с LinuxThreads (не смеяться!) и т.п.), load averages в сотни во время старта части контейнеров/сервисов (да, неприятно очень, но было - слишком много всего на одном сервере и бывает что очередной сервис запускается еще до того как предыдущие полностью завершили инициализацию и начали лишь ждать запросов). Не думаю, что в mysqld есть подобный workaround, однако все экземпляры (около одной тысячи) успешно стартовали в таких условиях (хоть и не быстро и создав упомянутую загрузку во время старта) и жили себе дальше.

solardiz
()
Ответ на: комментарий от Casus

Возможно, это в тему:

«Race condition in the setsid function in Linux before 2.6.8.1 allows local users to cause a denial of service (crash) and possibly access portions of kernel memory, related to TTY changes, locking, and semaphores.»

http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2005-0178
http://linux.bkbits.net:8080/linux-2.6/?PAGE=cset&REV=41ddda70CWJb5nNL71T...
https://bugzilla.redhat.com/show_bug.cgi?id=146101

Это не непосредственно setsid vs. fork race, но если после завершения родительского процесса шелл сразу что-то делал с tty, то мы вполне могли получить некорректное поведение в setsid.

А как именно проблема проявлялась? setsid не давал эффекта? Процесс убивался? Oops был?

solardiz
()
Ответ на: комментарий от solardiz

> А как именно проблема проявлялась? setsid не давал эффекта? Процесс убивался? Oops был?

Дело было на Linux примерно в 2002-2003, поэтому не могу уверенно утверждать, но, кажется, процесс просто не запускался. Т.е. как я себе представлял проблему: форк -> родитель делает exit | -> потомок не успевает выполнить setsid и умирает вместе с родителем. После того как сделал синхронизацию родителя с потомком (видно, что родитель блокирутся на read, исполнение продолжится после того, как потомок сделает setsid и выполнит write), проблема исчезла. Из чего я сделал предположение, что был в чём-то прав ;)

Casus ★★★★★
()
Ответ на: комментарий от Casus

> потомок не успевает выполнить setsid и умирает вместе с родителем.

Могу предположить вот что: демон запускался таким образом, что уже через долю секунды после того, как он отдавал управление в запустивший его скрипт, ему требовалось «отделиться». Если он не успевал этого сделать, то какое-то действие из запустившего скрипта или, скорее, завершение этого скрипта вместе с сессией (например, ssh) приводило к убиванию еще не до конца запущенного демона. Т.е. он умирал не «вместе с родителем», а чуточку позже. Да, согласен, подход с ожиданием возврата из setsid (и завершения других подобных действий, если они есть) до завершения родителя решает эту проблему. Также, согласен что pipe - хорошее решение (насколько я помню, sshd из OpenSSH использует похожий прием для реализации MaxStartups, только там требуется не ожидание, а проверка открыт ли другой конец трубы; пишу по памяти).

P.S. Почему-то на IBM dW все комментарии магически исчезли (по крайней мере, на «Части 6», но я не вижу там ни одного комментария и в других местах). Может, глюк (ясно, что там задействован не один сервер, а нить комментариев отображается JavaScript'ом отдельно от тела статьи; надеюсь, она еще вернется). Так что если кто по мотивам этого обсуждения туда пойдет, но комментариев не увидит вообще - не удивляйтесь.

solardiz
()
Ответ на: комментарий от solardiz

> Могу предположить вот что: демон запускался таким образом, что уже через долю секунды после того, как он отдавал управление в запустивший его скрипт, ему требовалось «отделиться»

Врать не буду, дело было давнее. Но если я правильно помню, то доходило до того, что даже при ручном запуске демона из шелла, он в 1 из 10 (условно) раз просто не запускался. Но, возможно, что действительно падал по какому-либо сигналу типа SIGSEGV или SIGBUS, не могу вспомнить что именно было 8 лет назад.

Почему-то на IBM dW все комментарии магически исчезли...

Сейчас на месте.

Кстати, про строки уж. Работа со строками в Си, всё-таки, не очень «прямо» сделана. Ради эксперимента сделал такое: http://pastebin.com/8u031gcy . Оказалось, очень даже удобно. Реальную библиотеку из этого не делал, очень многие static inline надо вынести в отдельный str.c, это я знаю ;) Но сам эксперимент, можно сказать, удался.

Casus ★★★★★
()
Ответ на: комментарий от Casus

Спасибо за дискуссию про setsid/fork.

Работа со строками в Си, всё-таки, не очень «прямо» сделана.

Конечно. Тот же strncpy() вообще не должен был появиться с такой семантикой, или должен был называться иначе. strlc*() - попытка исправить это, при этом не привнося более высокоуровневый API.

Ради эксперимента сделал такое: http://pastebin.com/8u031gcy

Глянул. В целом, да, примерно так это и делают в qmail, vsftpd, Postfix.

К сожалению, в самой такой реализации могут быть баги, в том числе уязвимости. Ей надо уделить много времени - максимально сократить объем кода, провести аудит, тестирование (fuzzing). Либо взять готовую и уже «проверенную».

Из замеченного в твоем str.h:

Надо избегать integer overflows при вычислении размеров. Например, realloc(strv->vector, (strv->length + 1) * sizeof(str_t*)) может отработать не как задумывалось если strv->length очень большой. (Аналогичная проблема была в qmail, хотя DJB ее так и не признал - мол, не давайте процессу резервировать 2 GB памяти и все будет в порядке. Это было технически верно (для конкретного случая), но все же по-моему он был не прав (далеко не все ставят rlimit'ы на qmail). Программа тоже не должна допускать таких проблем.) Также, calloc() до недавнего времени был подвержен аналогичному переполнению при умножении «внутри себя». Думаю, еще используется много систем, где calloc() все еще содержит эту проблему, так что лучше избегать опасных вызовов calloc() (потенциально приводящих к переполнению при умножении в нем).

Макросы из ctype принимают int, а не char. Их использование на char приводило к проблемам. Конкретно в glibc для этого есть workaround, но я бы советовал делать isspace((int)(unsigned char)c) (если c определен как char).

Мои примеры в тему:
http://openwall.info/wiki/people/solar/software/public-domain-source-code/concat
http://cvsweb.openwall.com/blists (там buffer.c, buffer.h)

Кода гораздо меньше, функциональность другая и более ограниченная, но достаточная конкретно для тех программ, где эти вещи мне были нужны. Риск integer overflow учтен.

solardiz
()
Ответ на: комментарий от solardiz

> К сожалению, в самой такой реализации могут быть баги, в том числе уязвимости. Ей надо уделить много времени - максимально сократить объем кода, провести аудит, тестирование (fuzzing).

Безусловно. Ошибки +-1 или размера типа часто просачиваются.

Из замеченного в твоем str.h:

Спасибо ;) В своё оправдание, я изначально сказал, что это был эксперимент, а не настоящая библиотека. Использовал несколько раз в проектах, где вреда быть не могло, да и платформа 64 бита.

if ((m += l) < l)

Это попытка поймать целочисленное переполнение? Прикольно выглядит ;) Да, действительно, либцеписателям должно быть нелегко...

Casus ★★★★★
()
Ответ на: комментарий от Casus

В своё оправдание, я изначально сказал, что это был эксперимент ...

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

Также, мы просто обсуждаем тему, поэтому никаких «оправданий» не требуется. Не-идеальный код встречается у всех, да и требования к коду могут отличаться.

> if ((m += l) < l)

Это попытка поймать целочисленное переполнение?

Да, но здесь есть одна важная деталь, которую из кода «не видно»: такая проверка гарантированно работает только для unsigned типа (что у меня учтено при определении переменных). Для signed формально получится undefined behavior (и, учитывая это, некоторые версии gcc такую проверку для signed при включенной оптимизации удаляют - мол, раз все равно undefined, то можно и код не формировать). Так что при работе с signed, надо проверять на возможное переполнение до выполнения операции (и не выполнять ее вовсе пока не убедились что переполнения не будет).

solardiz
()
Ответ на: комментарий от Casus

Ссылки по теме qmail:

http://www.guninski.com/where_do_you_want_billg_to_go_today_4.html

«Systems affected: qmail on 64 bit platforms with a lot of virtual memory ( ~ >8GB)» (я помнил немного неправильно).

http://cr.yp.to/qmail/guarantee.html

«In May 2005, Georgi Guninski claimed that some potential 64-bit portability problems allowed a ``remote exploit in qmail-smtpd." This claim is denied. Nobody gives gigabytes of memory to each qmail-smtpd process, so there is no problem with qmail's assumption that allocated array lengths fit comfortably into 32 bits.»

По теме ctype macros:

http://seclists.org/nmap-dev/2009/q3/209

(и далее по thread'у).

solardiz
()

!!! HLP

Как раз занимаюсь написанием сетевого сервера с доступом клиентов - дорогие коллеги, есть ли у вас что посоветовать? Сейчас всё самописое на C++, для сети использую enet.

xawari
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.