Студопедия

КАТЕГОРИИ:


Архитектура-(3434)Астрономия-(809)Биология-(7483)Биотехнологии-(1457)Военное дело-(14632)Высокие технологии-(1363)География-(913)Геология-(1438)Государство-(451)Демография-(1065)Дом-(47672)Журналистика и СМИ-(912)Изобретательство-(14524)Иностранные языки-(4268)Информатика-(17799)Искусство-(1338)История-(13644)Компьютеры-(11121)Косметика-(55)Кулинария-(373)Культура-(8427)Лингвистика-(374)Литература-(1642)Маркетинг-(23702)Математика-(16968)Машиностроение-(1700)Медицина-(12668)Менеджмент-(24684)Механика-(15423)Науковедение-(506)Образование-(11852)Охрана труда-(3308)Педагогика-(5571)Полиграфия-(1312)Политика-(7869)Право-(5454)Приборостроение-(1369)Программирование-(2801)Производство-(97182)Промышленность-(8706)Психология-(18388)Религия-(3217)Связь-(10668)Сельское хозяйство-(299)Социология-(6455)Спорт-(42831)Строительство-(4793)Торговля-(5050)Транспорт-(2929)Туризм-(1568)Физика-(3942)Философия-(17015)Финансы-(26596)Химия-(22929)Экология-(12095)Экономика-(9961)Электроника-(8441)Электротехника-(4623)Энергетика-(12629)Юриспруденция-(1492)Ядерная техника-(1748)

Socket-интерфейс




Данное средство было первоначально разработано для обеспечения прикладным программистам в среде ОС UNIX доступа к транспортному уровню стека протоколов TCP/IP. Позже оно было адаптировано для использования и иных протоколов (например, DECnet), а также реализовано в других операционных системах.

Socket (гнездо, разъем) - абстрактное программное понятие, используемое для обозначения в прикладной программе конечной точки канала связи с коммуникационной средой, образованной вычислительной сетью. При использовании протоколов TCP/IP можно говорить, что socket является средством подключения прикладной программы к порту (см. выше) локального узла сети.

Socket-интерфейс представляет собой просто набор системных вызовов и/или библиотечных функций языка программирования СИ, разделенных на четыре группы:

локального управления;

установления связи;

обмена данными (ввода/вывода);

закрытия связи.

Сокеты предоставляют весьма мощный и гибкий механизм межпроцессного взаимодействия (IPC). Они могут использоваться для организации взаимодействия программ на одном компьютере, по локальной сети или через Internet, что позволяет вам создавать распределённые приложения различной сложности. Кроме того, с их помощью можно организовать взаимодействие с программами, работающими под управлением других операционных систем. Например, под Windows существует интерфейс Window Sockets, спроектированный на основе socket API. Ниже мы увидим, насколько легко можно адаптировать существующую Unix-программу для работы под Windows.

Сокеты поддерживают многие стандартные сетевые протоколы (конкретный их список зависит от реализации) и предоставляют унифицированный интерфейс для работы с ними. Наиболее часто сокеты используются для работы в IP-сетях. В этом случае их можно использовать для взаимодействия приложений не только по специально разработанным, но и по стандартным протоколам - HTTP, FTP, Telnet и т. д. Например, вы можете написать собственный Web-броузер или Web-сервер, способный обслуживать одновременно множество клиентов.

Ниже рассматривается подмножество функций socket-интерфейса, достаточное для написания сетевых приложений, реализующих модель "клиент-сервер" в режиме с установлением соединения.

 

1.1. Функции локального управления

Функции локального управления используются, главным образом, для выполнения подготовительных действий, необходимых для организации взаимодействия двух программ-партнеров. Функции носят такое название, поскольку их выполнение носит локальный для программы характер.

1.1.1. Создание socket'а

Понятие сокета

Сокет (socket) - это конечная точка сетевых коммуникаций. Он является чем-то вроде "портала", через которое можно отправлять байты во внешний мир. Приложение просто пишет данные в сокет; их дальнейшая буферизация, отправка и транспортировка осуществляется используемым стеком протоколов и сетевой аппаратурой. Чтение данных из сокета происходит аналогичным образом.

В программе сокет идентифицируется дескриптором - это просто переменная типа int. Программа получает дескриптор от операционной системы при создании сокета, а затем передаёт его сервисам socket API для указания сокета, над которым необходимо выполнить то или иное действие.

Атрибуты сокета

С каждым сокет связываются три атрибута: домен, тип и протокол. Эти атрибуты задаются при создании сокета и остаются неизменными на протяжении всего времени его существования. Для создания сокета используется функция socket, имеющая следующий прототип.

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

Домен определяет пространство адресов, в котором располагается сокет, и множество протоколов, которые используются для передачи данных. Чаще других используются домены Unix и Internet, задаваемые константами AF_UNIX и AF_INET соответственно (префикс AF означает "address family" - "семейство адресов"). При задании AF_UNIX для передачи данных используется файловая система ввода/вывода Unix. В этом случае сокеты используются для межпроцессного взаимодействия на одном компьютере и не годятся для работы по сети. Константа AF_INET соответствует Internet-домену. Сокеты, размещённые в этом домене, могут использоваться для работы в любой IP-сети. Существуют и другие домены (AF_IPX для протоколов Novell, AF_INET6 для новой модификации протокола IP - IPv6 и т. д.).

Тип сокета определяет способ передачи данных по сети. Чаще других применяются:

SOCK_STREAM. Передача потока данных с предварительной установкой соединения. Обеспечивается надёжный канал передачи данных, при котором фрагменты отправленного блока не теряются, не переупорядочиваются и не дублируются. Поскольку этот тип сокетов является самым распространённым, до конца раздела мы будем говорить только о нём. Остальным типам будут посвящены отдельные разделы.

SOCK_DGRAM. Передача данных в виде отдельных сообщений (датаграмм). Предварительная установка соединения не требуется. Обмен данными происходит быстрее, но является ненадёжным: сообщения могут теряться в пути, дублироваться и переупорядочиваться. Допускается передача сообщения нескольким получателям (multicasting) и широковещательная передача (broadcasting).

SOCK_RAW. Этот тип присваивается низкоуровневым (т. н. "сырым") сокетам. Их отличие от обычных сокетов состоит в том, что с их помощью программа может взять на себя формирование некоторых заголовков, добавляемых к сообщению.

Обратите внимание, что не все домены допускают задание произвольного типа сокета. Например, совместно с доменом Unix используется только тип SOCK_STREAM. С другой стороны, для Internet-домена можно задавать любой из перечисленных типов. В этом случае для реализации SOCK_STREAM используется протокол TCP, для реализации SOCK_DGRAM - протокол UDP, а тип SOCK_RAW используется для низкоуровневой работы с протоколами IP, ICMP и т. д.

Наконец, последний атрибут определяет протокол, используемый для передачи данных. Как мы только что видели, часто протокол однозначно определяется по домену и типу сокета. В этом случае в качестве третьего параметра функции socket можно передать 0, что соответствует протоколу по умолчанию. Тем не менее, иногда (например, при работе с низкоуровневыми сокетами) требуется задать протокол явно. Числовые идентификаторы протоколов зависят от выбранного домена; их можно найти в документации.

Если этот аргумент задан равным 0, то будет использован протокол "по умолчанию" (TCP для SOCK_STREAM и UDP для SOCK_DGRAM при использовании комплекта протоколов TCP/IP).

При удачном завершении своей работы данная функция возвращает дескриптор socket'а - целое неотрицательное число, однозначно его идентифицирующее. Дескриптор socket'а аналогичен дескриптору файла ОС UNIX.

При обнаружении ошибки в ходе своей работы функция возвращает число "-1".

 

1.1.2. Связывание socket'а

Прежде чем передавать данные через сокет, его необходимо связать с адресом в выбранном домене (эту процедуру называют именованием сокета). Иногда связывание осуществляется неявно (внутри функций connect и accept), но выполнять его необходимо во всех случаях. Вид адреса зависит от выбранного вами домена. В Unix-домене это текстовая строка - имя файла, через который происходит обмен данными. В Internet-домене адрес задаётся комбинацией IP-адреса и 16-битного номера порта. IP-адрес определяет хост в сети, а порт - конкретный сокет на этом хосте. Протоколы TCP и UDP используют различные пространства портов.

В сетях TCP/IP socket связывается с локальным портом.

Для явного связывания сокета с некоторым адресом используется функция bind. Её прототип имеет вид:

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

int bind (s, addr, addrlen)

int s;

struct sockaddr *addr;

int addrlen;

Аргумент s задает дескриптор связываемого socket'а.

Аргумент addr в общем случае должен указывать на структуру данных, содержащую локальный адрес, приписываемый socket'у. Для сетей TCP/IP такой структурой является sockaddr_in.

struct sockaddr {

unsigned short sa_family; // Семейство адресов, AF_xxx

char sa_data[14]; // 14 байтов для хранения адреса

};

Поле sa_family содержит идентификатор домена, тот же, что и первый параметр функции socket. В зависимости от значения этого поля по-разному интерпретируется содержимое массива sa_data. Разумеется, работать с этим массивом напрямую не очень удобно, поэтому вы можете использовать вместо sockaddr одну из альтернативных структур вида sockaddr_XX ( XX - суффикс, обозначающий домен: "un" - Unix, "in" - Internet и т. д.). При передаче в функцию bind указатель на эту структуру приводится к указателю на sockaddr. Рассмотрим для примера структуру sockaddr_in.

Структура sockaddr_in используется несколькими системными вызовами и функциями socket-интерфейса и определена в include-файле in.h следующим образом:

struct sockaddr_in {

short int sin_family; // Семейство адресов

unsigned short int sin_port; // Номер порта

struct in_addr sin_addr; // IP-адрес

unsigned char sin_zero[8]; // "Дополнение" до размера структуры sockaddr

};

Здесь поле sin_family соответствует полю sa_family в sockaddr, в sin_port записывается номер порта, а в sin_addr - IP-адрес хоста. Поле sin_family определяет используемый формат адреса (набор протоколов), в нашем случае (для TCP/IP) оно должно иметь значение AF_INET.

Поле sin_addr само является структурой, которая имеет вид:

struct in_addr {

unsigned long s_addr;

};

Зачем понадобилось заключать всего одно поле в структуру? Дело в том, что раньше in_addr представляла собой объединение (union), содержащее гораздо большее число полей. Сейчас, когда в ней осталось всего одно поле, она продолжает использоваться для обратной совместимости.

И ещё одно важное замечание. Существует два порядка хранения байтов в слове и двойном слове. Один из них называется порядком хоста (host byte order), другой - сетевым порядком (network byte order) хранения байтов. При указании IP-адреса и номера порта необходимо преобразовать число из порядка хоста в сетевой. Для этого используются функции htons (Host TO Network Short) и htonl (Host TO Network Long). Обратное преобразование выполняют функции ntohs и ntohl.

ПРИМЕЧАНИЕ

На некоторых машинах (к PC это не относится) порядок хоста и сетевой порядок хранения байтов совпадают. Тем не менее, функции преобразования лучше применять и там, поскольку это улучшит переносимость программы. Это никак не скажется на производительности, так как препроцессор сам уберёт все "лишние" вызовы этих функций, оставив их только там, где преобразование действительно необходимо.

Поле sin_addr содержит адрес (номер) узла сети.

Поле sin_port содержит номер порта на узле сети.

Поле sin_zero не используется.

Структура sockaddr_in должна быть полностью заполнена перед выдачей системного вызова bind. При этом, если поле sin_addr.s_addr имеет значение INADDR_ANY, то системный вызов будет привязывать к socket'у номер (адрес) локального узла сети.

Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.

В случае успеха bind возвращает 0, в противном случае - "-1".

 

1.2. Функции установления связи

Установка соединения на стороне сервера состоит из четырёх этапов, ни один из которых не может быть опущен. Сначала сокет создаётся и привязывается к локальному адресу. Если компьютер имеет несколько сетевых интерфейсов с различными IP-адресами, вы можете принимать соединения только с одного из них, передав его адрес функции bind. Если же вы готовы соединяться с клиентами через любой интерфейс, задайте в качестве адреса константу INADDR_ANY. Что касается номера порта, вы можете задать конкретный номер или 0 (в этом случае система сама выберет произвольный неиспользуемый в данный момент номер порта).

На следующем шаге создаётся очередь запросов на соединение. При этом сокет переводится в режим ожидания запросов со стороны клиентов. Всё это выполняет функция listen.

int listen(int sockfd, int backlog);

Первый параметр - дескриптор сокета, а второй задаёт размер очереди запросов. Если какой-либо клиент выдаст запрос на установление связи при полной очереди, то этот запрос будет отвергнут.

Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind.

Признаком удачного завершения системного вызова listen служит нулевой код возврата.

Когда сервер готов обслужить очередной запрос, он использует функцию accept.

#include <sys/socket.h>

#include <netinet/in.h>

int accept (s, addr, p_addrlen)

int s;

struct sockaddr_in *addr;

int *p_addrlen;

Аргумент s задает дескриптор socket'а, через который программа-сервер получила запрос на соединение (посредством системного запроса listen).

Аргумент addr должен указывать на область памяти, размер которой позволял бы разместить в ней структуру данных, содержащую адрес socket'а программы-клиента, сделавшей запрос на соединение. Никакой инициализации этой области не требуется.

Аргумент p_addrlen должен указывать на область памяти в виде целого числа, задающего размер (в байтах) области памяти, указываемой аргументом addr.

Функция accept создаёт для общения с клиентом новый сокет и возвращает его дескриптор. Параметр sockfd задаёт слушающий сокет. После вызова он остаётся в слушающем состоянии и может принимать другие соединения. В структуру, на которую ссылается addr, записывается адрес сокета клиента, который установил соединение с сервером. В переменную, адресуемую указателем р_addrlen, изначально записывается размер структуры; функция accept записывает туда длину, которая реально была использована. Если вас не интересует адрес клиента, вы можете просто передать NULL в качестве второго и третьего параметров.

Итак, системный вызов accept извлекает из очереди, организованной системным вызовом listen, первый запрос на соединение и возвращает дескриптор нового (автоматически созданного) socket'а с теми же свойствами, что и socket, задаваемый аргументом s. Этот новый дескриптор необходимо использовать во всех последующих операциях обмена данными.

Кроме того после удачного завершения accept:

область памяти, указываемая аргументом addr, будет содержать структуру данных (для сетей TCP/IP это sockaddr_in), описывающую адрес socket'а программы-клиента, через который она сделала свой запрос на соединение;

целое число, на которое указывает аргумент p_addrlen, будет равно размеру этой структуры данных.

Если очередь запросов на момент выполнения accept пуста, то программа переходит в состояние ожидания поступления запросов от клиентов на неопределенное время (хотя такое поведение accept можно и изменить).

Признаком неудачного завершения accept служит отрицательное возвращенное значение (дескриптор socket'а отрицательным быть не может).

Примечание. Системный вызов accept используется в программах-серверах, функционирующих только в режиме с установлением соединения.

Обратите внимание, что полученный от accept новый сокет связан с тем же самым адресом, что и слушающий сокет. Сначала это может показаться странным. Но дело в том, что адрес TCP-сокета не обязан быть уникальным в Internet-домене. Уникальными должны быть только соединения, для идентификации которых используются два адреса сокетов, между которыми происходит обмен данными.

 

Запрос на установление соединения (клиент)

Для обращения программы-клиента к серверу с запросом на установление логической соединения используется системный вызов connect, имеющий следующий вид

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

int connect (s, addr, addrlen)

int s;

struct sockaddr_in *addr;

int addrlen;

Аргумент s задает дескриптор socket'а, через который программа обращается к серверу с запросом на соединение. Socket должен быть предварительно создан системным вызовом socket и обеспечен адресом с помощью системного вызова bind.

Аргумент addr должен указывать на структуру данных, содержащую адрес, приписанный socket'у программы-сервера, к которой делается запрос на соединение. Для сетей TCP/IP такой структурой является sockaddr_in. Для формирования значений полей структуры sockaddr_in удобно использовать функцию gethostbyname.

Аргумент addrlen задает размер (в байтах) структуры данных, указываемой аргументом addr.

Для того, чтобы запрос на соединение был успешным, необходимо, по крайней мере, чтобы программа-сервер выполнила к этому моменту системный вызов listen для socket'а с указанным адресом.

При успешном выполнении запроса системный вызов connect возвращает 0, в противном случае - "-1" (устанавливая код причины неуспеха в глобальной переменной errno).

Примечание. Если к моменту выполнения connect используемый им socket не был привязан к адресу посредством bind,то такая привязка будет выполнена автоматически. Обычно сокет не требуется предварительно привязывать к локальному адресу, так как функция connect сделает это за вас, подобрав подходящий свободный порт. Вы можете принудительно назначить клиентскому сокету некоторый номер порта, используя bind перед вызовом connect. Делать это следует в случае, когда сервер соединяется с только с клиентами, использующими определённый порт (примерами таких серверов являются rlogind и rshd). В остальных случаях проще и надёжнее предоставить системе выбрать порт за вас.

Примечание. В режиме взаимодействия без установления соединения необходимости в выполнении системного вызова connect нет. Однако, его выполнение в таком режиме не является ошибкой - просто меняется смысл выполняемых при этом действий: устанавливается адрес "по умолчанию" для всех последующих посылок дейтаграмм.

 

Формирование адреса узла сети

Для получения адреса узла сети TCP/IP по его символическому имени используется библиотечная функция

#include <netinet/in.h>

#include <netdb.h>

struct hostent *gethostbyname (name)

char *name;

Аргумент 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 для формирования запроса на установления соединения с программой-сервером на этом узле, рассматривается ниже.

 

Функции обмена данными

В режиме с установлением логического соединения после удачного выполнения пары взаимосвязанных системных вызовов connect (в клиенте) и accept (в сервере) становится возможным обмен данными.

Этот обмен может быть реализован обычными системными вызовами read и write, используемыми для работы с файлами (при этом вместо дескрипторов файлов в них задаются дескрипторы socket'ов). В Unix для работы с сокетами можно использовать также файловые функции read и write, но они обладают меньшими возможностями, а кроме того не будут работать на других платформах (например, под Windows), поэтому не рекомендуется ими пользоваться.

Кроме того могут быть дополнительно использованы системные вызовы send и recv, ориентированные специально на работу с socket'ами.

Примечание. Для обмена данными в режиме без установления логического соединения используются, как правило, системные вызовы sendto и recvfrom. Sendto позволяет специфицировать вместе с передаваемыми данными (составляющими дейтаграмму) адрес их получателя. Recvfrom одновременно с доставкой данных получателю информирует его и об адресе отправителя.

 

Посылка данных

Для посылки данных партнеру по сетевому взаимодействию используется системный вызов send, имеющий следующий вид

#include <sys/types.h>

#include <sys/socket.h>

int send (s, buf, len, flags)

int s;

char *buf;

int len;

int flags;

Аргумент s задает дескриптор socket'а, через который посылаются данные.

Аргумент buf указывает на область памяти, содержащую передаваемые данные.

Аргумент len задает длину (в байтах) передаваемых данных.

Аргумент flags модифицирует исполнение системного вызова send. При нулевом значении этого аргумента вызов send полностью аналогичен системному вызову write.

Вот некоторые из них (полный список можно найти в документации):

MSG_OOB. Предписывает отправить данные как срочные (out of band data, OOB). Концепция срочных данных позволяет иметь два параллельных канала данных в одном соединении. Иногда это бывает удобно. Например, Telnet использует срочные данные для передачи команд типа Ctrl+C. В настоящее время использовать их не рекомендуется из-за проблем с совместимостью (существует два разных стандарта их использования, описанные в RFC793 и RFC1122). Безопаснее просто создать для срочных данных отдельное соединение.

MSG_DONTROUTE. Запрещает маршрутизацию пакетов. Нижележащие транспортные слои могут проигнорировать этот флаг.

При успешном завершении send возвращает количество переданных из области, указанной аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается "переполненным", то send переводит программу в состояние ожидания до момента его освобождения.

Функция send возвращает число байтов, которое на самом деле было отправлено (или -1 в случае ошибки). Это число может быть меньше указанного размера буфера. Если вы хотите отправить весь буфер целиком, вам придётся написать свою функцию и вызывать в ней send, пока все данные не будут отправлены. Она может выглядеть примерно так.

int sendall(int s, char *buf, int len, int flags)

{

int total = 0;

int n;

while(total < len)

{

n = send(s, buf+total, len-total, flags);

if(n == -1) { break; }

total += n;

}

return (n==-1? -1: total);

}

Использование sendall ничем не отличается от использования send, но она отправляет весь буфер с данными целиком.

 

Получение данных

Для получения данных от партнера по сетевому взаимодействию используется системный вызов recv, имеющий следующий вид

#include <sys/types.h>

#include <sys/socket.h>

int recv (s, buf, len, flags)

int s;

char *buf;

int len;

int flags;

Аргумент s задает дескриптор socket'а, через который принимаются данные.

Аргумент buf указывает на область памяти, предназначенную для размещения принимаемых данных.

Аргумент len задает длину (в байтах) этой области.

Аргумент flags модифицирует исполнение системного вызова recv. При нулевом значении этого аргумента вызов recv полностью аналогичен системному вызову read. Флаг MSG_OOB используется для приёма срочных данных, а MSG_PEEK позволяет "подсмотреть" данные, полученные от удалённого хоста, не удаляя их из системного буфера (это означает, что при следующем обращении к recv вы получите те же самые данные). Полный список флагов можно найти в документации. По аналогии с send функция recv возвращает количество прочитанных байтов, которое может быть меньше размера буфера. Вы без труда сможете написать собственную функцию recvall, заполняющую буфер целиком.

При успешном завершении recv возвращает количество принятых в область, указанную аргументом buf, байт данных. Если канал данных, определяемый дескриптором s, оказывается "пустым", то recv переводит программу в состояние ожидания до момента появления в нем данных.

Существует ещё один особый случай, при котором recv возвращает 0. Это означает, что соединение было разорвано.

 

Функции закрытия связи

Для закрытия связи с партнером по сетевому взаимодействию используются системные вызовы close и shutdown.

Системный вызов close

Для закрытия ранее созданного socket'а используется обычный системный вызов close, применяемый в ОС UNIX для закрытия ранее открытых файлов и имеющий следующий вид

#include <unistd.h>

int close(int fd);

Однако в режиме с установлением логического соединения (обеспечивающем, как правило, надежную доставку данных) внутрисистемные механизмы обмена будут пытаться передать/принять данные, оставшиеся в канале передачи на момент закрытия socket'а. На это может потребоваться значительный интервал времени, неприемлемый для некоторых приложений. В такой ситуации необходимо использовать описываемый далее системный вызов shutdown. Вы можете запретить передачу данных в каком-то одном направлении, используя shutdown.

int shutdown(int sockfd, int how);

Параметр how может принимать одно из следующих значений:

0 - запретить чтение из сокета

1 - запретить запись в сокет

2 - запретить и то и другое

Хотя после вызова shutdown с параметром how, равным 2, вы больше не сможете использовать сокет для обмена данными, вам всё равно потребуется вызвать close, чтобы освободить связанные с ним системные ресурсы.

 

Обработка ошибок

До сих пор я ни слова не сказал об ошибках, которые могут происходить (и часто происходят) в процессе работы с сокетами. Так вот: если что-то пошло не так, все рассмотренные нами функции возвращают -1, записывая в глобальную переменную errno код ошибки. Соответственно, вы можете проанализировать значение этой переменной и предпринять действия по восстановлению нормальной работы программы, не прерывая её выполнения. А можете просто выдать диагностическое сообщение (для этого удобно использовать функцию perror), а затем завершить программу с помощью exit.

 

Отладка программ

Начинающие программисты часто спрашивают, как можно отлаживать сетевую программу, если под рукой нет сети. Оказывается, можно обойтись и без неё. Достаточно запустить клиента и сервера на одной машине, а затем использовать для соединения адрес интерфейса внутренней петли (loopback interface). В программе ему соответствует константа INADDR_LOOPBACK (не забудьте применять к ней функцию htonl!). Пакеты, направляемые по этому адресу, в сеть не попадают. Вместо этого они передаются стеку протоколов TCP/IP как только что принятые. Таким образом моделируется наличие виртуальной сети, в которой вы можете отлаживать ваши сетевые приложения.

Для простоты будем использовать в демонстрационных примерах интерфейс внутренней петли.

 

1.5. Пример использования socket-интерфейса

В данном разделе рассматривается использование socket-интерфейса в режиме взаимодействия с установлением логического соединения на очень простом примере взаимодействия двух программ (сервера и клиента), функционирующих на разных узлах сети TCP/IP.

Эхо-клиент посылает сообщение "Hello there!" и выводит на экран ответ сервера. Его код приведён в листинге 1. Эхо-сервер читает всё, что передаёт ему клиент, а затем отправляет полученные данные обратно, при этом инициализируя повторную от правку, чтобы передать клиенту дополнительные (свои) данные. Его код содержится в листинге 2.

Примечание. Приводимые ниже программы являются полностью работоспособными (по крайней мере, в Ubuntu). Но, они работают несколько «не так». Для проверки работоспособности компилируем их при помощи g++, после чего запускаем клиент. Видим ответ системы: «доступ отказан»... Правильно, ибо сервер еще не запущен. Теперь запускаем сервер, который (благодаря бесконечному циклу) переходит в режим ожидания. Потом запускаем несколько раз клиент (не забудьте, что клиент и сервер необходимо запускать в двух разных консолях, перед этим их следует открыть). После каждого запуска наблюдаем ответ сервера клиенту и информацию, которую возвращает сервер. Видим, что что-то не так. Так вот – Ваша задача состоит в том, чтобы исправить сервер и клиент таким образом, чтобы:

1. Клиент получал в ответ от сервера и помещал на экран (той консоли, на которой он запущен) не только собственное (предварительно направленное серверу) сообщение “Hello there!”, но и дополнительное сообщение лично от сервера «123456789», выводя его в следующей строке.

2. Сервер, получая от клиента сообщение “Hello there!”, помещал бы на экране (той консоли, на которой он запущен) не только его, но информацию об успешности отправки этого сообщения клиенту: “Сообщение от клиента получено и успешно перенаправлено”.

Примечание. Предлагаемые ниже тексты программ предназначены только для иллюстрации логики взаимодействия программ через сеть, поэтому в них отсутствуют такие атрибуты программ, предназначенных для практического применения, как обработка кодов возврата системных вызовов и функций, анализ кодов ошибок в глобальной переменной errno, реакция на асинхронные события и т.п.

Примечание. Необходимо разобраться полностью, что делает КАЖДАЯ строчка программы, для чего она присутствует (если есть предложения и по оптимизации, то – как лучше ее изменить) и т.п.

Листинг 1. Эхо-клиент.

#include <sys/types.h>

#include <sys/socket.h>

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/stat.h>

#include <sys/types.h>

#include <netinet/in.h>

char message[] = "Hello there!\n";

char buf1[sizeof(message)];

int fs, fr;

int main()

{

int sock;

struct sockaddr_in addr;

sock = socket(AF_INET, SOCK_STREAM, 0);

if(sock < 0)

{

perror("socket");

exit(1);

}

addr.sin_family = AF_INET;

addr.sin_port = htons(3425); //Или любой другой порт

addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)

{

perror("connect");

exit(2);

}

fs=send(sock, message, sizeof(message), 0);

//write(1,message, fs);

 

fr=recv(sock, buf1, sizeof(message), 0);

printf ("B\n");

printf(buf1);

//write(1,message, fr);

close(sock);

return 0;

}

Листинг 2. Эхо-сервер.

#include <sys/types.h>

#include <sys/socket.h>

#include <stdlib.h>

#include <stdio.h>

#include <unistd.h>

#include <fcntl.h>

#include <sys/stat.h>

#include <sys/types.h>

#include <netinet/in.h>

char message1[] = "123456789\n";

unsigned int sz;

int main()

{

int sock, listener;

struct sockaddr_in addr;

char buf[1024];

int bytes_read, bytes_read1;

listener = socket(AF_INET, SOCK_STREAM, 0);

if(listener < 0)

{

perror("socket");

exit(1);

}

 

addr.sin_family = AF_INET;

addr.sin_port = htons(3425);

addr.sin_addr.s_addr = htonl(INADDR_ANY);

if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)

{

 

perror("bind");

exit(2);

}

listen(listener, 1);

 

while(1)

{

sz=sizeof(addr);

sock = accept(listener, (struct sockaddr *)&addr, &sz);

write(1,message1, sock);

printf ("\n");

if(sock < 0)

{

perror("accept");

exit(3);

}

 

while(1)

 

{

bytes_read = recv(sock, buf, 1024, 0);

send(sock, buf, bytes_read, 0);

if(bytes_read <= 0) break;

sz=sizeof(addr);

sock = accept(listener, (struct sockaddr *)&addr, &sz);

write(1,message1, sock);

if(sock < 0)

{

perror("accept");

exit(3);

}

 

bytes_read1 = recv(sock, message1, 1024, 0);

if(bytes_read1 <= 0) break;

 

send(sock, message1, bytes_read1, 0);

}

 

close(sock);

}

 

return 0;

}

 

Примечание. Данная программа (как и большинство реальных программ-серверов) самостоятельно своей работы не завершает, находясь в бесконечном цикле обработки запросов клиентов. Ее выполнение может быть прервано только извне путем посылки ей сигналов (прерываний) завершения. Правильно разработанная программа-сервер должна обрабатывать такие сигналы, корректно завершая работу (закрывая, в частности, посредством close socket с дескриптором s). Ваша задача – СДЕЛАТЬ это, доработав соответствующим образом клиент и сервер. Например, клиент выводит на клавиатуру запрос: завершить программу сервер? Если отвачаем «да», то после отправки соответствующего сообщения серверу последний должен выйти из бесконечного цикла, завершив свою работу с выдачей сообщения: «сервер остановлен по требованию клиента».

 

 




Поделиться с друзьями:


Дата добавления: 2017-02-01; Просмотров: 160; Нарушение авторских прав?; Мы поможем в написании вашей работы!


Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет



studopediasu.com - Студопедия (2013 - 2026) год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! Последнее добавление




Генерация страницы за: 0.01 сек.