Рейтинг@Mail.ru
[Войти] [Зарегистрироваться]

Наши друзья и партнеры

UnixForum
Беспроводные выключатели nooLite купить дешевый 
компьютер родом из Dhgate.com Лучшая автошкола Самары по рейтингу ГИБДД тут

Lines Club

Ищем достойных соперников.

Библиотека сайта или "Мой Linux Documentation Project"

Интерфейс прикладного программирования 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


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

Комментарии отсутствуют