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

Источник: fastcode

Введение

Данная статья посвящена созданию приложений архитектуры клиент/сервер в Borland Delphi на основе сокетов ("sockets" - гнезда ). А написал я эту статью не просто так, а потому что в последнее время этот вопрос очень многих стал интересовать. Пока что затронем лишь создание клиентской части сокетного приложения.

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

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

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

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

Ниже следует примерная схема работы с сокетами в Дельфи-приложениях

Разберем схему подробнее:

  • Определение св-в 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 исчерпываются. А Вы спросите: "А как же работать с сокетом? Как тогда пересылать данные?". Об этом Вы узнаете чуть дальше.  

   OnConnect - как следует из названия, это событие возникает при установлении соединения. Т.е. в обработчике этого события уже можно начинать авторизацию или прием/передачу данных;
   OnConnecting - возникает при установлении соединения. Отличие от OnConnect в том, что соединение еще не установлено. Обычно такие промежуточные события используются для обновления статуса;
   OnDisconnect - возникает при закрытии сокета. Причем, закрытия как из Вашей программы, так и со строноны удаленного компьютера (либо из-за сбоя);
   OnError - продолжает грустную тему предыдущего события :). Возникает при ошибке в работе сокета. Следует отметить, что это событие не поможет Вам отловить ошибку в момент открытия сокета (Open). Для того, чтобы избежать выдачи виндозного сообщения об ошибке, надо заключить операторы открытия сокета в блок try..except (обработка исключительных ситуаций);
   OnLookup - возникает при попытке получения от DNS IP-адреса указанного хоста;
   OnRead - возникает, когда удаленный компьютер послал Вам какие-либо данные. При возникновении этого события возможна обработка данных;
   OnWrite - возникает, когда Вам разрешена запись данных в сокет.
 

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

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

Пример 1. Простейшая сокетная программа
{... Здесь идет заголовок файла и определение формы TForm1 и ее экземпляра Form1}

{В форму нужно поместить кнопку 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. Посылка/прием текстовых сообщений по сокетам
{... Здесь идет заголовок файла и определение формы TForm1 и ее экземпляра Form1}

{В форму нужно поместить две кнопки 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;

Эпилог

В этой статье я написал лишь самую малость из того, что можно было бы сказать про сокеты. Может быть, когда у меня появится новый прилив сил, я дополню эту статью еще более интересным материалом.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=2428