О. С. Попова Хіхловська І. В. Системне та прикладне програмне забезпечення у телекомунікаціях Конспект

Вид материалаКонспект
Программная реализация архитектуры КЛИЕНТ – СЕРВЕР
Привязка известного порта и вывод listen
Принятие соединения.
Обмен данными
Программный интерфейс сокетов
Подобный материал:
1   ...   6   7   8   9   10   11   12   13   14

Программная реализация архитектуры КЛИЕНТ – СЕРВЕР



Роль программ клиента и сервера бывает разной. Иногда они равноправные.

В случае с ТСР/IР различие четкое. Сервер обслуживает порт, чтобы обнаружить входящие соединения или ИДР-дейтограммы от одного или нескольких клиентов с другой стороны клиент – это тот, кто первым начал диалог.

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

Элементы АРI сокетов




Рисунок 1 – основные вызовы сокетов для клиентов

Адрес удаленного Хоста задается с помощью структуры sockaddr_in, которая передается функции connect. Первый шаг – это получение сокета для логического соединения с помощью системного вызова socket

#include /*UNIX*/

#include /*windows*/

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

Возвращаемое значение: дескрипторов в случае успеха; -1 (UNIX) или INVALID_SOCKET(Windows)-ошибка АРI сокетов не зависит от протокола и может поддерживать разные адресные домены. Параметр domain—константа, указывающая, какой домен нужен сокету. Чаще используются домены AF_INET (Internet) и AF_LOCAL(или AF_UNIX). AF_LOCAL применяется для межпроцесного взаимодействия (IC) на одной машине. Вместо AF можно использовать PF_*. Параметр type задает тип создаваемого сокета.

Возможные значения сокетов:
  1. SOCK_STREAM – обеспечивает нужный дуплексный протокол на основе установления логического соединения (ТСР)
  2. SOCK_DGRAM – обеспечивает ненадежный сервис доставки дейтограмм (ИДР)
  3. SOCK_RAW – предоставляет доступ и некоторым дейтограмм на уровне IP, используется, например, для просмотра всех ICMP—сообщений

Параметр protocol показывает, какой протокол следует использовать с данным сокетом. В качестве TCP/IP он обычно неявно определяется типом сокета, поэтому в качестве значения задают 0.

Для самого простого TCP-клиента требуется еще один вызов АРI сокетов, обеспечивающий установление соединения:

#include /*UNIX*/

#include /*windows*/

int connect (SOCKET_s, const struct sockaddr*peer, int peer_len);

Возвращаемое значение: 0-нормально, -1 (UNIX) или не 0 – ошибка

s-параметр, который является дескриптором сокета, который вернул системный вызов SOCKET.

Параметр peer указывает на структуру, в которой хранится адрес удаленного хоста и дополнительная информация.

Для домена AF_INET –это структура типа sockaddr_in. Параметр peer_len содержит размер структуры в байтах, на которую указывает peer.

После установления соединения можно передавать данные. В ОС UNIX нужно обратиться к системным вызовам read и write и передать или дескриптор сокета.

#include /*UNIX*/

#include /*windows*/

int recv (socket s, woid*buf, suze_tlen, int flags);

int send (socket s, const void*buf, size_tlen, int flags);

Возвращаемое значение: число принять на переданных байтов в случае успеха или –1 в случае ошибки.

Параметры s, buf и len аналогичные используемым для read и write. Flags зависит от системы, но и UNIX и windows поддерживают следующие флаги:
  1. MSG_OOB – Следует послать или принять срочные данные
  2. MSG_PEEK – Используется для просмотра поступивших данных без их удаления из приемного буфера. После возврата из системного вызова данные еще могут быть получены при последующем вызове read или recv;
  3. MSG_DONTROUTEK – сообщает ядру, что не надо выполнять обычный алгоритм маршрутизации, как правило, используется программами маршрутизации и для диагностических целей.

При работе с протоколом TCP больше ничего не нужно. При работе с ИДР нужны системные вызовы recvfrom и sendto; они отличаются тем, что при отправлении дайтограмм позволяют задать адрес назначения, а при приеме -- получить адрес источника.

#include /*UNIX*/

#include /*windows*/

int recvfrom (sockets, const void*size_t len, int flags, const struct sockaddr*to, int tolen);

Возвращаемое значение: число принятых и переданных байтов в случае успеха или –1 при ошибке. Параметры s, buf, Len, flags, --такие же, как в вызовах recv и send. Параметр from в вызове recvfrom указывает на структуру, в которую ядро помещает адрес источника пришедшей дейтограммы. Длина этого адреса хранится в целом числе, на которое указывает параметр fromLen (указатель на целое).

Параметр to в вызове sendto указывает на адрес структуры, содержащий адреса назначения дейтограммы, а параметр tolen – длина этого адреса. To – это целое, а не указатель.


1.2 Разработка программ в архитектуре “клиент-сервер”

Большинство прикладных программ является не единым программным модулем, а набором взаимодействующих между собой подпрограмм , одни из которых являются модулями ОС, а могли вообще принадлежать другим разработчикам – их надо смело поставлять и устанавливать, например, из Интернет. В прикладной программе можно выделить два уровня:
  1. Нижний, отвечающий за методы хранения доступа, разделения данных,-- поставляемый
  2. Верхний, отвечающий за логику обработки данных и интерфейс пользователя –собой.

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

Второй не требовал наличия высокопроизводительной системы вычислений и включая в себе собственно алгоритм логику и весь интерфейс, созданный разработчиком программы. Так сложилось понятие приложения, построенного на архитектуре «клиент -сервер».

Первый уровень, сервер, включает методы, связанные с доступом данных; их реализует сервер БД из соответствующей СУБД в комплекте с драйверами доступа к телу.

Второй уровень, клиент, взаимодействует, с одной стороны, с сервером, получая от него данные, а с другой стороны – с пользователем, ресурсами приложения и ОС, осуществляя обработку данных и отображение результатов. Результаты обработки клиент опять-таки может сохранить в обработку данных и отображение результатов. Результаты обработки клиент опять-таки может сохранить в БД, используя функции серверной части. Многие известные фирмы стали предлагать стандартные интерфейсы для доступа к создаваемым им СУБД. Изменилась и структура систем программирования; они стали ориентироваться на создание в архитектуре, клиент - сервер. Предлагаемые или средства поставляются в составе систем программирования и поддерживают возможность работы с широким диапазоном известных серверов данных через один или несколько доступных интерфейсов обмена данными. Разработчик прикладной программы выбирает одно из доступных средств и возможный тип сервера. Тогда задача сводится только к созданию клиентской части приложения, построенной на основе выбранного интерфейса. В дальнейшем использовать приложение можно только в комплексе. Интерфейс обмена данными входит в систему программирования и свободно распространяется.

Серверная часть распространяется только по лицензиям, надо лицензировать средства создания и отладки БД, но результаты работы могут распространяться. Либо в случае мощных серверов, лицензия нужна на и распространение серверной части приложение. В этом случае конечный пользователь получает комплекс программы, продуктов от множества разработчиков. Развитием архитектуры программирования «клиент – сервер» явилась трехуровневая архитектура. Клиентская часть разделилась на две составляющие: сервер приложений и «тонкий клиент, обеспечивающий интерфейс доступ к результатам обработки. Серверная часть, сервер без данных, осталась без изменений. Взаимодействие между сервером приложений и тонким клиентом должно быть тесным, а, с другой стороны, должен обмениваться данными по протоколам интернет, которые стандартизованы, например, COM/DCOM, CORBA и другие (COMMON Object Reguest Broklr Architecture)

Пример простого ТСР – клента
  1. #include
  2. #include
  3. #include
  4. #include
  5. #include
  6. int main (void)
  7. {
  8. struct sockaddr_in peer;
  9. int d;
  10. int rc;
  11. char buf [ 1 ];
  12. peer.sin_family = AF_INET;
  13. peer.sin_pott = Atons( 7500 );
  14. peer.sin_addr.s addr = inet_addr(“127.0.0.1”);
  15. S = socket (AF_INET, SOCK_STREAM, 0 );
  16. If ( s < 0)
  17. {
  18. perror( «ошибка вызова sockett»);
  19. exit ( 1 );
  20. }
  21. rc=connect ( s, ( struct sockaddr * )&peer, sizeof( peer) );
  22. if ( rc<=0 )
  23. {
  24. perror ( «ошибка вызова connect» );
  25. exit ( 1 );
  26. }
  27. rc = send ( s, “1”, 1, 0 );
  28. if ( rc <= 0 )
  29. {
  30. perror («ошибка вызова send» );
  31. exit ( 1 );
  32. }
  33. rc = recv( s, buf, 1, 0 );
  34. if ( rc <= 0 )
  35. perror( «ошибка вызова reccv» );
  36. else
  37. printf ( “%\n”, buf [ 0 ];
  38. exit( 0 );
  39. }


Подготовка адреса сервера:

12 – 14 Заполнение структуры sockaddr­_in путем записи в ее поля номера порта (7500) и адреса 127.0.0.1 – это возвратный адрес, который означает, что сервер находится на той же машине, что и клиент.


Получение сокета и соединение с сервером:

15 – 20 Получение сокета типа SOCK_STREAM, т.к. используется потоковый протокол ТСР.

21 – 26 Установка соединения с сервером путем обращения к системному вызову connect. Это вызов нужен, чтобы сообщить ядру адрес сервера.


Отправка и получение одного байта

27 – 38 Сначала один байт серверу, затем из сокета читается один байт и записывается в стандартный вывод. Сеанс завершается.


Перед тестированием клиента следует подготовить сервер.

Сервер должен быть готов к установлению соединений с клиентами. Для этого он должен прослуживать известный ему порт с помощью системного вызова listen.





Рисунок – Основные вызовы API сокетов для сервера.

Но предварительно необходимо привязать адрес интерфейса и номер к прослеживающему сокету. Для этого предназначен вызов bind.

#include /*UNIX*/

#include /*windows*/

int bind( socket s, const sockaddr*name, int namelen );

Возвращаемое значение:0 – нормально, -1 или SOCKET_ERROR (Windows) – ошибка

Параметр s – это дескриптор просуживающего сокета. С помощью параметров name и namelen передаются порт и сетевой интерфейс, которые нужно прослеживать. Обычно в качестве адреса задается константа INADDR_ANY. Это означает, что будет прямое соединение, запрашиваемое по любому интерфейсу. Если хосту с несколькими сетевыми адресами нужно принимать соединение только по одному интерфейсу, нужно указать IP – адрес этого интерфейса. Через namelen обозначается длина структуры sockaddr_in.

После привязки локального адреса к сокету нужно перевести сокет в режим прослушивания входящих соединений с помощью системного вывода listen. Его задача – пометить сокет как прислуживающий. Когда хосту поступает запрос на установление, ядро ищет для которого адрес назначения и номер порта соответствуют указанным в запросе.

#include /*UNIX*/

#include /*windows*/

int listen( SOCKET s, int backlog );

0 – нормально; -1(UNIX) или SOCKET_ERROR (Windows) – ошибка.

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

Вызов accept служит для приема соединения, ожидающего во входной очереди. После того, как соединение принято, его можно использовать для передачи данных, например, с помощью вызовов recv и send. В случае успеха accept возвращает дескриптор нового сокета, по которому будет происходить обмен данными. Номер локального порта для этого сокета такой же, как и для прослушивающего сокета. Адрес интерфейса, на котором поступил запрос о соединении, называется локальным. Адрес и номер порта клиента считаются удаленными. Оба сокета имеют один и тот же номер локального порта. Это справедливо, т.к. ТСР – соединение полностью определяется четырьмя параметрами – локальным адресом, локальным портом, удаленным адресом, удаленным портом. Т.к. удаленный адрес и порт для этих двух сокетов различны, то ядро может отличить их друг от друга.

#include /*UNIX*/

#include /*windows*/

int assept (socket s, structs sockaddr*addr, int*addrlen);

возвращаемое значение: 0 – нормально, -1 (UNIX) или WALID_SOCKET(Windows) – ошибка.

Параметр s – это дескриптор прослушивающего сокета. Как показано на рисунке 3, accept возвращает адрес приложения на другом конце соединения в структуре sockaddr_in, на которое указывает параметр addr, ядро присваивает значение, равное длине этой структуры. Часто нет необходимости знать адрес клиентского приложения, поэтому в качестве addr будет перезадаваться NULL. Приведенная ниже программа демонстрирует структуру сервера и элементарные вызовы API сокетов, которые обязан выполнить любой сервер.

Простой ТСР – сервер.
  1. #include
  2. #include
  3. #include
  4. #include
  5. int main (void)
  6. {
  7. struct sockaddr_in local;
  8. int s;
  9. int sl;
  10. int rb;
  11. char buf [ 1 ];
  12. local.sin_family = AF_INET;
  13. local.sin_port = htons ( 7500);
  14. local.sin_addr.s addr = htonl (INADDL_ANY);
  15. s=socket (AF_INET, SOCK_STREAM,0);
  16. if (s<0)
  17. {
  18. perror («ошибка вызова socket»);
  19. exit (1);
  20. }
  21. rc = bind (s, (struct sockaddr*)&local, sizeof(local));
  22. if (rc<0)
  23. {
  24. perror («ошибка вызова bind»);
  25. exit (t);
  26. }
  27. rc = listen (s,5);
  28. if (rc)
  29. {
  30. perror («ошибка вызова listen»);
  31. exit (1);
  32. }
  33. s1=assept (s,NULL, NULL);
  34. if (s1<0)
  35. {
  36. perror («ошибка вызова assept»);
  37. exit (1);
  38. }
  39. rc = recv (sl, buf, 1, 0);
  40. if (rc<=0)
  41. {
  42. perror («ошибка вызова recv»);
  43. exit (1);
  44. }
  45. print (“%c\n”, buf [0]);
  46. rc = send (s1,”2”, 1, 0);
  47. if (rc <=0)
  48. perror («ошибка вызова send»);
  49. exit (0)
  50. }


Заполнение адресной структуры и получение сокета

12 – 20 Заполняем структуру sochaddr_in, записывая все поля известные адреса и номера порта, получаем сокет типа SOCK_STREAM, который и будет прослушивающим.

Привязка известного порта и вывод listen

21 – 32 привязываем известные порты и адрес, записанные в структуру local, к полученному сокету. Затем вызываем listen, чтобы пометить как прослушивающий.

Принятие соединения.

33 – 39 вызываем assept для приема новых соединений. Вызов assept блокирует выполнение программы до тех пор, пока не поступил запрос на соединение, после чего возвращает новый сокет для этого соединения.

Обмен данными
  1. – 49 сначала читаем и печатаем байт со значением «1», полученный от клиента. Затем посылаем один байт со значением «2» назад клиенту и завершаем программу.


На одном компьютере можно протестировать клиент и сервер, запустив их на одном компьютере в разных окнах. Сервер должен быть запущен первым, иначе клиент аварийно завершится с сообщением connection refused (в соединении отказано)

Bsd:$sinplec

Ошибка вызова connect: connection refused

Bsd:$

Ошибка произошла потому, что при покрытии клиента установить соединение не было сервера, прослушивающего порт 7500.

Для правильного соединения необходимо запустить сервер до запуска клиента.


Программный интерфейс сокетов


Задача: клиент отправляет серверу сообщение, сервер передает ему обратно, а клиент выводит полученное сообщение на экран. отличием от ранее рассмотренных программ является коммуникационный домен сокетов – AF_INTEL. Изменилась также схема адресации коммуникационного узла. согласно схеме адресации TCP/IP, коммуникационный узел однозначно идентифицируется двумя значениями: адресом хоста (IP-адрес) и адресом процесса (адрес порта). Это отражает структуру sockaddr in, которая является конкретным видом общей структуры адреса сокета sockaddr. Структура sockaddr_in имеет следующий вид:

strukt sockaddr_ in {

short sin_ family; коммуникационный домен – AF_INTEL

u_ short sin_ port; номер порта,процесса sin_ port- 16 разрядов

struct_ in_ addr sin_addr. IP- адрес хоста

(32-разряда число целое sin_ addr).

Char sin_ zero [&];};


Для домена Internet формат адреса определен в файле .

Адрес порта должен быть предварительно оговорен между клиентом и сервером. В качестве транспортного протокола используется ТСР. Это означает, что перед передачей данных клиент должен установить соединение с сервером.

В соответствии с этой схемой сервер производит связывание с портом, номер которого предполагается известным для клиентов (bind(2)),и сообщает о готовности приема запросов (listen(2)).При помощи запроса он с помощью функции accept(2) создает новый сокет, который обслуживает обмен данными между клиентом и сервером. Сервер порождает отдельный процесс на каждый поступивший запрос. Дочерний процесс принимает сообщение от клиента (recv(2)) и передает их обратно (send(2)). Клиент не выполняет связывания, т.к. ему безразлично, какой адрес будет иметь его коммуникационный узел. Эту операцию выполняет система, выбирая свободный адрес порта и установленный адрес хоста. Далее клиент направляет запрос на установление соединения (connect(2)),указывая адрес сервера (IP-адрес и номер порта). Такие установления соединения (тройное “рукопожатие”) клиент передает сообщение (send(2)), принимает ответ (recv(2)) и выводит его на экран.

Функция gethostbyname (3N) транслирует доменное имя хоста в его IP-адрес.

Функция htons(3N) приводит в соответствие порядок следования байтов в структурах данных, т.к. порядок может отличаться для хоста и сети.

Функция inet_ntoa(3N) преобразует IP-адреса и их составные части в соответствии с привычной “человеческой” нотацией, например 127.0.0.1.

Информация обо всех этих функциях есть в справочнике man (1).


Сервер

#include

#include

#include

#include Преобразует IP-адрес в структуру

#include типа in_addr_t с помощью процедуры

#include inet_addr:

#include #includein_addr_t

inet_addr(const char*ip_address

/*Номер порта сервера, известный клиентом*/

#define PORTNUM 1500

main (arge,argv)

int arge;

char*argv[ ];

{

int s, ns;

int pid;

int nport;

struct sockaddr_in serv_addr, clnt_addr;

struct hostent*hp;

char buf[80],hname[80];

/*Преобразует порядок следования байтов к сетевому формату*/

nport=PORTNUM;

nport=htons((u_short)nport);

/*Создадим сокет,использующий протокол TCP*/

if((s=socket(AF_INET,SOCK_STREAM,0))==-1)

{perror(“Ошибка вызова socket( )”); exit(1);

}

/*Зададим адрес коммуникационного узла */

bzero(&serv_addr,siseof(serv_addr));

serv_addr.sin_family=AF_INET;

serv_addr.sin_addr.s_addr=INADDR_ANY;

serv_addr.sin_port=nport;

/*Свяжем сокет с этим адресом*/

if(bind(s,(struct sockaddr*)&serv_addr,sizeof(serv.addr))==-1)

{

perror(“ошибка вызова bind()”;exit(1);

}

/*Выведем сообщение с указанием адреса сервера*/

fprintf(stderr,”Сервер готов% s\n”,inet_ntoa(serv_addr.sin_addr));

/*Сервер готов принимать запросы на установление соединения. Максимальное число запросов, ожидающих обработки –5.Как правило этого числа достаточно, чтобы успеть выполнить accept(2) и породить дочерний процесс*/

if (listen (s,5)==-1)

{

perror(“Ошибка вызова listen( );exit(1);

}

/*Бесконечный цикл получения запросов и их обработки*/

while(1)

{

intaddrlen;

bzero(&clnt_addr,sizeof(clnt_addr));

addrlen=sizeof(clnt_addr);

/*Примем запрос. Новый сокет становится коммуникационным узлом созданного виртуального канала , accept(2) возвращает адрес клиента clntaddr и его размер addrlen */

if((ns=accept(s,(struct sockaddr*)&clnt_addr&addrlen==-1)

{

perror(“Ошибка вызова accept( )”);exit(1);

}

/*Выведем информацию о клиенте*/

fprintf(stderr,”клиент =%s\n”,inet_ntoa(clnt_addr.sin_addr));

/*Создадим процесс для работы с клиентом*/

if((pid=fork( )==-1)

{

perror(“Ошибка вызова fork( )”);exit(1);

}

if(pid==0)

{

int nbytes;

int fout;

(вых. файл).

/*Дочерний процесс: этот сокет нам не нужен. Он по-прежнему используется для получения запросов*/

close(s);

/*получим сообщение от клиента и передадим его обратно*/

while((nbutes=recv(ns,buf,sizeof(buf),0))!=0)

{

send(ns,buf,sizeof(buf),0);

}close(ns);

exit(0);

}

/*Родительский процесс: этот сокет нам не нужен. Он используется дочерним процессом для обмена данными*/

close(ns);

}

Описание системного вызова connect

#include

#include

int connect(int csockfd,const struckt sockaddr*address,size_t.add_len);

Клиент

#include

#include

#include

#include

#include

#include

#include

/*Номер порта, который обслуживается сервером*/

#define PORTNUM 1500

main(argec,argv)

char*argv[];

intarge;

{

int s;

int pid;

int i,j;

struct sochadr_in serv_addr;

struct hosten *hp;

char bub[80]=”Hello”;

/*в качестве аргумента клиенту передается доменное имя хоста, на котором запущен сервер. Произведем трансляцию доменного имени в адрес*/

if((hp=gethostbuname(argv[1]))==0)

{

perror(“Ошибка вызова gethostbuname()”); exit(3);

}

bzero(&serv_addr, sizeof(serv_addr.));

bcopy(hph_addr,&serv_addr.sin_addr,hph_length);

serv_addr.sin_family=hph_addrtype;

serv_addr.sin_port=htons(PORTNUM);

/*Создадим сокет*/

if((s=socket(AF_INET,sock_STREAM,0))==-1)

{

perror(“Ошибка вызова socket()”);exit(1);

}

fprintf(stderr,”Адрес клиента:% s\n”, inet_ntoa(serv_addr.sin_addr));

/*Создадим виртуальный канал*/

if(connect(s,(struct sockaddr*)& serv_addr,sizcof(serv_addr))==-1)

{

perror(“Ошибка вызова connect()”); exit(1);

}

/*Отправим серверу сообщение и получим его обратно*/

send(s,buf,sizeof(buf,0);

if(recv(s,buf,sizeof(buf),0)<0)

{

perror(“Ошибка вызова recv()”); exit(1);

/*выведем полученное сообщение на экране*/

if(recv,buf,sizeof(buf),0)<0)

}

/*Выведем полученное сообщение на экран*/

printf(“Клиент завершил работу \\n”);

}

Схема установления связи и передачи данных между клиентом и сервером

Сервер

(IP=192.80.165.20 port=1500)




Сокеты во FreeBSD


К межпроцессному взаимодействию предъявляются следующие требования:
  1. Взаимодействие между процессами должно быть унифицировано независимо от того, выполняются они одном компьютере или на разных мостах сети. При этом могут использоваться различные схемы адресации объектов, протоколы передачи данных и т. д. Это входит понятие коммуникационный домен. Для обозначения коммуникационного узла, обеспечивающего прием и передачу данных, введен специальный объект сокет (socket). Сокеты имеют соответствующий интерфейс доступа в файловой системе UNIX и как обычные файлы, адресуются некоторым целым числом – дескриптором. Сокеты создаются в рамках определенного коммуникационного домена. Так же, как файлы, создаются в рамках файловой системы. В отличие от файлов, сокеты представляют собой виртуальный объект, который существует , пока на него ссылается хотя бы один из процессов.
  2. Коммуникационные характеристики взаимодействия должны быть доступны всем процессам в унифицированной форме:

2.1 упорядоченная доставка данных

2.2 отсутствие дублирования данных

2.3 надежная доставка данных

2.4 сохранение границ сообщений

2.5 поддержка передачи экстренных сообщений

2.6 предварительное установление соединения

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