Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Программирование сокетов 111
int n; // Длина принятой строки
void main(void)
{
WSAStartup(0x202,&wsd); // Инициализация сокетной библиотеки
SOCKET my_sock=socket(AF_INET, SOCK_DGRAM, 0); // Создание сокета
//Заполнение полей структуры адреса
dest_addr.sin_port=htons(5555); // Задание порта сервера
dest_addr.sin_addr.s_addr=inet_addr("127.0.0.1"); //Задание IP-адреса
// сервера
// Бесконечный цикл
while(1)
{
// Чтение сообщения с клавиатуры
printf("To break print quit\nPrint thr String and press ENTER: ");
fgets(buff,sizeof(buff)-1,stdin);
if (!strcmp(buff,"quit\n")) break; //Прерывание цикла по условию
//Посылка строки на сервер
sendto(my_sock,buff,strlen(buff),0,(sockaddr*)&dest_addr,sizeof(dest_addr));
// Прием строки с сервера
n=recvfrom(my_sock,buff,sizeof(buff)-1,0,NULL,NULL);
buff[n]=0;
// Вывод на экран принятой строки
printf("Returning Sring: %s",&buff[0]);
}
closesocket(my_sock);
WSACleanup();
}
Итак, остановимся на некоторых моментах, требующих пояснения. Бу­фер-строку buff задаем достаточно длинной — 1000 символов хватит на все. Порт в принципе можно задать произвольным. Следует только позаботиться о том, чтобы во время работы программы не произошло обращение к порту из другого приложения. Выберем порт 5555. IP-адрес 127.0.0.1 — это адрес ло­кальной машины, не подключенной к сети. Будем считать, что опробовать программу начнем в связке с сервером, запущенным на том же компьютере.
В цикле в первую очередь на экран выводится строчка, приглашающая пользователя ввести с клавиатуры текст, передаваемый на сервер. Здесь же го­ворится, что для окончания работы необходимо ввести слово quit. Далее чита­ется строка с клавиатуры. Если она и есть quit, то цикл принудительно преры­вается, выполняются два последних выражения — закрытие того, что должно быть закрыто. Если строка отлична от quit, то она отсылается серверу. После этого цикл ждет прихода ответной строки, и, когда это происходит, срабаты­вает функция чтения из сокета и принятый текст выводится на экран. Заме­тим, что символьный массив становится полноценной строкой, когда его по следний символ будет нулем, о чем мы и позаботились.
-
112 Глава 3. Передача информации в сетях
Теперь обратимся к программе сервера. Назначение сервера будет очень простым. Он должен принимать строки от клиента и отсылать их обратно. Сервер также будет оформлен в виде консольного приложения, в окне кото­рого вообще ничего не отображается. Текст главного модуля программы пред­ставлен ниже.
#include <vcl.h> #include <stdio.h> #include <winsock2.h>
WSADATA wsd; sockaddr_in local_addr,client_addr; int client_addr_size = sizeof(client_addr); int insize;
void main(void)
{
char buff[1000]; WSAStartup(0x202,&wsd); SOCKET my_sock; my_sock=socket(AF_INET,SOCK_DGRAM,0); local_addr.sin_addr.s_addr=INADDR_ANY; // Сокет работает с любым адресом local_addr.sin_port=htons(5555);
//Привязка локального адреса к сокету
bind(my_sock,(sockaddr *) &local_addr,sizeof(local_addr)); while(1)
{
insize=recvfrom(my_sock,buff,sizeof(buff)-1,0,
(sockaddr *) &client_addr, &client_addr_size); buff[insize]=0; sendto(my_sock,buff,insize,0,(sockaddr *)&client_addr,
sizeof(client_addr));
}
}
Нам понадобится две адресные структуры. Одна — для локального адреса
сервера, вторая — для адреса клиента. Необходима также переменная для хра­нения размера последней структуры. После создания сокета заполняются два поля адресной структуры. Порт идентифицируется как и в программе клиен­та. В качестве определителя локального IP-адреса мы используем системную переменную INADDR_ANY. Это означает, что сокет готов принимать инфор­мацию с любого допустимого локального адреса. Для тестирования программ на одном компьютере здесь, конечно, можно было бы организовать адрес
127.0.0.1. Для первично принимающего серверного сокета (в отличие от первично
передающего, клиентского) совершенно необходимо сделать связывание этого сокета с локальным адресом. Выполняется привязка функцией bind, синтак­сис которой понятен из текста программы.
В цикле, в противоположность клиенту, сначала производится прием
строки, а затем ее отсылка обратно. Первой вызывается функция recvfrom. Âîò
Программирование сокетов 113
тут необходимо вернуться к двум ее последним аргументам, которые мы оста­вили без комментариев в клиентской части. Наряду с приемом самой дейта­граммы, функция recvfrom также получает информацию об адресе передающе­го сокета, то есть адресе клиента. Эта информация заносится в структуру, ад­рес которой указывается предпоследним аргументом. Вот зачем нам понадобилось client_addr. Последний аргумент — указатель на переменную с размером структуры. Получив строку, нужно добавить в ее конец ноль и от­править обратно. Для отправки воспользуемся функцией sendto с только что установленным клиентским адресом. И это все о нем — о сервере.
Работа с потоковыми (TCP) сокетами организуется несколько иначе. Это
связано с тем, что, как мы уже говорили, потоковые сокеты устанавливают постоянное соединение, а дейтаграммные — временное, «одноразовое». При создании TCP-сокета вторым аргументом функции socket должно быть значе­ние SOCK_STREAM. Созданный сокет-клиент должен в первую очередь орга­низовать соединение функцией connect. Первым аргументом здесь будет имя сокета-клиента, инициирующего соединение. Второй параметр — адрес ад­ресной структуры, относящейся к вызываемому сокет-серверу. Последним же аргументом является размер этой структуры. Функция пытается установить постоянное соединение и ожидает подтверждения соединения со стороны сервера.
После успешного соединения сокет может выполнять операции передачи
и приема данных функциями send è recv соответственно. Эти методы практи­чески эквивалентны sendto è recvfrom, применяемым дейтаграммными сокета­ми. Несущественные отличия в синтаксисе обусловлены тем, что для ТСР-со­единения в функциях приема и передачи нет нужды указывать явно адреса корреспондентов. Действительно, ведь соединение уже установлено.
Еще большие различия имеют место при организации сервера. На сер-
верной стороне сначала создается так называемый слушающий сокет. Его на­значение заключается в приеме заявок на соединение от клиентов, посылае­мых функцией connect. Включение в работу слушающего сокета выполняется вызовом функции listen с двумя аргументами. Первый содержит дескриптор сокета, а второй — длину очереди соединений. Что это за очередь? Сервер может одновременно обслуживать несколько соединений. Так вот длина оче­реди — это и есть максимально разрешенное количество одновременно об­служиваемых связей.
Для подтверждения согласия на создание соединения серверная програм-
ма должна выполнить функцию accept. При этом клиенту отсылается опове­щение подтверждения, а accept производит еще одно важнейшее действие — создает новый, так сказать, рабочий серверный сокет. Далее операции приема и передачи на сервере будут производиться уже не слушающим, а новым ра­бочим сокетом. Рабочий сокет должен быть объявлен заранее, а его инициа­лизация производится в операторе присваивания, в левой части которого сто­ит имя сокета, а в правой — сама функция accept. У метода имеются три аргу­мента. Первый из них — имя слушающего сокета, второй — указатель на адресную структуру, в которую автоматически заносится информация о под ключаемом клиенте, третий — размер структуры.
-
114 Глава 3. Передача информации в сетях
Проиллюстрируем использование потоковых сокетов более сложным при-
мером. Разработаем клиент-серверную систему, в которой клиент устанавли­вает соединение с сервером и в цикле передает ему графический файл (кар­тинку). Сервер принимает графический файл и в цикле отображает картинку на экране. Работа системы может быть прервана как на клиентской, так и на серверной стороне. Может ли практически потребоваться подобная кли­ент-серверная система? Да, но об этом немного позже...
Начнем с клиента. На форму проекта необходимо установить две кнопки.
START включает процесс циклической передачи, STOP прерывает цикл и за­вершает программу. Рассмотрим текст основного модуля программы.
#include <fcntl.h>
#include <stdio.h>
#include <io.h>
#include <vcl.h>
#include <winsock2.h>
#include <windows.h>
#define PORT 666
#define SERVERADDR "127.0.0.1"
#include "Unit1.h"
#pragma resource "*.dfm"
TForm1 *Form1;
FILE *fil;
SOCKET my_sock;
sockaddr_in dest_addr;
BOOL KEY=true;
WSADATA WSA;
int handle;
long fillen;
char cc[16000];
char buf1[32];
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
WSAStartup(0x202,&WSA);
my_sock=socket(AF_INET,SOCK_STREAM,0);
dest_addr.sin_family=AF_INET;
dest_addr.sin_port=htons(PORT);
dest_addr.sin_addr.s_addr=inet_addr(SERVERADDR);
}
void __fastcall TForm1::Button1Click(TObject *Sender)
{
KEY = false;
closesocket(my_sock);
WSACleanup();
Close();
}
Программирование сокетов 115
void __fastcall TForm1::Button2Click(TObject *Sender) {
connect(my_sock,(sockaddr *)&dest_addr, sizeof(dest_addr)); send(my_sock,"START",5,0); while(KEY&&recv(my_sock,buf1,sizeof(buf1),0)>0) {
handle = open("image.jpg",O_RDONLY); fillen=filelength(handle); close(handle); fil=fopen("image.jpg","rb"); fread(cc,fillen,1,fil); fclose(fil); send(my_sock,cc,fillen,0); Application->ProcessMessages();
}
}
Исключительно для разнообразия мы задали порт и IP-адрес с помощью
препроцессорных директив #define так, что значение 666 и строка «127.0.0.1» в программе заменяются псевдонимами PORT è SERVERADDR. Предваритель­ные действия — подключение сокетной библиотеки, создание сокета и запол­нение необходимых полей адресной структуры мы поместили непосредствен­но в конструктор формы.
В обработчике нажатия кнопки START (Button2) сначала запускается
функция установки соединения connect. После получения от сервера подтвер­ждения соединения на него отсылается строка START — сигнал к тому, чтобы сервер приготовился к приему файла. Спроектируем сервер так, чтобы его ра­бочий сокет после получения сигнала послал клиенту сообщение, разрешаю­щее передачу файла. Итак, в клиентской программе запускается цикл, в кото­ром по получении сообщения (когда функция recv в условии возвращает не ноль) производится следующая последовательность операций.
1. Нам необходимо определить размер отсылаемого файла. Для этого нуж-
но получить дескриптор handle, возвращаемый функцией open("image.jpg", O_RDONLY), где последний аргумент предписывает открыть файл для чте­ния. Через дескриптор функция filelength(handle) возвращает в переменную fillen длину файла. Далее открытый файл закрывается.
2. Теперь функцией fopen(«image.jpg»,"rb") производится обычное откры-
тие файла с созданием файловой переменной fil. Параметр «rb» означает, что файл открывается для чтения и как бинарный — это важно, и именно так сле­дует открывать графический файл.
3. Функция fread читает из файла, указанного переменной fil, заданное
количество (fillen) байтов одной порцией в блок памяти, указанный адресом сс. Ранее мы объявили ññ как символьный массив размером 16 000 байтов.
4. Сформировав таким образом символьную строку, в которую на самом
деле просто скопировано содержимое графического файла, можно закрыть файл и выполнить отправку строки на сервер.
Как и в UDP-программе, в цикл необходимо поместить Application->
ProcessMessages();. Цикл будет работать до тех пор, пока не прервется извне
116 Глава 3. Передача информации в сетях
по переприсваиванию значения логической переменной KEY. Уточним, что каждый раз по приходу с сервера оповещения ему пересылается один и тот же файл.
Обработчик нажатия кнопки STOP выполняет присваивание переменной
KEY значения false и закрывает все, что нужно, в том числе и само прило­жение.
Переходим к серверной программе, текст которой представлен ниже. При
проектировании не забудьте кроме кнопок START è STOP разместить на фор­ме графическое пространство Image.
#include <stdio.h> #include <vcl.h> #include <winsock2.h> #include <windows.h> #define MY_PORT 666 #include "Unit1.h" #pragma resource "*.dfm"
sockaddr_in local_addr; SOCKET client_socket; // Рабочий сокет sockaddr_in client_addr; int client_addr_size=sizeof(client_addr); SOCKET mysocket; // Слушающий сокет char buff[16000]; BOOL KEY=true; WSADATA WSA; FILE *fil;
TForm1 *Form1; //—————————————————————————————————————­__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
WSAStartup(0x202,&WSA); mysocket=socket(AF_INET,SOCK_STREAM,0); local_addr.sin_family=AF_INET; local_addr.sin_port=htons(MY_PORT); local_addr.sin_addr.s_addr=0; bind(mysocket,(sockaddr *) &local_addr, sizeof(local_addr));
listen(mysocket, 1); } //—————————————————————————————————————­void __fastcall TForm1::Button1Click(TObject *Sender) {
KEY=false;
closesocket(mysocket);
WSACleanup();
Close(); } //—————————————————————————————————————-
Программирование сокетов 117
void __fastcall TForm1::Button2Click(TObject *Sender) {int recsize;
client_socket=accept(mysocket,(sockaddr *)&client_addr, &client_addr_size);
while(((recsize=recv(client_socket,buff,sizeof(buff),0))>0)&&KEY)
{
if(recsize>12) {
fil=fopen("image1.jpg","wb"); fwrite(buff,recsize,1,fil); fclose(fil);
Image1->Picture->LoadFromFile("Image1.jpg"); } send(client_socket,"GO",2,0); Application->ProcessMessages();
}
}
В объявлениях глобальных переменных присутствуют объявления двух серверных сокетов — рабочего и слушающего и двух адресных структур. Сим­вольный массив buff предназначен для приема данных.
В конструктор формы мы включили, как и для клиентской программы, первичные операции. Здесь дополнительно производится связывание слу­шающего сокета с локальным адресом, как это полагается для сервера. Функ­ция listen запускает в работу слушающий сервер. Последний параметр здесь разрешает только одно соединение.
Главные действия разворачиваются в обработчике нажатия кнопки START (Button1). Целая локальная переменная recsize предназначена для хра­нения количества байтов, получаемых методом recv. Нажатие кнопки в пер­вую очередь подтверждает разрешение на соединение: функция accept создает рабочий сокет client_socket.
Далее запускается бесконечный цикл, в котором, если recv возвращает не­нулевое значение (произведен прием данных), происходит следующее:
Если принято более 12 байтов, то... Остановимся на секунду. Много байтов будет принято, если клиент послал графический файл (а он довольно большой). 12 байтов будет принято только первый раз. Вспомните, что сначала клиент по­сылает строку START с нулевым символом в конце. Итак, если принят файл, то создается дисковый файл «image1.jpg» — срабатывает функция fopen("image1.jpg", "wb"). Последний аргумент означает, что файл создается заново как бинарный. Функция fwrite копирует в созданный файл содержимое буфера buff, заполнен­ное при получении данных сокетом. Далее файл закрывается.
Строка Image1->Picture->LoadFromFile("Image1.jpg");, по-видимому, по­нятна. Здесь производится отображение принятой картинки в графическом пространстве. Сразу отметим, что это не самый лучший способ отображения графики, но, пожалуй, самый компактный по синтаксису. Поскольку качест­венное отображение графики является для нас здесь отнюдь не главной зада­чей, ограничимся тем, что представлено. Итак, полученный файл сервер у себя показывает каждый раз в цикле, как только он пришел от клиента.
118 Глава 3. Передача информации в сетях
В любом случае в цикле производится отсылка клиенту уведомления «GO» о том, что сервер с предыдущей посылкой «расправился» и готов обра­ботать с новой. Это — то самое оповещение, о котором мы говорили, разби­рая работу клиента. Как и везде ранее, в цикл включен метод Application-> ProcessMessages(). Ну а с кнопкой STOP, надеемся, все ясно.
Запустив на одной машине и клиента и сервера, вы увидите в окне по­следнего мелькание картинки файла image.jpg. Вы увидите именно мелькание. В этом и заключается недостаток используемого метода отображения. Однако, если закрыть программу клиента, то мелькание остановится, и вы увидите примерно то, что показано на рис. 3.7.
Ðèñ. 3.7
Так имеет ли какое-то практическое приложение подобная система? Не­посредственно в таком виде, наверное, нет. Но представьте себе, что вы по­сылаете на сервер не один и тот же файл, а картину, непрерывно снимаемую подключенной к компьютеру видеокамерой. Тогда у вас будет организовано некое подобие видеотелефона. А если учесть, что систему нетрудно распро­странить на разнесенные компьютеры, то становится понятным, что пред­ставленная разработка является первым шагом к созданию интересной теле­коммуникационной системы. Но это уже совсем другая история, о которой мы поговорим позже.
Коммуникации по телефонным линиям
Несмотря на грандиозное проникновение Интернета с его разветвленной сетевой структурой во все коммуникационные системы, по-прежнему акту альными являются и коммуникации по телефонным каналам связи. Более
-
Коммуникации по телефонным линиям 119
того, и само подключение к Интернету «бытовых» пользователей во многих случаях осуществляется именно по телефонным линиям. Средства интеграции телефонных каналов связи с компьютерными сетями называются системой удаленного доступа (RAS). Разумеется, для реализации удаленного доступа разработано большое количество различных программ, которые с успехом мо­гут быть использованы пользователями для решения самых разнообразных программ, но в контексте нашей книги выглядит естественным попытаться запрограммировать такие средства и своими руками. Итак, в данном разделе мы разработаем программу, которая позволит двум компьютерам, снабжен­ным модемами, осуществлять связь между собой. Подчеркнем, что Интернет при этом никак не используется, что избавляет пользователей от оплаты тра­фика, но не избавляет от абонентской или повременной оплаты телефонных разговоров.
Упомянутые готовые программы представляют собой, прежде всего, так называемые
«звонилки» — приложения, осуществляющие дозвон по модему до удаленного компьютера. «Звонилки» обычно используют подготовленные средствами Windows соединения. В частности, в Windows XP для создания соединения необходимо открыть панель управления, перейти в раздел сете­вых подключений и там с помощью мастера новых соединений создать соот­ветствующий модуль соединения по телефонной линии. Все созданные таким образом соединения объединяются в так называемую телефонную Книгу Windows. Задачей программы дозвона является выбор из книги необходимого соединения и выполнение непосредственно подключения.
Мы поставим задачу существенно шире. Разработаем программу, которая позволяет самостоятельно создавать новое соединение, редактировать свойст­ва существующих соединений, вплоть до их удаления из телефонной книги, осуществлять дозвон и отображать параметры выполненного подключения. Кроме того, дополним программу полезной функцией — возможностью пере­сылки файла на удаленный компьютер.
Сначала рассмотрим практическое использование программы. Интерфейс приложения иллюстрирует приведенный ниже рис. 3.8.
Очевидно, первое, что предстоит сделать, работая с приложением, это создать новое соединение. После нажатия кнопки Создать соединение начина­ет работать стандартный мастер Windows, который проведет вас по всем необ­ходимым этапам. В частности, придется выбрать имя соединения, задать но­мер телефона, пароль и прочее. В результате в телефонную книгу будет поме­щен соответствующий модуль.
Теперь соединение можно вызвать, выбрав его из списка в компоненте Имя соединения. На этом этапе выбранное соединение можно удалить из теле­фонной книги с помощью кнопки Удалить, изменить его свойства кнопкой Редакт св-ва, после чего следует обновить телефонную книгу, используя кнопку Обновить. При подключении к программе соединения основные его свойства будут отображены в полях редактирования левой средней части окна, а сам процесс подключения выдаст ряд сообщений в информационное поле в правой части окна.
120 Глава 3. Передача информации в сетях
Ðèñ. 3.8
На удаленном компьютере нужно также подготовить новое соединение, которое в Windows называется входящим. Это можно сделать также из панели управления и раздела сетевых соединений, но можно воспользоваться и на­шей программой, которая подключит необходимый мастер.
Что же, можно приступать к вызову удаленного компьютера. На нем в этот момент уже должна быть запущена аналогичная программа в пассивном режиме — следует просто запустить исполняемый файл и, ничего не делая в окне, следить за информационными сообщениями. На вызывающем конце нужно проследить за тем, чтобы имя пользователя и пароль соответствовали тому, что задано в качестве свойств входящего соединения на удаленной ма­шине — при необходимости можно оперативно (прямо в этом окне) внести изменения. Остается нажать кнопку Подключиться и ждать успешного соеди­нения.
Как можно видеть из рисунка, наша программа оперирует с некими IP-адресами. Действительно, приложение построено на использовании прото кола TCP/IP. В зависимости от настройки соединений IP-адреса могут быть
-