Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Интерфейс прикладного программирования Socket API, Часть 1: Создание собственного сервера

Оригинал: "Creating Your Own Server: The Socket API, Part 1"
Автор: Pankaj Tanwar
Дата публикации: August 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июль 2012 г.

В этой серии статей, передназначенных для новичков сетевого программирования (знание языка C является обязательным условием), мы узнаем, как с помощью интерфейса прикладного программирования Socket API системы UNIX создавать сервера и сетевые клиентские программы. Мы начнем с создания простых программ типа "клиент-сервер", а затем попробуем сделать что-нибудь более сложное. Мы также попытаемся понять, как работают различные сервера. Я постарался включить в опиание множество подробностей, но если вы обнаружите, что некоторая информация отсутствует, то, пожалуйста, не стесняйтесь сообщить мне об этом в комментариях.

Поскольку мы при сетевом программировании рассматриваем сокеты, новички должны сначала разобраться с уровнями модели OSI и с протоколами, используемыми на этих уровнях. Каждый уровень в этой модели отвечает за выполнение определенной работы, что в результате делает возможным передачу данных по сети. В каждом уровне происходит абстрагирование работы, выполняемой на более низких уровнях, и представление этой работы на уровень, находящийся выше. Если вы не знакомы с эталонной моделью взаимодействия открытых систем ISO OSI (Open Systems Interconnection) Reference Model, я рекомендую о ней почитать. Хорошей отправной точкой является Википедия.

Здесь мы сосредоточимся на сессионном уровне (в котором происходит создание сессий и поддержка работы с ними) и транспортном уровне, на котором обеспечивает надежная или ненадежная передача данных от отправителя к получателю. Есть несколько протоколов - TCP (для надежных соединений), UDP (для ненадежных соединений) и SCTP (расширенный протокол с возможностью множественного подключения). Информацию о протоколах TCP / IP, пожалуйста, смотрите здесь и здесь.

Протокол Transmission Control Protocol (TCP)

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

Протокол TCP обладает рядом важных особенностей. Это надежный протокол (в отличие от не обрабатывающего соединения протокола UDP, который мы рассмотрим в следующих статьях). После того, как пакет будет передан, протокол ждет подтверждение о приеме; если оно не вернулось, то пакет ретранслируется несколько раз (в зависимости от реализации). Если данные не могут быть переданы, протокол уведомляет пользователя и закрывает соединение.

В протоколе TCP также определяется, как долго ждать подтверждения, - для этого используется оценочное значение RTT (Round Trip Time — время прохождения маршрута), задаваемое между сервером и клиентом. В протоколе также происходит назначение сегментам порядковых номеров, так что если сегменты принимаются в неправильной последовательности, их можно будет переупорядочить на принимающей стороне. Благодаря этому можно игнорировать дублирующие сегменты (передаваемые повторно из-за задержек). В протоколе TCP осуществляется управление потоком данных: принимающая сторона может сообщить отправителю, сколько байтов данных будет принято, так что медленно работающий приемник не будет выведен из строя слишком большим количеством данных.

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

Простые сервера

Теперь для того, чтобы разобраться с сокетами, используемыми в интернете, давайте создадим простой сервер (server.c). Первоначально наш код будет создан для версии протокола IPv4, но в последующих статьях мы рассмотрим версию протокола IPv6, а затем перейдем к коду, не зависящему от версии протокола.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main()
{
        int sfd, cfd;
        int ch='k';
        struct sockaddr_in saddr, caddr;
 
        sfd= socket(AF_INET, SOCK_STREAM, 0);
        saddr.sin_family=AF_INET;           /* Set Address Family to Internet */
        saddr.sin_addr.s_addr=htonl(INADDR_ANY);    /* Any Internet address */
        saddr.sin_port=htons(29008);            /* Set server port to 29008 */
                            /* select any arbitrary Port >1024 */
        bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr));
        listen(sfd, 1);
        while(1) {
                printf("Server waitingn");
                cfd=accept(sfd, (struct sockaddr *)NULL, NULL);
                if(read(cfd, &ch, 1)<0) perror("read");
                ch++;
                if(write(cfd, &ch, 1)<0) perror("write");
                close(cfd);
        }
}

О коде

Первое, на что нужно взглянуть, это структура сstruct sockaddr_in. Эта структура используется для хранения интернет адреса (IP) в поле sin_addr, которое является структурой типа struct in_addr и в которой может храниться 32-битное беззнаковое целочисленное значение. Номер порта хранится в поле sin_port, это беззнаковое 16-битное целое (посольку номер порта должен быть меньше 65536).

Далее, давайте посмотрим на вызов функции socket()includes требуется указать sys/types.h и sys/socket.h):

int socket(int domain, int type, int protocol);

Если обращение к socket() будет успешным, то будет возвращен дескриптор, который будет использован на завершающей стадии соединения. В первом аргументе, domain, определяется домен соединения - семейство протоколов, которые будут использоваться при соединении. Согласно sys/sockets.h, это следующие протоколы:

ИмяНазначение
AF_UNIX, AF_LOCALЛокальное соединение
AF_INETИнтернет протокол IPv4
AF_INET6Интернет протокол IPv6
AF_IPXПротоколы IPX — Novell
AF_NETLINKПользовательский интерфейс с ядром
AF_X25Протокол ITU-T X.25 / ISO-820
AF_AX25Радиолюбительский протокол AX.25
AF_ATMPVCДоступ к данным пластиковой карточки (ATM PVC)
AF_APPLETALKПротокол AppleTalk
AF_PACKETНизкоуровневый пакетный интерфейс

AF является сокращением от Address Family — семейство адресов. Здесь мы используем AF_INET - интернет-протокол IPv4. В следующем аргументе, type, указывается тип соединения; может использоваться один из следующих вариантов:

SOCK_STREAMПоследовательный, надежный, двусторонний, с использованием потока байтов (TCP, SCTP и т.д.)
SOCK_DGRAMДатаграммы — без соединений, ненадежный (UDP)
SOCK_SEQPACKETПоследовательный, надежный, двусторонний, с использованием при передаче датаграмм с фиксированной максимальной длиной (SCTP)
SOCK_RAWНепосредственный доступ к сетевому протоколу (протоколы транспортного уровня не требуются)

В аргументе protocol определяется протокол, который будет использоваться совместно с сокетом. Как правило, для поддержки конкретного типа сокета в заданном семействе протоколов существует только один протокол (который выше был указан в скобках). В таком случае, этот аргумент равен 0.

Далее давайте поместим адрес в поле sin_addr, так как это было показано выше. Когда происходит обращение к функции socket(), то создается сокет, но ему адрес не назначается. Поэтому нам нужна функция bind():

Эта функция используется для того, чтобы связать дескриптор сокета sockfd с адресом addr; а в addrlen указывается длина адреса. Эта операция называется назначением имени сокету. Затем будем слушать сокет с помощью listen():

int listen(int sockfd, int backlog);

С помощью вызова listen() соответствующий сокет помечается демоном sockfd как пассивный сокет — т. е. такой, который будет использоваться для приема входящих подключений. В качестве типа сокета должен быть SOCK_STREAM или SOCK_SEQPACKET, т.е. должно обеспечиваться надежное соединение. В аргументе backlog определяется максимальная длина очереди ожидающих соединений с sockfd. Если очередь превысит указанное значение, то клиентской программе будет отказано в соединении.

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

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Системный вызов accept() используется с типами сокетов SOCK_STREAM и SOCK_SEQPACKET. Он извлекает первый запрос на соединение из очереди запросов, ожидающих соединений с демоном sockfd, слушающим сокеты, создает сокет нового соединения и возвращает новый дескриптор, относящийся к этому сокету — в нашей программе это cfd.

Новый сокет не находится в состоянии прослушивания. Исходный сокет sockfd не оказывает влияние на этот вызов. В аргументе addr находится адрес удаленного компьютера, с которым мы связываемся, но т. к. заранее мы не знаем адрес клиента, здесь это значение равно NULL.

Затем давайте с помощью операции read() прочитаем из дескриптора символ (отправленный клиентом на сервер), увеличим его значение на единицу и с помощью команды write() запишем в дескриптор его новое значение, которое будет отправлено клиенту. Затем закроем дескриптор с помощью вызова close().

Обработка ошибок, которую я выбрал, базируется на том, что в случае неудачи эти функции возвращают отрицательные значения; для того, чтобы отобразить сообщение о номере ошибки, я использую функцию perror().

Клиентская программа

И теперь клиентская программа (client.c). Этот клиент посылает символ на сервер, работающий на порту 29008 (или любом другом произвольном порту):

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc, char* argv[])
{
    int cfd;
    struct sockaddr_in addr;
    char ch='r';
    cfd=socket(AF_INET, SOCK_STREAM, 0);
    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=inet_addr("127.0.0.1"); /* Check for server on loopback */
    addr.sin_port=htons(29008);
    if(connect(cfd, (struct sockaddr *)&addr,
    sizeof(addr))<0) {
        perror("connect error");
        return -1;
    }
    if(write(cfd, &ch, 1)<0) perror("write");
    if(read(cfd, &ch, 1)<0) perror("read");
    printf("nReply from Server: %cnn",ch);
    close(cfd);
    return 0;
}

Порядок работы клиентской программы аналогичен порядку работы сервера. Первое отличие заключается в том, что в sin_addr указывается интернет-адрес сервера (адрес localhost, указывающий на ту же самую машину).

Далее, вместо прослушивания вызывается системный вызов connect(), с помощью которого выполняется подключение sockfd по адресу, указанному в addr. Возвращаемый дескриптор будет использоваться для связи с указанным адресом.

Затем в программе мы для того, чтобы отправить символ на сервер и получить символ, используем команды write() и read(), а затем - закрываем дескриптор.

Запуск программ

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

cc server.c -o server
cc client.c -o client

Затем, запускаем программы:

./server &
./client

Чтобы было проще следить за работой каждой из программ, запускайте их в разных терминалах. На рис.1 показаны данные, выдаваемые на терминал сервером, а на рис.2 — клиентской программой.

Рис.1: Работающий сервер

Рис.2: Данные, выдаваемые клиентской программой

Хорошее начало, не прада ли? В следующей статье мы рассмотрим, как переписать обе эти программы для протокола IPv6 и будем двигаться дальше к UDP. И да, FOSS — это круто!

Продолжение серии статей о Socket API