Система 4.3BSD сокетов. 1. Введение. Одним из наиболее важных дополнений в семействе UNIX была система межпроцессорных коммуникаций (далее, именуемая IPC). Эта система стала результатом 2-х летних дискуссий и исследований. Представленная в 4.2BSD система, обьединяла множество идей, пы- таясь сохранить философию UNIX простой и сжатой. Текущая реали- зация, 4.3BSD, пополнилась некоторыми средствами IPC и предоста- ляет вверх-совместимый интерфейс. Это позволяет надеяться на то, что IPC, включенные в 4.3BSD станут стандартом для UNIX. Первоначально область межпроцессорных взаимодействий была представлена весьма бедно. Существовал стандартный механизм, позволяющий взаимодействовать процессам двум. Основным ограинче- нием было то, что два взаимодействующих процесса должны были быть родственными. Ранние попытки расширить возможности IPC в UNIX, наталкива- лись на неодназначную реакцию. Основной проблемой было то что, эти идеи базировались на использовании файловой системы UNIX, это требовало ее изменения. Поэтому, IPC система, представленная в 4.3BSD, была изначально разработана полностью независимой под- системой. Процессы могут взаимодействовать через имена типа имен файловой системы так же, как и через сетевые имена. В этом документе представлено описание IPC и способы ее ис- пользования. Этот документ разработан для дополнения "Руководс- тва программиста" примерами использования IPC примитивов. Доку- мент состоит из пяти частей. Часть 2 знакомит с основами комму- никационного взаимодействия и IPC-зависимыми системными вызова- ми. Часть 3 описывает некоторые библиотечные процедуры, которые программист может найти интересными. Часть 4 концентирируется на модели клиент/сервер, используемой для разработки приложений и включает примеры двух основных типов серверов. Часть 5 рассмат- ривает сложные темы для искушенных пользователей. 2. ОСНОВЫ IPC. Базовым понятием всех коммуникаций является понятие сокета. Сокет - это точка присоединения к устройству связи ( англ. soc- ket - гнездо). При использовании каждый сокет имеет тип и одну или более ассоциированых с ним программ. Сокеты существуют внут- ри коммуникационных доменов. Коммуникационный домен это абстрак- ция введенная для связывания процессов взаимодействующих через сокеты. Одно из таких свойств - это схема используемая для обоз- начения сокетов. Например, в домене UNIX сокеты обозначаются полным путем имени: сокет может быть обозначен "/dev/foo". Соке- ты обычно обмениваются данными с сокетами из такого-же домена, однако возможна передача между сокетами из разных доменов, если выполняется процесс ретранслирующий данные. В 4.3BSD IPC имеется поддержка трех доменов: 1. UNIX домен - для внутримашинного взаимодействия. 2. Internet домен - для межмашинного взаимо дейс- твия, используя семейство протоколов TCP/IP (DARPA стандарт). 3. NS домен - для межмашинного взаимодействия, ис- пользуя протоколы стандарта Xerox. 2.1. Типы сокетов. В настоящее время для использования доступно 4-е типа. Сокет типа "stream" (от англ. stream - поток) предоставляет двунаправленную, надежную, последовательную связь, без дублиро- вания данных и без ограничения на размер передаваемых данных. То есть пара соединенных сокетов предоставляет интерфейс похожий на стандартный pipe (за исключением двунаправленности). Сокет типа "datagram" предоставляет двунаправленную связь, которая не гарантирует последовательность, надежность, исключе- ние дублирования передаваемых данных. То есть процесс принимаю- щий сообщения из сокета "datagram" может получить дубликат како- го-либо сообщения или получить сообщения в порядке отличном от порядка посылки их. Очень важной характеристикой сокета типа "datagram" является ограничение на размер данных посылаемых. Сокет типа "raw" предоставляет программистам доступ к ос- новным коммуникационным протоколам которые поддерживают абстрак- тный сокет. Эти сокеты обычно ориентированны на датаграммы, хотя их точные характеристики зависят от интерфейса предоставляемого протоколом. Сокеты "raw" главным образом предназначенны для тех программистов, кто интересуется разработкой новых коммуникацион- ных протоколов или получением доступа к более изощренным особен- ностям уже сущестующего протокола. Использование сокетов типа "raw" рассматривается в 5 части данного документа. Сокет типа "sequenced packet" похож на сокет типа "stream", за исключением того, что размер посылаемого за один раз сообще- ния ограничен. Этот интерфейс представлен только как часть абс- трактного NS сокета и очень важен для некоторых сложных NS при- ложений. Этот тип сокета позволяет программисту манипулировать SPP и IDP заголовками пакета или группой пакетов записав прото- тип заголовка для всех посылаемых данных или указывая заголовок используемый по умолчанимю для всех посылаемых пакетов и позво- ляет программисту принимать заголовки приходящих пакетов. Ис- пользование этих особенностей описано в части 5. Другой возможный тип сокета, обладающий интересными свойс- твами - это "reliable delivered message", что означает "надежная доставка сообщений". Этот тип сокета очень похож на "datagram" и отлнчается от него лишь тем, что гарантирует доставку сообщения. В настоящее время этот тип сокета не поддерживается, но в прин- ципе может быть реализован на прикладном уровне, то есть не на уровне ядра. Более подробную информацию Вы можете найти в ч. 5. 2.2. Создание сокета. Для создания сокета используется системный вызов socket: s = socket( domain, type, protocol ) Этот системный вызов создает сокет в указанном домене и указан- ного типа. Дополнительно может быть указан. Если протокол не указан (т.е. protocol = 0) то система выберет подходящий прото- кол из тех протоколов, которые входят в домен domain и которые могут быть использованы для поддержки запрашиваемого типа сокета type. Возвращается дескриптор ( целое число ), который позднее может быть использован в системных вызовах для действий с этим сокетом. Домен domain - это одна из констант, определенных в файле . Для выбора домена UNIX определена констан- та AF_UNIX; для домена Internet AF_INET; для NS - AF_NS. Для ти- пов сокетов также определены константы ( в том же файле ): 1. тип "stream" - константа SOCK_STREAM; 2. тип "datagram" - константа SOCK_DGRAM; 3. тип "raw" - константа SOCK_RAW; 4. тип "sequenced packet" - константа SOCK_SEQPACKET; Например, для создания потокового сокета в Internet домене может использоваться следующий вызов: s = socket( AF_INET, SOCK_STREAM, 0); В результате этого вызова в s будет находится дескриптор сокета "stream" типа, протокол будет TCP. Еще пример: для создания сокета для внутримашинной дейтаграмной передачи модно изпользовать следующий вызов: s = socket( AF_UNIX, SOCK_DGRAM, 0) В приведенных выше примерах не используется принудительное ука- зание протокола, но это возможно. Эта возможность будет подроб- нее рассмотрена в части 5. Существует несколько причин по которым создание сокета будет не- возможно. Исключая случай нехватки памяти (ENOBUFS), запрос на создание сокета может провалиться в случае указания несущестую- щего протокола для указанного домена (EPROTONOSUPPORT), а также в случае указания несуществующего протокола для указанного типа сокета (EPROTOTYPE). 2.3. Связывание локальных имен. Сокет создается без имени. Пока сокет не будет связан с ка- ким-либо именем, процесс не может ссылаться на него, следова- тельно он не может обмениваться посредством этого сокета данны- ми. Коммуникационный процесс может получить имя (binding) с по- мощью связывания. Имя зависит от домена сокета. Так в случае до- менов Internet и NS имя состоит из локального и удаленного сете- вых адресов и портов. Для UNIX домена - это полные пути до ло- кального и удаленного сокетов ("удаленный путь" означает не путь на другой системе, а путь к сокету созданному длугим процессом). В большинсте доменов имя должно быть уникальным. В домене Inter- net не должно быть дубликата для имени <протокол, локальный ад- рес, локальный порт, удаленный адрес, удаленный порт>. В домене UNIX не должно быть дубликатов типа <протокол, локальный путь, удаленный путь>, пути также не должны ссылаться на уже существу- ющие файлы в системе; хотя последняя особенность может изме- ниться в последующих реализациях. Системный вызов bind позволяет назначить сокету только часть (точнее половину) имени. Например: <локальный адрес, ло- кальный порт> или <локальный путь>. Полностью связать сокет мож- но с помощью системных вызовов connect и accept. В домене Internet, связывание имен с сокетами является дос- таточно сложной задачей. К счастью, обычно не требуется указы- вать точный порт и адрес сокету, потому что, системные вызовы connect и send будут автоматически связаны с подходящим адресом, если они используются с несвязанным сокетом. Процесс связывания сокета в NS домене очень похож на связывание в Internet домене. Системный вызов bind используется следующим образом: bind( s, name, namelen); Имя name - это строка переменной длины, которая будет ин- терпретироваться соотиетсвующим этому сокету протоколом. Эта ин- трепретация может изменяться от домена к домену (это одно из различий доменов - система адресации). Как упоминалось ранее в Internet домене имя содержит Internet адрес и порт. Аналогично, в NS домене имя состоит из сетевого адреса и порта. В UNIX доме- не имя содержит полный путь и тип (тип всегда должен быть AF_ UNIX). Если некто хочет связать сокет с именем /tmp/foo в домене AF_UNIX, должен использоваться вызов типа следующего: #include ... struct sockaddr_un addr; ... strcpy(addr.sun_path,"/tmp/foo"); addr.sun_family = AF_UNIX; bind(s, (struct sockaddr*) &addr, strlen(addr.sun_path) + sizeof (addr.sun_family)); Помните, что при определении размера имени в домене UNIX нулевые байты не считаются, именно поэтому используется strlen. В текущей реализации UNIX домена имя файла на которое ссылается поле sun_path создается как сокет в пространстве файловой систе- ме. Поэтому, вызывающий должен иметь права на запись в директо- рию, где будет создан сокет и должен будет быть удален, в случае его закрытия. В Internet домене имя адрес более сложный. В пос- леднем случае связывание выглядит следущим образом: #include #include ... struct sockaddr_in sin; ... bind( s, (struct sockaddr *) &sin, sizeof (sin)); Но разбор системы адресации Internet и структуры требует большого обсуждения, к этому вопросу мы вернемся в части 3, ког- да будем обсуждать процедуры используемые в разрешении Internet адресов. Связывание сокетов в NS домене еще более сложное, так как библиотека процедур для работы с Internet адресами не работает с NS адресами. Этот вопрос также будет обсуждаться в части 3. 2.4. Установление соединения. Установление соединения обычно асимметрично, с одним про- цессом , так называемым "клиентом" и другим - "сервером". Когда сервер согласен предоставить какой-либо сервис, он связывает свой сокет с так называемым "хорошо известным" (well-known)име- нем, котоое в свою очередь обычно асоциируется с этим сервисом и пассивно "слушает" сокет. Если клиент запрашивает некоторый сер- вис у сервера, он инициирует соединение на сокет сервера. Со стороны клиента это выгдядит следующим образом: Для UNIX домена: struct sockaddr_un ser; ... connect( s,(struct sockaddr *) &ser,strlen(ser.sun_path) + sizeof (ser.sun_family)); Для Internet домена: struct sockaddr_in server; ... connect( s, (struct sockaddr *) &server, sizeof (server)); Для NS домена: struct sockaddr_ns server; ... connect( s, (struct sockaddr *) &server, sizeof (server)); Здесь предпологается, что в server уже находится соответс- твующее домену имя: путь для UNIX домена, адрес и порт для Un- ternet и NS доменов, с которыми хочет установить соединение кли- ент. Если во время вызова connect сокет клиента не связан, то ядро само назначит сокету подходящее имя, это обычно означает, что сокет будет связан с локальным именем. Ошибка возвращается если соединения не произошло (однако сокет автоматически связан- ный системным вызовом connect остается связанным). В случае ус- пешного завершения, устанавливается соединение с сервером и те- перь через сокет возможен обмен данными. Далее описаны возможные ошибки возвращаемые в случае неудачного завершения вызова con- nect: 1. ETIMEDOUT - после невозможности установления соединения ядро решает, что сервер не доступен. Это может произойти в случае, если сервер не работает в данное время или с по носителю (обычно это сеть) не могут быть переданы данные из-за потери пакетов данных. 2. ECONNREFUSED - удаленный хост отказал в сервисе по неко- торым причинам. Обычно это происходит потому, что сервер- ный процесс на удаленной машине не запущен. 3. ENETDOWN или EHOSTDOWN - эти ошибки возвращаются основными коммуникационными сервисами 4. ENETUNREACH или EHOSTUNREACH - Эти ошибки возникают если сеть или хост неизвестны (нет маршрута до сети или хост не существует), или ошибки возникают на основе информации возвращенной промежуточным шлюзом. Если поступающая инфор- мация не позволяет определить, что недоступно: хост или вся сеть, ядро вернет ошибку недоступности всей сети. Для сервера принимающего запросы клиентов на установление связи, должны быть выполнены две операции после связывания соке- та с именем. Первая, сообщает ядру о установлении сокета в режим "слушания" на предмет входящих запросов: listen( s, 5); Второй параметр, в системном вызове listen, определяет макси- мальное количество запросов на соединение, которые могут быть поставлены в очередь для подтверждения серверным процессом; это число может быть ограниченно системой. Если запросы на соедине- ние поступает в то время как очередь занята, они не будут отвер- гаться, но будут игнорироваться индивидуальные пакеты, в которых собственно раполагается сам запрос. Это дает серверу время для освобождения очереди, в то время как клиент пытается снова уста- новить соединение. Если клиент после таких попыток получил ошиб- ку ECONNREFUSED, он не сможет решить работал сервис или нет. Возможно получить ошибку ETIMEDOUT, однако это неправдоподобно. Сервер может принять запрос на соединение с сокетом, поме- ченным как "слушающий", с помощью системного вызова accept: struct sockaddr_in from; ... fromlen = sizeof (from); newsock = accept( s, (struct sockaddr *) &from, &fromlen); Для UNIX и NS доменов структура from должна быть соответственно sockaddr_un и sockaddr_ns. В этом и следующих приимерах будет использоваться домен Internet. Системный вызов accept возвращает дескриптор нового сокета. Если сервер хочет узнать, кто является его клиентом, он может зарезервировать буфер для имени сокета коиента. Значение fromlen устанавливается сервером, для того чтобы ограничить размер буфера. Если сервер не интересуется име- нем клиентского сокета, он может вместо указателя на буфер пере- давать нулевой указатель (NULL). Системный вызов обычно блокируется. То есть вызов не вернет управление процессу, пока не будет запроса на соединение или вы- зов не будет прерван сигналом. Нет возможности указать сокет или группу сокетов с которыми возможно соединение, однако сервер должен подтвердить соединение, узнать с каким процессом произош- ло соединение и если необходимо, закрыть его. 2.5. Обмен данными. Когда соединение установлено, через сокеты можно передавать данные. Для посылки и принятия данных имеется несколько систем- ных вызовов. На любом конце соединения (сокете) программист мо- жет посылать и принимать сообщения без указания другого (парно- го) сокета. При этом могут использоваться системные вызовы write и read: write(s, buf, sizeof (buf)); read(s, buf, sizeof (buf)); Кроме того, можно использовать вызовы send и recv: send(s, buf, sizeof (buf), flags); recv(s, buf, sizeof (buf), flags); Несмотря на то, что write и read очень похожи на send и recv, дополнительный параметр очень важен. Параметр flags, определен- ный в , может иметь ненулевой значение, а именно: MSG_OOB послать/принять "out of band" данные MSG_PEEK получение данных без удаления их из потока MSG_DONTROUTE послать сообщение без маршрутизирующих пакетов "Out of band" данные - это специфическое понятие для "STRE- AM" сокетов (потоковых) и мы не будем сейчас подробно их рас- сматривать. Флаг MSG_DONTROUTE применяется к выходящим пакетам, в настоящее время используется только процессом, управляющим таблицей маршрутов (роутером) и не представляет интереса для прикладного программиста. Флаг MSG_PEEK предоставляет интересную возможность: он позволять прочитать данные из потока без удале- ния данных из оного. Это означает, что при следующей операции чтения будут выданы те же самые данные. 2.6. Сброс сокетов. Если работа с сокетом закончена, то к нему можно применить обыкновенную операцию close как для файлового дескриптора (собс- твенно сокет - это тоже файловый дескриптор): close(s); Если имеет место закрытие сокета с которым ассоциированны данные и этот сокет гарантирует надежную доставку (то есть "STREAM" со- кет или "потоковый"), то система будет продолжать пытаться пере- дать данные. Однако, если по истечение некоторого периода време- ни система не сможет передать данные то они будут сброшены, ина- че говоря, всесвязанные с сокетом буферы будут уничтожены. Если программист не ожидает данные на сокете он может выполнить сис- темный вызов shutdown перед закрытием сокета. Системный вызов shutdown используется следующим образом: shutdown( s, how); Где how может принимать следующие значения: 0 - данные из сокета не будут больше читаться,это означает, что из сокета ничего нельзя будет прочитать; 1 - данные больше не будут посылаться; 2 - данные не будут ни приниматься,ни посылаться на сокете. 2.7. Сокеты без соединения. До сих пор мы рассматривали сокеты, которые были ориентиро- ванны на модель с соединением. Однако существует также поддержка взаимодействия без соединения. Сокеты типа "DATAGRAM" предостав- ляют симметричный интерфейс для обмена данными. Пока процессы остаются похожими на сервер и клиента, нет необходимости уста- навливать соединение. Вместо этого, каждое сообщение включает в себя адрес назначения. Сокеты "DATAGRAM" создаются также как было описано ранее. Если есть необходимость в установлении особенного локального ад- реса сокета, то операциям передачи данных должен предшествовать системный вызов bind. В противном случае, система установит ло- кальный адрес и/или порт в момент вызова первой операции пере- сылки данных. Для посылки сообщений, используется системный вы- зов sendto: sendto(s, buf, buflen, flags, (struct sockaddr *)&to,tolen); Параметры s, buf, buflen и flags несут такуюже смысловую нагруз- ку, как и в вызове send. Параметры to и tolen используются для указания назначения сообщения. Когда используется ненадежный "DATAGRAM" интерфейс, то посылающий может не получить всех сооб- щений об ошибках. Эти ошибки возвращаются только в том случае, если невозможность достаки сообщения обнруживается локально (например, сеть не доступна). Системный вызов возвращает -1 в случае обнаружения ошибки и глобальная переменная errno содержит номер ошибки. Для принятия сообщения на несоединенном "DATAGRAM" сокете, используется системный вызов recvfrom: recvfrom(s, buf, buflen, flags, (struct sockaddr *)&from, &fromlen); Параметр fromlen содержит при вызове размер буфера from, после возврата из вызова содержит реальный размер адреса из которого было принято сообщение. В дополнение к вызовам описаным ранее, "DATAGRAM" сокеты также могут использовать системный вызов connect для соединения с указанным адресом назначения. В этом случае, любые посланные данные автоматически будут адресованны парному сокету, адрес ко- торого был указан при вызове connect. Аналогично, только данные принятые с указанного в connect адреса будут приниматься соке- том. В один момент времени каждый сокет может быть соединен только с одним адресом, второе соединение (имеется ввиду вызов connect на уже соединенном сокете) будет менять адрес парного сокета. Запрос соединения на "DATAGRAM" сокете connect возвраща- ется немедленно, так как система только сохраняет адрес назначе- ния (на "STREAM" сокете соединение устанавливается обоими конца- ми). Системные вызовы accept и listen не используются с "DATAG- RAM" сокетами. Пока "DATAGRAM" сокет находится в состоянии сое- динения, ошибки от нового вызова send могут быть возвращены асинхронно. Эти ошибки могут быть возвращены следующими операци- ями на сокете или с помощью специальной опцией SO_ERROR, ис- пользуемой в системном вызове getsockopt, для опрос сокета на предмет ошибочного состояния. Системный вызов select будет не будет возвращать ошибку, если она имела место. Следующая опера- ция будет возвращать ошибку и очистит статус ошибки. Другие ме- нее важные детали "DATAGRAM" сокетов будут обсуждаться в 5 части. 2.8. Мультиплексирование ввода/вывода. Последняя возможность часто используется при разработке приложений способных мультиплексирвать запросы между многими со- кетами и/или файлами. Это делается с помощью системного вызова select: #include #include ... fd_set readmask, writemask, exceptmask; struct timeval timeout; ... select(nfds, &readmask, &writemask, &exceptmask, &timeout); Вызов select берет как аргументы указатели на 3-и множества, первое множество сокетов на которых возможен прием сообщений, второе сокеты на которых возможна посылка сообщений и третье - сокеты для соторых ожидается исключительное состояние; out-of- band данные в настоящее время реализованы только как исключи- тельное состояние. Если програмист не интересуется каким-либо состоянием (прием, посылка или исключительное), то соответствую- щий аргумент для вызова select должен быть равен NULL. Каждое множество представляет собой структуру содержащую массив длинных целых битовых масок; размер массива устанавлива- ется определением FD_SETSIZE. Массив должен быть достаточно длинным, для того что бы содержать 1 бит для каждого дескрипторы сокета в FD_SETSIZE. Макрос FD_SET(fd, &mask) и FD_CLR(fd, &mask) предоставляют возможность добавлять и удалять дескрипторы в множестве mask. Множество должно быть очищено перед использованием, макрос FD_ ZERO(&mask) очищает множество mask. Параметр nfds в вызове se- lect определяет кол-во дескрипторов (т.е. один плюс значение на- ибольшего дескриптора) для проверки в множестве. Значение поля timeout может быть точно указано, если вызов select не занимает больше, чем предопределенный период времени. Если timeout установлен в 0, то вызов select возвращается немед- ленно. Если последний параметр - NULL, то вызов select будет заблокирован на неопределенное время. Системный вызов select возвращает число выбранных дескрипторов; если select вернется по причине истечения тайм-аута, то будет возвращен 0. Если select возвращается по причине возникнования ошибки или прерывания, бу- дет возвращен -1 и номер ошибки в переменной errno, маска дес- крипторов не изменится. В случае нормального завершенения, три множества будут показывать дескрипторы, которые готовы для чте- ния, записи и/или содержащие исключительные состояния. Статус дескриптора в выбранной маске может быть протестирован с помощью макроса FD_ISSET(fd, &mask), который возвращает не-нулевое зна- чение если fd - член множества mask и 0 в противном случае. Для определения ждет ли какой-либо сокет соединения с помощью вызова accept, может исподьзоваться вызов select. С помощью макроса FD_ ISSET можно определить такой сокет. Если FD_ISSET возвращает не- нулевое значение, то тестируемый сокет ожидает соединения. В качестве примера приведен следующий код: #include #include ... fd_set read_template; struct timeval wait; ... for (;;) { wait.tv_sec = 1; /* одна секунда */ wait.tv_usec = 0; FD_ZERO(&read_template); FD_SET(s1, &read_template); FD_SET(s2, &read_template); nb = select(FD_SETSIZE, &read_template, (fd_set *) 0, (fd_set *) 0, &wait); if (nb <= 0) { Во время выполнения select произошла ошибка или select прекратил свою работу по истечению тайм-аута. } if (FD_ISSET(s1, &read_template)) { Сокет s1 готов к чтению из него данных. } if (FD_ISSET(s2, &read_template)) { Сокет s2 готов к чтению из него данных. } } В параграфе 4.2 аргументы для select - указатели на целые числа, вместо указателей на fd_sets. Этот тип вызова select бу- дет работать до тех пор пока число проверяемых дескрипторов меньше чем число бит в целом; однако методы описные выше ис- пользуются во всех текущих программах. Системный вызов select предоставляет синхронную схему мультиплексирования. Асинхронноя схема (уведомление о завершении вывода, доступности ввода и исключительных состояний) возможна при использовании сигналов SIGIO и SIGURG, опмсанных в части 5. 3. Сетевые библиотечные процедуры. Когда используется система сокетов в распределенной среде, то наверняка возникнет необходимость в создании и размещении се- тевых адресов. Для решения этих и многих других задач в стандар- тной библиотеке языка C имеются соответствующие функции. Хотя система сокетов поддерживает оба стандарта: Darpa и Xerox NS, большинство описанных в данной части функций, применимы только к сокетам Darpa стандарта (т.е. сокеты домена AF_INET). Далее предполагается, что все рассматриваемые функции не применимы к домену AF_NS, если не указанно противоположное. Нахождение какого-либо сервиса на удаленном хосте требует многих уровней отображения до того как клиент и сервер смогут соединиться. Сервис связан с именем, которое предназначено для лучшего понимания человеком; например: `` сервер login на хосте monet``. Это имя и имя удаленного хоста, затем будут переведены в сетевые адреса, которые менее пригодеы для человека. В конце- -концов, адрес будет использоваться для определения физического месторасположения сервера и маршрута к серверу. Все эти отобра- жения зависят от архитектуры сети. Например, это желательно для сетей не требующих давать хосту имя так, что-бы их физическое расположение было известно клиентом. Вместо этого, основные сер- висы в сети могут узнать местоположения хоста в то время, когда клиент желает подключиться. Это возможность - именовать хосты независимо независимо от их расположения - может потребовать на- личея некоторого процесса, выполняющего это отображение, но из- бавляет хост от необходимости уведомлять всех слиентов о своем текущем расположении. Стандартные функции предоставляются для: преобразования имен хостов в сетевые адреса, преобразования имен сетей в номера сетей, преобразования имен протоколов в номера протоколов, преобразования имени сервиса в номер порта и подходящий протокол для использования при соединении к серверу. Для использования всех этих процедур, вы должны включить свою программу файл netdb.h: #include 3.1. Имена хостов Преобразование имен хостов в форме Internet в адреса пред- ставлено структурой hostent: struct hostent { char *h_name; /* официальное имя хоста */ char **h_aliases; /* список алиасов */ int h_addrtype; /* тип адреса (например, AF_INET) */ int h_length; /* длина адреса */ char **h_addr_list; /* список адресов,заканчивается нулем */ }; #define h_addr h_addr_list[0] /* первый адрес, сетевой порядок следования байт */ Функция gethostbyname(3) берет Inetrnet имя хоста и возвращает структуру hostent, в то время как функция gethostbyaddr отобра- жает Internet адреса в структуру hostent. Официальное имя хоста и его алиасы возвращаются этими про- цедурами, вместе с типом адреса и список адресов переменной дли- ны, оканчивающийся нулем. Этот список необходим, потому-что каж- дый хост может иметь несколько имен. Определение h_addr пред- ставлено для обратной совместимости и определено ,как первый ад- рес в списке адресов в структуре hostent. База данных для этих функций представлена файлом /etc/hosts и/или используется сервер имен named. В силу различия этих баз данных и протоколом доступа к ним, возвращаемая информация может быть различна. Когда используется база данных на основе /etc/ hosts то будет возвращатся один адрес и все имеющиеся алиасы в этой базе на данный адрес. В случае использования сервера имен, как базы данных, может быть возвращен альтернативный адрес, но никакие алиасы, кроме данного в аргументе, не будут возвращены. В отличие от Internet имен, NS имена всегда отображаются в NS адреса с помощью стандартного NS Clearinghouse сервиса, рас- пределенного сервера аутентификации и имен. Функция преобразова- ния NS имен через Clearinghouse сложна и не является частью стандартной библиотеки. Адреса NS хостов представлены следующими структурами: union ns_host { u_char c_host[6]; u_short s_host[3]; }; union ns_net { u_char c_net[4]; u_short s_net[2]; }; struct ns_addr { union ns_net x_net; union ns_host x_host; u_short x_port; }; Представленный ниже фрагмент кода вставляет в ns_addr из- вестные NS адреса: #include #include #include ... u_long netnum; struct sockaddr_ns dst; ... bzero((char *)&dst, sizeof(dst)); netnum = htonl(2266); dst.sns_addr.x_net = *(union ns_net *) &netnum; dst.sns_family = AF_NS; /* * хост 2.7.1.0.2a.18="gyre:Computer Science:UofMaryland" */ dst.sns_addr.x_host.c_host[0] = 0x02; dst.sns_addr.x_host.c_host[1] = 0x07; dst.sns_addr.x_host.c_host[2] = 0x01; dst.sns_addr.x_host.c_host[3] = 0x00; dst.sns_addr.x_host.c_host[4] = 0x2a; dst.sns_addr.x_host.c_host[5] = 0x18; dst.sns_addr.x_port = htons(75); 3.2. Имена сетей Как и для имен хостов, представлены функции для преобразо- вания имен сетей в числа и обратно. Эти функции возврашают структуру netent: struct netent { char *n_name; /* официальное имя сети */ char **n_aliases; /* список алиасов */ int n_addrtype; /* тип адреса сети */ int n_net; /* номер сети, в машинном * следовании байт */ }; Функции getnetbyname(3), getnetbynumber(3), and getnetent(3) аналогичны функциям, описанным выше. Эти функции извлекают ин- формацию из файла /etc/networks. 3.3. Имена протоколов Для протоколов, которые определены в файле /etc/protocols, структура protoent определяет преобразование протокол-имя, ис- пользуемое функциями getprotobyname(3), getprotobynumber(3) и getprotoent(3): struct protoent { char *p_name; /* официальное имя протокола*/ char **p_aliases; /* список алиасов */ int p_proto; /* номер протокола */ }; При использовании NS домена, протоколы указываются полем "тип клиента" в IDP заголовке. Базы данных протоколов не сущес- твует; для более подробной информации смотрите часть 5. 3.4. Имена сервисов Информация относительно сервисов более сложна. Сервис "на- вешивается" на определенный "порт" и использует определенный протокол. Этот способ согласуется с доменом AF_INET, но не не работает с другими. Сервис может занимать нескольео "портов". В последнем случае, высокоуровневые функции стандартной библиоте- ки, должны быть заменены или расширины. Имеющиеся сервисы содер- жатся в файле /etc/services. Отобоажение сервиса описывается структурой servent. struct servent { char *s_name; /* официальное имя сервиса */ char **s_aliases; /* списко алиасов */ int s_port; /* номер порта, */ /* в сетевом представлении */ char *s_proto; /* протокол для использования */ }; Функция getservbyname(3) преобразует имена сервисов в servent структуру, указанием имени и, не обязательно, протокол. Таким образом, вызов sp = getservbyname("telnet", (char *) 0); вернет описание для telnet сервиса, использующего любой прото- кол, в то время как вызов sp = getservbyname("telnet", "tcp"); вернет только telnet сервис, использующий TCP протокол. Кроме того, имеются функции getservbyport(3) and getservent(3). Фун- кция getservbyport имеет интерфейс похожий на предоставляемый функцией getservbyname; не обязательно, но может быть указан протокол. В NS домене, сервисы управляются центральным диспетчером, предоставляемым как часть системы вызовов удаленных процедур Co- urier. 3.5. Разное С поддержкой функций, описанных выше, приложения редко дол- жны напрямую с адресами. Это позволяет разрабатывать сервисы, в максимально независимом от сети стиле. Это весьма круто, однако, написание независимых от архитектуры сети приложений весьма сложно. До тех пор, пока требуется поддержка сетевых адресов, при именовании сервисов и сокетов, всегда в программах будет сохраняться зависимость от сетевой архитектуры. Если вы хотите сделать программу remote login независимой от Internet протоколов и схемы адресации, вы вынуждены будите добавить уровень функций, которые скроют зависимость от сетевой архитектуры от основного кода. Кроме функций, работающих с базой данных адресов, существу- ют некоторые другие функции в стандартной библиотеке, который представляют интерес для программиста. Имеются в виду функции для упрощения работы с адресами и именами. В таблице 1 перечис- лены функции длч манипулирования строками переменной длины и пе- рестановкой байт в сетевых адресах. Таблица 1. +-----------------+-------------------------------------------+ |Вызов | Описание | +-----------------+-------------------------------------------+ |bcmp(s1, s2, n) | сравнение строк; 0 если одинаковы, | | | иначе не 0 | |bcopy(s1, s2, n) | копирует n байт из s1 в s2 | |bzero(base, n) | обнуляет n байт,начиная с base | |htonl(val) | конверитерует 32-битное число из машинного| | | в сетевой порядок байт | |htons(val) | то-же самое ^^, но с 16-битным числом | |ntohl(val) | конверитерует 32-битное число из сетевого | | | в машинный порядок байт | |ntohs(val) | то-же самое ^^, но с 16-битным числом | +-------------------------------------------------------------+ Функции перестановки байт предоставлены, потому-что операц- понная система предполагает использование их в сетевом порядке. На некоторых архитертурах: например VAX, машинный порядок следо- вания байт отличается от сетевого. Поэтому, программам иногда необходима операция перестановки байт. Библиотечная функции, ко- торые возвращают сетевой адрес, предоставляют их в сетевом по- рядке следования байт. Таким образом, они могут быть просто ско- пированы в структуры, представленные в системе. Это означает, что программисты встретятся с проблемой следования байт, только при интерпретации сетевых адресов. Например, если требуется на- печатать Internet порт, то можно использовать следющий код: printf("port number %d\n", ntohs(sp->s_port)); На машинах, где порядок следования байт не отличается от сетево- го, все эти функции определены как пустые макросы. #include #include #include #include #include ... main(argc, argv) int argc; char *argv[]; { struct sockaddr_in server; struct servent *sp; struct hostent *hp; int s; ... sp = getservbyname("login", "tcp"); if (sp == NULL) { fprintf(stderr, "rlogin: tcp/login: неизвестный сервис\n"); exit(1); } hp = gethostbyname(argv[1]); if (hp == NULL) { fprintf(stderr, "rlogin: %s: неизвестный хост\n", argv[1]); exit(2); } bzero((char *)&server, sizeof (server)); bcopy(hp->h_addr,(char *)&server.sin_addr,hp->h_length); server.sin_family = hp->h_addrtype; server.sin_port = sp->s_port; s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { perror("rlogin: сокет"); exit(3); } ... /* Вызов connect выполняет связывание (bind) за нас */ if (connect(s, (char *)&server, sizeof (server)) < 0) { perror("rlogin: connect"); exit(5); } ... } Рисунок 1. Код клиента удаленного сервиса login. 4.1. Модель клиент/сервер При конструировании распределенных приложений наидолее час- то используется модель клиент/сервер. Согласно этой схеме, кли- ентское приложение запрашивает услуги у серверного приложения. В этой части мы более близко рассмотрим взаимодействие межлу кли- ентом и сервером, а также разберем некоторые проблемы в разра- ботке распределенных приложений этого типа. Клиент и сервер требуют хорошо известный набор правил, до того как сервис может быть предоставлен. Эти правила содержат протокол, который должен быть использован на обоих концах соеди- нения. В зависимсти от ситуации, протокол может быть симметрич- ным или асимметричным. В первом случае, каждая сторона может иг- рать роль клиента и сервера. Во втором слуяае, одно сторона пос- тоянно остается сервером, в то время как другая клиентом. В ка- честве примера первого случая можно привести протокол TELNET, используемый в Internet для эмуляции удаленного терминала. Как пример асимметричного протокола, можно привести протокол переме- щения файлов в Internet - FTP. Серверный процес, обычно, "слушает" некоторый хорошо извес- тный адрес на предмет поступления запросов от клиентов. То есть, ждет (также употребляется термин "спит"), до тех пор пока не по- явится запрос на соединение от клиента. Если это событие проис- ходит, то сервер "просыпается" и обслуживает клиента, выполняя подходяшие действия на запрос клиента. Альтернанивная схема может быть использована для сокрытия серверных процессов ьольшаю часть времени проводящих в "спячке". Для серверов Internet в 4.3BSD это реализовано яерез inetd, так называемый "Internet супер-сервер". Inetd слушает несколько пор- тов, описаных в конфигурационном файле. Когда на один из этих портов запрашивается соединение, inetd выполняет подходящюю сер- верную программу для работы с клиентом. Согласно этой схеме, клиент незнает о таком посреднике , как inetd. Inetd будет под- робно описан в части 5. 4.1. Серверы В 4.3BSD большинство серверов доступны на хорошо известны Internet адреса или имена домена UNIX. Например, главный цикл сервера удаленных заходов, показанный рисунке 2. В первую очередь, сервер ищет описание сервиса: sp = getservbyname("login", "tcp"); if (sp == NULL) { fprintf(stderr, "rlogind: tcp/login: unknown service\n"); exit(1); } Затем, отсоединяется от терминала, вызвавшего его процесса: for (i = 0; i < 3; ++i) close(i); open("/", O_RDONLY); dup2(0, 1); dup2(0, 2); i = open("/dev/tty", O_RDWR); if (i >= 0) { ioctl(i, TIOCNOTTY, 0); close(i); } Последнее очень важно, так как сервер, возможно, не захочет при- нимать сигналы, доставленные группе процессов терминала. Помните однако, раз сервер сам разорвал связь, он не может более посы- лать сообщения об ошибках на терминал, и должен сообщать об ошибках посредством syslog. Теперь, после того как сервер установил первоначальное ок- ружение, он создает сокет и начинает подтверждать запросы. Вызов bind требуется для для установки сервера в определенное место. Так как сервер удаленного захода слушает порт с ограниченным доступом, и поэтому должен быть запущен пользовательским иденти- фикатором 0 (обычно это root). Эта коцепция "порта с ограничен- ным доступом" специфична для 4BSD и будет обьеснена в части 5. Основная часть цикла довольно проста: main(argc, argv) int argc; char *argv[]; { int f; struct sockaddr_in from; struct servent *sp; sp = getservbyname("login", "tcp"); if (sp == NULL) { fprintf(stderr, "rlogind: tcp/login: неизвестнай сервис\n"); exit(1); } ... #ifndef DEBUG /* Разорвать связь сервера с контрольным терминалом */ ... #endif sin.sin_port = sp->s_port; /* Порт с ограниченым */ /* доступом, см. часть 5 */ ... f = socket(AF_INET, SOCK_STREAM, 0); ... if (bind(f, (struct sockaddr *) &sin, sizeof(sin))<0) { ... } ... listen(f, 5); for (;;) { int g, len = sizeof (from); g = accept(f, (struct sockaddr *) &from, &len); if (g < 0) { if (errno != EINTR) syslog(LOG_ERR, "rlogind: принят: %m"); continue; } if (fork() == 0) { close(f); doit(g, &from); } close(g); } } Рисунок 2. Сервер удаленных заходов. for (;;) { int g, len = sizeof (from); g = accept(f, (struct sockaddr *)&from, &len); if (g < 0) { if (errno != EINTR) syslog(LOG_ERR, "rlogind: принят: %m"); continue; } if (fork() == 0) { /* Потомок */ close(f); doit(g, &from); } close(g); /* Предок */ } Системный вызов accept блокирует сервер, до тех пор пока какой- либо клиент не запросит сервер. Этот вызов может вернуть ошибку, если вызов прерван сигналом, таким как SIGCHLD (будет обсужденн в части 5). Поэтому, значение возвращаемое accept проверяется на предмет действительности соединения и сообщает о ошибках ис- пользует syslog. Когда соединение установлено, сервер порождает потомка и тот, в свою очередь, выполняет функцию сервиса в соответсвии с заданным протоколом. Помните, что сокет используемый предком для организации очереди запросов закрывается в потомке, в то время как сокет созданный как результат вызова accept закрывается в предке. Адрес клиента также передается функции doit, потому что она требует адрес для аутентификации. 4.2. Клиенты Клиентская сторона сервиса удаленного захода была показа раньше на рисунке 1. Вы можете видеть резделенные, асимметричные роли клиента и сервера. Суть сервера пассивна, он "слушает" порт в ожидании запроса от клиента. Клиент - активный процесс, иници- ирующий соединение. Рассмотрим более внимательно действия, выполняемые клиен- том. Как и сервер, в первую очередь клиент ищет информацию о сервисе: sp = getservbyname("login", "tcp"); if (sp == NULL) { fprintf(stderr, "rlogin: tcp/login: неизвестный сервис\n"); exit(1); } Затем ищется адрес сервера: hp = gethostbyname(argv[1]); if (hp == NULL) { fprintf(stderr, "rlogin: %s: unknown host\n", argv[1]); exit(2); } Когда это выполнено, все, что требуется - это установить соеди- нение с сервером и начать работу по протоколу. Буфер адреса очи- щается, затем заполняется Internet адресом сервера и номером порта, который "слушает" сервер на удаленном хосте: bzero((char *)&server, sizeof (server)); bcopy(hp->h_addr, (char *) &server.sin_addr, hp->h_length); server.sin_family = hp->h_addrtype; server.sin_port = sp->s_port; Создается сокет и инициализируется соединение. Помните, что con- nect просто выполняет вызов bind, так как сокет не связан. s = socket(hp->h_addrtype, SOCK_STREAM, 0); if (s < 0) { perror("rlogin: socket"); exit(3); } ... if (connect(s,(struct sockaddr*)&server,sizeof(server))<0){ perror("rlogin: connect"); exit(4); } Детали самого протокола здесь не будут обсуждаться. 4.3. Серверы без соединения. В то время, как сервисы, базирующиеся на на концепции сое- динения являются нормой, некоторые серверы базируются на ис- пользовании сокетов типа DATAGRAM. Например, сервер rwho, кото- рый предоставляет информацию о пользователях и статусную инфор- мацию для хостов соединенных в локальную сеть. Этот сервер, ис- пользующий возможность множественной рассылки сообщений на все хосты соединенные в локальную сеть, интересен как пример ис- пользования дейтаграммных сокетов. Пользователь на любой машине с работающим сервером rwho мо- жет узнать текущий статус машины с помощью программы ruptime. Пример работы можно увидеть на рмсунке 3. Информация о статусе каждого хоста периодически рассылает- ся, используя broadcast, сервером rwho на каждом хосте. Все эти- же серверные процессы принимают эту информацию и используют ее для обновления базы данных. Эта база данных используется в дальнейшем для генерации статуса каждого хоста. Серверы работают автономно, используя возможность broadcast, только в пределах локальной сети. Помните, что использование broadcast для такой задачи неэф- фективно, так как все хосты должны обрабатывать каждое сообще- ние, даже если не используется сервер rwho. Если такой сервис часто используется, то накладные расходы множественной рассылки перевешивают простоту. arpa up 9:45, 5 users, load 1.15, 1.39, 1.31 cad up 2+12:04, 8 users, load 4.67, 5.13, 4.59 calder up 10:10, 0 users, load 0.27, 0.15, 0.14 dali up 2+06:28, 9 users, load 1.04, 1.20, 1.65 degas up 25+09:48, 0 users, load 1.49, 1.43, 1.41 ear up 5+00:05, 0 users, load 1.51, 1.54, 1.56 ernie down 0:24 esvax down 17:04 ingres down 0:26 kim up 3+09:16, 8 users, load 2.03, 2.46, 3.11 matisse up 3+06:18, 0 users, load 0.03, 0.03, 0.05 medea up 3+09:39, 2 users, load 0.35, 0.37, 0.50 merlin down 19+15:37 miro up 1+07:20, 7 users, load 4.59, 3.28, 2.12 monet up 1+00:43, 2 users, load 0.22, 0.09, 0.07 oz down 16:09 statvax up 2+15:57, 3 users, load 1.52, 1.81, 1.86 ucbvax up 9:34, 2 users, load 6.08, 5.16, 3.28 Рисунок 3. Результаты работы ruptime. Сервер rwho, в упрощенной форме, показан наи рисунке 4. Сервером выполняются две разделенные задачи. Первая задача при- нимает статусную информацию рассылаемую другими хостами в сети. Это выполняется в главном цикле программы. Пакеты принятые на порту rwho проверяются на принадлежность их другому хосту (свои пакеты не нужны), затем используются для обновления статуса хос- тов. Если в течении длительного времени о каком-то хосте не пос- тупало информации, функция интерпретации базы данных предполага- ет, что хост прекрталил свою рабту и отмечает это в своих ре- зультатах. Этот алгоритм зависит от того, запущен на хосте rwho сервер или нет, так как хост может работать, а информация о его состоюнии не рассылается, и другие хосты предполагают, что сер- вер не работает. Второй задачей является снабжение информацией, касающейся этого хоста. Она включает в себя периодический сбор информации о состоянии системы, упаковки этих данных в сообщения и рассылку их всем хостам в локальной сети, использую broadcast. Функция снабжения запускается по сигналу таймера. Размещение статусной информации не представляет для нас интереса. Напротив, решить куда посылать сообщения более проблематично. Статусная информация должна быть послана в локальную сеть с помощью broadcast. Для сетей неподерживающих broadcast, должна использоваться другая схема, имитирующая или заменяющая broad- cast. Например, перечислять всех известных соседних хостов (ба- зирующихся на статусной информации принятой с других rwho серве- ров). Это требует некоторой начальной информации, так как хост не узнает какие машины являются его соседями, до тех пор пока какой-либо хост не пришлет ему статусную информацию. Аналогичная прблема встает у процесса управляющего таблицей маршрутизации при распостранении информации о маршрутах. Стандартное решение - это информирование одного или более сервера из известных сосе- дей. Если каждый сервер имеет, по крайней мере, одного соседа, напрямую присоединенного к нему, то статусная информация может быть перемещена на хост, который (возможно) не является непос- редственно подсоединенным. Если сервер может поддерживать как сеть с возможность broadcast, так и без нее, то сеть с произ- вольной топологией может разделять статусную информацию. Очень важно то, что программное обеспечение в распределен- ной среде, не имеет какой-либо зависимости от хоста. В противном случае, это требовало бы на каждый хост отдельную копию програм- мы. 4.3BSD пытается изолировать специфичную для каждого хоста информацию от приложений, предоставляя системные вызовы, которые предосталяют необходимую информацию. Механизм существует, в фор- ме ioctl вызова, для поиска сетей с которыми хост имеет прямое соединение. Кроме того, broadcast механизм реализован на уровне сокетов. Комбинирование этих двух возможностей позволяет процес- су рассылать в любые сети, напрямую соединенные с хостом и под- держивающие возможность broadcasrt. Это позволяет 4.3BSD резре- шать проблему решения как распостранять статусную информацию в случае rwho или, в более широком смысле, в broadcast: такая ста- тусная информация распостраняется в сетях на уровне сокетов, причем сети, имеющие прямое соединение к хосту, могут быть полу- чены через подходящий вызов ioctl. Все это будет более подробно рассмотрено в части 5. main() { ... sp = getservbyname("who", "udp"); net = getnetbyname("localnet"); sin.sin_addr = inet_makeaddr(INADDR_ANY, net); sin.sin_port = sp->s_port; ... s = socket(AF_INET, SOCK_DGRAM, 0); ... on = 1; if(setsockopt(s,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on))<0){ syslog(LOG_ERR, "setsockopt SO_BROADCAST: %m"); exit(1); } bind(s, (struct sockaddr *) &sin, sizeof (sin)); ... signal(SIGALRM, onalrm); onalrm(); for (;;) { struct whod wd; int cc, whod, len = sizeof (from); c = recvfrom(s, (char *)&wd, sizeof (struct whod), 0, (struct sockaddr *)&from, &len); if (cc <= 0) { if (cc < 0 && errno != EINTR) syslog(LOG_ERR, "rwhod: recv: %m"); continue; } if (from.sin_port != sp->s_port) { syslog(LOG_ERR, "rwhod: %d: bad from port", ntohs(from.sin_port)); continue; } ... if (!verify(wd.wd_hostname)) { syslog(LOG_ERR,"rwhod:malformed host name from %x", ntohl(from.sin_addr.s_addr)); continue; } (void)sprintf(path,"%swhod.%s",RWHODIR,wd.wd_hostname); whod = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666); ... (void) time(&wd.wd_recvtime); (void) write(whod, (char *)&wd, cc); (void) close(whod); } } Рис. 4. rwho сервер. 5. Информация для "продвинутых" пользователей. Некоторые возможности также должны быть обсуждены. Для большинства пользователей IPC, уже описанных возможностей будет вполне достаточно для создания распределенных приложений. Одна- ко, возможно, кто-нибудь будет нуждаться в некоторых возможнос- тях, которые будут обсуждены в этой части. 5.1. Внепотоковые данные. Абстрактное понятие "STREAM" сокета включает в себя понятие внепотоковых данных. Внепотоковые данные - это логически незави- симый канал, ассоционированный с каждой парой соединенных соке- тов. Внепотоковые данные доставляются пользователю независимо от нормальных данных. Это понятие требует что-бы возможность внепо- токовых данных поддерживала надежную доставку по крайней мере одного такого сообщения в каждый момент времени. Это сообщение может содержать по крайней мере один байт данных и по крайней мере одно сообщение может ожидаться пользователем в каждай мо- мент времени. Для коммуникационных протоколов поддерживающих только только внутрипотоковые сообщения (т.е. срочные данные доставляются в последовательности с нормальными данными), систе- ма, обычно, выбирает данные из потока нормальных данных и хранит их отдельно. Это позволяет пользователям выбирать между приняти- ем срочных данных по порядку и принятием их вне последова- тельности, без очищения буфера. Это возможно, используя флаг MSG_PEEK. Если сокет имеет группу группу процесса, то генериру- ется сигнал SIGURG, когда протокол уведомлен о их сужествовании. Процесс может установить группу процесса или идентификатор про- цесса для последущего уведомления о их существовании, подходящим fcntl вызовом. как определено выше для SIGIO. Если несколько со- кетов могут иметь внепотоковые данные, ожидающие доставки, сис- темный вызов select для исключительных состояний может быть ис- пользован дл определения таких сокетов. Ни сигнал, ни select не показывают какие именно данные находятся в буфере, а только уве- домляют о наличии таковых. В дополнение к проходящей информации, в потоке данных ста- вится логическая метка для указания места, с которого идут вне- потоковые данные. Программы удаленного login'a и shell'a ис- пользуют эту возможность для передачи сигналов между клиентом и сервером. Когда сигнал "выбрасывает" любые данные, ожидаемые от удаленного процесса, все данные в потоке выше логической метки также отбрасываются. Для посылки внепотоковых сообщений, в системных вызовах send и sendto используется флаг MSG_OOB. Точно также, для чтения внепотоковых сообщений, с помощью recvfrom и recv, используется флаг MSG_OOB. Если возникает необходимость узнать, указывает ли текущий указатель на логическую метку в потоке, применяется сис- темный вызов ioctl с параметром SIOCATMARK: ioctl(s, SIOCATMARK, &yes); Если yes равняется 1, после возврата, то следущая опереция чте- ния вернет данные после метки. В противном случае (предположим, что были приняты внепотоковые данные), следущая опереция чтения вернет данные, посланные клиентом до посылки сигнала. Процедура используется в remote login'е для выброса вывода, как реакция на принятие сигнала прерывания или сигнала выхода, как показано на рис. 5. Программа читает нормальные данные, что выше логической метки, а затем читает внепотоковые данные. #include #include ... oob() { int out = FWRITE, mark; char waste[BUFSIZ]; /* выбрасывает вывод локального терминала */ ioctl(1, TIOCFLUSH, (char *)&out); for (;;) { if (ioctl(rem, SIOCATMARK, &mark) < 0) { perror("ioctl"); break; } if (mark) break; (void) read(rem, waste, sizeof (waste)); } if (recv(rem, &mark, 1, MSG_OOB) < 0) { perror("recv"); ... } ... } Рисунок 5. Процесс также может читать или выбирать без удаления внепо- токовые данные без первоначального чтения выше метки. Это услож- няется когда основной протокол доставляет внепотоковые данные вместе с нормальными данными и посылает уведомление. При работе такими протоколами, когда вызов recv с флагом MSG_OOB заканчива- ет свою работу, внепотоковые данные могут еще не прийти. В этом случае, вызов вернет ошибку EWOULDBLOCK. Хуже того, может воз- никнуть ситуация когда входной буфер заполнен и поэтому напарник не может послать срочные данные, до тех пор пока буффер не будет очищен. Поэтому процесс должен читать достаточно норамльных дан- ных, для того чтобы срочные данные могли быть доставлены. Некоторые программы использующие многобайтные срочные сооб- щения и нуждающиеся в управлении множеством срочных сигналов ( например, telnet(1)) должны сохранять расположение срочных дан- ных внутри потока. Эта обработка доступна как опция уровня соке- та, SO_OOBLINE; см. setsockopt(2) для использования. С этой оп- цией, позиция срочных данных ( "метка" ) сохраняется, но срочные данные следуют непосредственно за меткой внутри потока нор- мальных данных возвращенных без флага MSG_OOB. Получение множес- тва "меток" заставляют "метку" переместиться, но внепотоковые не теряются. 5.2. Неблокирующиеся сокеты Иногда удобно использовать сокеьы, которые не блокируются; то есть, запросы ввода/вывода которые не могут быть выполнены немедленно и соответсвенно заставят процесс приостановиться и ждать завершения не выполняются и возвращается ошибка. Сокет мо- жет быть помечен как неблокирующийся с помощью системного вызова fcntl: #include ... int s; ... s = socket(AF_INET, SOCK_STREAM, 0); ... if (fcntl(s, F_SETFL, FNDELAY) < 0) perror("fcntl F_SETFL, FNDELAY"); exit(1); } ... Затем выполняя неблокирующий ввод/вывод на сокете, Вы дол- жны выполнить проверку на наличее ошибки EWOULDBLOCK в гло- бальной переменной errno, которая происходит когда операция дол- жна будет блокироваться, а сокет помечен как неблокирующийся. В особенности, вызовы accept, connect, send, recv, read и write могут вернуть ошибку EWOULDBLOCK, так что процесс должен быть готов их обрабатывать. Если операция, такая как send, не может быть выполнена полностью, но частично возможна (например, когда используется потоковый сокет), данные, которые могут быть посла- ны немедленно будут посланы и возвращенное значение будут содер- жать реально посланное количество данных. 5.3. Сокеты, управляемые прерываниями Сигнал SIGIO позволяет процессу быть уведомленным посредс- твом сигнала, когда сокет ( или в более общем случае, файловый дескриптор) имеет данные готовые для чтения. Использование SIGIO требует выполнения следующих трех шагов: 1. Процесс должен установить обработчик сигнала SIGIO, используя вызовы signal или sigvec; 2. Процесс должен установить PID или GID сокета равными своим собственным PID или GID (по умолчанию GID сокета равен 0); 3. Процесс должен разрешить асинхронное уведомление с помощью вызова fcntl. Пример кода, реализующего этот алгоритм, приведен на рисунке 6. При добавлении обработчика сигналов SIGURG, программа будет при- нимать уведомление при поступлении внепотоковых данных. #include ... int io_handler(); ... signal(SIGIO, io_handler); /* Реализуем себя как владельца сокета */ if (fcntl(s, F_SETOWN, getpid()) < 0) { perror("fcntl F_SETOWN"); exit(1); } /* Разрешаем принимать уведомления */ if (fcntl(s, F_SETFL, FASYNC) < 0) { perror("fcntl F_SETFL, FASYNC"); exit(1); } Рис. 6. Использование асинхронного уведомления ввода/вывода. 5.4. Сигналы и группы процессов. Для существования SIGURG и SIGIO сигналов, каждый сокет должен иметь ассоционированный с ним номер процесса, также как для терминала. Это значение при инициализации принимает значение равное нулю, но оно может быть изменено с помощью системного вы- зова fcntl с параметром F_SETOWN, как было сделано в коде на ри- сунке 6. Для установки PID сокета, необходимо передать fcntl по- ложительный аргумент. Для установки GID сокета, необходимо пере- дать отрицательный аргумент. Помните, что PID показывает ассоци- онированный с сокетом процесс или группу процессов; невозможно одновременно указать оба параметра. Вызов fcntl с параметром F_ GETOWN может быть использован для получения текущего PID сокета. Для создания серверных процессов очень полезен сигнал SIGCHLD. Этот сигнал посылается процессу, когда один из порож- денных им процессов меняет свое состояние. Обычно этот сигнал используют когда хотят получить значение возвращаемое потомком по выходу, но не желают ждать его завершения с помощью вызовов типа wait, waitpid. Пример такого сервера показан на рисунке 7. Если процесс не будет "ловить" коды завершения своих потом- ков, то в системе может возникнуть большое число так называемых зомби-процессов. int reaper(); ... signal(SIGCHLD, reaper); listen(f, 5); for (;;) { int g, len = sizeof (from); g = accept(f, (struct sockaddr *)&from, &len,); if (g < 0) { if (errno != EINTR) syslog(LOG_ERR, "rlogind: accept: %m"); continue; } ... } ... #include reaper() { union wait status; while (wait3(&status, WNOHANG, 0) > 0) ; } Рисунок 7. Использование сигнала SIGCHLD. 5.5. Псевдотерминалы Большое количество программ не могут нормально функциониро- вать без терминала для стандартного вводы/вывода. Так как сокеты не предоставляют семантики терминалов, часто необходимо иметь процесс, соединяющийся через псевдотерминал. Псевдотерминал - это пара устройств, "ведущего" и "ведомого", который позволяют процессу выглядеть как активный агент в соединении между процес- сом и пользователем. Данные, выведенные на "ведомой" стороне псевдотерминала выглядят как ввод для процесса читающего из "ве- домой" стороны, а данные введенные на "ведущей" стороне выглядят как терминальный ввод для "ведомой" стороны. Таким образом, про- цесс манипулирующий "ведущей" стороной контролирует информацию читаемую и записываемую на "ведомой" стороне также как если бы он манипулировал клавиатурой и чтением экрана на реальном терми- нале. Цель этой абстракции - предоставить семантику терминала для сетевых соединений, то есть, "ведомая" сторона воспринимает- ся как обычный терминал для любых процессов работающих на этом псевдотерминале. Например, сервер удаленного захода (login server) использу- ет псевдотерминал для организации удаленных сессий. Пользователь "зашедший" на хост по сети получает оболлочку (shell) с "ведо- мым" псевдотерминалом как стандартный ввод, вывод и вывод ошибок (потоки stdin, stdout и stderr). Серверный процесс управляет со- единением между программой выполняемой удаленной оболочкой и пользовательским локальным процессом. Когда пользователь посыла- ет символ который генерирует прерывание на удаленном хосте, ко- торое принуждает терминал к выводу, псевдотерминал генерирует окнтрольное сообщение для серверного процесса. Затем сервер по- сылает внепотоковое сообщение, которое заставляет клиентский процесс вывести данные на реальный терминал. В 4.3BSD, имя "ведомой" стороне псевдотерминала имеет форму /dev/ttyxy, где x - символ от 'p' до 't', а y - шестнадцатирич- ная цифра ( т.е. от '0' до '9' и от 'a' до 'f'). Имя "ведущей" стороны псевдотерминала - /dev/ttyxy, где x и y соответствуют "ведомой" строне псевдотерминала. Обычно, метод получения пары "ведомый" и "ведущий" псевдо- терминалы состоит в нахождении псевдотерминала который не ис- пользуется в данный момент. "Ведущая" половина может быть откры- та только один раз. Затем, открывается "ведомая" сторона. Затем процесс раздваивается (с помощью вызова fork), потомок закрывает "ведомую" сторону псевдотерминала и выполняет программу (с по- мощью вызова exec). Тем временем, предок закрывает "ведомую" сторону и начинает чтение и запись на "ведущей" стороне. Пример кода, реализуешего этот алгоритм приведен на рисунке 8, в этом коде предполагается, что сокет s уже существует, соединен и про- цесс не ассоциирован с терминалом. gotpty = 0; for (c = 'p'; !gotpty && c <= 's'; c++) { line = "/dev/ptyXX"; line[sizeof("/dev/pty")-1] = c; line[sizeof("/dev/ptyp")-1] = '0'; if (stat(line, &statbuf) < 0) break; for (i = 0; i < 16; i++) { line[sizeof("/dev/ptyp")-1]="0123456789abcdef"[i]; master = open(line, O_RDWR); if (master > 0) { gotpty = 1; break; } } } if (!gotpty) { syslog(LOG_ERR, "Все терминалы заняты"); exit(1); } line[sizeof("/dev/")-1] = 't'; /* "ведомый" - на "ведомой" стороне */ slave = open(line, O_RDWR); if (slave < 0) { syslog(LOG_ERR, "Невозможно открыть pty %s", line); exit(1); } /* Устанавливаем режим терминала */ ioctl(slave, TIOCGETP, &b); b.sg_flags = CRMOD|XTABS|ANYP; ioctl(slave, TIOCSETP, &b); i = fork(); if (i < 0) { syslog(LOG_ERR, "fork: %m"); exit(1); } else if (i) { /* Предок */ close(slave); ... } else { /* Помоток */ (void) close(s); (void) close(master); dup2(slave, 0); dup2(slave, 1); dup2(slave, 2); if (slave > 2) (void) close(slave); ... } Рисунок 8. Создание и использрвание псевдотерминала 5.6. Выбор протокола Если третий аргумент вызова socket равен 0, вызов будет вы- бирать протокол по умолчанию для использования с возвращенным сокетом запрошеного типа. Протокол по умолчанию обычно выбирает- ся корректно и альтернативы обычно не существует. Однако, когда используется низкоуровневый raw протокол для соединения напрямую с низкоуровневыми протоколами или физическим оборудованием, тре- тий аргумент может быть очерь важным для использования мультип- лексирования. Например, raw сокеты в Internet домене могут быть использованы для нового протокола над IP, и сокет будет прини- мать пакеты для указанного протоколы и только. Для получения оп- ределенного протокола существуют номера протоколов, уникальный среди некоторого домена. Для домена Internet можно использовать функции из библиотеки, которую мы обсудили в 3 главе, например getprotobyname: #include #include #include #include ... pp = getprotobyname("newtcp"); s = socket(AF_INET, SOCK_STREAM, pp->p_proto); В NS домене, доступные протоколы описаны в . Для создания raw сокета для Xerox Error Protocol можно использовать следующий код: #include #include #include ... s = socket(AF_NS, SOCK_RAW, NSPROTO_ERROR); 5.7. Связывание адресов Как было сказано в главе 2, привязывание к сокету адреса в Internet и NS доменах может быть достаточно сложным. Напомним, что эта привязка состоит в назначении локальных и удаленных се- тевых адресов и портов. С помощью системного вызова bind, про- цесс может назначить половину привязки - пару <локальный адрес, локальный порт>, а затем систымным вызовом connect или accept назначается другая половина - пара <удаленный адрес, удаленный порт>. Для упрощения привязки сокета к локальной паре <локальный адрес, локальный порт>, в Internet домене можно использовать специальный адрес INADDR_ANY (эта константа определена в ), ядро интерпретирует этот адрес как "любой правильный адрес". Например, для привязка сокета к определенному порту, но не указывая локальный сетевой адрес, можно использовать следую- щий код: #include #include ... struct sockaddr_in sin; ... s = socket(AF_INET, SOCK_STREAM, 0); sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(INADDR_ANY); sin.sin_port = htons(MYPORT); bind(s, (struct sockaddr *) &sin, sizeof (sin)); Сокеты с этим адресом могут принимать сообщения направленный указанному порту и посылать на любой возможный адрес. Например, если хоста имеет адреса 128.32.0.4 и 10.0.0.78, и сокет привяз как показано выше, то процесс будет принимать запромы на соеди- нение с обоими адресами. Если серсерный процесс желает позволить соединяться только с данной сетью, он может связать свой сокет с определенным локальным адресом. В простом случае, нет необходимости связывать сокет с опре- деленным портом (то есть sin.sin_port должно быть равно 0)ю В этом случае ядро само выберет подходящий порт для использования сокетом. Например, для связывания сокета с определенным локаля- ным адресо, но без указания локального порта, можно использовть следующий код: hp = gethostbyname(hostname); if (hp == NULL) { ... } bcopy(hp->h_addr, (char *) sin.sin_addr, hp->h_length); sin.sin_port = htons(0); bind(s, (struct sockaddr *) &sin, sizeof (sin)); Ядро выберет локальный порт, основываясь на двух критериях: 1. в Internet домене номер порта должен быть больше IPPORT_RE- SERVED (1024) (для NS домена, от 0 до 3000), так как они зарезервированы для привелигированного пользователя ( т.е. root'а). 2. Этот порт не должен быть уже связан с каким-либо другим со- кетом. Для поиска свободного привелигированного порта в Internet домене можно использовать функцию rresvport: int lport = IPPORT_RESERVED - 1; int s; s = rresvport(&lport); if (s < 0) { if (errno == EAGAIN) fprintf(stderr, "socket: все порты заняты\n"); else perror("rresvport: socket"); ... } Ограничение на распределение портов было сделано для позволения процессу выполняться в "безопасной" среде для выполнения аутен- тификации, базирующейся на адресе и номере порта. Например, rlo- gin команда позволяет пользователю зайти на хост без процедуры аутентификации (т.е. не надо будет воодить пароль), если выпол- нены два условая: 1. Имя хоста, с которого пользователь заходит на данный хост должно быть в файле /etc/hosts.equiv или имя хоста и имя пользователя должно быть в .rhosts файле в домашнем каталоге пользователя на данной машине; 2. Пользовательский процесс rlogin должен работать с привелиги- рованного порта. Адрес хоста и удаленный порт может быть определен с помощью вы- зова accept или getpeername. В некоторых случаях алгоритм, используемый ядром для выбора порта, может не удовлетворять программиста. Например, по стан- дарту протокола передачи файлов (File Transfer Protocol - FTP), соединение для передачи данных всегда должно происходить того-же локального порта. Однако, дублирование невозможно. В такой ситу- ации адро запретит привязку сокета к такому же порту, если пре- дыдущее моединение все еще существует. Для обработки такой ситу- ации можно использовать системный вызов setsockopt, как показано в следующем коде: ... int on = 1; ... (s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); bind(s, (struct sockaddr *) &sin, sizeof (sin)); После этого вызова, локальный адрес может быть переназначен на новый сокет, даже если он был уже связан. Это не нарушает сис- темного требования уникальности, так как ядро проверяет во время вызова connect, что-бы быть уверенным в незанятости пары <адрес, порт>. Если пара уже связана, то будет возвращена ошибка EADDRI- NUSE. 5.8. Множественная рассылка и определение конфигурации сети Используя datagram сокеты, можно посылать сообщения на мно- жество адресов одновременно (broadcast пакет), если сеть физи- чески поддерживает такую возможность; ядро не предоставляет программной реализации этой возможности. Эти сообщения сильно загружают сеть, так как их принимают все станции в сети. Возмож- ность посылать такие сообщения ограничена сокетами, которые по- мечены как обладающие этой возможностью. Множественная рассылка обычно используется в двух случаях: 1. Поиск ресурса в сети без предварительного определения его точного адреса 2. Маршрутиза- ция, требующая уведомления соседей. Для посылки broadcast пакета должен быть создан datagram сокет: s = socket(AF_INET, SOCK_DGRAM, 0); или s = socket(AF_NS, SOCK_DGRAM, 0); Сокет должен быть помечен как broadcast: int on = 1; setsockopt(s, SOL_SOCKET, SO_BROADCAST, &on, sizeof (on)); Сокет должен быть связан по крайней мере с портом: sin.sin_family = AF_INET; sin.sin_addr.s_addr = htonl(INADDR_ANY); sin.sin_port = htons(MYPORT); bind(s, (struct sockaddr *) &sin, sizeof (sin)); или для NS домена, sns.sns_family = AF_NS; netnum = htonl(net); sns.sns_addr.x_net = *(union ns_net *) &netnum; sns.sns_addr.x_port = htons(MYPORT); Удаленный адрес для посылки broadcast пакета зависит от физичес- кой организации сети. Internet домен поддерживает адрес для рас- сылки broadcast пакетов - INADDR_BROADCAST (определен в . Для определения списка адресов для всех доступных со- седей, требуется знать сети к которым соединен данный хост. Так как это системно-зависимая информация, 4.3BSD предоставляет спе- циальный ioctl вызов для получения этой информации. Системный вызов SIOCGIFCONF ioctlвозвращает конфигурцию интефейсов в форме ifconf структуры; эта структура содержит массив ifreq структур, каждая из которых содержит информацию о одном интерфейсе. Вот эти структуры, описанные в : struct ifconf { int ifc_len; /* размер буфера */ union { caddr_t ifcu_buf; struct ifreq *ifcu_req; } ifc_ifcu; }; #define ifc_buf ifc_ifcu.ifcu_buf/* адрес буфера */ #define ifc_req ifc_ifcu.ifcu_req/* массив структур */ #define IFNAMSIZ 16 struct ifreq { char ifr_name[IFNAMSIZ]; /* имя, например "eth0" */ union { struct sockaddr ifru_addr; struct sockaddr ifru_dstaddr; struct sockaddr ifru_broadaddr; short ifru_flags; caddr_t ifru_data; } ifr_ifru; }; /* адрес */ #define ifr_addr ifr_ifru.ifru_addr /* парный адрес, если связь типа point-to-point */ #define ifr_dstaddr ifr_ifru.ifru_dstaddr /* broadcast адрес */ #define ifr_broadaddr ifr_ifru.ifru_broadaddr /* флаги */ #define ifr_flags ifr_ifru.ifru_flags /* для использования интерфейсом */ #define ifr_data ifr_ifru.ifru_data Ниже - пример кода, получающего эту информацию: struct ifconf ifc; char buf[BUFSIZ]; ifc.ifc_len = sizeof (buf); ifc.ifc_buf = buf; if (ioctl(s, SIOCGIFCONF, (char *) &ifc) < 0) { ... } После этого вызова, buf будет содержать одну ifreq структуру для каждого интерфейса, ifc.ifc_len будет содержать количество байт использованных структурами ifreq. Для каждой структуры заполняется поле флагов, содержащее ин- формацию о состоянии интерфейса, его типе и другие. Вызов SIOC- GIFFLAGS ioctl возвращает эти флаги для интерфейса, указанного в ifreq структуре, как показано ниже: struct ifreq *ifr; ifr = ifc.ifc_req; for(n=ifc.ifc_len/sizeof (struct ifreq); --n >= 0; ifr++) { /* * Обязательно проверяем, что тип адреса у интерфейса * тот, что нам необходим -IF_INET или IF_NS */ if (ifr->ifr_addr.sa_family != AF_INET) continue; if (ioctl(s, SIOCGIFFLAGS, (char *) ifr) < 0) { ... } /* * Исключаем всякую чепуху */ if ((ifr->ifr_flags & IFF_UP) == 0 || (ifr->ifr_flags & IFF_LOOPBACK) || (ifr->ifr_flags & (IFF_BROADCAST | IFF_POINTTOPOINT)) == 0) continue; Так как мы получили флаги, можем получить и broadcast адрес интерфейса. В этом случае нужно использовать системный вызов SI- OCGIFBRDADDR ioctl, адрес для сети типа point-to-point можно по- луить используя вызов SIOCGIFDSTADDR ioctl. struct sockaddr dst; if (ifr->ifr_flags & IFF_POINTTOPOINT) { if (ioctl(s, SIOCGIFDSTADDR, (char *) ifr) < 0) { ... } bcopy((char *) ifr->ifr_dstaddr, (char *) &dst, sizeof (ifr->ifr_dstaddr)); } else if (ifr->ifr_flags & IFF_BROADCAST) { if (ioctl(s, SIOCGIFBRDADDR, (char *) ifr) < 0) { ... } bcopy((char *) ifr->ifr_broadaddr, (char *) &dst, sizeof (ifr->ifr_broadaddr)); } Теперь, после получения broadcast адреса или парного адреса сети типа point-to-point, их можно использовать в вызове sendto: sendto(s, buf, buflen, 0, (struct sockaddr *)&dst, sizeof (dst)); } В вышеприведенном цикле sendto выполняется для каждого интерфей- са, который поддерживает broadcast или принадлежит к типу point- to-point. 5.9. Опции сокетов Для сокетов возможно устанавливать и считывать опции посредством системных вызовов setsockopt и getsockopt. Эти опции выполняют применяются для множества целей: помечают сокет для работы в broadcast режиме, запрещают маршрутизацию, устанавливают задер- жку при закрытии сокета и многие другие операции. Форма ис- пользования этих вызовов следующая: setsockopt(s, level, optname, optval, optlen); и getsockopt(s, level, optname, optval, optlen); Параметры этих вызовов: 1. s - сокет, для которого изменяются или считываются опции; 2. level - специфицирует уровень протокола; в большинстве случа- ев это уровень сокета (SOL_SOCKET,определен в ) 3. optname - это соббственно изменяемая или считываемая опция; возможные опции описаны также в ; 4. optval - значение опции, обычно 0 или 1; 5. optlen - длина значения. При вызове getsockopt в optlen возвращается длина возвращенного значения опции, первоначально устанавливается равной размеру бу- фера для сохранения значения опции. Ниже пример ипользования getsockopt. Эти алгоритмы иногда полезны для определения типа сокета и для других ситуаций: #include #include int type, size; size = sizeof (int); if (getsockopt(s,SOL_SOCKET,SO_TYPE,(char *)&type,&size)<0){ ... } }