Лабораторная работа № 3

Сетевое программирование с использованием сокетов

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

Указания к выполнению работы

Перед выполнением работы необходимо повторить следующие разделы теории, изучавшиеся в предыдущих курсах:

·  Особенности модульного и объектно-ориентированного проектирования;

·  Принципы работы с современными средствами визуального программирования.

·  Основы сетевых технологий. Модель взаимодействия открытых систем OSI. Стек протоколов TCP/IP.

Что такое сокеты

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

По своей сути программный интерфейс сокетов представляет собой набор функций, которые позволяют программисту решать задачи, связанные с передачей информации между ЭВМ по сети. Если локальная или глобальная сеть построена на основе протокола TCP/IP, то на прикладном уровне взаимодействие между узлами сети можно реализовывать при помощи технологии сокетов.

Изначально интерфейс сокетов разрабатывался в рамках работы над операционной системой Unix (стандарт Berkeley Sockets). Позднее, фирма Microsoft доработала указанный интерфейс (например, добавила в него некоторые необходимые для ОС Windows функции), что привело к появлению интерфейса Windows Sockets или WinSock. Функции, составляющие современную версию интерфейса WinSock, собраны в динамически подключаемой библиотеке WS2_32.DLL.

НЕ нашли? Не то? Что вы ищете?

Этапы работы с объектами Windows Sockets

1.  Инициализация приложения, то есть получение доступа к интерфейсу Windows Sockets. Для этой цели служит функция WSAStartup, определяемая следующим образом:

int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData);

Здесь wVersionRequested – двухбайтный параметр, указывающий номер версии интерфейса WinSock, который требуется для приложения. Младший байт параметра должен содержать старший номер версии, старший байт – младший номер версии. В современных операционных системах семейсва Windows используется интерфейс Windows Sockets версии 2.0 и выше. Параметр lpWSAData – это указатель на структуру типа WSADATA, в которую будут записаны сведения о конкретной реализации интерфейса Windows Sockets. Функция возвращает нулевое значение, если ее выполнение завершилось успешно. В противном случае будет возвращен код ошибки.

2.  Создание и инициализация сокета – объекта, через который будет происходить обмен данными. Для этих целей используется функция socket:

SOCKET socket (int af, int type, int protocol);

Здесь:

af – целочисленный параметр, определяющий формат сетевых адресов тех узлов, с которыми предполагается осуществлять взаимодействие. Для работы с адресами в формате, принятом для Internet, этот параметр должен принять значение AF_INET.

type – целочисленный параметр, определяющий тип сокета. Для передачи данных через организованный канал связи значение этого параметра должно быть равно SOCK_STREAM. Если сокет создается для обмена информацией без организации каналов связи (более быстрый, но менее надежный способ), то значение параметра устанавливается равным SOCK_ DGRAM).

protocol – целочисленный параметр, определяющий протокол для работы сокета. В большинстве случаев его значение можно положить равным 0.

Функция socket в случае успешного выполнения возвращает дескриптор сокета. В случае возникновения ошибки ее результатом будет значение INVALID_SOCKET (или -1).

Перед тем, как начинать работу с сокетом, его необходимо инициализировать. Параметры сокета задаются с использованием специальных структур данных sockaddr или sockaddr_in. Структуру sockaddr_in нужно использовать в тех случаях, когда выюран формат сетевых адресов, принятый для сетей Internet (параметр af при создании сокета был равен AF_INET). Чтобы взаимодействие с другими приложениями по сети стало возможным, необходимо инициализировать следующие элементы указанной структуры:

·  Сетевой адрес узла, к которому мы планируем обращаться используя сокет. Адрес задается указанием его типа и собственно значения. Тип адреса указывается в поле sin_family – в нашем случае его значение должно быть равным константе AF_INET. Чтобы задать значение адреса, используется поле sin_addr, которое, в свою очередь, является структурой специального формата. Для корректного определения сетевого адреса в поле s_addr указанной структуры достаточно записать результат работы функции inet_addr(const char *cp), которая преобразует строку с сетевым адресом, записанным в виде четырех десятичных чисел (например, “217.71.128.65”), разделенных точкой, в некоторое целое число. Следует помнить, что в некоторых ситуациях, мы не можем указать для сокета конкретное значение сетевого адреса. Например, если разрабатываемое приложение является сервером, то есть предоставляет удаленным приложениям (клиентам) какие-либо информационные услуги, оно может соединяться и обмениваться информацией со многими компьютерами в сети, у которых могут быть различные сетевые адреса. Для таких случаев предусмотрена константа INADDR_ANY, значение которой присваивается полю sin_addr.s_addr и позволяет сокету устанавливать соединение по любому сетевому адресу.

·  Номер порта, используемого сокетом при передаче данных. На каждом компьютере может быть одновременно запущено большое количество приложений, осуществляющих обмен данных по сети. Чтобы определить, для какого приложения предназначены те или иные поступившие по сети данные, в протоколе TCP введено понятие порта. Сетевой порт представляет собой число от 1 до 65535 и фактически является идентификатором установленного между двумя приложениями взаимодействия. Разработчик волен по своему желанию выбирать номер порта для своих приложений (у каждой пары взаимодействующих по сети сокетов номер порта должен быть один и тот же). Однако первые 1024 номера портов считаются зарезервированными для специального использования и не рекомендуются для произвольного использования. Номер порта хранится в поле sin_port в специальном универсальном сетевом формате. Для преобразования 16-разрядного номера порта в универсальный сетевой формат в интерфейсе WinSock предусмотрена функция htons(u_short pt), результат выполнения которой можно присваивать в указанное выше поле структуры sockaddr_in.

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

·  Создать сокет с помощью функции socket( );

·  Создать переменную для структуры данных sockaddr_in;

·  Инциализировать поля созданной переменной: sin_family, sin_addr, sin_port;

·  Инициализировать созданный сокет путем его «привязки» к конкретному сетевому адресу.

Здесь необходимо сделать некоторые пояснения относительно технологии клиент-сервер, которая неразрывно связана с использованием сокетов. Суть этой технологии заключается в том, что любое взаимодействие между двумя приложениями рассматривается как предоставление некоторой информационной услуги одного приложения (сервера) в ответ на запрос второго приложения (клиента). Характер этой услуги может быть самым различным – выдача файлов, предоставление данных, передача сообщения и т. д. В некоторых случаях роли приложений жестко зафиксированы, то есть при их взаимодействии одно всегда выступает как сервер, а другое – как клиент. В других ситуациях оба взаимодействующих приложения могут выступать по отношению друг к другу и в качестве сервера, и в качестве клиента.

Интерфейс Windows Socket ориентирован на реализацию технологии клиент-сервер при взаимодействии между приложениями. По этой причине порядок инициализации сокета различается в зависимости от того, открывается ли сокет на стороне сервера или на стороне клиента. На стороне сервера этап инициализации может происходить отдельно от этапов установления канала связи и соединения с клиентом, поскольку сервер должен быть готов к установке соединения, но точного момента, в который клиент обратится к нему с запросом, он не знает. Клиентские сокеты не требуется инициализировать отдельно от установки соединения, так как клиент сам инициирует обмен информацией с сервером (посылает запрос и ожидает ответ от сервера) и, следовательно, знает точный момент начала сетевого взаимодействия.

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

int bind (SOCKET sock, const struct sockaddr FAR * addr, int addrlen);

Здесь sock – дескриптор сокета (полученный в результате вызова функции socket), addr – указатель на подготовленную структуру sockaddr, addrlen – параметр, указывающий размер этой структуры в байтах. В случае, когда подготавливалась структура sockaddr_in, а не sockaddr, необходимо провести соответствующее приведение типов для указателя addr (см. пример далее). При успешно выполнении инициализации, функция bind() возвращает нулевое значение. В противном случае будет возвращен код ошибки.

Инициализация сокетов клиентских приложений, как уже было сказано, осуществляется в момент установки связи (см. далее).

3.  Создание канала связи. Данный этап выполняется только в том случае, если для взаимодействия приложений по сети используется протокол TCP. Кроме того, этот этап выделяется отдельно только для сокетов, которые открываются в приложениях-серверах. Для создания канала связи используется функция listen():

int listen(SOCKET sock, int backlog);

Здесь sock – это дескриптор сокета, а целочисленный параметр backlog определяет максимальный размер очереди для ожидания соединения (то есть, сколько клиентов одновременно могут ждать установки соединения с сервером). Чтобы задать для сокета очередь максимального размера в качестве указанного параметра можно передать значение константы SOMAXCONN. В случае успешного выполнения функция listen() возвращает 0.

4.  Установка соединения. Реализация этого этапа выполняется по-разному для серверных и клиентских сокетов. Приложение-сервер должно подготовить сокет к установке соединения и перейти в режим ожидания запросов от клиента (одного или нескольких). Для этого необходимо вызывать функцию accept():

SOCKET accept (SOCKET sock, struct sockaddr FAR * addr, int FAR * addrlen);

Здесь sock – дескриптор сокета, параметр addr указывает на структуру, в которую будет записана информация о сетевом адресе подключившегося клиента, addrlen – указатель на длину этой структуры. Если соединение установлено успешно, функция accept() возвращает идентификатор сокета подключившегося клиента. Если в момент вызова сервером функции accept() нет ожидающих подключения клиентов, то результат ее исполнения будет зависеть от режима работы сокета. При работе в блокирующем режиме функция заблокирует исполнение серверного приложения и не завершиться до тех пор, пока соединение с клиентом не будет установлено. При работе сокета в неблокирующем режиме функция завершит исполнение со специальным кодом ошибки (WSAEWOULDBLOCK). По умолчанию, все сокеты Windows Sockets работают в блокирующем режиме, рассмотрением которого мы ограничимся.

Клиентские приложения после создания сокета и инициализации структуры sockaddr_in с информацией о сетевом адресе и номере порта для серверного приложения должны инициировать установку соединения. Для этой цели служит функция connect():

int connect(SOCKET sock, const struct sockaddr * addr, int addrlen);

Здесь sock – дескриптор ранее созданного сокета, addr – указатель на структуру с информацией о сетевом адресе и номере порта сервера, addrelen – длина этой структуры. При работе сокета в блокирующем режиме функция возвратит 0 при успешной установке соединения. Чтобы соединение могло быть установлено, к моменту вызова клиентом функции connect() серверное приложение должно успеть создать канал связи (функция listen()) и перевести свой сокет в режим ожидания соединения (функция accept()).

5.  Обмен данными. Сокеты, относящиеся к типу SOCK_STREAM, обеспечивают приложениям полнодуплексное взаимодействие. Передача и получение данных при взаимодействии по протоколу TCP осуществляется при помощи функций send() и recv().

Функция передачи данных send() имеет четыре параметра – дескриптор сокета sock, через который выполняется передача, адрес буфера buf, содержащего передаваемое сообщение, размер этого буфера bufsize и флаги flags:

int send (SOCKET sock, const char FAR* buf, int bufsize, int flags);

Набор флагов позволяет настроить некоторые специфические параметры передачи данных. В данной работе достаточно указать значение параметра flags равным нулю.

Параметры функции recv(), предназначенной для приема данных, аналогичны параметрам функции send():

int recv (SOCKET sock, char FAR * buf, int bufsize, int flags);

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

6.  Закрытие сокета и деинициализация. После завершения сетевого взаимодействия между приложениями следует закрыть сокеты и прекратить использование в приложении интерфейса Windows Socket. Закрытие сокета осуществляется при помощи функции closesocket(SOCKET sock), единственным аргументом которой является дескриптор сокета sock, который должен быть закрыт. Отключение доступа к интерфейсу Windows Socket (отключение соответствующей DLL-библиотеки от процесса пользовательского приложения) выполняется функцией WSACleanup(), которая не имеет входных параметров. Обе указанные функции возвращают ноль, если соответствующая операция была вызвана успешно.

Рекомендации к выполнению лабораторной работы

1.  Интерфейс Windows Sockets является частью прикладного программного интерфейса Windows, поэтому приложения, взаимодействующие по сети с использованием описанной технологии, можно реализовывать в любой среде разработки Windows-приложений. В данной лабораторной работе рекомендуется использовать такие средства разработки как Borland C++ Builder или Borland Delphi, хотя это условие не является обязательным.

2.  При работе с интерфейсом Windows Sockets в среде разработки Borland C++ Builder к программному модулю должен быть подключен заголовочный файл <winsock2.h>. При работе в среде разработки Borland Delphi к модулю программы необходимо подключить файлы “windows. pas” и “winsock. pas”.

3.  Для корректного задания параметра wVersionRequested функции WSAStartup() рекомендуется использовать макрос MAKEWORD(BYTE bLow, BYTE bHigh), который соединяет заданные величины bLow и bHigh в одно значение типа WORD. В параметр bLow следует передавать старший номер версии интерфейса WinSock, а в параметр bHigh – младший. Например:

Procedure Init;

Var

WSD: WSAData;

Res: integer;

Begin

// подключаем интерфейс Windows Sockets версии 2.0

Res := WSAStartup(MAKEWORD(2,0),WSD);

End;

4.  Поскольку язык Delphi не различает прописных и строчных букв, то тип данных SOCKET и функция socket() интерфейса WinSock оказываются неразличимыми. Для решения этой проблемы разработчики Borland Delphi заменили стандартное наименование типа SOCKET на TSocket, которое и следует использовать при объявлении и работе с дескрипторами сокетов.

5.  Следует заметить, что для разработку и отладку приложений, взаимодействующих через сеть с использованием стека протоколов TCP/IP, можно осуществлять на одной единственной ЭВМ. Для этого нужно одновременно запускать и приложение-сервер, и приложение-клиент, а в качестве сетевого IP-адреса указывать значение “127.0.0.1”.

6.  Чтобы передать в функции bind(), connect() или accept() указатель на структуру типа sockaddr_in, необходимо выполнять преобразование типов указателей:

Пример создания и инициализации сокета в среде Borland C++ Builder

#include <winsock2.h>

// определим размер структуры sockaddr_in

#define SIZE sizeof(struct sockaddr_in)

...

int socketCreate() {

...

SOCKET sock;

// создаем сокет

sock = socket(AF_INET, SOCK_STREAM, 0);

if (sock == -1) {

// Ошибка!

WSACleanup();

return -1;

}

// инициализируем структуру sockaddr_in с параметрами для

// сокета

sockaddr_in Q;

Q. sin_family = AF_INET;

Q. sin_port = htons(2006); // номер порта = 2006

// для серверного приложения укажем, что к нему может

// подключаться клиент с любым сетевым адресом

Q. sin_addr. s_addr = INADDR_ANY;

// Осуществляем привязку сокета

int res;

res = bind(sock,(struct sockaddr *) &Q, SIZE);

...

}

При работе с сокетами в среде Borland Delphi никакого преобразования делать не требуется, поскольку язык Delphi позволяет объединить структуры sockaddr и sockaddr_in интерфейса WinSock в единый тип данных, за которым разработчики Borland Delphi сохранили название sockaddr_in.

Пример создания и инициализации сокета в среде Borland Delphi

uses <winsock2.h>

...

function socketCreate():integer;

var

sock, clientsock : TSocket;

Q : sockaddr_in;

SIZE: integer;

...

begin

...

// создаем сокет

sock := socket(AF_INET, SOCK_STREAM, 0);

if sock = -1 then

begin

// Ошибка!

WSACleanup();

Result := -1;

Exit;

end;

// определим размер структуры sockaddr_in

SIZE := sizeof(Q);

// инициализируем структуру sockaddr_in с параметрами для

// сокета

Q. sin_family := AF_INET;

Q. sin_port := htons(2006); // номер порта = 2006

// укажем для приложения укажем локальный сетевой адрес

Q. sin_addr. s_addr := inet_addr(‘127.0.0.1’);

// Выполняем привязку сокета

Result := bind(sock, Q, SIZE);

if Result = SOCKET_ERROR then

begin

{обработка ошибки}

WSACleanup();

Result := -1;

Exit;

end;

// Организуем канал связи

Result := listen(sock, SOMAXCONN);

if Result = SOCKET_ERROR then

begin

{обработка ошибки}

WSACleanup();

Result := -1;

Exit;

end;

// Переходим к ожиданию подключения клиента

clientsock := accept(sock,@Q,@SIZE)

// Если предыдущая команда выполнится успешно, то в переменную

// Q будет помещена информация о сетевом адресе подключившегося

// клиента

...

end;

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

Задание к лабораторной работе

Ознакомиться с технологией сетевого взаимодействия приложений посредством технологии WinSock. Изучить основные функции интерфейса сокетов. При необходимости использовать документацию MSDN (Microsoft Developer Network, http://msdn. /library/default. asp). Написать Windows-приложение, реализующее задание своего варианта. Программа должная удовлетворять следующим требованиям:
    Приложение должно предоставлять возможность работы и в качестве сервера, и в качестве клиента – роль приложения задается пользователем в начале работы. Если приложение запускается в качестве сервера, то оно должно иметь возможность обслуживать клиентов по любому сетевому адресу. Если приложение запускается в качестве клиента, то сетевой адрес соответствующего сервера должен указываться пользователем. В начале работы приложения пользователь в любом случае указывает номер порта для сетевого взаимодействия. Программа должна проверять корректность введенного параметра. Все действия по организации соединения должны проверяться на возникновение ошибок. При возникновении ошибки необходимо оповестить об этом пользователя и корректно завершить работу приложения.
Отладить написанное приложение на локальной машине и проверить его работоспособность в локальной сети ФПМИ. В отчете к лабораторной работе отразить ход выполнения работы, привести текст основных программных функций, изложить сделанные выводы.

Варианты заданий

1.  Двусторонний обмен сообщениями (чат). При установке соединения приложение, которое работает в качестве сервера, должно выводить пользователю информацию о сетевом адресе подключившегося клиента. Для этих целей необходимо использовать функцию inet_ntoa(struct in_addr in), которая преобразует значение заданного адреса из универсального сетевого формата в стандартный строковый формат, принятый в Internet.

2.  Игра в города. Осуществлять автоматическую проверку соответствия сообщений игроков правилам игры.

3.  Статистическая обработка текста. Приложение-сервер подсчитывает частоту появления каждого символа в полученном тексте и посылает эту информацию клиенту.

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

5.  Двусторонний обмен сообщениями (чат). Предусмотреть передачу служебных сообщения, обозначающие изменение состояния пользователя: «занят», «отошел от компьютера», «готов общаться» и т. п.

6.  Сетевая игра «крестики-нолики».

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

Контрольные вопросы

1.  Понятие технологии Windows Sockets.

2.  Основные этапы работы с сокетами.

3.  Понятие технологии «клиент-сервер»

4.  Что такое полнодуплексное взаимодействие?

5.  Что такое динамически подключаемые библиотеки (DLL)?

6.  Что такое прикладной программный интерфейс (WinAPI)?

7.  Блокирующие и неблокирующие сокеты.