Delphi. Программирование сокетов в Дельфи

Автор статьи: inprise.hotbox.ru
Сайт Автора: inprise.hotbox.ru
E-mail Автора: нет
Дата публикации: 13.02.2006

Введение

Данная статья посвящена созданию приложений архитектуры клиент/сервер в Borland Delphi на основе сокетов ("sockets" - гнезда). А написал я эту статью не просто так, а потому что в последнее время этот вопрос очень многих стал интересовать. Пока что затронем лишь создание клиентской части сокетного приложения.
Впервые я познакомился с сокетами, если не ошибаюсь, год или полтора назад. Тогда стояла задача разработать прикладной протокол, который бы передавал на серверную машину (работающую на ОС Unix/Linux) запрос и получал ответ по сокетному каналу. Надо заметить, что в отличие от любых других протоколов (FTP, POP, SMTP, HTTP, и т.д.), сокеты - это база для этих протоколов. Таким образом, пользуясь сокетами, можно самому создать (симитировать) и FTP, и POP, и любой другой протокол, причем не обязательно уже созданный, а даже свой собственный!

Итак, начнем с теории. Если Вы убежденный практик (и в глаза не можете видеть всяких алгоритмов), то Вам следует пропустить этот раздел.

Алгоритм работы с сокетными протоколами

Так что же позволяют нам делать сокеты?... Да все что угодно! И в этом одно из главных достоинств этого способа обмена данными в сети. Дело в том, что при работе с сокетом Вы просто посылаете другому компьютеру последовательность символов. Так что этим методом Вы можете посылать как простые сообщения, так и целые файлы! Причем, контролировать правильность передачи Вам не нужно (как это было при работе с COM-портами)!
Ниже следует примерная схема работы с сокетами в Дельфи-приложениях:

Определение св-в Host и Port >>> Запуск Сокета (ClientSocket1.Open) >>> Авторизация >>> Посылка/прием данных >>> Закрытие Сокета

Разберем схему подробнее:
Определение св-в Host и Port - чтобы успешно установить соединение, нужно присвоить свойствам Host и Port компонента TClientSocket требуемые значения. Host - это хост-имя (например: nitro.borland.com) либо IP-адрес (например: 192.168.0.88) компьютера, с которым надо соединиться. Port - номер порта (от 1 до 65535) для установления соединения. Обычно номера портов берутся, начиная с 1001 - т.к. номера меньше 1000 могут быть заняты системными службами (например, POP - 110). Подробнее о практической части см.ниже;
Открытие сокета - после того, как Вы назначили свойствам Host и Port соответствующие значения, можно приступить непосредственно к открытию сокета (сокет здесь рассматривается как очередь, в которой содержатся символы, передающиеся от одного компьютера к другому). Для этого можно вызвать метод Open компонента TClientSocket, либо присвоить свойству Active значение True. Здесь полезно ставить обработчик исключительной ситуации на тот случай, если соединиться не удалось. Подробнее об этом можно прочитать ниже, в практической части;
Авторизация - этот пункт можно пропустить, если сервер не требует ввода каких-либо логинов и/или паролей. На этом этапе Вы посылаете серверу свой логин (имя пользователя) и пароль. Но механизм авторизации зависит уже от конкретного сервера;
Посылка/прием данных - это, собственно и есть то, для чего открывалось сокетное соединение. Протокол обмена данными также зависит от сервера;
Закрытие сокета - после всех выполненных операций необходимо закрыть сокет с помощью метода Close компонента TClientSocket (либо присвоить свойству Active значение False).

Описание свойств и методов компонента TClientSocket

Здесь мы познакомимся с основными свойствами, методами и событиями компонента TClientSocket.

Свойства
Active - показывает, открыт сокет или нет. Тип: Boolean. Соответственно, True - открыт, а False - закрыт. Это свойство доступно для записи;
Host - строка (Тип: string), указывающая на хост-имя компьютера, к которому следует подключиться;
Address - строка (Тип: string), указывающая на IP-адрес компьютера, к которому следует подключиться. В отличие от Host, здесь может содержаться лишь IP. Отличие в том, что если Вы укажете в Host символьное имя компьютера, то IP адрес, соответствующий этому имени будет запрошен у DNS;
Port - номер порта (Тип: Integer (Word)), к которому следует подключиться. Допустимые значения - от 1 до 65535;
Service - строка (Тип: string), определяющая службу (ftp, http, pop, и т.д.), к порту которой произойдет подключение. Это своеобразный справочник соответствия номеров портов различным стандартным протоколам;
ClientType - тип соединения. ctNonBlocking - асинхронная передача данных, т.е. посылать и принимать данные по сокету можно одновременно с помощью OnRead и OnWrite. ctBlocking - синхронная передача данных. События OnRead и OnWrite не работают. Этот тип соединения полезен для организации обмена данными с помощью потоков (т.е. работа с сокетом как с файлом);

Методы
Open - открытие сокета (аналогично присвоению значения True свойству Active);
Close - закрытие сокета (аналогично присвоению значения False свойству Active);

На этом все методы компонента TClientSocket исчерпываются. А Вы спросите: "А как же работать с сокетом? Как тогда пересылать данные?". Об этом Вы узнаете чуть дальше.

Практика и примеры

Легче всего (и полезней) изучать любые методы программирования на практике. Поэтому далее приведены примеры с некоторыми комментариями:

Пример 1. Простейшая сокетная программа

{В форму нужно поместить кнопку TButton и два TEdit. При нажатии на кнопку вызывается обработчик события OnClick - Button1Click. Перед этим в первый из TEdit-ов нужно ввести хост-имя, а во второй - порт удаленного компьютера. НЕ ЗАБУДЬТЕ ПОМЕСТИТЬ В ФОРМУ КОМПОНЕНТ TClientSocket!}

procedure Button1Click(Sender: TObject);
begin
{Присваиваем свойствам Host и Port нужные значения}
ClientSocket1.Host := Edit1.Text;
ClientSocket1.Port := StrToInt(Edit2.Text);
{Пытаемся открыть сокет и установить соединение}
ClientSocket1.Open;
end;

procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);
begin
{Как только произошло соединение - закрываем сокет и прерываем связь}
ClientSocket1.Close;
end;

Если Вы думаете, что данный пример программы совершенно бесполезен и не может принести никакой пользы, то глубоко ошибаетесь. Приведенный код - простейший пример сканера портов (PortScanner). Суть такой утилиты в том, чтобы проверять, включен ли указанный порт и готов ли он к приему/передаче данных. Именно на таком принципе основан PortScanner из программы NetTools Pro.

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

Пример 2. Посылка/прием текстовых сообщений по сокетам

{В форму нужно поместить две кнопки TButton и три TEdit. При нажатии на первую кнопку вызывается обработчик события OnClick - Button1Click. Перед этим в первый из TEdit-ов нужно ввести хост-имя, а во второй - порт удаленного компьютера. После установления соединения можно посылать текстовые сообщения, вводя текст в третий TEdit и нажимая вторую кнопку TButton. Чтобы отсоединиться, нужно еще раз нажать первую TButton. Еще нужно добавить TListBox, в который будем помещать принятые и отправленные сообщения. НЕ ЗАБУДЬТЕ ПОМЕСТИТЬ В ФОРМУ КОМПОНЕНТ TClientSocket!}

procedure Button1Click(Sender: TObject);
begin
{Если соединение уже установлено - прерываем его.}
if ClientSocket1.Active then begin
ClientSocket1.Close;
Exit; {...и выходим из обработчика}
end;
{Присваиваем свойствам Host и Port нужные значения}
ClientSocket1.Host := Edit1.Text;
ClientSocket1.Port := StrToInt(Edit2.Text);
{Пытаемся открыть сокет и установить соединение}
ClientSocket1.Open;
end;

procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);
begin
{Как только произошло соединение - посылаем приветствие}
Socket.SendText(′Hello!′);
ListBox1.Items.Add(′< Hello!′);
end;

procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
begin
{Если пришло сообщение - добавляем его в ListBox}
ListBox1.Items.Add(′> ′+Socket.ReceiveText);
end;

procedure Button2Click(Sender: TObject);
begin
{Нажата кнопка - посылаем текст из третьего TEdit}
ClientSocket1.Socket.SendText(Edit3.Text);
ListBox1.Items.Add(′< ′+Edit3.Text);
end;

ПРИМЕЧАНИЕ: В некоторых случаях (зависящих от сервера) нужно после каждого сообщения посылать перевод строки:
ClientSocket1.Socket.SendText(Edit3.Text+#10);

Работа с сокетным потоком

"А как еще можно работать с сокетом?", - спросите Вы. Естественно, приведенный выше метод - не самое лучшее решение. Самих методов организации работы с сокетами очень много. Я приведу лишь еще один дополнительный - работа через поток. Наверняка, многие из Вас уже имеют опыт работы, если не с потоками (stream), то с файлами - точно. Для тех, кто не знает, поток - это канал для обмена данными, работа с которым аналогична работе с обычным файлом. Нижеприведенный пример показывает, как организовать поток для работы с сокетом:

Пример 3. Поток для работы с сокетом

procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);
var c: Char;
MySocket: TWinSocketStream;
begin
{Как только произошло соединение - создаем поток и ассоциируем его с сокетом (60000 - таймаут в мсек)}
MySocket := TWinSocketStream.Create(Socket,60000);
{Оператор WaitForData ждет данных из потока указанное время в мсек (в данном примере - 100) и возвращает True, если получен хотя бы один байт данных, False - если нет никаких данных из потока.}
while not MySocket.WaitForData(100) do
Application.ProcessMessages;
{Application.ProcessMessages позволяет Windows перерисовать нужные элементы окна и дает время другим программам. Если бы этого оператора не было и данные бы довольно долго не поступали, то система бы слегка "подвисла".}
MySocket.Read(c,1);
{Оператор Read читает указанное количество байт из потока (в данном примере - 1) в указанную переменную определенного типа (в примере - в переменную c типа Char). Обратите внимание на то, что Read, в отличие от ReadBuffer, не устанавливает строгих ограничений на количество принятой информации. Т.е. Read читает не больше n байтов из потока (где n - указанное число). Эта функция возвращает количество полученных байтов данных.}
MySocket.Write(c,1);
{Оператор Write аналогичен оператору Read, за тем лишь исключением, что Write пишет данные в поток.}
MySocket.Free;
{Не забудем освободить память, выделенную под поток}
end;

ПРИМЕЧАНИЕ: Для использования потока не забудьте установить свойство ClientType в ctBlocking.

Посылка/прием сложных данных

Иногда необходимо пересылать по сети не только простые текстовые сообщения, но и сложные структуры (тип record в Паскале), или даже файлы. И тогда Вам необходимо использовать специальные операторы. Некоторые из них перечислены ниже:

Методы TClientSocket.Socket (TCustomWinSocket, TClientWinSocket):
SendBuf(var Buf; Count: Integer) - Посылка буфера через сокет. Буфером может являться любой тип, будь то структура (record), либо простой Integer. Буфер указывается параметром Buf, вторым параметром Вы должны указать размер пересылаемых данных в байтах (Count);
SendText(const S: string) - Посылка текстовой строки через сокет. Этот метод рассматривался в примере 2 (см.выше);
SendStream(AStream: TStream) - Посылка содержимого указанного потока через сокет. Пересылаемый поток должен быть открыт. Поток может быть любого типа - файловый, из ОЗУ, и т.д. Описание работы непосредственно с потоками выходит за рамки данной статьи;
Всем перечисленным методам соответствуют методы Receive... Их описание можно посмотреть в справочном файле по Дельфи (VCL help).

Авторизация на сервере

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

Пример 4. Авторизация

{В данном примере нужно добавить в форму еще два TEdit - Edit3 и Edit4 для ввода логина и пароля}

procedure ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket);
var c: Char;
MySocket: TWinSocketStream;
login,password: string;
begin
MySocket := TWinSocketStream.Create(Socket,60000);
{Добавляем к логину и паролю символ перевода строки, чтобы сервер смог отделить логин и пароль.}
login := Edit3.Text+#10;
password := Edit4.Text+#10;
MySocket.Write(login,Length(Edit3.Text)+1);
MySocket.Write(password,Length(Edit4.Text)+1);
while not MySocket.WaitForData(100) do
Application.ProcessMessages;
MySocket.Read(c,1);
{Здесь сервер посылает нам один байт, значение 1 которого соответствует подтверждению успешной авторизации, а 0 - ошибку (это лишь пример). Далее мы выполняем нужные действия (прием/пересылку данных) и закрываем поток.}
MySocket.Free;
end