Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений
.pdf
Простейшие коммуникационные программы 101
TForm1 *Form1;
BYTE masIN[MAX],masOUT[MAX];
int i,sum=0;
//—————————————————————————————————————__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
for(i=0; i<MAX; i++)
{
masIN[i]=random(10);
sum+=masIN[i];
}
}
//—————————————————————————————————————void __fastcall TForm1::Button1Click(TObject *Sender)
{
NMUDP1->SendBuffer(masIN,MAX,MAX);
}
//—————————————————————————————————————void __fastcall TForm1::NMUDP1DataReceived(TComponent *Sender,
int NumberBytes, AnsiString FromIP, int Port)
{
NMUDP1->ReadBuffer(masOUT,NumberBytes,NumberBytes);
sum=0;
for(i=0; i<NumberBytes; i++) sum+=masOUT[i];
Memo1->Lines->Add("Сумма переданных чисел");
Memo1->Lines->Add(IntToStr(sum));
}
//—————————————————————————————————————void __fastcall TForm1::Button2Click(TObject *Sender)
{
Close();
}
//—————————————————————————————————————void __fastcall TForm1::FormCreate(TObject *Sender)
{
Memo1->Clear();
Memo1->Lines->Add("Сумма принятых чисел");
Memo1->Lines->Add(IntToStr(sum));
}
Прежде всего, подключим заголовочный файл stdlib.h, который обеспечит
нам работу с генератором случайных чисел. С помощью препроцессорной инструкции #define MAX 2048 создаем синоним выражению 2048 — символическую конструкцию MAX. Далее мы увидим, зачем это нужно. Объявляются
два глобальных массива masIN è masOUT для хранения исходных и пересланных данных. Тип массива BYTE подразумевает, что каждый элемент размещается в одном байте, почти как в типе char, íî òèï BYTE более универсален.
При инициализации формы создается массив случайных чисел в диапазоне от 0 до 9 с помощью функции random и подсчитывается сумма этих чисел.
Первая кнопка вызывает метод SendBuffer. Ранее для отправки данных мы ис-

102 Глава 3. Передача информации в сетях
пользовали посылку строки и посылку потока. Теперь разберемся с буфером.
Метод имеет первым аргументом адрес начала блока оперативной памяти.
В С++, как известно, имя массива является адресом начала массива, так что с
этим все ясно. Далее имеет место особая ситуация! В борландовской справке
(системе помощи) указывается, что метод SendBuffer имеет два аргумента, но
компилятор требует и третьего параметра. В чем тут дело, для автора остается
загадкой, но эмпирическим путем установлено, что все работает, если второй
и третий аргументы одинаковы. Здесь указывается количество пересылаемых
(и принимаемых в методе ReadBuffer) байтов — наша длина массива MAX.
Далее следует обработчик события OnReceived. Здесь, наверное, все ясно:
читаем полученный буфер, используя аргумент обработчика NumberBytes в качестве размера буфера, подсчитываем и выводим в информационное поле
сумму.
И еще одно важное замечание по протоколу UDP. По умолчанию для используемого компонента имеется ограничение на размер блока данных, передаваемых по сети. Ограничение составляет 2 килобайта. Этот лимит разумен,
поскольку области применения UDP таковы, что здесь нецелесообразно за
раз посылать большие объемы. Именно значение 2048 байтов установлено в
нашей программе в определении ÌÀÕ. Вы можете проверить то, что увеличение ÌÀÕ уже на единицу препятствует правильной передачи данных, хотя никаких ошибок компиляции, линковки или времени исполнения не будет выявлено. Для быстрого изменения размера передачи данных мы и использовали определение #define. Изменив значение ÌÀÕ в определении, мы сразу
применим новое значение ко всей программе.
Теперь продемонстрируем использование сокетов для передачи данных.
Разработаем программы, выполняющие следующую задачу. На клиентской
стороне сокетной пары имеется графический файл (картинка) image.jpg. Устанавливается соединение между клиентом и сервером. Клиент передает серверу
содержимое графического файла. Сервер принимает данные и сохраняет их в
виде того же файла в своем рабочем каталоге. При этом потребуем, чтобы в
информационных полях клиентской и серверной программ отображалось
протоколирование всех основных действий, происходящих на компьютерах.
На рисунке ниже представлены рабочие окна клиента и сервера в состоянии после выполнения передачи данных. Кнопка START на сервере приводит
его в состояние ожидания соединения с клиентом, а кнопка STOP завершает
работу сервера. В информационном поле показана последовательность всех
основных событий, возникающих на сервере. В клиентском приложении
кнопка CONNECT выполняет подключение к серверу, а кнопка SEND непосредственно передает серверу содержимое файла. Здесь также отображаются
все происшедшие события (рис. 3.6).
Для реализации сокетного соединения в Builder'е можно воспользоваться
визуальными компонентами ClientSocket è ServerSocket, которые расположены на вкладке Internet палитры компонентов. Эти компоненты необходимо
разместить на формах соответствующих проектов и добавить кнопки и информационные поля Memo, согласно рисунку. В инспекторе объектов для
клиентского сокета необходимо задать порт и адрес серверного компьютера.

Простейшие коммуникационные программы 103
Ðèñ. 3.6
В нашей разработке порт выбран с номером 6767. Адрес 127.0.0.1 позволяет
работать серверу и клиенту на одной машине. На сервере порт сокета необходимо установить в это же значение.
Посмотрим на текст клиентской программы.
#include <io.h>
#include <stdio.h>
#include <fcntl.h>
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//—————————————————————————————————————#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
FILE *fil;
//—————————————————————————————————————__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Memo1->Clear();
}
//—————————————————————————————————————-

104 Глава 3. Передача информации в сетях
void __fastcall TForm1::Button1Click(TObject *Sender)
{
ClientSocket1->Active=true;
}
//—————————————————————————————————————void __fastcall TForm1::Button2Click(TObject *Sender)
{
int handle = open("image.jpg",O_RDONLY);
long fillen=filelength(handle);
close(handle);
char cc[10000];
fil=fopen("image.jpg","rb");
fread(cc,fillen,1,fil);
fclose(fil);
ClientSocket1->Socket->SendBuf(cc,fillen);
}
//—————————————————————————————————————void __fastcall TForm1::Button3Click(TObject *Sender)
{
Close();
}
//—————————————————————————————————————void __fastcall TForm1::ClientSocket1Connect(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("CONNECT");
}
//—————————————————————————————————————void __fastcall TForm1::ClientSocket1Connecting(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("CONNECTING");
}
//—————————————————————————————————————void __fastcall TForm1::ClientSocket1Disconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("DISCONNECT");
}
//—————————————————————————————————————void __fastcall TForm1::ClientSocket1Error(TObject *Sender,
TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode)
{
Memo1->Lines->Add("ERROR");
}
//—————————————————————————————————————void __fastcall TForm1::ClientSocket1Read(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("READ");
}
//—————————————————————————————————————-

Простейшие коммуникационные программы 105
void __fastcall TForm1::ClientSocket1Write(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("WRITE");
}
Прежде всего, обратим внимание на несколько инструкций подключения
дополнительных заголовочных файлов io.h, stdio.h, fcntl.h. Эти заголовки необходимы для некоторых файловых операций, рассмотренных ниже. Также
для организации работы с файлами в качестве глобальной описана файловая
переменная fil.
В программе присутствует ряд функций-обработчиков для различных событий. Наверное, сами события и реакции на них системы достаточно очевидны и понятны из текста функции. Комментировать их не будем.
При инициализации формы производится очистка информационного
поля Memo. Первая кнопка Button1 устанавливает свойство клиентского сокета Active в значение true. Тем самым выполняется подключение клиента к серверу. Обработчик кнопки Button3 просто завершает работу программу.
Самые важные операции выполняются в теле функции-обработчика
кнопки Button2. Первые три строки здесь описывают действия, задачей которых определить размер файла. Функция open открывает наш файл в данном
случае для чтения с инициализацией дескриптора файла handle. Функция
filelength по дескриптору определяет длину файла, сохраняемую в переменной
fillen. Наконец, функция close закрывает файл.
Следующая задача — подготовка буфера данных, в который будет скопировано содержимое файла. В качестве такого буфера мы возьмет массив символьного типа ññ с длиной, заведомо превышающей размер файла. Теперь открываем файл функцией fopen. Подчеркнем, что это уже другая операция, не
идентичная только что использованной функции open. Второй аргумент функции fopen — «rb» означает, что файл открывается для чтения как бинарный
(двоичный). Операция fread копирует заданное количество байтов fillen из
файла, заданного файловой переменной fil в буфер с начальным адресом ññ.
Напомним, что имя массива является указателем (адресом) его начала. Далее
файл закрывается. Последнее действие в функции-обработчике — отправка
буфера на сервер. Как можно видеть, вызывается промежуточное свойство
Socket клиентского сокета и его метод SendBuf, имеющий аргументами адрес
начала буфера и число байтов, подлежащих передаче.
Если все проходит «штатно», то есть если соединение установлено и сервер готов к приему данных, то происходит акт отсылки информации по сети.
В случае различных ошибок возникают события, реакции на которые мы предусмотрели.
Листинг серверного приложения представлен далее.
#include <stdio.h>
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"

106 Глава 3. Передача информации в сетях
//—————————————————————————————————————#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
FILE *fil;
//—————————————————————————————————————__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
Memo1->Clear();
}
//—————————————————————————————————————void __fastcall TForm1::Button1Click(TObject *Sender)
{
ServerSocket1->Active=true;
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1Accept(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("ACCEPT");
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1ClientConnect(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("CLIENT CONNECT");
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1ClientDisconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("CLIENT DISCONNECT");
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1ClientError(TObject *Sender,
TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode)
{
Memo1->Lines->Add("CLIENT ERROR");
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1ClientWrite(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("CLIENT WRITE");
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1ClientRead(TObject *Sender,
TCustomWinSocket *Socket)
{
char ccc[10000]; long RL=Socket->ReceiveLength();
Socket->ReceiveBuf(ccc,RL);
fil=fopen("image1.jpg","wb");

Программирование сокетов 107
fwrite(ccc,RL,1,fil);
fclose(fil);
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1GetSocket(TObject *Sender, int Socket,
TServerClientWinSocket *&ClientSocket)
{
Memo1->Lines->Add("GET SOCKET");
}
//—————————————————————————————————————void __fastcall TForm1::ServerSocket1Listen(TObject *Sender,
TCustomWinSocket *Socket)
{
Memo1->Lines->Add("LISTEN");
}
//—————————————————————————————————————void __fastcall TForm1::Button2Click(TObject *Sender)
{
Close();
}
После запуска программы необходимо перевести сервер в режим прослушивания заявок на соединение, поступающих от клиентов. Обработчик первой кнопки ServerSocket1->Active=true; как раз это и делает. Посмотрев на рисунок, можно убедиться, что установка свойства Active в значение true порождает целую цепочку событий, отображенных в информационном поле. Теперь,
после подключения клиента, произойдет событие OnClientRead — чтение данных, поступивших от клиента. Обработчик этого события организует символьный массив достаточной длины с именем ñññ. Через метод ReceiveLength() переменная RL у нас получает количество байтов, переданных клиентом. В нашем случае это длина пересланного файла. Легко понять, что функция
ReceiveBuf считывает необходимое количество байтов из сокета и помещает их
в буфер, который здесь задается объявленным символьным массивом. Далее
содержимое буфера записывается в файл, и мы на нашей серверной машине
имеем копию файла, размещенного на клиентском компьютере.
Применения сокетных соединений могут быть самыми разнообразными.
Мы продемонстрировали передачу данных в виде буфера. Сокеты использованных типов могут производить прием и передачу также символьных строк и
потоков. Кроме того, сами типы сокетов весьма разнообразны — существует
целая иерархия классов, соответствующих различным сокетам.
Программирование сокетов
С сокетами мы уже кратко познакомились в предыдущем разделе, однако
знакомство это было пока слишком поверхностным для столь важного элемента коммуникационных систем. Использование сокетов на уровне визуальных компонентов, как мы это делали в предыдущих разделах, явно не доста-

108 Глава 3. Передача информации в сетях
точно для реализации разнообразных особенностей этих объектов. В данном
разделе постараемся восполнить эти пробелы.
Начнем с того, что исчерпывающего определения понятия сокет, по-видимому, не существует. В этой связи ограничимся тем, что будем называть сокетом программируемый инструмент для передачи информации между двумя или
более приложениями, запущенными на одном или нескольких компьютерах,
связанных между собой некоторой средой передачи данных. Транспортировка
информации сокетами производится по протоколам IP, то есть адресация корреспондентов осуществляется посредством IP-адресов. Поскольку обмен данными производится между приложениями, вторым атрибутом сокетного контакта является порт программы. Что же касается среды передачи, то ею может
быть локальная сеть, Интернет, модемный канал связи по телефонным линиям, широкополосная линия или последовательный или параллельный кабель.
По характеру работы в программе сокеты делятся на блокирующие и не
блокирующие. Существует близкое, но не в полной мере эквивалентное деление на синхронные и асинхронные сокеты. Блокирующие сокеты приостанавливают выполнение программы на время передачи или приема данных, а не
блокирующие производят прием и передачу в фоновом режиме. Потребность
в не блокирующих сокетах возникает весьма редко. Во-первых, прием и передача выполняются весьма быстро. Во-вторых, и это более существенно, механизм многопотоковости (создания нитей thread) позволяет обойти проблему
блокировки, так как она распространяется только на данную нить. В этой
связи мы остановимся только на рассмотрении блокирующих сокетов, программирование которых выглядит более просто.
В настоящее время широко используются два типа сокетов, разделяемые
по виду организации соединения узлов — потоковые и дейтаграммные сокеты.
При использовании потоковых сокетов между двумя узлами устанавливается
постоянное (в данном сеансе связи) соединение. Передача данных при этом
производится по протоколу TCP, который, как мы знаем, является протоколом обязательной доставки информации адресату. Дейтаграммный сокет работает по протоколу UDP, и доставка данных адресату здесь не гарантируется.
Постоянное соединение здесь не устанавливается, а связь каждый раз инициируется по необходимости для передачи порции данных — дейтаграммы.
Переходим непосредственно к программированию сокетов. Что для этого
нужно в первую очередь? Прежде всего, в программу, предназначенную для работы с сокетами, следует подключить заголовочный файл winsock2.h. Цифра 2
здесь обозначает порядковый номер используемой библиотеки — первая библиотека разрабатывалась для версий Windows ниже 95. Сама сокетная библиотека находится в файле ws2_32.lib, и, если у вас правильно установлен Builder, то
путь к ней в системе известен и вам о дальнейшем заботиться не нужно. Для работы с сокетами библиотека должна быть подготовлена специальной функцией
WSAStartup(), которая имеет два аргумента. Первый из них имеет тип WORD и
представляет собой число, определяющее версию библиотеки. Ломать голову
над этим числом не нужно, а следует просто использовать значение 0x202. Вторым аргументом должен быть указатель на структуру типа WSADATA, предназначенную для хранения некоторой служебной информации, которая нам не

Программирование сокетов 109
понадобится. Тем не менее, это структура должна быть объявлена в программе
(без инициализации ее полей). При успешном выполнении функция
WSAStartup возвращает нулевое значение. При неудаче необходимо через систему помощи разбираться с кодом ошибки, возвращаемым функцией.
Объекты сокетов объявляются, как и другие переменные, локально или
глобально. Тип переменной-сокета называется SOCKET. Непосредственное
создание сокета производится вызовом функции socket в правой части оператора присваивания. В левой же части оператора пишется имя переменной-сокета.
Понятно, что возвращаемое значение при этом является дескриптором сокета.
Функция socket зависит от трех аргументов. Первый определяет так называемое
семейство сетевых протоколов. Для использования программы в коммуникациях по локальным сетям и через Интернет этот параметр должен иметь значение
AF_INET. Третий параметр уточняет, какой протокол из семейства следует использовать. Рекомендуется не задавать этот параметр вручную, а поручить выбор системе, для чего аргумент устанавливается равным нулю. Наконец, второй аргумент определяет тип создаваемого сокета: потоковый или дейтаграммный. В первом случае значение параметра задается системной константой
SOCK_STREAM, во втором — SOCK_DGRAM. При неудачном исходе создания сокета генерируется возвращаемое значение INVALID_SOCKET. К сожалению, непосредственно код ошибки функция socket не возвращает, и для анализа ситуации придется вызвать макрос WSAGetLastError.
Дальнейшие действия в программах, использующих сокеты, зависят от
того, работаем ли мы с дейтаграммными или потоковыми сокетами, и от того,
будет ли сокет первично вызывающим (клиентским) или первично принимающим вызов (серверным). Начнем с более простого — сокет дейтаграммный и вызывающий. В любом случае нам в программе понадобится объект
структуры sockaddr_in, которая содержит поля с информацией об адресах и
портах сокетов и многое другое. Пусть для клиентского сокета этот объект называется dest_addr. Необходимо вручную инициализировать поля структуры
sin_port è sin_addr.s_addr. Первое задает порт назначения. Второе, являющееся, как можно видеть, полем внутренней по отношению к dest_addr структуры
sin.addr, определяет IP-адрес назначения. И порт, и адрес хранятся в структуре в специализированном формате. В этой связи инициализация полей производится с помощью специальных функций. Для задания порта эта операция
может выглядеть следующим образом:
dest_addr.sin_port=htons(5555);
Функция htons принимает целый (без знака) аргумент и преобразует его в
нужный вид. Для задания IP-адреса используется функция inet_addr, которая
имеет своим аргументом строку с адресом и также конвертирует ее в необходимый формат, как это показано ниже:
dest_addr.sin_addr.s_addr=inet_addr("127.0.0.1");
Остальные поля структуры вручную заполнять не нужно, по крайней
мере, в простейших программах.

110 Глава 3. Передача информации в сетях
Теперь можно выполнить пересылку данных на сервер. Для UDP-сокетов
отправку информации корреспонденту рекомендуется производить с помощью функции sendto. Первым аргументом функции является имя созданного
сокета. Второй аргумент задает адрес строки, подлежащей пересылке. Разумеется, пересылать можно не только текстовую строку, но и любой фрагмент в
оперативной памяти, однако данные должны быть приведены к символьному
виду так, чтобы второй аргумент имел тип const char FAR *. Третий параметр
задает размер в байтах порции передаваемой информации. Следующий аргумент управляет особенностями приема и передачи данных. На данном этапе
нам нет необходимости вникать в детали такого рода, поэтому раз и навсегда
установим нулевое значение для этого параметра. Предпоследний аргумент
должен содержать указатель на адресную структуру. Необходимо отметить,
что наряду с адресной структурой sockaddr_in используется устаревшая модификация sockaddr. Вот к ее типу и должен быть приведен этот указатель. Тогда
этот аргумент может быть записан следующим образом:
(sockaddr*)&dest_addr
Ну и, наконец, последний параметр функции sendto должен содержать
размер адресной структуры, получаемый, например, с помощью оператора
sizeof.
Прием данных UDP-сокетом производится функцией recvfrom. Функция
ждет поступления информации и, получив ее, сохраняет в формате строки.
Возвращаемое значение имеет тип int и устанавливается (в случае успешного
приема, разумеется) равным числу полученных данных. Первый аргумент —
имя сокета, второй — указатель на буфер-строку, в которую запишутся полученные данные, третий параметр — размер буфера-строки. Здесь также имеются еще три аргумента, не представляющие для нас сейчас (для клиентского
сокета) интереса, которые можно со спокойной душой установить в ноль.
Для штатного выхода из программы с сокетами все их следует закрыть
функцией closesocket, а для выгрузки сокетной библиотеки используется метод WSACleanup. Вот, собственно, и весь арсенал сведений, позволяющий написать программу для дейтаграммного клиента. Теперь мы приведем текст и
прокомментируем текст такой программы. Будем считать, что клиент создается в форме консольного приложения, которое передает серверу вводимую с
клавиатуры строку и принимает от сервера и выводит на экран ответ — ту же
самую строку.
#include <vcl.h>
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
#include <windows.h>
WSADATA wsd; // Структура для инициализации
// библиотеки
sockaddr_in dest_addr; // Адресная структура
char buff[1000]; // Буфер для хранения строки текста
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
