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

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

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Простейшие коммуникационные программы 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]; // Буфер для хранения строки текста