классический TCP сервер

мой первый TCP Сервер был создан пару лет назад. Основой создания послужила книга Р.Стивенсона «Unix — Профессиональное программирование.» Есть несколько подходов к созданию TCP-серверов. В данном посте хочется рассказать про классический TCP сервер.



При построение классического TCP Сервера можно выделить три составные части:
  • демонизация процесса
  • работа с сокетами
  • создание дочернего процесса и обработка его завершения

При работе с сокетами необходимо:
  • создать дескриптор сокетов
  • назначить сокету адрес
  • принять запрос на установку соединения
  • установить соединение
  • принять / отправить данные

рассмотрим код, смотрим комментарии:
1 struct sockaddr_in addr;
2 struct sockaddr_un local;
3
4 int err,len;
5
6 // создаем дескриптор сокета
7 if( -1 == ( ls = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ) )) {
8 perror( "Socket can not created!\n");
9 return ;
10 }
11
12 // задаем сокету опцию SO_REUSEADDR
13 // повторное использование локальных адресов для функц. bind()
14 setsockopt( ls, SOL_SOCKET, SO_REUSEADDR, &rc, sizeof(rc) );
15 memset( &addr , 0, sizeof(addr) );
16
17 // задаем адрес прослушивания и порт
18 addr.sin_family = AF_INET;
19 addr.sin_port = htons (port);
20 addr.sin_addr.s_addr = INADDR_ANY ;
21
22 // связываем адрес с дескриптором слушающего сокета
23 if ( err = bind( ls, (struct sockaddr*) &addr, sizeof(addr) ) < 0 ) {
24 close(ls);
25 perror( "bind error!\n%s ", gai_strerror(err) );
26 return ;
27 }
28
29
30 // начинаем слушать сокет
31 if ( listen( ls, 25) < 0) { ;
32 close(ls);
33 perror( "listen error!\n");
34 return ;
35 }
36
37 // демонизируем процесс
38 pid_t pid = Demonize();
39
40 if ( pid==-1 ) {
41 close(ls);
42 perror( "demonize error!\n");
43 return;
44 }
45
46 // бесконечный цикл на прием соединения
47 while (true) {
48 // прием соединения, создание дескриптора
49 rc = accept( ls, NULL, NULL );
50 // пораждаем дочерний процесс
51 pid = fork();
52
53 if ( pid < 0) syslog( LOG_ERR, " fork abort" );
54 if (pid == 0 ) { // дочерний процесс
55 close( ls ); // закрываем дескриптор сдушающего сокетов
56
57 process(rc); // пользовательская функция,
58 // параметром передается дескриптор открытого соединения
59 close(rc);
60 return;
61 } else {
62 // Родительский процесс
63 close( rc ); // закрываем дескриптор соединения
64 }
65
66 } // end while


А теперь немного пояснений:

Создаем дескриптор слушающего сокета Строки 7-10:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

константа domain:
AF_INET — IP сокет
AF_UNIX — UNIX сокет

константа type:
SOCK_STREAM — сокет постоянного соединения
SOCK_DGRAM — сокет датаграм

константа int protocol:
IPPROTO_TCP — тип протокола

Функция socket возвращает номер дескриптора прослушающего сокета. Если результат меньше 0, то ошибка системы.

Строки 12-28 Назначаем сокету адрес.

#include <sys/socket.h>
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
задает сокету опциии:
SO_REUSEADDR повторное использование локальных адресов для функции bind()
Если результат выполнение операции меньше 0, то ошибка системы.

Строки 18-20 задаем адрес прослушивания и порт в структуре sockaddr_in
addr.sin_family = AF_INET; // тип домена INET
addr.sin_port = htons (port); // назначаем порт
addr.sin_addr.s_addr = INADDR_ANY; // любой входящий адрес

Функция bind ассоциирует адрес с безымяннымсокетом:
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
int socket — дескриптор сокета
address — адресс структуры sockaddr с
address_len — размер структуры sockaddr
Если результат выполнение операции меньше 0, то ошибка системы.

Начинаем прослушивать сокет Строки 31-35. С помощью функции listen сервер заявляет свое желание принимать запросы на установку соединения:

#include <sys/socket.h>
int listen(int socket, int backlog);
int socket — дескриптор сокета
int backlog — максимальная длинна очереди ожидающих запросов на соединение
Если результат выполнение операции меньше 0, то ошибка системы.

Строка 38-44, Демонизируем процесс. Более подробно в сл. статье о процессах-демонах.

Строки 47-66, обработка соединения

Строка 49, Функция accept прием запроса и преобразование его в соединение.
#include <sys/socket.h>
int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
int socket — дескриптор слушающего сокета, он не связан для соединения и остается для прослушивания последующих соединений,
address — указатель на структуру адреса сокета, в которой по окончании будет хранится адрес клиента
address_len — размер буфера, для размещения структуры адреса, если address и address_len передать NULL — то нас не интересует адрес клиента.
Функция accept возвращает дескриптор сокета, связанного с соединением или -1 в случае неудачи.

Мы можем обработать соединение и принять следующее, но как правило, соединения обрабатываются какое-то длительное время а сервер ждать не может, по этому, после установки соединения порождается дочерний процесс строка 51, командой fork. Далее, в родительском процессе закрываем дескриптор соединения, а в дочернем процессе закрываем дескриптор слушающего сокета и вызываем пользовательскую функцию, которая обработает наше соединение.
Обработка соединения осуществляется командами read/write:
запись в сокет: write (rc, buff, len )
чтение из сокета: read (rc, buff, len )
rc — дескриптор соединения
buff — буфер чтения/записи
len длинна буфера.
По окончанию обработки соединения, дескриптор сокета rc необходимо закрыть: close(rc).

Что не вошло и будет отдельно изложено другими статьями: описание команды fork, обработка сигналов, демонизация процесса.
+17
20 ноября 2009, 23:47
57
akalend 29,4

комментарии (58)

+23
Frosty #
Ни подсветки, ни отступов. Если топик целиком о коде, то не поленитесь его оформить так, чтобы не хотелось читать по диагонали.
0
skobkin #
Хотя бы в цитаты оформить с чертой…
+5
mephisto #
Тема интересная, но воспользуйтесь пожалуйста Source Code Highlighter для упрощения чтения кода =)
0
michurin #
Интересно. Буду рад и другим статьям такой же направленности. (Но отступов не хватает.)
–1
akalend #
спасибо,
код отформатировать стандартными средствами как-то не получается

была идея прогнать через аппач и вставить с подсветкой
так аппач снес… :)
+2
MefBezTufel #
Первое же, что увидел в этом коде — ни в одном из return ничего не выдается. Насколько я понимаю, тут есть два варианта, в зависимости от реализации системы:
1) Всегда будет возвращаться 0. Тогда некий скрипт, запускающий этот демон, не узнает, что уже на стадии инициализации все плохо.
2) Код возврата будет тот же, что и у прошлой вызванной команды. Тогда если прошлый демон не смог стартовать, а этот в порядке, тот же некий скрипт будет думать, что и этому демону не удалось стартовать.
+4
MefBezTufel #
Конечно, меня за это заминусуют, но я очень прошу плюсануть мою карму хотя бы до того, чтобы я смог написать статью в свой блог. Ибо готова первая статья о том, чем учебники отличаются от реального программирования под Linux.
0
akalend #
это не программа а кусок кода, взятый из реальной программы,
по этому, для лучшего понимания все лишнее порезано и упрощено
+1
mChief #
С удовольствием бы прочел вашу статью
0
akalend #
спасибо,
постараюсь в короткие сроки написать продолжение
0
akasilov #
это ваша статья или скопирована/переведена? если второе, предоставте линк пожалийста. или это из книги?
НЛО прилетело и опубликовало эту надпись здесь
0
akasilov #
уж больно похоже на вытяжку из какого-то мануала. прошу прощение если я неправ
–2
akalend #
автор этой статьи я,
основой послужила программа, которую я смог написать после прочтения книги Стивенсона и решил поделиться своими знаниями.

если лох -я, значить статья неудачная
Стивенсон — уважаемый в мире специалист по Unix и признанный писатель,
я и намека не сделал, что лох он

да, есть вытяжки из мануалов, например:
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, socklen_t address_len);

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

возможно, можно было написать больше буковок
0
akalend #
интересно за что минуснули
0
zloe #
уже где-то читал это…
0
shandor #
В мануалах?
0
zloe #
вполне возможно
или на опеннете
+1
akalend #
у Стивенсона наверное…
на Опеннете — его переводы

что касается материала, то хотелось внести в новый блог что-то новое,
чего мне самому хотелось бы почитать,
по принципу «Кто если не Я»

что касается плагиата, то см мой коммент выше
+2
max_m #
Тема полезная но подача материала неудачная ИМХО
минусы:
1. форматирование кода отсутствуют
2. пример нельзя скопировать и запустить (ни инклюдов, ни функции main, ни processrc)
3. как-то все очень сумбурно :)
0
akalend #
что-то не получается отформатировать
0
Captcha #
Держи: quickhighlighter.com/

Спасибо за статью. Думаю, новичкам будет полезна.
–2
akalend #
спасибо, сейчас поправлю

у нас удивительная «Страна Советов»
все хотят сразу и на готовое, все плохо, но при этом ничего не сделать…
увы — менталитет…

если говорят: переписал маны, но увы,
сами не могут написать-то лучше…

если говорят что уже где-то читали и материал устарел,
то почему он, такой старый еще не размещен на таком популярном сервисе?

если говорят, что есть новые методы и подходы:
но увы… где они??? почему мы о них умалчиваем???

извини, наболело, просто это не первый случай.
0
akalend #
ага, минуснули
правда-матка глаза режет

заметит тенденцию,
критикует тот кто меньше всего пишет.
0
akalend #
получилась такая ерунда
5
6 // создаем дескриптор сокета
7 if( -1 == ( ls = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ) )) {
8 perror( «Socket can not created!\n»);
0
akalend #
странно, а здесь не подсветки и символы в порядке…
0
Captcha #
В камментариях, наверное, не все теги можно использовать. Попробуй вставить сгенерированный хайлайтером код в твою статью и сделать предпросмотр.
0
akalend #
увы не получается…
там стили пропадают
0
akalend #
GeSHi © 2004 — 2007 ни когда не дружила с отличными от ANSI кодировками.
не получается нормально подсветить через этот билдер

можно закописастить в govnokod.ru :)

+3
redchrom #
Есть такой журнал «Хакер», они там тоже занимаются вольным изложением манов.
+3
aleks_raiden #
ложь.
+3
Captcha #
Ну не знаю. Иногда там попадаются очень даже полезные статьи. ИМХО
+1
erley #
Хм, а функция select() (ну или там poll(), epoll(), и прочие kqueue()) уже не нужны в BSD socket API? :)
0
DurRandir #
Сокеты — они не BSD, они Berkley
select — относится к стандартному сокет апи, а вот ни poll, ни epoll, ни kqueue — нет. Более того, в BSD нет epoll — это Linux-расширение.
0
akalend #
а об этом как нибудь напишу еще
времени нет совсем на разработку,
а на писательство и подавно.

это уже не относится к классическому TCP серверу
0
DurRandir #
groups.google.com/group/fido7.ru.unix.prog/browse_thread/thread/c8952c3f53a067c0

Читать до просветления. Сейчас fsm-based сервера — тоже классика.
0
akalend #
и почему бы тебе это не оформить в ввиде статьи с красивыми примерами?
0
erley #
Да ладно вам придираться к словам! именно поэтому эти функции я указал в скобках, что они имплементируют по-другому то, для чего существует select().
И что же, по-вашему слова «Berkley» нет в расшифровке аббревиатуры «BSD»?
Человек про главную вещь не сказал, а вы…
0
DurRandir #
Поздравляю, вы написали форк-бомбу :) Несколько тысяч ничего не делающих клиентов убьют ваш сервер, и не создадут никакой нагрузки на атакующую машину :)
+1
akalend #
спасибо за поздравление…

сотни тысяч таких бобм удачно работали в течении последних двух десятилетий
–1
DurRandir #
Примеры, примеры :) Вот вы сейчас назовёте апач/любой mta — и я сразу покажу, в чём разница.
–2
akalend #
я не хочу спорить на тему hiload, WEB и высокой производительности
сотни тысяч таких серверов крутилось в течении 30 и более лет и крутится по настоящее время и они успешно решают свои задачи.

тема статьи классический сервер у которого есть свои недостатки и свои достоинства.
а не суперпроизводительный, мультиплексорный или тредовый… Я думаю, Вы об этом напишете лучше. Критиковать умеют все, а заминусовать — дай хлебом не корми…

Ладно, попробую на пальцах:
Если взять любой курс, например литературу, то сперва изучается классика,
а потом рассматриваются современные направления…

через 5-10 лет, мы будем смеяться и над производительностью мультиплексорного сервера
прогресс несомненно движется
0
akalend #
интересно, а в чем я не прав?
+2
hellomax #
А че примеры? Сначала сплоит с тебя. ;)
0
DurRandir #
Рабочий код, который откроет дцать тычяч подключений?) Разве это серьёзный эксплоит, который стоит вставлять сюда?)

А намекал я на то, что везде в форкающхся серверах стоит ограничение на максимальное количество одновременно обслуживаемых клиентов. Остальные, традиционно, ждут в backlog сокета. В таком случае атакующий может съесть ресурсы приложения, но не всей системы. А здесь 10к подключений = 10к форков, а лимит на количество процессов в системе не бесконечный, да и форк небесплатен.
0
olegi #
ограничение реализуется простым int count?
0
DurRandir #
Не совсем. Сколько мы запустили — легко можем сказать, а сколько умерло? Надо перехватить SIGCHLD и вызывать waitpid.
–1
akalend #
цитирую конец статьи:
Что не вошло и будет отдельно изложено другими статьями: описание команды fork, обработка сигналов, демонизация процесса.
0
akalend #
хм… согласен:
когда-то аппач считался прогрессивным сервером
а теперь когда говорим о хайлоаде и называем слово аппач, то у меня появляется улыбка
0
DurRandir #
А кто-то не может слезть с апача. Как тяжёлый бекенд, он вполне в своей нише. Ставить его наружу — это другое дело.
НЛО прилетело и опубликовало эту надпись здесь
0
akalend #
надо начинать с самого простого,
еще две части дописать осталось,
ну а далее рассмотрим обработку процесса в в потоке.
0
shitware #
лабораторная работа 2 курс
+1
skobkin #
Если бы :) Не везде. Правда, те, кому надо делают это еще на первом или в школе :)
0
tswr #
>> то почему он, такой старый еще не размещен на таком популярном сервисе?
А может быть не нужно все, что есть в интернете, постить на хабр?
Особенно несложные вещи… И особенно старые… по которым есть уйма howto, tutorials и книг с хорошим оформлением.
–1
akalend #
может быть ты и прав…
+1
reddot #
>#include <sys/socket.h>
>int socket(int domain, int type, int protocol);
>
>константа domain:
>AF_INET — IP сокет
>AF_UNIX — UNIX сокет

На самом деле в рамках вызова socket в качестве параметра domain идеологически правильно использовать константы PF_ (protocol family), т.е. соответственно PF_INET и PF_UNIX. А константы AF_ (address family) должны использоваться в поле family структур sockaddr_. Хотя по значениям эти константы сейчас равны, а в man socket линукса написано про AF_, в мане фрибсд написано про PF_
0
akalend #
ценное замечание,
спасибо

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.