C++ — Сервер на c++


Содержание

C++ — Сервер на c++

Использование языка C/C++ в разработке

четверг, 7 мая 2009 г.

Написание своего HTTP сервера с использованием libevent

Библиотека libevent содержит в себе простейший асинхронный HTTP сервер, который можно
без особого труда встроить в собственное приложение для обслуживания HTTP запросов.

Для использования этой возможности достаточно добавить в код следующее:

1. Подключить заголовочный файл :

2. Инициализировать базу событийного движка:

3. Инициализировать HTTP сервер:

4. Указать, на каком сокете слушать подключения:

5. Выставить callback’и на запросы. Можно добавлять на каждый URI свой обработчик:

6. Выставить обработчик на остальные запросы:

7. Запустить цикл обработки запросов:

Реализация HTTP сервера является потоко-безопасной (thread safe).

Есть еще один нюанс: настоятельно рекомендуется игнорировать сигнал SIGPIPE.
Делается это следующим вызовом:

Описание всех функция для работы с HTTP протоколом
можно найти в документации.

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

Makefile для сборки:

А вот результаты тестирования Apache Benchmark:

Сервер был запущен локально на моем ноутбуке, на сервере результаты конечно же получше.
Но для сотни параллельных запросов – это неплохо.

Введение в сетевое программирование

Содержание

Введение в сетевое программирование

Постановка задачи

Для знакомства с сетевым программированием разберем пример. Пусть требуется запрограммировать службу удаленных вычислений. Клиенты просят сервер вычислить выражение (для начала содержащее только одну арифметическую операцию +, -, *, / ), сервер возвращает результат.

Разработка протокола

Назовем наш протокол Calculation 0.1. Для начала определим формат запроса и ответа. Пусть запрос клиента должен начинаться со слова CALC, далее через пробел операция и потом два числа — аргументы. Тогда запрос клиента на вычисление произведения 12 и 6 будет выглядеть так:

Ответ сервера будет начинаться со слова OK, если запрос был корректен и далее через пробел число – результат вычислений. Если же запрос некорректен, ответом будет слово ERR.

ENTER нужен, чтобы узнать где конец строки.

После определения правил работы протокола, можно перейти к реализации. И тут встает вопрос: используем TCP или UDP? Мы разберем оба случая. И, для начала, разберем алгоритм взаимодействия сервера с клиентом, а потом реализуем его программно.

Алгоритм работы сервера (TCP)


  1. Запускается заранее, до подключения клиентов.
  2. Сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт №12345.
  3. Выделяет память для очереди подключений.
  4. В цикле:
  • устанавливает соединение с клиентом из очереди; если очередь пуста, то ждет подключения клиента;
  • принимает/передает данные;
  • закрывает соединение с клиентом.

Алгоритм работы клиента (TCP)

  1. Получает от ОС случайный номер порта для общения с сервером.
  2. Устанавливает соединение с сервером.
  3. Передает/принимает данные.
  4. Закрывает соединение с сервером.

Алгоритм работы сервера (UDP)

  1. Запускается заранее, до подключения клиентов.
  2. сообщает ОС, что будет ожидать сообщений, посланных на заранее утвержденный порт №12345.
  3. В цикле:
  • ждет прихода сообщения;
  • обрабатывает данные;
  • передает результат.

Основное отличие UDP от TCP — не нужно возиться с соединениями.


Программирование сетевых приложений (TCP/IP) на C/C++

Простейшие примеры

TCP/IP

Что следует иметь ввиду при разработке с TCP

  1. TCP не выполняет опрос соединения (не поможет даже keep alive — он нужен для уборки мусора, а не контроля за состоянием соединения). Исправляется на прикладном уровне, например, реализацией пульсации. Причем на дополнительном соединении.
  2. Задержки при падении хостов, разрыве связи.
  3. Необходимо следить за порядком получения сообщений.
  4. Заранее неизвестно сколько данных будет прочитано из сокета. Может быть прочитано несколько пакетов сразу!
  5. Надо быть готовым ко всем внештатным ситуациям:
    • постоянный или временный сбой сети
    • отказ принимающего приложения
    • аварийный сбой самого хоста на принимающей стороне
    • неверное поведение хоста на принимающей стороне
    • учитывать особенности сети функционирования приложения (глобальная или локальная)

OSI и TCP/IP

OSI TCP/IP
Прикладной уровень Прикладной уровень
Уровень представления
Сеансовый уровень Транспортный уровень
Транспортный уровень Межсетевой уровень
Сетевой уровень Интерфейсный уровень
Канальный уровень
Физический уровень

Порты

Порт Кем контроллируется
0 — 1023 Контролируется IANA
1024 — 49151 Регистрируется в IANA
49152 — 65535 Эфимерные

Полный список зарегистрированных портов расположен по адресу: http://www.isi.edu/in-notes/iana/assignment/port-numbers. Подать заявку на получение хорошо известного или зарегистрированного номера порта можно по адресу http://www.isi.edu/cgi-bin/iana/port-numbers.pl.

Состояние TIME-WAIT

После активного закрытия для данного конкретного соединения стек входит в состояние TIME-WAIT на время 2MSL (максимальное время жизни пакета) для того, чтобы

  1. заблудившийся пакет не попал в новое соединение с такими же параметрами.
  2. если потерялся ACK, подтверждающий закрытие соединения, с активной стороны, пассивная снова пощлёт FIN, активная, игнорируя TIME-WAIT уже закрыла соединение, поэтому пассивная сторона получит RST.

Отключение состояния TIME-WAIT крайне не рекомендуется, так как это нарушает безопасность TCP соединения, тем не менее существует возможность сделать это — опция сокета SO_LINGER.

Штатная ситуация — перезагрузка сервера может пострадать из-за наличия TIME-WAIT. Эта проблема решается заданием опции SO_REUSEADDR.

Отложенное подтверждение и алгоритм Нейгла.

Алгоритм Нейгла используется для предотвращения забивания сети мелкими пакетами и имеет очень простую формулировку — запрещено посылать второй маленький пакет до тех пор, пока не придет подтверждение на первый. Тем не менее данные отправляются при выполнении хотя бы одного из следующих условий:

  • можно послать полный сегмент размером MSS (максимальный размер сегмента)
  • соединение простаивает, и можно опустошить буфер передачи
  • алгоритм Нейгла отключен, и можно опустошить буфер передачи
  • есть срочные данные для отправки
  • есть маленьки сегмент, но его отправка уже задержана на достаточно длительное время (таймер терпения persist timer на тайм-аут ретрансмиссии RTO )
  • окно приема, объявленное хостом на другом конце, открыто не менее чем на половину
  • необходимо повторно передать сегмент
  • требуется послать ACK на принятые данные
  • нужно объявить об обновлении окна

Кроме того, существует отложенное подтверждение. При этом хост, получивший сегмент, старается задержать отправку ACK, чтобы послать её вместе с данными.

Алгоритм Нейгла в купе с отложенным подтверждением в резонансе дают нежелательные задержки. Поэтому часто его отключают. Отключение алгоритма Нейгла производится заданием опции TCP_NODELAY

Но более правильным было бы проектировать приложение таким образом, чтобы было как можно меньше маленьких блоков. Лучше писать большие. Для этого можно объединять данные самостоятельно, а можно пользоваться аналогом write, работающим с несколькими буферами:


Сервер на Си, как и зачем?

Самые шустрые и распространённые библиотеки.
Первая используется в ядре Node.js.
Вторая — в куче проектов типа хрома, тора, файрфокса, мускуля. Умеет http-роутер из коробки.

На Си дурной тон писать сервер, когда не знаешь Си. Бред наркомана.
90% всех существующих серверов написано на Си. Другое дело, что надо знать одновременно и язык, и тонкости ОС, и тонкости сетей, уметь пользоваться профилировщиками памяти. Ну, и времени нужно очень приличное количество.

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

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

Работа с WinSocket в Visual C++

Работа с WinSocket в Visual C++

Socket (гнездо, разъем) — абстрактное программное понятие, используемое для обозначения в прикладной программе конечной точки канала связи с коммуникационной средой, образованной вычислительной сетью. При использовании протоколов TCP/IP можно говорить, что socket является средством подключения прикладной программы к порту (см. выше) локального узла сети.

Socket-интерфейс представляет собой просто набор системных вызовов и/или библиотечных функций языка программирования СИ, разделенных на четыре группы:

Ниже рассматривается подмножество функций socket-интерфейса, достаточное для написания сетевых приложений, реализующих модель «клиент-сервер» в режиме с установлением соединения.

1. Функции локального управления

Функции локального управления используются, главным образом, для выполнения подготовительных действий, необходимых для организации взаимодействия двух программ-партнеров. Функции носят такое название, поскольку их выполнение носит локальный для программы характер.

1.1 Создание socket’а

Создание socket’а осуществляется следующим системным вызовом #include int socket (domain, type, protocol) int domain; int type; int protocol;

Аргумент domain задает используемый для взаимодействия набор протоколов (вид коммуникационной области), для стека протоколов TCP/IP он должен иметь символьное значение AF_INET (определено в sys/socket.h).

Аргумент type задает режим взаимодействия:

  • SOCK_STREAM — с установлением соединения;
  • SOCK_DGRAM — без установления соединения.

Аргумент protocolзадает конкретный протокол транспортного уровня (из нескольких возможных в стеке протоколов). Если этот аргумент задан равным 0, то будет использован протокол «по умолчанию» (TCP для SOCK_STREAM и UDP для SOCK_DGRAM при использовании комплекта протоколов TCP/IP).

При удачном завершении своей работы данная функция возвращает дескриптор socket’а — целое неотрицательное число, однозначно его идентифицирующее. Дескриптор socket’а аналогичен дескриптору файла ОС UNIX.

При обнаружении ошибки в ходе своей работы функция возвращает число «-1».

1.2. Связывание socket’а

Для подключения socket’а к коммуникационной среде, образованной вычислительной сетью, необходимо выполнить системный вызов bind, определяющий в принятом для сети формате локальный адрес канала связи со средой. В сетях TCP/IP socket связывается с локальным портом. Системный вызов bind имеет следующий синтаксис:

Аргумент s задает дескриптор связываемого socket’а.

Аргумент addr в общем случае должен указывать на структуру данных, содержащую локальный адрес, приписываемый socket’у. Для сетей TCP/IP такой структурой является sockaddr_in.

Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.

Структура sockaddr_in используется несколькими системными вызовами и функциями socket-интерфейса и определена в include-файле in.h следующим образом:

Поле sin_family определяет используемый формат адреса (набор протоколов), в нашем случае (для TCP/IP) оно должно иметь значение AF_INET.

Поле sin_addr содержит адрес (номер) узла сети.

Поле sin_port содержит номер порта на узле сети.

Поле sin_zero не используется.


Определение структуры in_addr (из того же include-файла) таково:

Структура sockaddr_in должна быть полностью заполнена перед выдачей системного вызова bind. При этом, если поле sin_addr.s_addr имеет значение INADDR_ANY, то системный вызов будет привязывать к socket’у номер (адрес) локального узла сети.

В случае успеха bind возвращает 0, в противном случае — «-1».

2. Функции установления связи

Для установления связи «клиент-сервер» используются системные вызовы listen и accept (на стороне сервера), а также connect (на стороне клиента). Для заполнения полей структуры socaddr_in, используемой в вызове connect, обычно используется библиотечная функция gethostbyname, транслирующая символическое имя узла сети в его номер (адрес).

2.1. Ожидание установления связи

Системный вызов listen выражает желание выдавшей его программы-сервера ожидать запросы к ней от программ-клиентов и имеет следующий вид:

Аргумент s задает дескриптор socket’а, через который программа будет ожидать запросы к ней от клиентов. Socket должен быть предварительно создан системным вызовом socketи обеспечен адресом с помощью системного вызова bind.

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

Признаком удачного завершения системного вызова listen служит нулевой код возврата.

2.2. Запрос на установление соединения

Для обращения программы-клиента к серверу с запросом на установление логической соединения используется системный вызов connect, имеющий следующий вид #include #include #include int connect (s, addr, addrlen) int s; struct sockaddr_in *addr; int addrlen;

Аргумент s задает дескриптор socket’а, через который программа обращается к серверу с запросом на соединение. Socket должен быть предварительно создан системным вызовом socketи обеспечен адресом с помощью системного вызова bind.

Аргумент addr должен указывать на структуру данных, содержащую адрес, приписанный socket’у программы-сервера, к которой делается запрос на соединение. Для сетей TCP/IP такой структурой является sockaddr_in. Для формирования значений полей структуры sockaddr_in удобно использовать функцию gethostbyname.

Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.

Для того, чтобы запрос на соединение был успешным, необходимо, по крайней мере, чтобы программа-сервер выполнила к этому моменту системный вызов listen для socket’а с указанным адресом.

При успешном выполнении запроса системный вызов connect возвращает 0, в противном случае — «-1» (устанавливая код причины неуспеха в глобальной переменной errno).

Примечание. Если к моменту выполнения connect используемый им socket не был привязан к адресу посредством bind ,то такая привязка будет выполнена автоматически.

Примечание. В режиме взаимодействия без установления соединения необходимости в выполнении системного вызова connect нет. Однако, его выполнение в таком режиме не является ошибкой — просто меняется смысл выполняемых при этом действий: устанавливается адрес «по умолчанию» для всех последующих посылок дейтаграмм.

2.3. Прием запроса на установление связи

Для приема запросов от программ-клиентов на установление связи в программах-серверах используется системный вызов accept, имеющий следующий вид:

Аргумент s задает дескриптор socket’а, через который программа-сервер получила запрос на соединение (посредством системного запроса listen ).

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

Аргумент p_addrlen должен указывать на область памяти в виде целого числа, задающего размер (в байтах) области памяти, указываемой аргументом addr.

Системный вызов accept извлекает из очереди, организованной системным вызовом listen, первый запрос на соединение и возвращает дескриптор нового (автоматически созданного) socket’а с теми же свойствами, что и socket, задаваемый аргументом s. Этот новый дескриптор необходимо использовать во всех последующих операциях обмена данными.

Кроме того после удачного завершения accept:

  • область памяти, указываемая аргументом addr, будет содержать структуру данных (для сетей TCP/IP это sockaddr_in), описывающую адрес socket’а программы-клиента, через который она сделала свой запрос на соединение;
  • целое число, на которое указывает аргумент p_addrlen, будет равно размеру этой структуры данных.

Если очередь запросов на момент выполнения accept пуста, то программа переходит в состояние ожидания поступления запросов от клиентов на неопределенное время (хотя такое поведение accept можно и изменить).

Признаком неудачного завершения accept служит отрицательное возвращенное значение (дескриптор socket’а отрицательным быть не может).


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

2.4. Формирование адреса узла сети

Для получения адреса узла сети TCP/IP по его символическому имени используется библиотечная функция

Аргумент name задает адрес последовательности литер, образующих символическое имя узла сети.

При успешном завершении функция возвращает указатель на структуру hostent, определенную в include-файле netdb.h и имеющую следующий вид struct hostent < char *h_name; char **h_aliases; int h_addrtype; int h_lenght; char *h_addr; >;

Поле h_name указывает на официальное (основное) имя узла.

Поле h_aliases указывает на список дополнительных имен узла (синонимов), если они есть.

Поле h_addrtype содержит идентификатор используемого набора протоколов, для сетей TCP/IP это поле будет иметь значение AF_INET.

Поле h_lenght содержит длину адреса узла.

Поле h_addr указывает на область памяти, содержащую адрес узла в том виде, в котором его используют системные вызовы и функции socket-интерфейса.

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

3. Функции обмена данными

В режиме с установлением логического соединения после удачного выполнения пары взаимосвязанных системных вызовов connect (в клиенте) и accept (в сервере) становится возможным обмен данными.

Этот обмен может быть реализован обычными системными вызовами read и write, используемыми для работы с файлами (при этом вместо дескрипторов файлов в них задаются дескрипторы socket’ов).

Кроме того могут быть дополнительно использованы системные вызовы send и recv, ориентированные специально на работу с socket’ами.

Примечание. Для обмена данными в режиме без установления логического соединения используются, как правило, системные вызовы sendtoи recvfrom. Sendto позволяет специфицировать вместе с передаваемыми данными (составляющими дейтаграмму) адрес их получателя. Recvfrom одновременно с доставкой данных получателю информирует его и об адресе отправителя.

3.1. Посылка данных

Для посылки данных партнеру по сетевому взаимодействию используется системный вызов send, имеющий следующий вид

Аргумент s задает дескриптор socket’а, через который посылаются данные.

Аргумент buf указывает на область памяти, содержащую передаваемые данные.

Аргумент len задает длину (в байтах) передаваемых данных.

Аргумент flags модифицирует исполнение системного вызова send. При нулевом значении этого аргумента вызов send полностью аналогичен системному вызову write.

При успешном завершении send возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается «переполненным», то send переводит программу в состояние ожидания до момента его освобождения.

3.2. Получение данных

Для получения данных от партнера по сетевому взаимодействию используется системный вызов recv, имеющий следующий вид

Аргумент s задает дескриптор socket’а, через который принимаются данные.

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

Аргумент len задает длину (в байтах) этой области.

Аргумент flags модифицирует исполнение системного вызова recv. При нулевом значении этого аргумента вызов recv полностью аналогичен системному вызову read.

При успешном завершении recv возвращает количество принятых в область, указанную аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается «пустым», то recv переводит программу в состояние ожидания до момента появления в нем данных.


4. Функции закрытия связи

Для закрытия связи с партнером по сетевому взаимодействию используются системные вызовы close и shutdown.

4.1. Системный вызов close

Для закрытия ранее созданного socket’а используется обычный системный вызов close, применяемый в ОС UNIX для закрытия ранее открытых файлов и имеющий следующий вид

Аргумент s задает дескриптор ранее созданного socket’а.

Однако в режиме с установлением логического соединения (обеспечивающем, как правило, надежную доставку данных) внутрисистемные механизмы обмена будут пытаться передать/принять данные, оставшиеся в канале передачи на момент закрытия socket’а. На это может потребоваться значительный интервал времени, неприемлемый для некоторых приложений. В такой ситуации необходимо использовать описываемый далее системный вызов shutdown.

4.2. Сброс буферизованных данных

Для «экстренного» закрытия связи с партнером (путем «сброса» еще не переданных данных) используется системный вызов shutdown, выполняемый перед close и имеющий следующий вид

Аргумент s задает дескриптор ранее созданного socket’а.

Аргумент how задает действия, выполняемые при очистке системных буферов socket’а:

0 — сбросить и далее не принимать данные для чтения из socket’а;

1 — сбросить и далее не отправлять данные для посылки через socket;

2 — сбросить все данные, передаваемые через socket в любом направлении.

5. Пример использования socket-интерфейса

В данном разделе рассматривается использование socket-интерфейса в режиме взаимодействия с установлением логического соединения на очень простом примере взаимодействия двух программ (сервера и клиента), функционирующих на разных узлах сети TCP/IP.

Содержательная часть программ примитивна:

сервер, приняв запрос на соединение, передает клиенту вопрос «Who are you?»;

клиент, получив вопрос, выводит его в стандартный вывод и направляет серверу ответ «I am your client» и завершает на этом свою работу;

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

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

5.1. Программа-сервер

Текст программы-сервера на языке программирования СИ выглядит следующим образом

Строки 1. 5 описывают включаемые файлы, содержащие определения для всех необходимых структур данных и символических констант.

Строка 6 приписывает целочисленной константе 1234 символическое имя SRV_PORT. В дальнейшем эта константа будет использована в качестве номера порта сервера. Значение этой константы должно быть известно и программе-клиенту.

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

Строка 8 приписывает последовательности символов, составляющих текст вопроса клиенту, символическое имя TXT_QUEST. Последним символом в последовательности является символ перехода на новую строку ‘\n’. Сделано это для упрощения вывода текста вопроса на стороне клиента.

В строке 14 создается (открывается) socket для организации режима взаимодействия с установлением логического соединения (SOCK_STREAM) в сети TCP/IP (AF_INET), при выборе протокола транспортного уровня используется протокол «по умолчанию» (0).

В строках 15. 18 сначала обнуляется структура данных sin, а затем заполняются ее отдельные поля. Использование константы INADDR_ANY упрощает текст программы, избавляя от необходимости использовать функцию gethostbyname для получения адреса локального узла, на котором запускается сервер.

Строка 19 посредством системного вызова bind привязывает socket, задаваемый дескриптором s, к порту с номером SRV_PORT на локальном узле. Bind завершится успешно при условии, что в момент его выполнения на том же узле уже не функционирует программа, использующая этот номер порта.

Строка 20 посредством системного вызова listen организует очередь на три входящих к серверу запроса на соединение.

Строка 21 служит заголовком бесконечного цикла обслуживания запросов от клиентов.


На строке 23, содержащей системный вызов accept, выполнение программы приостанавливается на неопределенное время, если очередь запросов к серверу на установление связи оказывается пуста. При появлении такого запроса accept успешно завершается, возвращая в переменной s_new дескриптор socket’а для обмена информацией с клиентом.

В строке 24 сервер с помощью системного вызова write отправляет клиенту вопрос.

В строке 25 с помощью системного вызова read читается ответ клиента.

В строке 26 ответ направляется в стандартный вывод, имеющий дескриптор файла номер 1. Так как строка ответа содержит в себе символ перехода на новую строку, то текст ответа будет размещен на отдельной строке дисплея.

Строка 27 содержит системный вывод shutdown, обеспечивающий очистку системных буферов socket’а, содержащих данные для чтения («лишние» данные могут там оказаться в результате неверной работы клиента).

В строке 28 закрывается (удаляется) socket, использованный для обмена данными с очередным клиентом.

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

5.2. Программа-клиент

Текст программы-клиента на языке программирования СИ выглядит следующим образом

В строках 6 и 7 описываются константы SRV_HOST и SRV_PORT, определяющие имя удаленного узла, на котором функционирует программа-сервер, и номер порта, к которому привязан socket сервера.

Строка 8 приписывает целочисленной константе 1235 символическое имя CLNT_PORT. В дальнейшем эта константа будет использована в качестве номера порта клиента.

В строках 17. 22 создается привязанный к порту на локальном узле socket.

В строке 24 посредством библиотечной функции gethostbyname транслируется символическое имя удаленного узла (в данном случае «delta»), на котором должен функционировать сервер, в адрес этого узла, размещенный в структуре типа hostent.

В строке 26 адрес удаленного узла копируется из структуры типа hostent в соответствующее поле структуры srv_sin, которая позже (в строке 28) используется в системном вызове connect для идентификации программы-сервера.

В строках 29. 31 осуществляется обмен данными с сервером и вывод вопроса, поступившего от сервера, в стандартный вывод.

Строка 32 посредством системного вызова close закрывает (удаляет) socket.

C: сокеты и пример модели client-server

Перевод с дополнениями. Оригинал — тут>>>.

Как правило — два процесса общаются друг с другом с помощью одного из Inter Process Communication ( IPC ) механизма ядра, таких как:

  • pipe
  • очереди сообщений (Message queues)
  • общая память (shared memory)

Кроме перечисленных IPC — в ядре присутствует много других возможностей, но что если процессам необходимо обмениваться данными по сети?

Тут используется ещё один механизм IPC — сокеты.

Что такое сокет?

Сокеты (англ. socket — разъём) — название программного интерфейса для обеспечения обмена данными между процессами. Процессы при таком обмене могут исполняться как на одной ЭВМ, так и на различных ЭВМ, связанных между собой сетью. Сокет — абстрактный объект, представляющий конечную точку соединения.

Кратко говоря — существует два типа сокетов — UNIX-сокеты (или сокеты домена UNIXUnix domain sockets) и INET-сокеты (IP-сокеты, network sockets).

UNIX-сокеты чвляются частью механизма IPC и позволяют обмен данными в обоих направлениях между процессами, работающими на одной машине.

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

Грубо говоря — если UNIX-сокет использует файл в файловой системе, то INET-сокет — требует присваивания сетевого адреса и порта.

Больше про сокеты:


Коммуникация в среде TCP/IP происходит по клиент-серверной модели, т.е. — клиент инициализирует связь, а сервер его принимает.

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

Socket сервер

Наш сервер будет выглядеть следующим образом:

Флаг —tcp для netstat указывает на то, что требуется вывести информацию только по INET-сокетам.

Самый простой способ получить данные от нашего сервера — с помощью telnet , проверяем ещё раз:

Теперь — давайте рассмотрим сам код сервера.

  • с помощью вызова функции socket() в области ядра создаётся неименованный сокет, и возвращается его socket descriptor
  • первым аргументом этой функции передаётся тип домена. Т.к. мы будем использовать сеть — то используем тип сокета AF_INET (IPv4).
  • вторым аргументом — SOCK_STREAM , который указывает на тип протокола. Для TCP — это будет SOCK_STREAM , для UDP — SOCK_DGRAM
  • третий аргумент оставляем по умолчанию — тут ядро само решит какой тип протокола использовать (т.к. мы указали SOCK_STREAM — то будет выбран TCP)

Далее — вызывается функция bind () :

  • bind() создаёт сокет используя параметры из структуры serv_addr (протокол, IP-адрес и порт)
  • вызов функции listen() со вторым аргументом 10 указывает на макс. допустимое кол-во подключений. Первым аргументом — передаётся дескриптор сокета, который необходимо прослушивать.
  • сервер запускает бесконченый цикл, ожидая входящего соединения, и вызывает accept() , как только соединение установлено. В свою очередь accept() создаёт новый сокет для каждого соединения, вовзращает дескриптор сокета
  • как только соединение установлено (т.е. сокет создан) — функция snprintf() вписывает время и дату в буфер, после чего вызывается write() , которая вписывает данные из буфера в сокет

Socket клиент

Перейдём ко второй программе — клиенту.

Код её будет выглядеть следующим образом:

Кратко рассмотрим его:

  • аналогично серверу — создаём сокет
  • в структуру sockaddr_in с именем serv_addr заносятся протокол, порт (5000) и адрес сервера (первый аргумент — argv[1] )
  • функция connect() пытается установить соединение с хостом, используя данные из структуры serv_addr

И в конце-концов — клиент с помощью read() получает данные из своего сокета, в который поступают данные от сокета на сервере.


Собираем клиент, и пробуем подключиться к нашему серверу:

C++ Привет, TCP-сервер

пример

Позвольте мне начать с того, что вы должны сначала посетить Руководство Beej по программированию в сети и дать ему быстрое чтение, что объясняет большую часть этого материала немного более подробно. Здесь мы создадим простой TCP-сервер, который скажет «Hello World» всем входящим соединениям, а затем закроет их. Другое дело, что сервер будет обмениваться данными с клиентами итеративно, что означает один клиент за раз. Обязательно проверьте соответствующие справочные страницы, так как они могут содержать ценную информацию о каждом вызове функций и структурах сокетов.

Мы запустим сервер с портом, поэтому мы также примем аргумент для номера порта. Давайте начнем с кода —

Простой http-сервер на C++ Builder (Indy)

Сегодня мы будем писать на C++ Builder простейший http-сервер, но для начала, как всегда, немного теории (просто чтобы было понятно, что мы вообще собираемся делать).

Итак, http (HyperText Transfer Protocol) — протокол передачи гипертекста (например, html-страничек). Это протокол прикладного уровня, из набора протоколов TCP/IP. В настоящий момент он является основным протоколом передачи данных в интернете (ну просто потому, что большая часть интернета — это и есть так или иначе сгенерированные html-странички).

Протокол http заточен под «клиент-серверную» архитектуру и построен на основе обмена между клиентом и сервером специальными сообщениями. При этом сообщение клиента называется request (запрос), а сообщение сервера — response (ответ). Подробно почитать про этот протокол можно вот здесь: RFC2616, а я объясню «на пальцах» только базовые вещи.

Когда мы хотим зайти на какую-то страничку в интернете, набираем в адресной строке её адрес и жмём Enter, фактически происходит следующее:

1) наш браузер (это и есть программа-клиент) отправляет серверу, на котором расположена нужная страничка, запрос (request) по протоколу http (именно поэтому адреса всех страничек начинаются с «http://», — мы указываем браузеру каким нужно пользоваться протоколом);

2) программа-сервер, расположенная на сервере, этот запрос обрабатывает и отсылает нашему браузеру ответ, содержащий нужную информацию.

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

Вот именно такую программу мы и собираемся написать. Всё, что эта программа будет уметь — это обрабатывать входящие по протоколу http GET-запросы (это единственный тип запросов, который любой http-сервер должен уметь обрабатывать в обязательном порядке) и отдавать в теле ответа (сообщения в http состоят из стартовой строки, заголовков и тела) содержимое одной единственной странички. Для начальных экспериментов этого вполне достаточно, а дальше можете копать сами.

Самым простым вариантом реализации http-сервера в среде C++ Builder является использование готового компонента IdHTTPServer. Это в общем-то уже готовый http-сервер, умеющий разбирать «по косточкам» входящие запросы и формировать правильно структурированные ответы. Всё, что нам остаётся сделать — это проанализировать динамически создаваемую этим компонентом структуру RequestInfo (содержащую различные параметры запроса: команду, заголовки, путь, имя хоста …) и, в зависимости от содержания этой структуры, заполнить структуру ResponseInfo (содержащую параметры ответа: код ответа, текст ответа, заголовки, тело ответа…).

Итак, создаём новый проект и кидаем на форму компонент IdHTTPServer (его можно найти на вкладке Indy Servers):

Все его свойства пока оставим по умолчанию (только свойство active установим в true, чтобы http-сервер сразу запускался при старте приложения (можно прикрутить для старта и останова сервера отдельные кнопки и менять значение свойства active в обработчиках событий OnClick этих кнопок).

Теперь добавим на форму компонент Memo (его содержимое мы будем передавать в теле ответа) и впишем туда код тестовой html-странички:

Почти всё, осталось только написать обработчик события OnCommandGet (что мы будем делать, получив GET-запрос) компонента IdHTTPServer.

Давайте напишем его следующим образом:

void __fastcall TForm1::IdHTTPServer1CommandGet(TIdPeerThread *AThread, TIdHTTPRequestInfo *RequestInfo, TIdHTTPResponseInfo *ResponseInfo) < if(RequestInfo->Document==»/» || RequestInfo->Document==»/index.html») < ResponseInfo->ResponseNo=200; ResponseInfo->ContentText=Memo1->Lines->Text; > else < ResponseInfo->ResponseNo=404; > >

Параметр RequestInfo->Document, как вы уже поняли, содержит путь запрошенного документа. Таким образом мы написали, что будем отдавать тестовую страничку при обращении к корню или к странице index.html, а во всех остальных случаях будем возвращать код 404 (страница не найдена).

Проверим, что получилось. Компилируем и запускаем наше приложение. Далее запускаем браузер и пишем в командной строке localhost или localhost/index.html. В итоге видим нашу страничку (или не видим её, если введём любой другой адрес):

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

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

Теперь давайте добавим в запрос каких-нибудь параметров. Параметры GET-запроса указываются в адресной строке, после указания пути к файлу. Выглядит это следующим образом: ?parameter1_name[=value1]&parameter2_name[=value2]

Если свойство ParseParams компонента IdHTTPServer уcтановить в true, то параметры запроса будут автоматически распарсены в динамическую структуру. Получить их можно следующим образом:

  • общее количество распарсенных параметров: RequestInfo->Params->Count
  • имя i-го параметра: RequestInfo->Params->Names[i]
  • значение параметра с именем Name: RequestInfo->Params->Values[«Name»]

В 8-м Indy похоже есть глюк с распарсиванием. Для правильного распарсивания обязательно должны присутствовать и имя, и значение параметра, в противном случае количество параметров будет определено верно, но имена параметров, у которых не указано значение, окажутся пустыми.

Строка нераспарсенных параметров заносится в RequestInfo->UnparsedParams независимо от состояния флага ParseParams.

Итак, добавляем обработку параметров. Пусть, например, если параметр Show равен 1, — в страницу добавляется строка «Первое действие», а если он равен 2, то в страницу добавляется строка «Второе действие». Для этого изменим код обработчика следующим образом (не забудьте изменить свойство ParseParams компонента IdHTTPServer на true):

Есть ли литература по серверам на C++?

Пишу сервер, но что-то не очень красиво пишется. Что можно почитать по архитектуре и шаблонам проектирования?

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

Unit-тесты делаю, но вдруг не вижу что-то. Использую Qt, Tcp, Buffer Protocols, но наверно не важно.

Если тебе нужно «по архитектуре и шаблонам проектирования», то первое что приходит на ум — ACE Шмидта.

В электронке вроде этих книг не встречал.

Так и будет страшновато пока не сядеш и не начнеш писать)

alexes
> Пишу сервер, но что-то не очень красиво пишется. Что можно почитать по
> архитектуре и шаблонам проектирования?
На C++ сервера писать — это бороться с кучей проблем. В том числе и крестопроблемы, и бустопроблемы.
Выбирай платформу для серверов заточенную. Например, Эрланг, или там Яву, можно ещё C# ;)

kvakvs
> На C++ сервера писать — это бороться с кучей проблем. В том числе и
> крестопроблемы, и бустопроблемы.
> Выбирай платформу для серверов заточенную. Например, Эрланг, или там Яву, можно
> ещё C# ;)
Я конечно уважаю вашу любовь к Erlang`у и прочим. Но они пригодны далеко не для всех задач (недавно смотрел сравнения языков, так тотже erlang плетётся по скорости где-то в хвосте с 50-ти кратным отстованием). Если человек просит книги по С++, то вполне возможно, что ему именно C++ и надо.

Bishop
> erlang плетётся по скорости где-то в хвосте с 50-ти кратным отстованием).
Не всегда написание сервера требует СКОРОСТЬ, даже если ему для сервера надо будет писать всякие 3д поиски и А* поиск путей итд, вопрос решается с помощью С++ модуля к Эрлангу, в остальных местах бутылочного горлышка по производительности не возникает. Чаще проблемы в С++ возникают от множества открытых сокетов и тредов, которые это обслуживают, разные мьютексопроблемы итд. Эрланг с этим успешно борется.
> Если человек просит книги по С++, то вполне возможно, что ему именно C++ и надо.
Возможно. А ещё возможно, он не знает, что кроме С++ есть целый новый мир ;) Пусть он не выберет Эрланг сейчас, но хоть запомнит слово, в будущем присмотрится поближе.

Bishop
> Если человек просит книги по С++, то вполне возможно, что ему именно C++ и надо.
Если человек просит книгу по С++, то он просто не знает какой инструмент нужен для решения его задачи :-7

kvakvs
> Возможно. А ещё возможно, он не знает, что кроме С++ есть целый новый мир ;)
Я большую часть времени программирую на Ruby. Там нового каждый день появляется.

Раньше я всячески избегал C++, пока не начал использовать на полную все доступные библиотеки. Насчет трудностей отладки, что сейчас множество инструментов типа pvs-studio и я с удовольствием их преобрету, если получится что-то серьёзное.

И вообще, пока пишешь что-то для себя, в свободное время, лучше брать для разбора самое сложное.

C++ — Сервер на c++

Использование языка C/C++ в разработке

четверг, 7 мая 2009 г.

Написание своего HTTP сервера с использованием libevent

Библиотека libevent содержит в себе простейший асинхронный HTTP сервер, который можно
без особого труда встроить в собственное приложение для обслуживания HTTP запросов.

Для использования этой возможности достаточно добавить в код следующее:

1. Подключить заголовочный файл :

2. Инициализировать базу событийного движка:

3. Инициализировать HTTP сервер:

4. Указать, на каком сокете слушать подключения:

5. Выставить callback’и на запросы. Можно добавлять на каждый URI свой обработчик:

6. Выставить обработчик на остальные запросы:

7. Запустить цикл обработки запросов:

Реализация HTTP сервера является потоко-безопасной (thread safe).

Есть еще один нюанс: настоятельно рекомендуется игнорировать сигнал SIGPIPE.
Делается это следующим вызовом:

Описание всех функция для работы с HTTP протоколом
можно найти в документации.

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

Makefile для сборки:

А вот результаты тестирования Apache Benchmark:

Сервер был запущен локально на моем ноутбуке, на сервере результаты конечно же получше.
Но для сотни параллельных запросов – это неплохо.

Цукерберг рекомендует:  Яндекс - Яндекс Светофор
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих