Журнал LinuxFormat - перейти на главную

LXF83:Unix API

Материал из Linuxformat
(Различия между версиями)
Перейти к: навигация, поиск
(Новая: == Сокеты == '' '''ЧАСТЬ 3''' Сегодня ваши программы научатся общаться со внешним миром – следуйте за '''Андр...)

Версия 21:01, 10 марта 2008

Содержание

Сокеты

ЧАСТЬ 3 Сегодня ваши программы научатся общаться со внешним миром – следуйте за Андреем Боровским!

«Сто лет тому назад дед нынешнего императора, в те времена еще наследный принц, за завтраком разбил яйцо с тупого конца и скорлупой порезал себе палец... С того времени все население Лилипутии разделилось на два лагеря – тупоконечников и остроконечников»
Дж. Свифт, «Гулливер в стране лилипутов»

В конкурсе на лучшую компьютерную идею всех времен и народов сокеты, без сомнения, могли бы рассчитывать на призовое место. Как и другие средства межпроцессного взаимодействия, рассмотренные в этой серии статей, сокеты впервые были реализованы именно на платформе Unix (4.2BSD), однако концепция сокетов как универсального средства обмена данными между процессами оказалась настолько удачна, что все современные системы поддерживают, по крайней мере, некоторое подмножество сокетов. Причины успеха сокетов заключаются в их простоте и универсальности. Программы, обменивающиеся данными с помощью сокетов, могут работать в одной системе и в разных, используя для обмена данными как специальные объекты системы, так и сетевой стек. Как и каналы, сокеты используют простой интерфейс, основанный на «файловых» функциях read(2) и write(2) (открывая сокет, программа Unix получает дескриптор файла, благодаря которому можно работать с сокетами, используя файловые функции), но, в отличие от каналов, сокеты позволяют передавать данные в обоих направлениях, как в синхронном, так и в асинхронном режиме. Большинство программистов используют для работы с сокетами различные библиотеки высокого уровня, однако высокоуровневые библиотеки, как правило, не позволяют задействовать всю мощь и многообразие сокетов. Наглядный пример многообразия – файловые сокеты. Программистам Windows должны быть знакомы сетевые сокеты, которые обычно организуют обмен данными с помощью протоколов семейства TCP/IP, однако в Unix есть и другие типы сокетов, специально предназначенные для обмена данными между локальными процессами.

Сокеты в файловом пространстве имен

Сокеты в файловом пространстве имен (file namespace, их еще назы- вают «сокеты Unix») используют в качестве адресов имена файлов специального типа. Важной особенностью этих сокетов является то, что соединение с их помощью локального и удаленного приложений невоз- можно, даже если файловая система, в которой создан сокет, доступна удаленной операционной системе. В следующем фрагменте кода мы создаем сокет и связываем его с файлом socket.soc (полный текст программы-сервера fsserver.c можно найти на диске): sock = socket(AF_UNIX, SOCK_DGRAM, 0); if (sock < 0) { perror(“socket failed”); return EXIT_FAILURE; } srvr_name.sa_family = AF_UNIX; strcpy(srvr_name.sa_data, “socket.soc”); if (bind(sock, &srvr_name, strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family)) < 0) { perror(“bind failed”); return EXIT_FAILURE; } Константы и функции, необходимые для работы с сокетами в фай- ловом пространстве имен, объявлены в файлах sys/types.h и sys/ socket.h. Как и файлы, сокеты в программах представлены дескрип- торами. Дескриптор сокета можно получить с помощью функции socket(2). Первый параметр этой функции – домен, к которому прина- длежит сокет. Домен сокета обозначает тип соединения (а не доменное имя Интернета, как вы могли бы подумать). Домен, обозначенный конс- тантой AF_UNIX, соответствует сокетам в файловом пространстве имен. Второй параметр, socket(), определяет тип сокета. Значение SOCK_ DGRAM указывает датаграммный сокет (я предпочитаю этот вариант написания используемому в [1] «дейтаграммный»). Датаграммные сокеты осуществляют ненадежные соединения при передаче данных по сети и допускают широковещательную передачу данных. Другой часто используемый тип сокетов – SOCK_STREAM соответствует потоковым сокетам, реализующим соединения «точка-точка» с надежной переда- чей данных. Впрочем, в пространстве файловых имен датаграммные сокеты так же надежны, как и потоковые сокеты. Третий параметр функ- ции socket() позволяет указать протокол, используемый для передачи данных. Мы оставляем значение этого параметра равным нулю. В слу- чае ошибки функция socket() возвращает -1. После получения дескриптора сокета мы вызываем функцию bind(2), которая связывает сокет с заданным адресом (связывать сокет с адресом необходимо в программе-сервере, но не в клиенте). Первым параметром функции является дескриптор, а вторым – указатель на структуру sockaddr (переменная srvr_name), содержащую адрес, на котором регистрируется сервер (третий параметр функции – длина структуры, содержащей адрес). Вместо общей структуры sockaddr для сокетов Unix (сокетов в файловом пространстве имен) можно исполь- зовать специализированную структуру sockaddr_un. Поле sockaddr. sa_family позволяет указать семейство адресов, которым мы будем пользоваться. В нашем случае это семейство адресов файловых соке- тов Unix AF_UNIX. Сам адрес семейства AF_UNIX (поле sa_data) пред- ставляет собой обычное имя файла сокета. После вызова bind() наша программа-сервер становится доступна для соединения по заданному адресу (имени файла). При обмене данными с датаграммными сокетами мы используем не функции write() и read(), а специальные функции recvfrom(2) и sendto(2). Эти же функции могут применяться и при работе с пото- ковыми сокетами, но в соответствующем примере мы воспользуемся «сладкой парочкой» read()/write(). Для чтения данных из датаграм- много сокета мы используем функцию recvfrom(2), которая по умол- чанию блокирует программу до тех пор, пока на входе не появятся новые данные. bytes = recvfrom(sock, buf, sizeof(buf), 0, &rcvr_name, &namelen); Вызывая функцию recvfrom(), мы передаем ей указатель на еще одну структуру типа sockaddr, в которой функция возвращает дан- ные об адресе клиента, запросившего соединение (в случае файловых сокетов этот параметр не несет полезной информации). Последний параметр функции recvfrom() – указатель на переменную, в которой будет возвращена длина структуры с адресом. Если информация об адресе клиента нас не интересует, мы можем передать значения NULL в предпоследнем и последнем параметрах. По завершении работы с сокетом мы закрываем его с помощью «файловой» функции close(). Перед выходом из программы-сервера следует удалить файл сокета, созданный в результате вызова socket(), что мы и делаем с помощью функции unlink(). Если программа-сервер показалась вам простой, то программа- клиент (fsclient.c) будет еще проще. Мы открываем сокет с помощью функции socket() и передаем данные (тестовую строку) серверу с помо- щью «напарника» recvfrom(), функции sendto(2): srvr_name.sa_family = AF_UNIX; strcpy(srvr_name.sa_data, SOCK_NAME); strcpy(buf, “Hello, Unix sockets!”); sendto(sock, buf, strlen(buf), 0, &srvr_name, strlen(srvr_name.sa_data) + sizeof(srvr_name.sa_family)); Первый параметр функции sendto() – дескриптор сокета, второй и третий параметры позволяют указать адрес буфера для передачи данных и его длину. Четвертый параметр предназначен для передачи дополнительных флагов. Предпоследний и последний параметры несут информацию об адресе сервера и его длине, соответственно. Если при работе с датаграммными сокетами вызвать функцию connect(2) (см. ниже), то можно не указывать адрес назначения каждый раз (доста- точно указать его один раз, как параметр функции connect()). Перед вызовом функции sendto() нам надо заполнить структуру sockaddr (переменную srvr_name) данными об адресе сервера. После оконча- ния передачи данных мы закрываем сокет с помощью close(). Если вы запустите программу-сервер, а затем программу-клиент, то сервер рас- печатает тестовою строку, переданную клиентом.

Парные сокеты

Сокеты в файловом пространстве имен похожи на именованные каналы тем, что для идентификации сокетов используются файлы специально- го типа. В мире сокетов есть и аналог неименованных каналов – парные сокеты (socket pairs). Как и неименованные каналы, парные сокеты создаются парами и не имеют имен. Естественно, что область приме- нения парных сокетов та же, что и у неименованных каналов, – взаимо- действие между родительским и дочерним процессом. Так же как и в случае неименованного канала, первый из дескрипторов используется одним процессом, второй – другим. В качестве примера использования парных сокетов мы рассмотрим программу sockpair.c, создающую два процесса с помощью fork(). Дочерние процессы sockpair.c использу- ют парные сокеты для обмена вежливым английским приветствием.

  1. include <sys/types.h>
  2. include <sys/socket.h>
  3. include <stdlib.h>
  4. include <stdio.h>
  5. include <errno.h>
  6. define STR1 “How are you?”
  7. define STR2 “I’m ok, thank you.”
  8. define BUF_SIZE 1024

int main(int argc, char ** argv) { int sockets[2]; char buf[BUF_SIZE]; int pid; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) < 0) { perror(“socketpair() failed”); return EXIT_FAILURE; } pid = fork(); if (pid != 0) { close(sockets[1]); write(sockets[0], STR1, sizeof(STR1)); read(sockets[0], buf, sizeof(buf)); printf(“%s\n”, buf); close(sockets[0]); } else { close(sockets[0]); read(sockets[1], buf, sizeof(buf)); printf(“%s\n”, buf); write(sockets[1], STR2, sizeof(STR2)); close(sockets[1]); } } Парные сокеты создаются функцией socketpair(2). У функции socketpair() четыре параметра. Первые три параметра функции те же, что и у socket(), а четвертым параметром является массив из двух переменных, в которых возвращаются дескрипторы. Дескрипторы соке- тов, возвращенные socketpair(), уже готовы к передаче данных, так что мы сразу можем применять к ним функции read()/write(). После вызова fork() каждый процесс получает оба дескриптора, один из кото- рых он должен закрыть. Для закрытия сокета мы используем функцию close(). При взгляде на интерфейс программирования парных сокетов может возникнуть вопрос: а почему, собственно, эти функции относятся к соке- там? Ведь при работе с ними мы не используем ни адреса, ни модель «клиент-сервер». Это верно, но заметьте, что функции socketpair() передаются значения домена и типа сокета, так что и формально, и с точки зрения реализации в системе мы используем настоящие сокеты. Следует отметить, что указание домена в функции socketpair() выгля- дит явно излишне, поскольку для этой функции система поддерживает только сокеты в домене AF_UNIX (вполне логичное ограничение, если учесть, что парные сокеты не имеют имен и предназначены для обмена данными между родственными процессами).

Сетевые сокеты

Мы переходим к рассмотрению самого важного и универсального типа сокетов – сетевых сокетов. Думаю, что о значении, которое име- ют сетевые сокеты в Unix-системах, распространяться не нужно. Даже если вы пишете систему приложений, предназначенных для работы на одном компьютере, рассмотрите возможность использования сетевых сокетов для обмена данными между этими приложениями. Возможно, в будущем ваш программный комплекс наберет мощь и возникнет необходимость распределить его компоненты на нескольких машинах. Использование сетевых сокетов сделает процесс масштабирования проекта безболезненным. Впрочем, у сетевых сокетов есть и недостатки. Даже если сокеты используются для обмена данными на одной и той же машине, передаваемые данные должны пройти все уровни сетево- го стека, что отрицательно сказывается на быстродействии и нагрузке на систему. В качестве примера мы рассмотрим комплекс из двух приложений, клиента и сервера, использующих сетевые сокеты для обмена данными. Текст программы сервера вы найдете в файле netserver.c, ниже мы приводим некоторые фрагменты. Прежде всего, мы должны получить дескриптор сокета: sock = socket(AF_INET, SOCK_STREAM, 0); if (socket < 0) { printf(“socket() failed: %d\n”, errno); return EXIT_FAILURE; } В первом параметре функции socket() мы передаем константу AF_INET, указывающую на то, что открываемый сокет должен быть сетевым. Значение второго параметра требует, чтобы сокет был потоко- вым. Далее мы, как и в случае сокета в файловом пространстве имен, вызываем функцию bind(): serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(port); if (bind(sock, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { printf(“bind() failed: %d\n”, errno); return EXIT_FAILURE; } Переменная serv_addr, – это структура типа sockaddr_in. Тип sockaddr_in специально предназначен для хранения адресов в фор- мате Интернета. Самое главное отличие sockaddr_in от sockaddr_un – наличие параметра sin_port, предназначенного для хранения значения порта. Функция htons() переписывает двухбайтовое значение порта так, чтобы порядок байтов соответствовал принятому в Интернете (см. врез- ку). В качестве семейства адресов мы указываем AF_INET (семейство адресов Интернета), а в качестве самого адреса – специальную конс- танту INADDR_ANY. Благодаря этой константе наша программа-сер- вер зарегистрируется на всех адресах той машины, на которой она выполняется. Чтобы понять, что мы должны делать дальше, давайте вспомним, как работает сетевая подсистема Unix и, в данном случае, любой дру- гой ОС. Сетевой сервер должен уметь выполнять запросы множества клиентов одновременно (наш сервер netserver.c фактически может обработать запрос только одного клиента, но речь сейчас идет об общем случае). При этом в соединениях «точка-точка», например, при использовании потоковых сокетов, у сервера для каждого клиента дол- жен быть открыт отдельный сокет. Из этого следует, что мы не должны устанавливать соединение с клиентом через сам сокет sock, предназна- ченный для прослушивания входящих запросов (обычно, при использо- вании сетевых сокетов мы и не можем этого сделать), иначе все другие попытки соединиться с сервером по указанному адресу и порту будут заблокированы. Вместо этого мы вызываем функцию listen(2), которая переводит сервер в режим ожидания запроса на соединение: listen(sock, 1); Второй параметр listen() – максимальное число соединений, кото- рые сервер может обрабатывать одновременно. Далее мы вызываем функцию accept(2), которая устанавливает соединение в ответ на запрос клиента: newsock = accept(sock, (struct sockaddr *) &cli_addr, &clen); if (newsock < 0) { printf(“accept() failed: %d\n”, errno); return EXIT_FAILURE; } Получив запрос на соединение, функция accept() возвращает новый сокет, открытый для обмена данными с клиентом, запро- сившим соединение. Сервер как бы перенаправляет запрошенное соединение на другой сокет, оставляя сокет sock свободным для прослушивания запросов на установку соединения. Второй параметр функции accept() содержит сведения об адресе клиента, запросив- шего соединение, а третий параметр указывает размер второго. Так же, как и при вызове функции recvfom(), мы можем передать NULL в последнем и предпоследнем параметрах. Для чтения и записи данных сервер использует функции read() и write(), а для закрытия сокетов, естественно, close(). В программе-клиенте (netclient.c) нам, прежде всего, нужно решить задачу, с которой мы не сталкивались при написании серве- ра, а именно: выполнить преобразование доменного имени сервера в его сетевой адрес. Разрешение доменных имен выполняет функция gethostbyname(): server = gethostbyname(argv[1]); if (server == NULL) { printf(“Host not found\n”); return EXIT_FAILURE; } Функция получает указатель на строку с Интернет-именем сервера (например, www.unix.com или 192.168.1.16) и возвращает указатель на структуру hostent (переменная server), которая содержит имя сервера в приемлемом для дальнейшего использования виде. При этом, если необходимо, выполняется разрешение доменного имени в сетевой адрес. Далее мы заполняем поля переменной serv_addr (структуры sockaddr_in) значениями адреса и порта: serv_addr.sin_family = AF_INET; strncpy((char *)&serv_addr.sin_addr.s_addr, (char *)server->h_addr, server->h_length); serv_addr.sin_port = htons(port); Программа-клиент открывает новый сокет с помощью вызова функ- ции socket() аналогично тому, как это делает сервер (дескриптор соке- та, который возвращает socket() мы сохраним в переменной sock), и вызывает функцию connect(2) для установки соединения: if (connect(sock, &serv_addr, sizeof(serv_addr)) < 0) { printf(“connect() failed: %d”, errno); return EXIT_FAILURE; } Теперь сокет готов к передаче и приему данных. Программа-клиент считывает символы, вводимые пользователем в окне терминала. Когда пользователь нажимает Enter, программа передает данные серверу, ждет ответного сообщения сервера и распечатывает его. На протяжении этой статьи мы несколько раз упоминали не-бло- кирующие сокеты. Остановимся на них немного подробнее. О не-бло- кирующих сокетах вам нужно знать, прежде всего, то, что ими можно не пользоваться. Благодаря многопоточному программированию мы можем использовать блокирующие сокеты во всех ситуациях (и тогда, когда нам нужно обрабатывать несколько сокетов одновременно, и тог- да, когда нам требуется возможность прервать операцию, выполняемую над сокетом). Рассмотрим, тем не менее, две функции, необходимые для работы с не-блокирующими сокетами. По умолчанию функция socket() создает блокирующий сокет. Чтобы сделать его не-блокирую- щим, мы используем функцию fcntl(2): sock = socket(PF_INET, SOCK_STREAM, 0); fcntl(sock, F_SETFL, O_NONBLOCK); Теперь любой вызов функции read() для сокета sock будет возвра- щать управление сразу же. Если на входе сокета нет данных для чте- ния, функция read() вернет значение EAGAIN. Для поверки состояния не-блокирующих сокетов можно воспользоваться функцией select(2). Функция select() способна проверять состояние нескольких дескрип- торов сокетов (или файлов) сразу. Первый параметр функции – количе- ство проверяемых дескрипторов. Второй, третий и четвертый параметры функции представляют собой наборы дескрипторов, которые следует проверять, соответственно, на готовность к чтению, записи и на наличие исключительных ситуаций. Сама функция select() – блокирующая, она возвращает управление, если хотя бы один из проверяемых сокетов готов к выполнению соответствующей операции. В качестве последне- го параметра функции select() можно указать интервал времени, по прошествии которого она вернет управление в любом случае (тайм-аут). Вызов select() для проверки наличия входящих данных на сокете sock может выглядеть так: fd_set set; struct timeval interval; FD_SET(sock, &set); tv.tv_sec = 1; tv.tv_usec = 500000; ... select(1, &set, NULL, NULL, &tv); if (FD_ISSET(sock, &set) { // Есть данные для чтения } Все, что касается функции select() теперь объявляется в заголо- вочном файле sys/select.h (раньше объявления элементов функции select() были разбросаны по файлам sys/types. h , sys/time.h и stdlib.h). В приведенном фраг- мента кода, FD_SET и FD_ISSET – мак- росы, предназначенные для работы с набором дескрипторов fd_set. На этом мы закончим зна- комство с увлекательным миром межпроцессного взаимодействия Linux. Следующая статья будет посвящена управлению процесса- ми, сигналам и потокам.

Персональные инструменты
купить
подписаться
Яндекс.Метрика