close

Вход

Забыли?

вход по аккаунту

?

Kaluzhnyi

код для вставкиСкачать
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное государственное автономное
образовательное учреждение высшего образования
САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
АЭРОКОСМИЧЕСКОГО ПРИБОРОСТРОЕНИЯ
В. П. Калюжный Л. А. Осипов
СЕТЕВОЕ ПРОГРАММИРОВАНИЕ
Учебное пособие
УДК 004.9
ББК 32.973.26.04
К17
Рецензенты:
доктор технических наук, профессор С. А. Яковлев;
доктор технических наук, профессор С. И. Зиатдинов
Утверждено
редакционно-издательским советом университета
в качестве учебного пособия
Калюжный, В. П.
К17 Сетевое программирование: учеб. пособие / В. П. Калюжный, Л. А. Осипов. – СПб.: ГУАП, 2018. – 53 с.
ISBN 978-5-8088-1256-7
Изложены основы использования интерфейса Socket API для организации клиент – серверного взаимодействия в IP сетях. Материал пособия базируерся на применении языка СИ, возможностей ОС Linux, cистемных вызовов
этой операционной системы, и ее библиотек. Особое внимание уделено примерам и заданиям, что позволяет использовать их в качестве основы для лабораторного практикума по дисциплине. Приложение содержит описание функций библиотеки Socket API с примерами и коментариями.
Предназначено для студентов направления 230200 (Информационные системы) по специальностям: 230201 (Информационные системы и технологии в
бизнесе); 230203 (Информационные системы в дизайне ); 230204 (Информационные системы в медиаиндустрии).
УДК 004.9
ББК 32.973.26.04
ISBN 978-5-8088-1256-7
© Калюжный В. П., Осипов Л. А, 2018
© Санкт-Петербургский государственный
университет аэрокосмического
приборостроения, 2018
СПИСОК СОКРАЩЕНИЙ
ОС UNIX – операционная система
ОС Linux – операционная система
API – application programming interface
GNU – свободная unix подобная операционная система
TCP/IP – стек протоколов
UDP – протокол пользовательских дейтаграмм
TCP – протокол управления передачей
IPv.4 – Internet протокол четвертой версии
Ipv.6 – Internet протокол шестой версии
URL – Uniform Resource Locator (единообразный локатор определитель местонахождения ресурса)
3
ПРЕДИСЛОВИЕ
Cетевое программирование имеет ряд особенностей. Главным
из них является то, что приходиться обеспечивать взаимодействие
между программами, выполняющимися одновременно на разных
сетевых компьютерах, расположенных удаленно. Для этого небходимо обеспечить их правилами такого взаимодействия. Это означает, что требуется дополнительно решать вопросы синхронизации и
управления ресурсами. При этом могут возникать различного рода проблемы, например, взаимоблокировки процессов и зависания
программ. При тщательном проектировании приложений большинство таких проблем можно избежать. Этому способствует правильная синхронизация процессов и рациональное распределение
ресурсов. Материал пособия опирается на использование языка CИ,
возможностей ОС Linux, cистемных вызовов этой операционной системы, и ее библиотек. В первом разделе приведено описание основных функций интерфейса Socket API. Второй раздел пособия содержит задания, выполнение которых может послужить основой для
приобретения навыков сетевого программирования. В третьем разделе приведены несложные примеры с подробным описанием клиентских и серверных программ.
В приложении A cодержится подробное описание основных
функций библиотеки Sockets API c комментариями.
4
1. ОСНОВЫ ИНТЕРФЕЙСА SOCKET API
Socket (гнездо, разъем) – абстрактное понятие, используемое для
обозначения в прикладной программе конечной точки связи с коммуникационной средой, образованной компьютерной сетью. Socket
является средством подключения прикладной программы к сетевому интерфейсу локального узла сети. Socket-интерфейс представляет собой набор системных вызовов и/или библиотечных функций
языка программирования СИ, разделенных на четыре группы:
1) локального управления;
2) установления связи;
3) обмена данными (ввода/вывода);
4) закрытия связи.
Ниже рассматривается подмножество функций socket-интерфейса, достаточное для написания сетевых приложений, реализующих модель «клиент-сервер» в режиме с установлением соединения.
Функции локального управления используются, главным образом, для выполнения подготовительных действий, необходимых
для организации взаимодействия двух программ-партнеров. Эти
функции носят такое название, поскольку их выполнение носит локальный для программы характер.
1.1. Создание socket’а
Создание socket’а осуществляется следующим системным вызовом
#include <sys/socket.h>
int socket (domain, type, protocol)
Аргумент domain задает используемый для взаимодействия набор протоколов (вид коммуникационной области), для стека протоколов TCP/IP он должен иметь символьное значение AF_INET
(определено в sys/socket.h).
Аргумент type задает режим взаимодействия:
– SOCK_STREAM – с установлением соединения;
– SOCK_DGRAM – без установления соединения.
Аргумент protocol задает конкретный протокол транспортного
уровня (из нескольких возможных в стеке протоколов). Если этот
аргумент задан равным 0, то будет использован протокол «по умолчанию» (TCP для SOCK_STREAM и UDP для SOCK_DGRAM при
5
использовании комплекта протоколов TCP/IP). При удачном завершении своей работы данная функция возвращает дескриптор
socket’а – целое неотрицательное число, однозначно его идентифицирующее. Дескриптор socket’а аналогичен дескриптору файла ОС
UNIX. При обнаружении ошибки в ходе своей работы функция возвращает число «–1».
1.2. Связывание socket’а
Для подключения socket’а к коммуникационной среде, образованной сетью, необходимо выполнить системный вызов bind, определяющий в принятом для сети формате локальный адрес канала
связи со средой. В сетях TCP/IP socket связывается с локальным
портом. Системный вызов bind имеет следующий синтаксис:
#include
#include
#include
int bind
addrlen);
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
int sockfd, const struct sockaddr*addr, sosklen _ t
Аргумент sockfd задает дескриптор связываемого socket’а.
Аргумент onst struct sockaddr*addr должен указывать на структуру данных, содержащую локальный адрес, приписываемый
socket’у. Для сетей TCP/IP такой структурой является sockaddr_in.
Аргумент sosklen_t addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr. Структура sockaddr_in используется несколькими системными вызовами и функциями socketинтерфейса и определена в include-файле in.h следующим образом:
struct sockaddr _ in {
short sin _ family;
u _ short sin _ port;
struct in _ addr sin _ addr;
char sin _ zero[8];
};
Поле sin_family определяет используемый формат адреса (набор
протоколов), в нашем случае (для TCP/IP) оно должно иметь значение AF_INET.
Поле sin_addr содержит адрес узла сети.
Поле sin_port содержит номер порта на узле сети.
Поле sin_zero не используется.
6
Определение структуры in_addr (из того же include-файла) таково:
struct in _ addr {
union {
u _ long S _ addr;
/*
другие (не интересующие нас)
члены объединения
*/
} S _ un;
#define s _ addr S _ un.S _ addr
};
Структура sockaddr_in должна быть полностью заполнена перед
выдачей системного вызова bind. При этом, если поле sin_addr.s_
addr имеет значение INADDR_ANY, то системный вызов будет привязывать к socket’у адрес локального узла сети. В случае успеха
bind возвращает 0, в противном случае – «–1».
1.3. Ожидание установления связи
Для установления связи «клиент-сервер» используются системные вызовы listen и accept (на стороне сервера), а также connect (на
стороне клиента). Для заполнения полей структуры socaddr_in, используемой в вызове connect, обычно используется библиотечная
функция gethostbyname, транслирующая символическое имя узла
сети в его адрес.
Системный вызов listen выражает желание выдавшей его программы сервера ожидать запросы к ней от программ-клиентов и
имеет следующий вид:
#include <sys/socket.h>
#include <sys/types.h>
int listen (int sockfd, int backlog);
Аргумент sockfd задает дескриптор socket’а, через который программа будет ожидать запросы к ней от клиентов. Socket должен
быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind. Аргумент backlog
определяет максимальную длину очереди входящих запросов на
установление связи. Если какой-либо клиент выдаст запрос на установление связи при полной очереди, то этот запрос будет отвергнут.
Признаком удачного завершения системного вызова listen служит
нулевой код возврата.
7
1.4. Запрос на установление связи
Для обращения программы-клиента к серверу с запросом на
установление логического соединения используется системный вызов connect, имеющий следующий вид:
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, const struct sockaddr *addr soclen _ t _
addrlen);
Аргумент sockfd задает дескриптор socket’а, через который программа обращается к серверу с запросом на соединение. Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind.
Аргумент const struct sockaddr *addr должен указывать на структуру данных, содержащую адрес, приписанный socket’y у программы-сервера, к которой делается запрос на соединение. Для сетей
TCP/IP такой структурой является sockaddr_in. Для формирования
значений полей структуры sockaddr_in удобно использовать функцию gethostbyname.
Аргумент soclen_t_addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr. Для того, чтобы запрос
на соединение был успешным, необходимо, по крайней мере, чтобы программа-сервер выполнила к этому моменту системный вызов
listen для socket’а с указанным адресом.
При успешном выполнении запроса системный вызов connect
возвращает 0, в противном случае – «–1» (устанавливая код причины неуспеха в глобальной переменной errno). Если к моменту выполнения connect используемый им socket не был привязан к адресу
посредством bind, то такая привязка будет выполнена автоматически. В режиме взаимодействия без установления соединения необходимости в выполнении системного вызова connect нет. Однако,
его выполнение в таком режиме не является ошибкой – просто меняется смысл выполняемых при этом действий: устанавливается
адрес «по умолчанию» для всех последующих пакетов.
1.5. Прием запроса на установление связи
Для приема запросов от программ-клиентов на установление связи в программах-серверах используется системный вызов accept,
имеющий следующий вид:
#include <sys/types.h>
#include <sys/socket.h>
8
#include <netinet/in.h>
int acceptn(int sockfd, struct sockaddr *addr, soclken _ t
*addrlen)'
Аргумент sockfd задает дескриптор socket’а, через который программа-сервер получила запрос на соединение (посредством системного запроса listen).
Аргумент struct sockaddr *addr должен указывать на область памяти, размер которой позволял бы разместить в ней структуру данных, содержащую адрес socket’а программы-клиента, сделавшей
запрос на соединение. Никакой инициализации этой области не
требуется.
Аргумент soclken_t *addrlen должен указывать на область памяти в виде целого числа, задающего размер (в байтах) области памяти, указываемой аргументом struct sockaddr *addr. Системный вызов accept извлекает из очереди, организованной системным вызовом
listen, первый запрос на соединение и возвращает дескриптор нового (автоматически созданного) socket’а с теми же свойствами, что и
socket, задаваемый аргументом s. Этот новый дескриптор необходимо использовать во всех последующих операциях обмена данными.
Кроме того после удачного завершения accept:
– область памяти, указываемая аргументом addr, будет содержать структуру данных (для сетей TCP/IP это sockaddr_in), описывающую адрес socket’а программы клиента, через который она сделала свой запрос на соединение;
– целое число, на которое указывает аргумент p_addrlen, будет
равно размеру этой структуры данных. Если очередь запросов на момент выполнения accept пуста, то программа переходит в состояние
ожидания поступления запросов от клиентов на неопределенное время (хотя такое поведение accept можно и изменить). Признаком неудачного завершения accept служит отрицательное возвращенное значение (дескриптор socket’а отрицательным быть не может).
Системный вызов accept используется в программах-серверах,
функционирующих только в режиме с установлением соединения.
1.6. Формирование адреса узла сети
Для получения адреса узла сети TCP/IP по его символическому
имени используется библиотечная функция:
#include <netdb.h>
extern int h _ errno;
struct hostent *gethostbyname(const char *name);
9
Аргумент 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 для формирования запроса на установлениt
соединения с программой-сервером на этом узле, рассматривается
в приложении.
1.7. Функции обмена данными
В режиме с установлением логического соединения после удачного выполнения пары взаимосвязанных системных вызовов connect
(в клиенте) и accept (в сервере) становится возможным обмен данными.
Этот обмен может быть реализован обычными системными вызовами read и write, используемыми для работы с файлами (при
этом вместо дескрипторов файлов в них задаются дескрипторы
socket’ов). Кроме того могут быть дополнительно использованы системные вызовы send и recve ориентированные специально на работу с socket’ами. Для обмена данными в режиме без установления логического соединения используются, как правило, системные вызовы sendto и recvfrom. Sendto позволяет специфицировать вместе
10
с передаваемыми данными (составляющими дейтаграмму) адрес их
получателя. Recvfrom одновременно с доставкой данных получателю информирует его и об адресе отправителя.
1.8. Посылка данных
Для посылки данных партнеру по сетевому взаимодействию используется системный вызов send, имеющий следующий вид:
#include <sys/types.h>
#include <sys/socket.h>
int send (int sockfd, const void *buf, size _ t len, int flags)
Аргумент sockfd задает дескриптор socket’а, через который посылаются данные.
Аргумент const void указывает на область памяти, содержащую
передаваемые данные.
Аргумент size_t len задает длину (в байтах) передаваемых данных.
Аргумент int flags модифицирует исполнение системного вызова send. При нулевом значении этого аргумента вызов send полностью аналогичен системному вызову write. При успешном завершении send возвращает количество переданных из области, указанной
аргументом buf, байт данных. Если канал данных, определяемый
дескриптором s, оказывается «переполненным», то send переводит
программу в состояние ожидания до момента его освобождения.
1.9. Получение данных
Для получения данных от партнера по сетевому взаимодействию
используется системный вызов recv, имеющий следующий вид:
#include <sys/socket.h>
#include <sys/types.h>
int recv (int sockfd, void *buf, size _ t len, int flags);
Аргумент sockfd задает дескриптор socket’а, через который принимаются данные.
Аргумент *buf указывает на область памяти, предназначенную
для размещения принимаемых данных. Аргумент len задает длину (в байтах) этой области. Аргумент size_t len модифицирует исполнение системного вызова recv. При нулевом значении этого аргумента вызов recv полностью аналогичен системному вызову read.
При успешном завершении recv возвращает количество принятых
в область, указанную аргументом buf, байт данных. Если канал
11
данных, определяемый дескриптором s, оказывается «пустым»,
то recv переводит программу в состояние ожидания до момента появления в нем данных.
1.10. Функции закрытия связи
Для закрытия связи с партнером по сетевому взаимодействию
используются системные вызовы close и shutdown. Для закрытия
ранее созданного socket’а используется обычный системный вызов
close, применяемый в ОС UNIX для закрытия ранее открытых файлов и имеющий следующий вид:
#include <unistd>
it close (inf fd)
Аргумент fd задает дескриптор ранее созданного socket’а.
Однако в режиме с установлением логического соединения (обеспечивающем, как правило, надежную доставку данных) внутрисистемные механизмы обмена будут пытаться передать/принять данные, оставшиеся в канале передачи на момент закрытия socket’а.
На это может потребоваться значительный интервал времени, неприемлемый для некоторых приложений. В такой ситуации необходимо использовать описываемый далее системный вызов shutdown.
Для «экстренного» закрытия связи с партнером (путем «сброса» еще не переданных данных) используется системный вызов
shutdown, выполняемый перед close и имеющий следующий вид:
#include <sys/socket.h>
int shutdown (int sockfd, int how);
Аргумент sockfd задает дескриптор ранее созданного socket’а.
Аргумент how задает действия, выполняемые при очистке системных буферов socket’а:
– «0» сбросить и далее не принимать данные для чтения из
socket’а;
– «1» – сбросить и далее не отправлять данные для посылки через socket;
– «2» – сбросить все данные, передаваемые через socket в любом
направлении.
12
2. ЦИКЛ ЗАДАНИЙ
«СЕТЕВОЕ ПРОГРАММИРОВАНИЕ НА БАЗЕ Sockets API»
Цель цикла заданий
Получение навыков разработки сетевых приложений, с использованием библиотеки Sockets API.
Замечания по выполнению заданий
Разработать сетевое приложение в соответствии с вариантом задания.
Дополнительную информацию по использованию библиотеки
Socket API можно найти в приложении A.
Отчет должен содержать:
– цель работы;
– вариант задания на работу;
– тексты программ клиента и сервера на языке СИ (копии экрана);
– результаты компиляции и исполнения программ клиента и
сервера (копии экрана).
Задание 1. Разработать сетевое приложение, состоящее из клиентской и серверной програм. После запуска серверная программа
должна выводить на экран сообщение о готовности сервера к клиентскому запросу. Клиентская программа должна передать серверу символ. Сервер, должен изменить символ и передать измененный
символ клиенту.
Задание 2. Разработать сетевое приложение, состоящее из клиентской и серверной програм и взаимодействующим на одном
компьютере через localhost. После запуска серверной программы
и установлении соединения, cервер должен отправить сообщение
«Кто ты» Клиент должен ответить: «Я твой клиент».
Задание 3. Разработать сетевое приложение, состоящее из клиентской и серверной програм. Серверная программа должна быть
в состоянии умножать два целых числа. В ответ на запрос клиентской программы, передающей эти числа серверной программе,
cервер должен вернуть ей результат.
Задание 4. Разработать сетевое приложение, состоящее из клиентской и серверной програм. Серверная программа в ответ на запрос клиентской программы должна возвращать клиенту текущее
время.
Получить вариант задания у преподавателя.
13
Порядок выполнения:
1. Загрузить Linux.
2. Запустить терминал.
3. Перейти в каталог tmp (сd/tmp).
4. В дериктории tmp cоздать пустой файл server.c (touch server.c).
5. Подключить редактор mc (Midnight Commander) для набора
текста программы сервера (mcedit server.c).
6. Набрать и отредактировать программу сервера server.c
7. Сохранить текст программы в дериктории tmp (F2).
8. Выполнить пункты 3–7 для создания и сохранения программы клиента client.c.
9. Нажать дважды Esc, чтобы перейти в командную строку.
10.  Cкомпилировать программу server.c (gcc server.c –o server).
11.  Скомпилировать программу client.c (gcc client.c –o client).
12.  Убедиться, что в рабочем детиктории появились файлы
server и client (ls –la).
13.  Запустить на выполнение программу server (./ server ввод).
14.  Запустить на выполнение программу client (./client ввод).
15.  Убедиться, что получен правильный результат.
16.  Показать этот результат преподавателю.
17.  Вставить флэш накопитель.
18.  Открыть файловый менеджер.
19.  Cохранить тексты программ из /tmp в флэш память (или при
необходимости записать их в /tmp из флэш памяти если набор текстов программ и их редактирование не были завершены).
20. Cделать копию экрана с текстами программ клиента и сервера.
21. Отключить флэш накопитель в файловом менеджере и извлечь его.
22. Удалить файлы server и client из каталога /tmp ( rm /tmp/file).
23. Показать результат удаления текстов программ преподавателю.
14
3. ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ
ИНТЕРФЕЙСА SOCKET API ДЛЯ ОРГАНИЗАЦИИ
КЛИЕНТ – СЕРВЕРНОГО ВЗАИМОДЕЙСТВИЯ
В данном разделе рассматривается использование socket-интерфейса в режиме взаимодействия с установлением логического соединения на простых примерах взаимодействия двух программ (сервера и клиента), функционирующих на разных узлах сети TCP/IP.
Содержательная часть первой программы примитивна:
1) сервер, приняв запрос на соединение, передает клиенту вопрос
«Who are you?»;
2) клиент, получив вопрос, выводит его в стандартный вывод
и направляет серверу ответ «I am your client» и завершает на этом
свою работу;
3) сервер выводит в стандартный вывод ответ клиента, закрывает с ним связь и переходит в состояние ожидания следующего запроса к нему.
Тексты первых двух пар программ (клиент и север) предназначены только для иллюстрации логики взаимодействия программ
через сеть, поэтому в них отсутствуют такие атрибуты программ,
предназначенных для практического применения, как обработка
кодов возврата системных вызовов и функций, анализ кодов ошибок в глобальной переменной errno, реакция на асинхронные события и т.п. В последнем примере программы HttpClient эти атрибуты
реализованы.
Программа-сервер
Текст программы-сервера на языке программирования СИ выглядит следующим образом:
1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <netdb.h>
5 #include <memory.h>
6 #define SRV _ PORT 1234
7 #define BUF _ SIZE 64
8 #define TXT _ QUEST “Who are you?\n”
9 main () {
10 int s, s _ new;
11 int from _ len;
15
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
char buf[BUF _ SIZE];
struct sockaddr _ in sin, from _ sin;
s = socket (AF _ INET, SOCK _ STREAM, 0);
memset ((char *)&sin, ‘\0’, sizeof(sin));
sin.sin _ family = AF _ INET;
sin.sin _ addr.s _ addr = INADDR _ ANY;
sin.sin _ port = SRV _ PORT;
bind (s, (struct sockaddr *)&sin, sizeof(sin));
listen (s, 3);
while (1) {
from _ len = sizeof(from _ sin);
s _ new = accept (s, &from _ sin, &from _ len);
write (s _ new, TXT _ QUEST, sizeof(TXT _ QUEST));
from _ len = read (s _ new, buf, BUF _ SIZE);
write (1, buf, from _ len);
shutdown (s _ new, 0);
close (s _ new);
};
}
Строки 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 упрощает текст программы, избавляя от необходи16
мости использовать функцию gethostbyname для получения адреса
локального узла, на котором запускается сервер.
Строка 19 посредством системного вызова bind привязывает
socket, задаваемый дескриптором s, к порту с номером SRV_PORT
на локальном узле. Bind завершится успешно при условии, что
в момент его выполнения на том же узле уже не функционирует
программа, использующая этот номер порта.
Строка 20 посредством системного вызова listen организует очередь на три входящих к серверу запроса на соединение.
Строка 21 служит заголовком бесконечного цикла обслуживания запросов от клиентов.
На строке 23, содержащей системный вызов accept, выполнение программы приостанавливается на неопределенное время, если очередь запросов к серверу на установление связи оказывается
пуста. При появлении такого запроса accept успешно завершается,
возвращая в переменной s_new дескриптор socket’а для обмена информацией с клиентом.
В строке 24 сервер с помощью системного вызова write отправляет клиенту вопрос.
В строке 25 с помощью системного вызова read читается ответ
клиента.
В строке 26 ответ направляется в стандартный вывод.
Так как строка ответа содержит в себе символ перехода на новую строку, то текст ответа будет размещен на отдельной строке дисплея.
Строка 27 содержит системный вывод shutdown, обеспечивающий очистку системных буферов socket’а, содержащих данные для
чтения («лишние» данные могут там оказаться в результате неверной работы клиента).
В строке 28 закрывается (удаляется) socket, использованный для
обмена данными с очередным клиентом.
Программа-клиент
Текст программы-клиента на языке программирования СИ выглядит следующим образом:
1
2
3
4
5
#include
#include
#include
#include
#include
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
<netdb.h>
<memory.h>
17
6 #define SRV _ HOST “delta”
7 #define SRV _ PORT 1234
8 #define CLNT _ PORT 1235
9 #define BUF _ SIZE 64
10 #define TXT _ ANSW “I am your client\n”
11 main () {
12 int s;
13 int from _ len;
14 char buf[BUF _ SIZE];
15 struct hostent *hp;
16 struct sockaddr _ in clnt _ sin, srv _ sin;
17 s = socket (AF _ INET, SOCK _ STREAM, 0);
18 memset ((char *)&clnt _ sin, ‘\0’, sizeof(clnt _ sin));
19 clnt _ sin.sin _ family = AF _ INET;
20 clnt _ sin.sin _ addr.s _ addr = INADDR _ ANY;
21 clnt _ sin.sin _ port = CLNT _ PORT;
22 bind (s, (struct sockaddr *)&clnt _ sin, sizeof(clnt _ sin));
23 memset ((char *)&srv _ sin, ‘\0’, sizeof(srv _ sin));
24 hp = gethostbyname (SRV _ HOST);
25 srv _ sin.sin _ family = AF _ INET;
26 memcpy ((char *)&srv _ sin.sin _ addr,hp->h _ addr,hp->h _
length);
27 srv _ sin.sin _ port = SRV _ PORT;
28 connect (s, &srv _ sin, sizeof(srv _ sin));
29 from _ len = recv (s, buf, BUF _ SIZE, 0);
30 write (1, buf, from _ len);
31 send (s, TXT _ ANSW, sizeof(TXT _ ANSW), 0);
32 close (s);
33 exit (0);
34 }
В строках 6 и 7 описываются константы SRV_HOST и SRV_
PORT, определяющие имя удаленного узла, на котором функционирует программа-сервер, и номер порта, к которому привязан socket
сервера.
Строка 8 приписывает целочисленной константе 1235 символическое имя CLNT_PORT. В дальнейшем эта константа будет использована в качестве номера порта клиента.
В строках 17...22 создается привязанный к порту на локальном
узле socket.
В строке 24 посредством библиотечной функции gethostbyname
транслируется символическое имя удаленного узла (в данном слу18
чае «delta»), на котором должен функционировать сервер, в адрес
этого узла, размещенный в структуре типа hostent.
В строке 26 адрес удаленного узла копируется из структуры типа
hostent в соответствующее поле структуры srv_sin, которая позже
(в строке 28) используется в системном вызове connect для идентификации программы-сервера.
В строках 29...31 осуществляется обмен данными с сервером и
вывод вопроса, поступившего от сервера, в стандартный вывод.
Программа-сервер
Текст программы-сервера на языке программирования СИ выглядит следующим образом:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <tiuser.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <memory.h>
#include <sys/time.h>
#define SRV _ PORT 1234
#define CONT _ TXT “Continue\n”
#define CANC _ TXT “Cancel\n”
main () {
int fd;
int flags;
time _ t secs;
struct t _ bind *bind;
struct t _ unitdata *ud;
struct sockaddr _ in *p _ addr;
extern int t _ errno;
fd = t _ open(“/dev/udp”, O _ RDWR, NULL);
bind = (struct t _ bind *) t _ alloc (fd, T _ BIND, T _ ADDR);
memset (bind->addr.buf, ‘\0’, bind->addr.maxlen);
p _ addr = (struct sockaddr _ in *) bind->addr.buf;
p _ addr->sin _ family = AF _ INET;
p _ addr->sin _ addr.s _ addr = INADDR _ ANY;
p _ addr->sin _ port = SRV _ PORT;
bind->addr.len = sizeof(struct sockaddr _ in);
bind->qlen = 0;
19
29 t _ bind (fd, bind, bind);
30 ud = (struct t _ unitdata *)t _ alloc(fd,T _ UNITDATA,T _ ALL);
31 while (1) {
32 t _ rcvudata (fd, ud, &flags);
33 write (1, ud->udata.buf, ud->udata.len);
34 secs = time (NULL);
35 if (secs % 3) {
36 strcpy (ud->udata.buf, CONT _ TXT);
37 ud->udata.len = sizeof(CONT _ TXT); 38
}
39 else {
40 strcpy (ud->udata.buf, CANC _ TXT);
41 ud->udata.len = sizeof(CANC _ TXT); 42
};
43 t _ sndudata (fd, ud);
44};
45}
Строки 1...8 описывают включаемые файлы, содержащие определения для всех необходимых структур данных и символических
констант.
Строка 9 приписывает целочисленной константе 1234 символическое имя SRV_PORT. В дальнейшем эта константа будет использована в качестве номера порта сервера. Значение этой константы
должно быть известно и программе-клиенту.
Строки 10 и 11 приписывают последовательностям символов, составляющих тексты возможных ответов клиенту, символические
имена CONT_TXT и CANC_TXT. Последним символом в последовательностях является символ перехода на новую строку ‘\n’. Сделано
это для упрощения вывода текста ответа на стороне клиента.
В строке 20 создается (открывается) транспортная точка для организации режима взаимодействия без установления логического
соединения с помощью протокола транспортного уровня UDP в сети TCP/IP (/dev/udp). Транспортная точка будет использоваться для
двустороннего обмена информацией (O_RDWR). Третий аргумент
функции t_open задан как NULL, поскольку данную программу
особые характеристики поставщика транспортных услуг не интересуют.
В строке 21 выделяется оперативная память под структуру
данных типа struct t_bind и под буфер данных, определяемый полем addr этой структуры (размер памяти, выделяемой под этот буфер, функция t_alloc вычисляет самостоятельно на основе информации о конкретном поставщике транспортных услуг). Поле addr
этой структуры в своем буфере будет содержать транспортный адрес
20
транспортной точки, который для поставщиков транспортных услуг UDP и TCP имеет тот же формат, что и адрес socket’а.
В строках 22...27 сначала обнуляется структура данных типа
struct sockaddr_in, на которую указывает bind->addr.buf а затем заполняются ее отдельные поля. Использование константы INADDR_
ANY упрощает текст программы, избавляя от необходимости использовать функцию gethostbyname для получения адреса локального узла, на котором запускается сервер.
В строке 28 переменной bind->qlen присваивается значение 0, поскольку сервер предназначен для работы в режиме без установления соединения.
Строка 29 посредством функции t_bind привязывает к транспортной точке транспортный адрес, описанный в структуре, на которую указывает bind. T_bind завершится успешно при условии,
что в момент его выполнения на том же узле уже не функционирует
программа, использующая этот же транспортный адрес.
Строка 31 служит заголовком бесконечного цикла обслуживания запросов от клиентов.
В строке 32 с помощью функции t_rcvudata читается запрос клиента.
В строке 33 текст запроса направляется в стандартный вывод,
имеющий дескриптор файла номер 1. Так как строка ответа содержит в себе символ перехода на новую строку, то текст ответа будет
размещен на отдельной строке дисплея.
Строка 34 содержит обращение к функции time, возвращающей
количество секунд времени, прошедших с 1 января 1970 г. до текущего момента. Это значение используется в программе-сервере для
выбора варианта ответа клиенту.
В строке 43 сервер с помощью функции t_sndudata отправляет
клиенту ответ на его запрос, выбранный из двух возможных вариантов псевдослучайным образом.
Программа-клиент
Текст программы-клиента на языке программирования СИ выглядит следующим образом:
1
2
3
4
5
#include
#include
#include
#include
#include
<tiuser.h>
<fcntl.h>
<stdio.h>
<sys/socket.h>
<netinet/in.h>
21
6 #include <netdb.h>
7 #include <memory.h>
8 #define SRV _ HOST «delta»
9 #define SRV _ PORT 1234
10 #define ASK _ TXT “What must I do?\n”
11 #define CONT _ TXT “Continue\n”
12 #define CANC _ TXT “Cancel\n”
13 main () {
14 int fd;
15 int flags;
16 struct t _ unitdata *ud;
17 struct sockaddr _ in *p _ addr;
18 struct hostent *hp;
19 extern int t _ errno;
20 fd = t _ open (“/dev/udp”, O _ RDWR, NULL);
21 t _ bind (fd, NULL, NULL);
22 ud = (struct t _ unitdata *) t _ alloc (fd, T _ UNITDATA,
T _ ALL);
23 memset (ud->addr.buf, ‘\0’, ud->addr.maxlen);
24 p _ addr = (struct sockaddr _ in *) ud->addr.buf;
25 hp = gethostbyname (SRV _ HOST);
26 p _ addr->sin _ family = AF _ INET;
27memcpy((char *)&(p _ addr->sin _ addr),hp->h _ addr,hp->h _
length);
28 p _ addr->sin _ port = SRV _ PORT;
29 ud->addr.len = sizeof(struct sockaddr _ in);
30 while (1) {
31 strcpy (ud->udata.buf, ASK _ TXT);
32 ud->udata.len = sizeof(ASK _ TXT);
33 t _ sndudata (fd, ud);
34 t _ rcvudata (fd, ud, &flags);
35 write (1, ud->udata.buf, ud->udata.len);
36 if (strcmp(ud->udata.buf, CONT _ TXT))
37break;
38};
39 t _ free ((char *) ud, T _ UNITDATA);
40 t _ close (fd);
41 exit (0);
42}
В строках 8 и 9 описываются константы SRV_HOST и SRV_PORT,
определяющие имя удаленного узла, на котором функционирует
программа-сервер, и номер порта, к которому привязана транспортная точка сервера.
22
В строках 20 и 21 создается транспортная точка, имеющая не интересующий нас в этой программе транспортный адрес.
В строке 22 выделяется оперативная память под структуру данных типа struct t_unitdata и под три буфера, определяемых полями
addr, opt и udata этой структуры (размер памяти, выделяемой под
эти буфера, функция t_alloc вычисляет самостоятельно на основе
информации о конкретном поставщике транспортных услуг).
В строке 25 посредством библиотечной функции gethostbyname
транслируется символическое имя удаленного узла (в данном случае «delta»), на котором должен функционировать сервер, в адрес
этого узла, размещенный в структуре типа hostent.
В строке 27 адрес удаленного узла копируется из структуры типа
struct hostent в соответствующее поле структуры типа struct sockaddr_in, которая размещена в буфере ud- >addr.
В строках 31 и 32 заполняются поля структуры ud->udata передаваемой серверу информации и длиной этой информации.
В строке 33 с помощью функции t_sndudata посылается запрос
серверу. В строке 34 с помощью функции t_rcvudata принимается
ответ от сервера. При этом транспортный адрес транспортной точки отправителя ответа (сервера) размещается функцией в ud->addr,
а сами данные, составляющие ответ, – в ud->udata.
В строке 39 освобождается оперативная память, занимавшаяся
структурой типа struct t_unitdata. Строка 40 посредством функции
t_close закрывает (удаляет) транспортную точку.
Приведенные выше программы серверов (как и большинство реальных программ) самостоятельно своей работы не завершают, находясь в бесконечном цикле обработки запросов клиентов. Их выполнение может быть прервано только извне путем посылки ей сигналов завершения. Правильно разработанная программа-сервер
должна брабатывать такие сигналы, корректно завершая работу.
Программа HttpClient
#include <stdlib.h> // stdlib functions
#include <unistd.h> // getopt
#include <stdio.h> // printf, etc
#include <string.h> // strncpy, etc
#include <sys/socket.h> // sockets API
#include <netdb.h> // gethostbyname
#include <errno.h> // errno
// Необходимо для getopt
extern char *optarg;
23
// Функция main, входная точка программы
int main(int argc /*количество аргументов*/, char* argv[]/*массив
аргументов*/)
{
// Текст помощи
const char* usageInfo =
«Использование: ./HttpClient [-u http://host[:port]
[/page]] [-h]\n»
«\t-u – URL для получения страницы.\n»
«\t-h – эта справка.\n»;
// Буффер для строчки с URL
static const int MAX _ URL _ LEN = 1024;
char urlBuffer[MAX _ URL _ LEN];
memset(urlBuffer, 0, MAX _ URL _ LEN);
// Чтение аргументов командной строки в стиле Си
int opt;
while ((opt = getopt(argc, argv, “hu:”)) != –1)
{
switch (opt)
{
case ‘u’:
{
// Использование strncpy вместо strcpy дает
защиту от переполнения буфера
strncpy(urlBuffer, optarg, MAX _ URL _ LEN);
break;
}
case ‘h’:
default:
{
// Напечатать помощь и выйти
printf(usageInfo);
exit(EXIT _ FAILURE);
}
}
}
// если конфигурация после прочтения всех опций неполна –
напечатать помощь и выйти
if (strnlen(urlBuffer, MAX _ URL _ LEN) == 0)
{
printf(usageInfo);
return EXIT _ FAILURE;
}
// Данные для разбора URL в стиле Си
char urlHost[MAX _ URL _ LEN]; // Хост
24
memset(urlHost, 0, MAX _ URL _ LEN);
char urlPage[MAX _ URL _ LEN]; // Адрес страницы
memset(urlPage, 0, MAX _ URL _ LEN);
strncpy(urlPage, “index.html”, MAX _ URL _ LEN); //
Предопределенное значение адреса страницы index.html
int urlPort = 80; // Порт, предопределенное значение 80
// Пропустить http://
char* pBuffer = strstr(urlBuffer, “http://”);
if (pBuffer != 0)
pBuffer = urlBuffer + strnlen(“http://”, MAX _ URL _
LEN);
else
pBuffer = urlBuffer;
// Разбиение строки в разных кобминациях имени хоста,
порта и адреса страницы на сервере
if (sscanf(pBuffer, “%99[^:]:%i/%199[^\n]”, urlHost, &urlPort, urlPage) == 3) {}
else if (sscanf(pBuffer, “%99[^/]/%199[^\n]”, urlHost, urlPage) == 2) {}
else if (sscanf(pBuffer, “%99[^:]:%i[^\n]”, urlHost, &urlPort) == 2) {}
else if (sscanf(pBuffer, “%99[^\n]”, urlHost) == 1) {}
else
{
printf(«Не удалось разобрать строчку URL»);
printf(usageInfo);
return EXIT _ FAILURE;
}
// Данные необходимые для соединения и обмена данными
// Структуры для адресов
struct addrinfo hints;
struct addrinfo *result;
struct addrinfo *rp;
// Сокет для обмена данными
int sfd; // socket file descriptor
// код ошибки
int s;
static const int BUF _ SIZE = 256*1024;
// Буфер памяти для сетевого обмена, фиксированной длинны
на стеке
char buf[BUF _ SIZE];
// Инициализация структур для получения ip адреса по
имени хоста
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai _ family = AF _ UNSPEC;
25
hints.ai _ socktype = SOCK _ STREAM;
snprintf(buf, BUF _ SIZE, “%d”, urlPort);
// Получение набора адресов соответствующих имени хоста
urlHost
s = getaddrinfo(urlHost, buf, &hints, &result);
if (s != 0)
{
fprintf(stderr, “Ошибка getaddrinfo: %s\n”, gai _
strerror(s));
perror(strerror(errno));
return EXIT _ FAILURE;
}
// getaddrinfo() Возвращает набор структур с адресами
// Будем пробовать все подряд пока не соединимся
for (rp = result; rp != NULL; rp = rp->ai _ next)
{
// Создание TCP сокета
sfd = socket(rp->ai _ family, rp->ai _ socktype, rp>ai _ protocol);
if (sfd == –1)
continue;
// Попытка соединения, выход из цикла если удачно
if (connect(sfd, rp->ai _ addr, rp->ai _ addrlen) != –1)
break;
// Закрытие сокета
close(sfd);
}
// Если ни один адрес не подошел
if (rp == NULL)
{
fprintf(stderr, «Неудается установить соединение\n»);
return EXIT _ FAILURE;
}
// Освобождение структур содержащих разрезолвленные
адреса
freeaddrinfo(result);
// Подготовка простейшего HTTP запроса
char requestPattern[] = “GET /%s HTTP/1.1\r\n”
“Host: %s\r\n”
“Connection: close\r\n”
“Accept: text/html;q=1.0\r\n”
“Accept-Language: en-US,en;q=1.0\r\n\r\n”;
snprintf(buf, BUF _ SIZE, requestPattern, urlPage, urlHost);
// Отправка запроса
// total – количество байт к отправке
26
int total = strnlen(buf, BUF _ SIZE);
// sent – общее количество отправленных байт
int sent = 0;
// bytes – количество байт отправленных за последний заход
int bytes;
do
{
// попытка записи в сокет с текущего места и до конца
bytes = write(sfd, buf + sent, total – sent);
if (bytes < 0)
{
printf(«Ошибка при записи в сокет»);
perror(strerror(errno));
return EXIT _ FAILURE;
}
if (bytes == 0)
break;
sent += bytes;
} while (sent < total); // Отсылка данных пока есть, что
посылать
// Очистка буфера сетевого обмена
memset(buf, 0, BUF _ SIZE);
// Максимальный размер получаемых данных равен размеру
буфера
total = BUF _ SIZE;
// Общее количество полученных байт
int received = 0;
do
{
// Чтение в буфер поэтапно
bytes = read(sfd, buf + received, total – received);
if (bytes < 0)
{
printf(«Ошибка при чтении из сокета»);
perror(strerror(errno));
return EXIT _ FAILURE;
}
// Получение ноля байт говорит о том, что противополжная
сторона закрыла соединение
if (bytes == 0)
break;
received += bytes;
} while (received < total); // Получение данных пока есть
место в буфере
27
// Если буфер закончился и нехватает места на терминирующий
ноль – случилось переполнение
if (received == total)
{
printf(«Ошибка при чтении из сокета – буфер переполнен»);
exit(EXIT _ FAILURE);
}
// Нультерминируем буфер после чтения
buf[received] = 0;
// Закрываем сокет
close(sfd);
// Выводим ответ сервера
printf(«Ответ:\n%s\n», buf);
// Успешное завершение
return EXIT _ SUCCESS;
}
Примечание. Данная клиентская программа по заданному URL
определяет у Web-сервера его IP-адреса и по первому адресу, содержащемуся в полученной структуре, запрашивает у сервера его главную страницу. В программе используется функция perror(), которая выводит текст ошибки, соответствующий коду ошибки errno.
28
Список используемых источников
1. Уолтон Ш. Создание сетевых приложений в среде Linux. : Пер.
с англ.— М. : Издательский дом «Вильямc», 2001.
2. Стивенс Р., Раго С. UNIX. Профессиональное программирование. Символ-Плюс, 2007.
3. Калюжный В.П., Осипов Л.А. Администрирование информационных сетей: Учеб. Пособие. CПб.: ГУАП. СПб., 2010.
4. Калюжный В.П. ОсиповЛ.А. Операционные системы: Учеб.
Пособие. СПбГУАП. СПб., 2012.
5. Калюжный В.П. Осипов Л.А. Инструментальные средства информационных систем: Учеб. Пособие. СПб.: ГУАП. СПб., 2015.
6. Калюжный В.П., Калюжный И.В. Технические основы удаленного доступа: Учеб. Пособие. СПб.: ГУАП. СПб., 2005.
7. Люсин О.Б. СЕТЕВОЕ ПРОГРАММИРОВАНИЕ ДЛЯ OC UNIX
и WINDOWS: Учеб. пособие. – Рига: ИТС, 2006.
8. Programming UNIX Sockets in C – Frequently Asked Questions – 1996. URL: www.tlab.ntua.gr/ (дата обращения 1.11.2017).
9. Пантелеичев Д. Разработка программного обеспечения для
Linux. Инструментарий URL: http://www.linuxcenter.ru/lib/books/
linuxdev/
10. Linux network programming. P. 1. From Issue 46 // Linux
Journal, Ftbruary1998.
29
ПРИЛОЖЕНИЕ
Основные функции библиотеки Sockets API c комментариями
1. accept()
Принимает входные подключения на слушающем сокете.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen _ t *addrlen);
Описание
accept() вызываетcя чтобы получить новый дескриптор сокета
для последующего общения с только что подключённым клиентом.
Возвращаемый accept()-ом дескриптор определяет уже открытый и подключённый к удаленному хосту сокет.
Возвращаемое значение
accept() возвращает дескриптор только что подключённого сокета, или –1 при ошибке, при этом соответствующим образом установив
errno.
Пример
struct sockaddr _ storage their _ addr; socklen _ t addr _ size;
struct addrinfo hints, *res; int sockfd, new _ fd;
// сначала заполняем адресные структуры с помощью getaddrinfo(): memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// использовать либо IPv4 либо IPv6
hints.ai _ socktype = SOCK _ STREAM;
hints.ai _ flags = AI _ PASSIVE;
// заполнить мой IP для меня
getaddrinfo(NULL, MYPORT, &hints, &res);
// создать сокет, связать и слушать:
sockfd = socket(res->ai _ family, res->ai _ socktype, res->ai _
protocol);
bind(sockfd, res->ai _ addr, res->ai _ addrlen);
listen(sockfd, BACKLOG);
// теперь принять входящие подключения:
addr _ size = sizeof their _ addr;
new _ fd = accept(sockfd, (struct sockaddr *)&their _ addr,
&addr _ size);
30
2. bind()
Связывает сокет с IP адресом и номером порта.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my _ addr, socklen _ t
addrlen);
Описание
Когда удалённая машина хочет связаться с вашей серверной программой, ей для этого нужны IP адрес и номер порта. Это можно сделать
вызвав bind().
Сначала нужно вызывать getaddrinfo(), заполнить struct
sockaddr адресом назначения и информацией порта. Затем вызывается socket() чтобы получить дескриптор сокета. Затем сокет и адрес
передается в bind(), в результате IP адрес привязан к сокету.
Если свой IP адрес неизвестен или известно, что у вашей машины только один IP адрес, или безразлично, какие IP адреса используются, нужно установить флаг AI_PASSIVE в параметре hints при вызове getaddrinfo(). При этом в часть IP адреса в struct sockaddr записывается специальное значение, которое указывает bind(), что ей нужно
автоматически заполнить этот IP адрес хоста.
Это специальное значение записывается в IP адрес struct
sockaddr чтобы автоматически установить адрес текущего хоста. Но нужно помнить, что это происходит только при заполнении struct sockaddr
вручную, иначе необходимо воспользоваться результатом getaddrinfo(),
как указано выше. В IPv4, поле sin_addr.s_addr структуры struct
sockaddr_in устанавливается INADDR_ANY. В IPv6, в поле sin6_addr
структуры sockaddr_in6 записывается значение глобальной переменной in6addr_any. Или, если объявлена новая struct in6_addr, можно инициализировать её IN6ADDR_ANY_INIT. Наконец, параметр
addrlen должен быть установлен в sizeof my_addr.
Возвращаемое значение
Возвращает 0 при успехе или –1 в случае ошибки (errno устанавливается соответственно).
Пример
// современный способ работы с getaddrinfo()
struct addrinfo hints, *res;
int sockfd;
31
// сначала заполняем адресные структуры с помощью getaddrinfo(): memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// использовать либо IPv4 либо IPv6
hints.ai _ socktype = SOCK _ STREAM;
hints.ai _ flags = AI _ PASSIVE;
// заполнить мой IP для меня
getaddrinfo(NULL, «3490», &hints, &res);
// создать сокет:
// нужно прогуляться по связанному списку «res» и проверить
на ошибки
sockfd = socket(res->ai _ family, res->ai _ socktype, res->ai _
protocol);
// связать с портом, переданным getaddrinfo(): bind(sockfd,
res->ai _ addr, res->ai _ addrlen);
// пример упаковки структуры вручную, IPv4
struct sockaddr _ in myaddr; int s;
myaddr.sin _ family = AF _ INET; myaddr.sin _ port = htons(3490);
// можете указать IP адрес:
inet _ pton(AF _ INET, “63.161.169.137”, &(myaddr.sin _ addr));
// или привязать сокет ко всем доступным интерфейсам:
myaddr.sin _ addr.s _ addr = =INADDR _ ANY;
s = socket(PF _ INET, SOCK _ STREAM, 0);
bind(s, (struct sockaddr*)&myaddr, sizeof myaddr);
3. connect()
Подключает сокет к серверу.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv _ addr,
socklen _ t addrlen);
Описание
После того, был создан дескриптор сокета вызовом socket(),
можно подключить его к удалённому серверу системным вызовом
connect(). Для этого нужно только передать ему дескриптор сокета и
адрес сервера, с которым вы хотите связаться, и длину адреса, которую принято передавать таким функциям.
Обычно эту информацию получают как результат вызова getaddrinfo(), но есть возможность заполнить свою собственную struct sockaddr.
Если bind() ещё не вызыван с этим дескриптором сокета, он автома32
тически привязывается к вашему IP адресу и случайному локальному
порту. Обычно это удобно, если вы не сервер, поскольку тогда безразличен номер вашего локального порта. Если номер удалённого порта важен, то необходимо указать его в параметре в serv_addr. Можно вызвать bind() если необходимо, чтобы сокет вашего клиента был привязан к определённому IP адресу и порту, но это бывает редко. Как только
сокет подключён (connect()), можно посылать (send()) и принимать
(recv()).
Возвращаемое значение
Возвращает 0 при успехе или –1 в случае ошибки (errno устанавливается соответственно).
Пример
// соединиться с www.guap.ru порт 80 (http) struct addrinfo
hints, *res;
int sockfd;
// сначала заполняем адресные структуры с помощью
getaddrinfo(): memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// использовать либо IPv4 либо IPv6
hints.ai _ socktype = SOCK _ STREAM;
// в это строке можно указать «80» вместо «http»:
getaddrinfo(“www.guap.ru”, “http”, &hints, &res);
// создать сокет:
sockfd = socket(res->ai _ family, res->ai _ socktype, res->ai _
protocol);
// соединить с адресом и портом, переданным getaddrinfo():
connect(sockfd, res->ai _ addr, res->ai _ addrlen);
4. close()
Закрывает дескриптор сокета.
Прототип
#include<unistd.h>
int
close (int s);
Описание
После того как закончилось использование сокета и больше нет
необходимости посылать (send()) или принимать (recv()) данные,
можно его закрыть. Удалённая сторона может узнать об этом одним
из двух способов. Первый: если удалённая сторона вызывает recv(),
33
он возвращает 0. Второй: удалённая сторона вызывает send(), он
примет сигнал SIGPIPE и вернёт –1, errno будет установлен в EPIPE.
Возвращаемое значение
Возвращает 0 при успехе или –1 в случае ошибки (errno) устанавливается соответственно.
Пример
s = socket(PF _ INET, SOCK _ DGRAM, 0);
..
// закрыть сокет.
close(s);
5. getaddrinfo(), freeaddrinfo(), gai_strerror()
Получает информацию об имени хоста и/или сервисе и записывает результат в struct sockaddr.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
nt getaddrinfo(const char *nodename, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
void freeaddrinfo(struct addrinfo *ai);
const char *gai _ strerror(int ecode);
Описание
getaddrinfo() это удобная функция, которая возвращает информацию об имени отдельного хоста (такую как IP адрес) и заполняет struct
sockaddr, заботится о мелких деталях (это IPv4 или IPv6.) Она заменяет старые функции gethostbyname() и getservbyname(). Имя хоста
передаётся в параметре nodename. Адрес может быть именем хоста,
как “www.guap.ru”, либо IPv4 или IPv6 адрес (передаваемый как строка). Этот параметр также может быть NULL если используется флаг
AI_PASSIVE. Обычно параметр servname это номер порта. Он может быть
номером (передаваемый строкой, как “80”), или он может быть именем сервиса, как “http” или “tftp” или “smtp”или “pop”, и т.д. Во
входных параметрах есть hints. Именно здесь можно определить что
функции getaddrinfo() нужно делать. Перед использованием необходимо обнулить всю структуру целиком функцией memset().
Поле ai_flags может содержать множество флагов, но основные это
AI_CANONNAME и AI_PASSIVEAI_CANONNAME заставляет запи34
сать в поле ai_canonname результата каноническое (настоящее) имя
хоста. AI_PASSIVE приводит к записи в IP адрес INADDR_ANY
(IPv4) или in6addr_any (IPv6); из-за этого последует вызов bind()
чтобы автоматически записать в IP адрес структуры struct sockaddr
адрес текущего хоста. Это удобно для запуска сервера если нет необходимости использовать постоянно установленный адрес. Если нужно использовать флаг AI_PASSIVE, то в nodename можно указать
NULL (поскольку bind() заполнит его позднее). В входных параметрах
лучше всего установить в ai_family AF_UNSPEC чтобы getaddrinfo() искала и IPv4 и IPv6 адреса. Хотя можно ограничиться одним
или другим, установив AF_INET или AF_INET6. В поле ai_socktype
нужно установить SOCK_STREAM или SOCK_DGRAM, в зависимости от того, какой тип сокета нужен. Можно оставить в ai_ protocol 0,
чтобы автоматически выбрать тип протокола. Теперь, можно вызвать
getaddrinfo(). res теперь указывает на связаный список struct addrinfo и можно посмотреть адреса, удовлетворяющие тому, что было передано в hints. Поэтому, можно определить какие из адресов по той или
иной причине не работают. Наконец, нужно вызвать freeaddrinfo()
чтобы освободить память, иначе она затрется.
Возвращаемое значение
При успехе возвращает ноль или не-ноль при ошибке. В таком
случае можно воспользоваться функцией gai_strerror() чтобы получить печатную версию кода возврата.
Пример
// код для подключения клиента к серверу, а именно потокового
сокета к www.guap.ru на порт 80 (http)
// IPv4 или IPv6
int sockfd;
struct addrinfo hints, *servinfo, *p; int rv;
memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// для задания IPv6 используйте
AF _ INET6 hints.ai _ socktype = SOCK _ STREAM;
if ((rv = getaddrinfo(“www.guap.ru”, “http”, &hints, &servinfo)) != 0) {
fprintf(stderr, “getaddrinfo: %s\n”, gai _ strerror(rv));
exit(1);
}
35
// цикл по всем результатам и подключение к первому возможному
for(p = servinfo; p != NULL; p = p->ai _ next) {
if ((sockfd = socket(p->ai _ family, p->ai _ socktype, p>ai _ protocol)) == –1) {
perror(“socket”);
continue;
}
if (connect(sockfd, p->ai _ addr, p->ai _ addrlen) == –1) {
close(sockfd);
perror(“connect”);
continue;
}
break;
// подключились удачно
}
if (p == NULL) {
// цикл закончился, а подключения нет
fprintf(stderr, “failed to connect\n”);
exit(2);
}
freeaddrinfo(servinfo);
// со структурой закончили
/ сервер, ожидающий подключений,
// а именно потоковый сокет порт 3490, IP этого хоста
// IPv4 либо IPv6.
int sockfd;
struct addrinfo hints, *servinfo, *p; int rv;
memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// для выбора IPv6 используйте
AF _ INET6 hints.ai _ socktype = SOCK _ STREAM;
hints.ai _ flags = AI _ PASSIVE;
// использовать мой IP адрес
if ((rv = getaddrinfo(NULL, “3490”, &hints, &servinfo)) != 0) {
fprintf(stderr, “getaddrinfo: %s\n”, gai _ strerror(rv));
exit(1);
}
// цикл по всем результатам и подключение к первому возможному
for(p = servinfo; p != NULL; p = p->ai _ next) {
36
if ((sockfd = socket(p->ai _ family, p->ai _ socktype,
p->ai _ protocol)) == –1) {
perror(“socket”);
continue;
}
if (bind(sockfd, p->ai _ addr, p->ai _ addrlen) == –1) {
close(sockfd);
perror(“bind”);
continue;
}
break;
// подключились удачно
}
if (p == NULL) {
// цикл закончился, а подключения нет
fprintf(stderr, “failed to bind socket\n”);
exit(2);
}
freeaddrinfo(servinfo);
// со структурой закончили
6. gethostbyname(), gethostbyaddr()
Получают IP адрес хоста по имени или наоборот.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, size _ t len, int flags);
Описание
Эти функции выполняют преобразование имён хостов в IP адреса и обратно. Например, если есть“ www. guap.ru”, можно использовать gethostbyname() чтобы получить IP адрес и сохранить его
в struct in_addr. И наооборот, если у вас есть struct in_addr или
struct in6_addr, можно воспользоваться gethostbyaddr() чтобы получить назад имя хоста. gethostbyaddr() совместима с IPv6, но
пользоваться нужно getnameinfo(). (Если есть строка с IP адресом
в формате цифр-и-точек для которой вы хотите узнать имя хоста, то
лучше воспользоваться getaddrinfo() с флагом AI_CANONNAME.)
37
gethostbyname() принимает строку вроде “www.guap.ru” и возвращает struct hostent, которая содержит много информации, включая IP
адрес. (Другая информация это официальное имя хоста, список псевдонимов, тип адреса и список адресов.) gethostbyaddr() принимает
struct in_addr или struct in6_addr и выдаёт соответствующее имя
хоста (если оно есть), так что это функция, обратная gethostbyname(). Насчёт параметров, даже хотя addr меет тип char*, в действительности необходимо передавать указатель на struct in_addr. len
должна быть sizeof(struct in_addr), и type должен быть AF_INET.
Эта структура содержит множество полей, содержащих информацию о
запрошенном хосте.
char *h_name – настоящее каноническое имя хоста
char **h_aliases – список псевдонимов, к нему можно обращаться, как
к массиву, последний элемент содержит NULL
int h_addrtype – тип адреса результата, в нашем случае должен
быть AF_INET
int length – длина адресов в байтах (4 для IPv4)
char **h_addr_list – список IP адресов этого хоста. Хоть он и char**,
в действительности это массив переодетых struct in_addr*.
Последний элемент массива равен NULL.
h_addr – псевдоним h_addr_list[0]. Если нужен любой старый IP
адрес этого хоста (их может быть несколько) используйте это поле.
Возвращаемое значение
Возвращает указатель на получившуюся struct hostent или NULL
при ошибке.
Вместо нормальной perror() и всего прилагающегося для выдачи
сообщений об ошибке, эти функции пишут результат в переменную h_errno, которую можно распечатать функциями herror() или
hstrerror(). Это подобно классической errno, perror(), и strerror().
Пример
#include
#include
#include
#include
#include
#include
<stdio.h>
<errno.h>
<netdb.h>
<sys/types.h>
<sys/socket.h>
<netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
38
int i;
struct hostent *he;
struct in _ addr **addr _ list;
if (argc != 2) {
fprintf(stderr,”usage: ghbn hostname\n”);
return 1;
}
if ((he = gethostbyname(argv[1])) == NULL) {
// получить информацию хоста
herror(«gethostbyname»);
return 2;
}
// распечатать информацию об этом хосте:
printf(“Official name is: %s\n”, he->h _ name);
printf(“
IP addresses: “);
addr _ list = (struct in _ addr **)he->h _ addr _ list;
for(i = 0; addr _ list[i] != NULL; i++) {
printf(“%s “, inet _ ntoa(*addr _ list[i]));
}
printf(“\n”);
return 0;
}
// это заменено
// взамен нужно getnameinfo()!
struct hostent *he; struct in _ addr ipv4addr;
struct in6 _ addr ipv6addr;
inet _ pton(AF _ INET, “192.0.2.34”, &ipv4addr);
he = gethostbyaddr(&ipv4addr, sizeof ipv4addr, AF _ INET);
printf(“Host name: %s\n”, he->h _ name);
inet _ pton(AF _ INET6, “2001:db8:63b3:1::beef”, &ipv6addr);
he = gethostbyaddr(&ipv6addr, sizeof ipv6addr, AF _ INET6);
printf(“Host name: %s\n”, he->h _ name);
39
7. errno
Содержит код ошибки последнего системного вызова.
Прототип
#include <errno.h>
int errno;
Описание
Эта переменная содержит информацию об ошибках для множества
системных вызовов. Если при вызове, например, socket() или listen()
происходит ошибка, то возвращается –1 и в errno устанавливается
код, позволяющий точно определить, что случилось.
В заголовочном файле errno.h
перечислены все символические
имена ошибок, как EADDRINUSE, EPIPE, ECONNREFUSED и т.д.
В большинстве систем errno определена потокобезопасным способом.
(То есть, в действительности она не глобальная переменная, но ведёт себя так, как должна вести себя глобальная переменная в однопотоковой среде.)
Возвращаемое значение
Значение переменной это код последней произошедшей ошибки, но может означать “успех” если последнее действие завершилось удачно.
Пример
s = socket(PF _ INET, SOCK _ STREAM, 0);
if (s == –1) {
perror(“socket”);
// или используйте strerror()
}
tryagain:
if (select(n, &readfds, NULL, NULL) == –1) {
lect():
}
40
// ошибка
// если мы просто прерваны, просто перезапуск вызовом seif (errno == EINTR) goto tryagain;
// иначе это ошибка посерь зней:
perror(«select»);
exit(1);
8. listen()
Предписывает сокету слушать входящие подключения.
Прототип
#include <sys/socket.h>
int listen(int s, int backlog);
Описание
Параметр backlog означает резерв – сколько ожидающих соединений можете иметь до того, как ядро начнёт отбрасывать новые. Так
что, как только придёт новое соединение, необходимо быстро принять (accept()) его, чтобы не переполнять резерв. Можно установить
10 или около того, и если при высокой нагрузке, ваши клиенты начнут получать “Connection refused” (“Соединение отвергнуто”), следует установить побольше.
Перед вызовом listen() сервер должен вызвать bind(), чтобы подключиться к определённому номеру порта. Клиенты будут подключаться к этому порту на IP адресе сервера.
Возвращаемое значение
Возвращает 0 при успехе или –1 в случае ошибки (errno устанавливается соответственно).
Пример
struct addrinfo hints, *res; int sockfd;
// сначала заполняем адресные структуры с помощью getaddrinfo():
memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// использовать либо IPv4 либо IPv6
hints.ai _ socktype = SOCK _ STREAM;
hints.ai _ flags = AI _ PASSIVE;
// заполнить мой IP для меня
getaddrinfo(NULL, «3490», &hints, &res);
// создать сокет:
sockfd = socket(res->ai _ family, res->ai _ socktype, res->ai _
protocol);
// связать с портом, переданным getaddrinfo(): bind(sockfd,
res->ai _ addr, res->ai _ addrlen); listen(sockfd, 10);
// теперь слушает
// дальше должен быть цикл accept()
41
9. perror(), strerror()
Распечатывает ошибку как читаемую строку
Прототип
#include <stdio.h>
#include <string.h>
// для strerror()
void perror(const char *s);
char *strerror(int errnum);
Описание
Очень много функций при ошибке возвращают –1 и записывают
в errno некоторое число, и было бы удобно, если бы можно было распечатать в понятной форме.
И perror() это делает. Если необходимо добавить описание перед
сообщением об ошибке, следует передать указатель на него в параметре
s (можно оставить s как NULL и ничего дополнительно не напечатается.) Эта функция берёт значение errno, вроде ECONNRESET, и печатает его как “Connection reset by peer.” Функция strerror() подобна
perror(), за исключением того, что она возвращает указатель на строку
с сообщением об ошибке для заданного параметром errnum значения
(обычно переменная передаётся в errno).
Возвращаемое значение
strerror() возвращает указатель на строку с сообщением об ошибке.
Пример
int
s =
if
//
s;
socket(PF _ INET, SOCK _ STREAM, 0);
(s == –1) {
ошибка
// печатает «socket error: « + сообщение об ошибке:
perror(“socket error”);
}
// подобно:
if (listen(s, 10) == –1) {
// это печатает «an error: « + сообщение об ошибке из errno:
printf(“an error: %s\n”, strerror(errno));
}
42
10. recv(), recvfrom()
Принимают данные из сокета.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
ssize _ t recv(int s, void *buf, size _ t len, int flags);
ssize _ t recvfrom(int s, void *buf, size _ t len, int flags,
struct sockaddr *from, socklen _ t *fromlen);
Описание
Как только сокет создан и подключен, можно принимать из него
данные вызовами recv() (для TCP SOCK_STREAM сокетов) и recvfrom() (для UDP SOCK_DGRAM сокетов). Обе функции принимают
дескриптор сокета s, указатель на буфер buf, длину буфера в байтах len,
и набор флагов flags, определяющих работу функций. Дополнительно,
recvfrom() принимает struct sockaddr* from, указывающую откуда принимать данные и запишет в fromlen размер struct sockaddr.
(Можно тоже инициализировать fromlen размером from или struct
sockaddr.)
Возвращаемое значение
Возвращает число действительно принятых данных (что может
быть меньше затребованного в параметре len), или –1 при ошибке
(errno будет установлен соответственно). Если удалённая сторона закрыла соединение, то recv() вернёт 0. Это нормальный способ определения того, что удаленная сторона закрыла соединение.
Пример
/ потоковые сокеты и recv()
struct addrinfo hints, *res; intsockfd;
char buf[512]; int byte _ count;
// получить информацию хоста, создать сокет и подключиться
memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// использовать либо IPv4 либо IPv6
hints.ai _ socktype = SOCK _ STREAM; getaddrinfo(“www.guap.
ru”, “3490”, &hints, &res);
sockfd = socket(res->ai _ family, res->ai _ socktype, res->ai _
protocol); connect(sockfd, res->ai _ addr, res->ai _ addrlen);
// Мы подключены и можем принимать
43
byte _ count = recv(sockfd, buf, sizeof buf, 0);
printf(“recv()’d %d bytes of data in buf\n”, byte _ count);
//дейтаграммные сокеты и recvfrom()
struct addrinfo hints, *res; int sockfd;
int byte _ count; socklen _ t fromlen;
struct sockaddr _ storage addr; char buf[512];
char ipstr[INET6 _ ADDRSTRLEN];
// получить информацию хоста, создать сокет и подключиться к
порту 4950
memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// использовать либо IPv4 либо IPv6
hints.ai _ socktype = SOCK _ DGRAM;
hints.ai _ flags = AI _ PASSIVE; getaddrinfo(NULL, “4950”,
&hints, &res);
sockfd = socket(res->ai _ family, res->ai _ socktype, res->ai _
protocol); bind(sockfd, res->ai _ addr, res >ai _ addrlen);
// accept() не нужен, только recvfrom(): fromlen = sizeof addr;
byte _ count = recvfrom(sockfd, buf, sizeof buf, 0, &addr,
&fromlen);
printf(“recv()’d %d bytes of data in buf\n”, byte _ count);
printf(“from IP address %s\n”,
inet _ ntop(addr.ss _ family,
addr.ss _ family == AF _ INET?
((struct sockadd _ in *)&addr)->sin _ addr:
((struct sockadd _ in6 *)&addr)->sin6 _ addr,
ipstr, sizeof ipstr);
11. select()
Проверяет готовы ли дескрипторы сокетов к чтению – записи.
Прототип
#include <sys/select.h>
int select(int n, fd _ set *readfds, fd _ set *writefds, fd _ set
*exceptfds, struct timeval *timeout);
FD _ SET(int fd, fd _ set *set); FD _ CLR(int fd, fd _ set *set);
FD _ ISSET(intfd,fd _ set*set);
FD _ ZERO(fd _ set *set);
44
Описание
Функция select() предоставляет способ одновременной проверки множества сокетов на предмет ожидания recv(), готовности к передаче данных через send() без блокирования или возникновения исключения. После того как макросы вроде FD_SET() использованы, массив структур
можно передать его функции как один из следующих параметров: readfds
если хотите знать, готов ли какой-нибудь сокет из массива к recv(),
writefds если какой-либо сокет готов к send(), и/или exceptfds если
нужно узнать произошло ли исключение на сокете. Любой их этих параметров может быть NULL если этот тип событий неинтересен. После
возврата из select() значения в массиве будут изменены, чтобы показать,
какие сокеты готовы к чтению – записи и какие имеют исключения.
Первый параметр, n это наивысший номер дескриптора сокета (они просто int) плюс один. Наконец, struct timeval *timeou t в конце позволяет указать select() как долго проверять эти массивы. Она вернёт управление при истечении таймаута или при возникновении события, смотря
что произойдет раньше. В struct timeval есть два поля: tv_sec это количество секунд, к которому добавляется tv_usec, количество микросекунд.
Возвращаемое значение
Возвращает количество дескрипторов с событиями в массиве, 0 если таймаут истёк и –1 при ошибке (errno устанавливается соответственно). Кроме того, массивы изменяются, чтобы показать готовые
сокеты.
Пример
t s1, s2, n;
fd _ set readfds;
struct timeval tv;
char buf1[256], buf2[256];
// полагаем, что здесь оба подключены к серверу
//s1 = socket(...);
//s2 = socket(...);
//connect(s1, ...)...
//connect(s2, …)...
// заранее очищаем массив FD _ ZERO(&readfds);
// добавляем наши дескрипторы в массив FD _ SET(s1, &readfds);
FD _ SET(s2, &readfds);
// поскольку s2 создан вторым, он “больше” и его используем
// в параметре n
select() n = s2 + 1;
// жд м появления данных на каком-либо сокете (таймаут 10.5
секунд)
45
tv.tv _ sec = 10;
tv.tv _ usec = 500000;
rv = select(n, &readfds, NULL, NULL, &tv);
if (rv == –1) {
perror(“select”);
// в select() произошла ошибка
} else if (rv == 0) {
printf(“Timeout occurred! No data after 10.5 seconds.\n”);
} else {
// на одном или обоих дескрипторах есть данные
if (FD _ ISSET(s1, &readfds)) {
recv(s1, buf1, sizeof buf1, 0);
}
if (FD _ ISSET(s2, &readfds)) {
recv(s1, buf2, sizeof buf2, 0);
}
}
12. socket()
Создаёт дескриптор сокета
Прототип
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Описание
Возвращает дескриптор сокета, который можно использовать. Обычно это первый шаг в процессе написания программ с сокетами и результат можно использовать в последующих вызовах listen(), bind(),
accept() или множества других функций.
Обычно значения параметров получается из вызова getaddrinfo(),
как в примере ниже, но можно их заполнять и вручную.
domain – определяет тип нужного вам сокета. Их может быть множество, но поскольку это пособие по сокетам tcp/ip, он будет PF_INET для IPv4 и PF_INET6 для IPv6.
type – хотя параметр type может принимать множество значений,
его следует установить в SOCK_STREAM для надёжных TCP сокетов
(send(), recv()) либо SOCK_DGRAM для ненадёжных быстрых UDP
сокетов (sendto(), recvfrom().)
protocol – параметр protocol указывает какой протокол использовать
для этого типа сокетов, например, SOCK_STREAM использует TCP.
Если будет использоваться SOCK_STREAM или SOCK_DGRAM,
46
можно просто установить protocol в 0, и он автоматически использует правильный протокол. Иначе можно использовать getprotobyname() для выбора номера нужного протокола.
Возвращаемое значение
Дескриптор нового сокета для последующих вызовов или –1 при
ошибке (и errno будет установлен соответственно.)
Пример
sruct addrinfo hints, *res; int sockfd;
// сначала заполняем адресные структуры с помощью getaddrinfo(): memset(&hints, 0, sizeof hints);
hints.ai _ family = AF _ UNSPEC;
// AF _ INET, AF _ INET6, или AF _ UNSPEC
hints.ai _ socktype = SOCK _ STREAM;
// SOCK _ STREAM или SOCK _ DGRAM getaddrinfo(“www.guap.ru”,
“3490”, &hints, &res);
// созда м сокет с помощью информации, которую получила getaddrinfo():
sockfd = socket(res->ai _ family, res->ai _ socktype, res>ai _ protocol);
13. setsockopt(), getsockopt()
Устанавливает для сокета различные опции.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int s, int level, int optname, void *optval,
socklen _ t *optlen);
int setsockopt(int s, int level, int optname, const void
*optval, socklen _ t optlen);
Описание
Эти функции получают и устанавливают определённые опции сокета.
Параметры это s это сокет, level должен быть установлен в SOL_
SOCKET. Затем, в optname установается имя которое связывает сокет
с символическим именем устройства вроде eth0 вместо использования
bind() для привязки к IP адресу. Позволяет другим сокетам связываться (bind()) с этим портом, несмотря на то, что уже существует активный сокет, слушающий этот порт. Это позволяет обойти сообщения “Address already in use”, когда вы пытаетесь перезапустить ваш
сервер после обрушения. Позволяет UDP дейтаграммным (SOCK_DGRAM)
47
сокетам посылать пакеты по широковещательным адресам и принимать с них.
Последний параметр, optlen, заполняется getsockopt() если необходимо указать его для setsockopt(), возможно он будет sizeof(int).
Возвращаемое значение
Возвращает 0 при успехе или –1 в случае ошибки (errno устанавливается соответственно).
Пример
int optval; int optlen;
char *optval2;
// установить SO _ REUSEADDR на сокете в ИСТИННО (1):
optval = 1;
setsockopt(s1, SOL _ SOCKET, SO _ REUSEADDR, &optval, sizeof
optval);
// связать сокет с именем устройства (может не работать на
некоторых системах):
optval2 = «eth1»;
// 4 байта длины, итого 4, ниже:
setsockopt(s2, SOL _ SOCKET, SO _ BINDTODEVICE, optval2, 4);
// посмотреть установлен ли флаг
SO _ BROADCAST: getsockopt(s3, SOL _ SOCKET, SO _ BROADCAST,
&optval, &optlen); if (optval != 0) {
print(“SO _ BROADCAST enabled on s3!\n”);
}
14. send(), sendto()
Посылают данные через сокет.
Прототип
#include <sys/types.h>
#include <sys/socket.h>
ssize _ t send(int s, const void *buf, size _ t len, int flags);
ssize _ t sendto(int s, const void *buf, size _ t len,
int flags, const struct sockaddr *to, socklen _ t tolen);
Описание
Эти функции посылают данные в сокет. В общем случае send()
используется для TCP SOCK_STREAM подключённых сокетов,
а sendto() д л я U D P SOCK_DGRAM неподключённых дейтаграммных сокетов. Каждый раз посылая пакет по неподключённому сокету вы должны указывать место назначения, поэтому последние параметры sendto() задают куда пакет направляется.
48
В обоих, send() и sendto(), параметр s это сокет, buf указатель на данные, которые необходимо послать, len число посылаемых байт и flags
позволяет определить дополнительную информацию как посылать
данные. Установите flags в ноль, если хотите иметь «нормальные»
данные. Ниже приведены несколько наиболее часто используемых
флагов:
MSG_OOB – посылает “out of band” данные. TCP поддерживает
этот способ.
Сообщить принимающей стороне, что у этих данных приоритет выше, чем у нормальных. Приёмник получит сигнал SIGURG
и примет эти данные без предварительной выборки нормальных
данных из очереди.
MSG_DONTROUTE – не посылать эти данные через маршрутизатор, они местные.
MSG_DONTWAIT – если send() должна блокироваться из-за загруженности внешнего трафика, она вернёт EAGAIN. Это вроде
“Разрешить не блокирование для этой посылки”
MSG_NOSIGNAL – send() на удалённый хост, который больше
не принимает (recv()) данные, обычно возбуждает сигнал SIGPIPE.
Этот флаг предотвращает возбуждение такого сигнала.
Возвращаемое значение
Возвращает число действительно посланных байт или –1 при
ошибке (errno устанавливается соответственно.) Это число может
быть меньше затребованного. Вспомогательная функция в разделе по
send() поможет обойти это. Также, если сокет был закрыт на противной стороне, процесс, вызвавший send(), получит сигнал SIGPIPE.
(Если только send() не был вызван с флагом MSG_NOSIGNAL).
Пример
int spatula _ count = 3490;
char *secret _ message = “The Cheese is in The Toaster”;
int stream _ socket, dgram _ socket; struct sockaddr _ in dest;
int temp;
// сначала с потоковым сокетом TCP:
// полагаем, что сокеты созданы и подключены
//stream _ socket = socket(...
//connect(stream _ socket, …
// преобразовать в порядок байтов сети
temp = htonl(spatula _ count);
49
// послать данные нормально: send(stream _ socket, &temp,
sizeof temp, 0);
// послать секретное out of band сообщение:
send(stream _ socket, secret _ message, strlen(secret _ message)+1, MSG _ OOB);
// теперь с дейтаграммным сокетом UDP:
//getaddrinfo(...
//dest = ...
// полагаем, что «dest» содержит адрес назначения
//dgram _ socket = socket(…
// послать секретное послание нормально:
sendto(dgram _ socket, secret _ message, strlen(secret _ message)+1, 0,
(struct sockaddr*)&dest, sizeof dest);
15. shutdown()
Останавливает обмен по сокету.
Прототип
#include <sys/socket.h>
int shutdown(int s, int how);
Описание
close()) закрывает обе стороны, для чтения и для записи, а дескриптор освобождается. Если необходимо закрыть только одну
или другую сторону, следует использовать shutdown(). Параметр s
это сокет с которым вы работаете, а что с ним делать определяет параметр how. Это может быть SHUT_RD для предотвращения дальнейших recv(), SHUT_WR для запрещения дальнейших send(), или
SHUT_RDWR для обоих. shutdown() не освобождает дескриптор
сокета и в итоге прийдется вызвать close(), чтобы закрыть его полностью. Этот системный вызов используется редко.
Возвращаемое значение
Возвращает 0 при успехе или –1 в случае ошибки (errno устанавливается соответственно).
Пример
nt s = socket(PF _ INET, SOCK _ STREAM, 0);
// …посылаем и обрабатываем …
// и когда вс сделано запрещаем дальнейшие посылки:
shutdown(s, SHUT _ WR);
50
16. struct sockaddr
Структуры для обработки интернет адресов.
Прототип
include <netinet/in.h>
Описание
Это базовые структуры для всех системных вызовов и функций,
работающих с интернет адресами. Для заполнения этих структур часто используется getaddinfo() а затем, по мере надобности читать их.
В памяти struct sockaddr_in и struct sockaddr_in6 начинаются
с одинаковой struct sockaddr, и можно приводить один тип к другому без какого- либо ущерба. Структура struct sockaddr_in используется с IPv4 адресами (вроде “192.0.2.10”). Она содержит семейство
адресов (AF_INET), порт в sin_ port и IPv4 адрес в sin_addr.
Кроме того в struct sockaddr_in есть поле sin_zero , которое должно содержать нули. Это можно сделать с помощью функцией memset().
В struct in_addr чаще всего используется только поле s_addr, поскольку многие системы реализуют только его. struct sockadd_in6
очень похожа на struct in6_addr , но используется для IPv6. struct
sockaddr_storage передаётся в accept() или recvfrom() когда необходимо написать код, не зависящий от версии IP, и неизвестно каким
будет новый адрес – IPv4 или IPv6. Структура struct sockaddr_storage достаточно велика, чтобы содержать оба типа, в отличие от оригинальной маленькой struct sockaddr.
Пример
// Все указатели на адресные структуры сокетов часто приводятся
// к этому типу перед их использованием в различных функциях
и вызовах:
struct sockaddr {
unsigned short sa _ family;
// семейство адресов, AF
_ xxx char sa _ data[14];
// 14 байт адреса протокола
};
// IPv4 AF _ INET сокеты: struct sockaddr _ in {
short
sin _ family;
// например, AF _ INET, AF _ INET6
unsigned short
sin _ port;
// например, htons(3490)
struct in _ addr sin _ addr;
// смотри struct in _ addr, ниже
51
Char sin _ zero[8]
// обнулите, если нужно
};
struct in _ addr {
unsigned long s _ addr;
// заполнить с помощью inet _ pton()
};
// IPv6 AF _ INET6 сокеты: struct sockaddr _ in6 {
u _ int16 _ t sin6 _ family;
// семейство адресов, AF _ INET6 u _ int16 _ t sin6 _ port;
// номер порта, порядок байтов сети
u _ int32 _ t sin6 _ flowinfo;
// IPv6 flow information
struct in6 _ addr sin6 _ addr;
// IPv6 адрес
u _ int32 _ tsin6 _ scope _ id;
// Scope ID
};
struct in6 _ addr {
unsigned char s6 _ addr[16];
// заполнить с помощью inet _ pton()
};
// Общая структура хранения адреса сокета достаточно велика
для хранения
// данных struct sockaddr _ in или struct sockaddr _ in6:
struct sockaddr _ storage {sa _ family _ t ss _ family;
// семейство адресов
// вс это расширение зависит от реализации, проигнорируйте:
char
ss _ pad1[ _ SS _ PAD1SIZE];
int64 _ t
ss _ align;
char
ss _ pad2[ _ SS _ PAD2SIZE];
};
// IPv4:
struct sockaddr _ in ip4addr; int s;
ip4addr.sin _ family =AF _ INET; ip4addr.sin _ port = htons(3490);
inet _ pton(AF _ INET, “10.0.0.1”, &ip4addr.sin _ addr); s =
socket(PF _ INET, SOCK _ STREAM, 0);
bind(s, (struct sockaddr*)&ip4addr, sizeof ip4addr);
// IPv6:
struct sockaddr _ in6 ip6addr;
int s;
52
СОДЕРЖАНИЕ
СПИСОК СОКРАЩЕНИЙ............................................. Предисловие............................................................... 1. Основы интерфейса Socket API................................... 1.1. Создание socket’а............................................... 1.2. Связывание socket’а........................................... 1.3. Ожидание установления связи............................. 1.4. Запрос на установление связи.............................. 1.5. Прием запроса на установление связи................... 1.6. Формирование адреса узла сети............................ 1.7. Функции обмена данными................................... 1.8. Посылка данных................................................ 1.9. Получение данных............................................. 1.10. Функции закрытия связи.................................. 2. Цикл заданий «Сетевое программирование
на базе Sockets API»..................................................... 3. Примеры использования интерфейса Socket API
для организации клиент – серверного взаимодействия...... Список используемых источников.................................. Приложение.Основные функции
библиотеки Sockets API c комментариями....................... 1. accept()................................................................ 2. bind().................................................................. 3. connect()........................................................................... 4. close().................................................................. 5. getaddrinfo(), freeaddrinfo(), gai_strerror()............... 6. gethostbyname(), gethostbyaddr()............................. 7. errno................................................................... 8. listen()................................................................. 9. perror(), strerror()................................................. 10. recv(), recvfrom()................................................. 11. select().............................................................. 12. socket().............................................................. 13. setsockopt(), getsockopt()...................................... 14. send(), sendto() ................................................... 15. shutdown()......................................................... 16. struct sockaddr ................................................... 3
4
5
5
6
7
8
8
9
10
11
11
12
14
16
30
31
31
32
33
34
35
38
41
42
43
44
45
47
48
49
51
52
53
Учебное издание
Калюжный Виталий Павлович
Осипов Леонид Андроникович
СЕТЕВОЕ ПРОГРАММИРОВАНИЕ
Учебное пособие
Публикуется в авторской редакции
Компьютерная верстка Н. Н. Караваевой
Сдано в набор 15.01.18. Подписано к печати 26.02.18. Формат 60 × 84 1/16.
Усл. печ. л. 2,2. Тираж 50 экз. Заказ № 74.
Редакционно-издательский центр ГУАП
190000, Санкт-Петербург, Б. Морская ул., 67
54
Для заметок
55
Для заметок
56
Документ
Категория
Без категории
Просмотров
0
Размер файла
1 457 Кб
Теги
kaluzhnyi
1/--страниц
Пожаловаться на содержимое документа