Библиотека сайта 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