Введение
Данная статья посвящена созданию приложений архитектуры клиент/сервер в 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;
|
Эпилог
В этой статье я написал лишь самую малость из того, что можно было бы сказать про сокеты. Может быть, когда у меня появится новый прилив сил, я дополню эту статью еще более интересным материалом.