При разработке приложений для клиента и сервера для обмена структурами данных или пакетами используются сокеты. Сокет – это абстрактный объект для обозначения одного из концов сетевого соединения. Он предназначен для создания механизма обмена данными. Реализация сокетов осуществляется в API WinSock.
В версии 1.1 WinSock любого поставщика имеется библиотека WSOCK32.DLL (или winsock .dll для 16-разрядных операционных систем), позволяющая реализовать программный интерфейс WinSock. Интерфейс версии 2 в системе Windows поддерживается одной динамической библиотекой WS2_32. dll, которая для обслуживания различных сетей может использовать протоколы и системы распознавания имен различных разработчиков. Библиотека WS2_32.dll поддерживает как функции WinSock 1.1, так и ряд дополнительных функций, впервые появившихся в спецификации WinSock 2. Данную библиотеку необходимо подключить к проекту, выполненному в VC++: Project –>Settings – вкладка Links – к списку подключаемых библиотек через пробел добавляем ws2_32.lib .
В тексте программы этот интерфейс разработки приложений подключается с помощью директивы #include:
#include <winsock2.h>
Кроме того, подключим уже известные заголовочные файлы :
#include <iostream.h>
#include <stdio.h>
#include <stdlib.h>
Для того чтобы можно было использовать WinSock API, он должен быть инициализирован. Это делается с помощью функции WSAStartup(wVersionRequested,&wsaData ).
Первый параметр функции WSAStartup ( ) — это значение типа word, которое определяет максимальный номер версии WinSock, доступный приложению. Первая цифра версии находится в младшем байте, вторая – в старшем.
Функция WSAStartup ( ) возвращает значение wsasysnotready, если динамическая библиотека поддержки WinSock или соответствующая подсистема сети не инициализирована, инициализирована некорректно или не найдена. Кроме того, с помощью этой функции приложение сообщает системе версию WinSock, которая должна использоваться. Как правило, при вызове функции WSAStartup ( ) необходимо указывать максимальный допустимый номер версии. Если он меньше, чем версии, поддерживаемые данной динамической библиотекой, функция WSAStartup ( ) возвратит значение wsavernotsupported.
Второй параметр – структура wsaData – содержит номер версии, которая должна использоваться (поле wVersion), максимальный номер версии, поддерживаемый данной библиотекой (поле wHighVersion), текстовые строки с описанием реализации WinSock, максимальное число сокетов, доступных процессу и максимально допустимый размер дейтаграмм.
В нашей программе это описывается так:
int main(){
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested=MAKEWORD(2,2);
WSAStartup(wVersionRequested,&wsaData);
В данной работе нас интересуют сокеты потоков, которые позволяют гарантировать бесперебойную доставку данных в нужном порядке и без дублирования. В TCP/IP-реализации WinSock сокеты потоков используют протокол TCP (Transmission Protocol). Сокеты потоков особенно полезны для пересылки больших объемов данных без потерь и нарушения порядка. Кроме того, при закрытии соединения приложение получат извещение об этом событии.
Для создания сокета используется функция socket(domain,type,protocol). Она принимает три параметра: домен, тип сокета и протокол. Домен – это абстракция, подразумевающая конкретную структуру адресации и протоколы, определяющие типы сокетов внутри домена. Примерами коммуникационных доменов могут быть: UNIX домен, Internet домен, и т.д. ВInternet домене сокет - это комбинация IP адреса и номера порта, которая однозначно определяет отдельный сетевой процесс во всей глобальной сети Internet. Два сокета, один для хоста-получателя, другой для хоста-отправителя, определяют соединение для протоколов, ориентированных на установление связи, таких, как TCP.
Вызов функции socket( ) выглядит следующим образом:
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
Первый параметр означает, что с этим сокетом будут использоваться адреса Internet; следующие два аргумента задают тип создаваемого сокета и протокол обмена данными через него. В приведенном примере создается сокет потока, использующий протокол TCP.
Если третий параметр функции socket( ) сделать равным нулю, протокол будет выбран автоматически в зависимости, от семейства адресов и типа сокета. Можно явно указать константы:
IPPROTO_UDP – протокол UDP (лаб. раб. №2),
IPPROTO_TCP – протокол TCP/IP.
Если функция socket( ) выполняется успешно, она возвращает дескриптор нового сокета. Если же ее работа завершается аварийно, возвращается значение 0, и для получения подробной информации об ( ошибке необходимо вызвать функцию WSAGetLastError ( ).
Для связывания конкретного адреса с сокетом используется функция bind (s, addr, addrlen). В нее передается дескриптор сокета, указатель на структуру адреса и длина этой структуры. Дескриптор сокета – это значение, которое возвращает функция socket( ). Структура адреса –это структура типа sockaddr_in.
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(1280);
local.sin_addr.s_addr=htonl(INADDR_ANY);
int c=bind(s,(struct sockaddr*)&local,sizeof(local));
В поле sin_addr структуры sockaddr_in хранится физический IP-адрес компьютера в формате стр туры in_addr, описанной в заголовочном файле winsock2.h. Вместо поля s_addr можно подставлять INADDR_ANY. это позволяет сокету посылать или принимать данные через любой IP-адрес данного компьютера. Обычно компьютер имеет только один IP-адрес, хотя, в принципе, на нем может быть установлено несколько сетевых адаптеров, каждый со своим IP-адресом. Если сокет должен использовать только один из них, его необходимо указать явно. Для этого нередко используется функция inet_addr(“…” ) , которая принимает в качеств аргумента ASCII-строку десятичной нотации IP-адреса с точкой и возвращает переменную типа u long, содержащую этот адрес в формате поля s_addr. Кроме нее, существует функция inet_ntoa(address), которая выполняет обратное преобразование, принимая переменную типа u_long и возвращая адрес в виде ASCII-строки.
Поле sin_family всегда имеет значение AF_INET. Поле sin_port определяет порт, который будет ассоциирован с сокетом.
Для привязки приложение может использовать любой номер порта от 1 до 65535, хотя этот диапазон обычно делится на следующие поддиапазоны:
0 – нe используется. Если передать 0 в качестве номера порта, будет автоматически выбран используемый порт с номером между 1 024 и 5 000.
1 – 255 – зарезервированы для сетевых служб: FTP, telnet, finger и т.д.
256 – 1 023 – зарезервированы для других служб общего назначения, например функций маршрутизации.
1024 – 4999 – служат для портов клиентов. Обычно сокеты приложений-клиентов используют номера портов именно из этого диапазона.
5000 – 65535. Используются для определяемых пользователем портов приложений-серверов.
Вместо простого присвоения констант полей sin_port и sin_addr использовалось преобразование типов с помощью функций htons (n ) и htonl(n) Эти функции предназначены для изменения порядка следования байтов в параметрах порта и адреса, для преобразования их в общий сетевой формат для 16-разрядных и 32-разрядных значений соответственно.
После создания сокета и привязки его к адресу необходимо каким-то образом установить соединение с клиентом. Для этого используется функция listen (s, l ) , которая помещает сокет в состояние прослушивания.
int r=listen(s,5);
Вызов этой функции инициирует ожидание запроса клиента на открытие соединения. Параметр l содержит количество запросов, которое должно поступить для того, чтобы приложение согласилось установить соединение. Например, если этот параметр равен 2 и приложение по каким-то причинам отказалось открыть соединение, третий клиент, который попытается подключиться к серверу, получит код ошибки wsaeconnrefused. Первые два запроса будут отправлены в очередь для их последующей обработки сервером.
При получении запроса клиента, открытие соединения выполняется с помощью функции accept( ) : SOCKET accept (SOCKET s, struct sockaddr FAR * addr, int FAR* addrlen)
Как обычно, в качестве первого параметра передается сокет, ожидающий запроса. Второй и третий параметры используются для получения адреса сокета клиента, который запрашивает соединение. Если соединение открывается успешно, функция accept ( ) возвращает дескриптор на новый сокет, который будет использоваться для управления новым соединением. Если произошла ошибка, функция accept ( ) возвращает код invalid_socket, и для получения более подробной информации об ошибке необходимо вызвать функцию WSAGetLastError ( ) .
Исходный сокет продолжит ожидание запросов на новые соединения, которые затем открываются снова с помощью функции accept ( ). Каждое открытое соединение управляется отдельным сокетом, дескриптор которого возвращается из этой функции.
Для выполнения задачи нам необходимо осуществлять прием и передачу данных. Ввод исходной строки выполнит клиент и передаст ее серверу, чтобы тот проанализировал ее и отослал назад клиенту количество букв «а» в этой строке.
Для приема данных через сокет потока используется функция recv ( ). Вот ее прототип: int recv (SOCKET s, char FAR* buf, int len, int flags); Параметры buf и len определяют соответственно буфер для приема данных и его длину. Параметр flags может принимать значения MSG_OOB для приема привилегированных данных или MSG_ PEEK для заполнения буфера без удаления данных из входной очереди, но, как правило, мы пишем его равным нолю.
Если во входной очереди находятся данные для сокета, функция recv ( ) возвращает количество прочитанных байтов, которое равно объему доступных данных во входной очереди и не превосходит значения len. При корректном закрытии соединения возвращается значение 0, а при аварийном – значение SOCKET_ERROR. Для определения точного кода ошибки необходимо вызвать функцию WSAGetLastError ( ) .
Пересылка данных выполняется с помощью функции send ( ) : int send (SOCKET s, const char FAR *buf, int len, int flags).
Функция send( ) принимает в качестве аргументов указатель на буфер, содержащий пересылаемые данные, и его длину, а также параметр flags. Если этот параметр равен msg_dontroute, в пересылаемый набор данных не включается информация о маршрутизации; если его значение равно msg_oob, посылается поток привилегированных (out-of-band) данных.
Объем данных, пересылаемых одним вызовом функции send( ) , не должен превышать размера пакета, максимально допустимого в данной сети. При попытке пересылки большего объема данных функция send( ) завершится аварийно, а функция WSAGetLastError( ) возвратит код ошибки WSAEMSGSIZE.
Работу с одним клиентом поместим в цикл, чтобы была возможность вводить несколько строк.
while (recv(s2,b,sizeof(b),0)!=0){
int i=0;
for (unsigned j=0;j<=strlen(b);j++)
if (b[j]=='a') i++;
_itoa(i,res,10);
Res=new char[strlen(res)+1];
strcpy(Res,res);
Res[strlen(res)]='\0';
//Посылает данные на соединенный сокет
send(s2,Res,sizeof(Res)-2,0);
}
Для завершения работы сокета его необходимо закрыть с помощью функции closesocket ( ) : int closesocket (SOCKET s). Эта функция принимает единственный аргумент — дескриптор закрываемого сокета, но ее поведение определяется также параметрами сокета, установленными с помощью функции setsockopt( ). Текущие параметры можно узнать, вызвав функцию getsockopt ( ) . Результат работы функции closesocket ( ) определяется параметрами SO_LINGER и SO_DONTLINGER.
Если параметр SO_DONTLINGER равен TRUE, функция closesocket( ) возвратит значение “немедленно”, но перед закрытием сокета будет предпринята попытка пересылки всех оставшихся данных. Обычно это называется корректным закрытием.
Если параметр SO_LINGER равен TRUE и установлена ненулевая задержка, также выполняется корректное закрытие с попыткой пересылки всех оставшихся в буфере данных, но функция closesocket( ) не возвращает значения до тех пор, пока не будут пересланы все данные или пока не истечет срок задержки. Если параметр SO_LINGER равен TRUE и задержка равна нулю, сокет закрывается немедленно, а все оставшиеся в буфере данные теряются.
Закроем сокет в нашей программе:
closesocket(s2);
}
Другие способы закрытия сокета. Если сокет больше не используется, процесс может закрыть его с помощью функции close (s), вызвав ее с соответствующим дескриптором сокета: close(s).
Если данные были ассоциированы с сокетом, обещающим доставку (сокет типа stream), система будет пытаться осуществить передачу этих данных. Тем не менее, по истечении довольно таки длительного промежутка времени, если данные все еще не доставлены, они будут отброшены. Если пользовательский процесс желает прекратить любую передачу данных, он может сделать это с помощью вызова shutdown на данном сокете для его закрытия. Вызов shutdown вызывает "моментальное" отбрасывание всех стоящих в очереди данных. Формат вызова следующий: shutdown(s, how), где how имеет одно из следующих значений:
0 - если пользователь больше не желает читать данные
1 - если данные больше не будут посылаться
2 - если данные не будут ни посылаться, ни получаться
Завершая программу, нужно прекратить работу с WinSock DLL, вызвав функцию:
WSACleanup();
}
Клиентская часть.
#include <winsock2.h>
#include <iostream.h>
#include <stdlib.h>
int main(){
WORD wVersionRequested;
WSADATA wsaData;
wVersionRequested=MAKEWORD(2,2);
WSAStartup(wVersionRequested,&wsaData);
struct sockaddr_in peer;
peer.sin_family=AF_INET;
peer.sin_port=htons(1280);
/ /т.к. клиент и сервер на одном компьютере,
/ / пишем адрес 127.0.0.1
peer.sin_addr.s_addr=inet_addr("127.0.0.1");
SOCKET s=socket(AF_INET,SOCK_STREAM,0);
connect(s,(struct sockaddr*) &peer,sizeof(peer));
char buf[255],b[255];
cout<<"Enter the string, please"<<endl;
cin.getline(buf,100,'\n');
send(s,buf,sizeof(buf),0);
if (recv(s,b,sizeof(b),0)!=0){
b[strlen(b)]='\0'; / /Удаление ненужных символов
/ / в конце строки
cout<<b<<endl;
}
closesocket(s);
WSACleanup();
return 0;
}
Клиентская часть использует функции, которые мы описывали ранее. Новая функция, которая НЕ вызывается в серверной части, – connect (s, addr, addrlen).С помощью этой функции приложение-клиент посылает запрос на открытие соединения. Параметры addr, addrlen используются для указания адреса и порта, к которому необходимо подсоединиться. Структура sockaddr, передаваемая в функцию connect ( ), должна быть идентичной структуре, передаваемой в функцию bind ( ) на сервере.
Контрольные вопросы
1. Какая технология называется межсетевым обменом (internetworking)?
2. Объясните понятие «протоколы» в контексте технологий обмена данными. Что они включают? Примеры.
3. Назовите отличия TCP/IP от других средств передачи данных.
4. Дайте определение понятию «сокет».
5. Опишите функцию, которая используется для приема данных через сокет потока (протокол TCP).
6. Какую библиотеку (и каким образом) необходимо подключить к проекту, выполненному в VC++?
7. Назовите функцию, используемую для создания сокета. Опишите ее параметры.
8. Опишите функцию, которая используется для пересылки данных через сокет потока (протокол TCP).
9. Что возвращает функция accept ( ), в том случае, если соединение открывается успешно?
10. Назовите функцию, которая используется в приложении-клиенте для посылки запроса на открытие соединения. Опишите ее параметры.
Варианты индивидуального задания
1. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает два числа серверу и одну из математических операций: «*», «/», «+», «–» ,– сервер соответственно умножает, делит, складывает либо вычитает эти два числа и ответ посылает ответ назад клиенту.
2. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает слово серверу, сервер возвращает назад в обратном порядке следования букв это слово клиенту.
3. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает два числа серверу m и n, сервер возвращает m!+n! этих чисел назад клиенту.
4. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает два слова серверу, сервер их сравнивает и возвращает «истина», если они одинаковы по количеству и порядку следования в них букв, и «ложь»– при невыполнении хотя бы одного из этих условий.
5. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает произвольный набор латинских букв серверу и получает их назад упорядоченными по алфавиту.
6. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу произвольный набор символов, сервер замещает каждый четвертый символ на «%».
7. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: сервер генерирует прогноз погоды на неделю. Клиент посылает день недели и получает соответствующий прогноз.
8. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу произвольные числа и получает назад количество чисел, кратных трем.
9. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу символьную строку, содержащую пробелы и получает назад ту же строку, но в ней между словами должен находиться только один пробел.
10. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу слово. Определить, является ли это слово палиндромом (палиндром – слово, читающееся одинаково как слева направо и справа налево).
11. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу два числа и получает назад НОД (наибольший общий делитель) этих чисел.
12. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу число от 0 до 10 и получает назад название этого числа прописью.
13. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу координаты точки Х и У в декартовой системе координат. Определить в какой координатной четверти находится данная точка.
14. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу координаты прямоугольной области и точки в декартовой системе координат. Определить, лежит ли данная точка в прямоугольной области.
15. Осуществить взаимодействие клиента и сервера на основе протокола TCP/IP. Функционирование клиента и сервера реализовать следующим образом: клиент посылает серверу шестизначный номер билета. Определить, является ли этот билет «счастливым». «Счастливым» называется такой билет, у которого сумма первых трех цифр равна сумме последних трех.