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

Введение

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

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

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

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

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

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

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

Описание свойств и методов компонента 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):

Всем перечисленным методам соответствуют методы 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;


Эпилог

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

В ближайшем будущем планирую также статью про сокетные серверы (TServerSocket).