Примитивная работа с сетью в Delphi. Сервер

Введение

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

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

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

{В данном примере нужно добавить в форму еще два 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).

Доброго времени суток, уважаемые!
Вопрос у меня немного поверхностный, но важный (по крайней мере, для меня:)), больше похоже на "посоветуйте" -).

Есть задача, написать аналог HTTP(S) прокси-сервера, но с небольшой спецификой: запросы некоторых подключенных клиентов будут обрабатываться по-другому и обмен с такими клиентами будет происходить маленькими пакетами. Остальным клиентам ("не особо одаренным") должно быть также хорошо, как и с обычным прокси:). Основные требования, предъявляемые к такому серверу - быть устойчивым к капризным клиентам (это те, кто маленькими пакетами будет пуляться) и всем остальным клиентам, и держать максимально возможное (насколько позволят ОС+hardware и др.) количество одновременных соединений (например, 100:)). В то же время, сервер не должен получиться громоздким и слишком сложным.

Из арсенала имеются только стандартные средства Delphi (Indy и другие библиотеки отбрасываем).

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

Первое, что приходит в голову - TTCP, TSocket или голое API of WinSock.

С TSocket работал давно (последний раз лет 6-7 назад) - в то время очень не понравилось. Некоторые пакеты пропускались (хотя TCP считают "гарантированным"). Когда тут на форуме спросил в чем дело, мне тогда намекнули, если мне не изменяет память, на метод Nagle. Но проблему так и не решил (в те времена).

Вчера "тест-драйв" попытался устроить TCPServer`у. НаписАл на базе него простенький прокси для обработки GET запросов, который, в общем-то, даже работал, но мееееедленно. Реагировал на событие OnAccept, режим блокировки - bmThreadBlocking, читал и писАл из\в сокеты используя ReceiveBuf и SendBuf.

В общем, уважаемые, каким образом лучше организовать работу сервера (прием соединений на сервере и создание временных подключений внутри потока для получения данных с другого сервера и отдачи их обратно клиенту) и с помощью чего:
* ПисАть на голом WinSock API, потоки создавать самому, работать в блокирующем режиме
* ПисАть на голом WinSock API, работать в неблокирующем режиме, тогда, как я понимаю, потоки создавать не нужно будет
* Аналогично описанному выше, только с использванием в качестве опорной точки один из компонентов\классов (например, (T)TCP, (T)Socket)
* Применять "фишку" Delphi - режим ThreadBlocking
* что-либо другое

Заранее благодарю всех дочитавших до конца и ответивших!


DVM ( 2010-05-11 18:43 )

чем Indy то не угодил?


kernel ( 2010-05-11 18:50 )


> DVM © (11.05.10 18:43)
> чем Indy то не угодил?

Лицензией, тяжестью исправления (в том смысле, что исправлять много приходится, чтобы что-либо добавить\изменить на более низком уровне), потом, помню, были проблемы с убиванием потоков (http://сайт/view/6-1236577141/).


DVM ( 2010-05-11 19:34 )


> kernel © (11.05.10 18:50)

Если хочешь максимум контроля над сервером - делай все сам на WinSock. Я бы наверное выбрал блокирующий режим и что-то типа пула потоков.


Slym ( 2010-05-11 20:37 )

kernel © (11.05.10 18:07)
НаписАл на базе него простенький прокси для обработки GET запросов

Твоя задача сводится
1. принять заголовок у клиента, выдрать от туда host:port заказаного сервера
2. сконектиться по host:port и полностью возможно с минимальными изменениями отправить принятые в п1. от клиента данные
3. обеспечить прозрачное тунеллирование трафика клиент/сервер
4. (ОПЦИЯ) затормозить особо ретивые конекты наример тупым sleep (при буфере сокета в 8кб + sleep(1000) получим 8кб/сек максимум)


Сергей М. ( 2010-05-11 22:02 )


> пакеты пропускались (хотя TCP считают "гарантированным").
> Когда тут на форуме спросил в чем дело, мне тогда намекнули,
> если мне не изменяет память, на метод Nagle

Похоже ты уже и сам не помнишь о чем был твой вопрос.
Используется или не используется Nagle - на "пропуск пакетов" он повлиять никак не может в принципе .


kernel ( 2010-05-13 14:08 )

Благодарю всех за ответы!


> DVM © (11.05.10 19:34)

Все что нужно из контроля - общение с клиентом с заданными размерами пакетов (маленькими пакетами для вредных клиентов и большими - для остальных) с возможностью их "переработки" и подсчет отправленных\полученных данных. Все это, если я не ошибаюсь, успешно выполняют и обычные сокетообразные компоненты. Или все равно лучше применить чистый WinSock? :) Тут, наверное, больше интересует у кого из них (сок. компоненты vs WinSock API) производительность будет побольше. Или в этом плане разницы никакой нет?

> Slym © (11.05.10 20:37)

Ну как работает прокси мне известно. Делал именно так, как Вы описали.

> Сергей М. © (11.05.10 22:02)

Действительно подзабыл. Нашел те старые исходники на этих компонентах, там скорее всего была проблема в том, что в короткий промежуток времени сервер сам цеплялся к клиентам и отправлял всем клиентам команды. Для того чтобы всем все успешно отправилось, необходимо было ставить задержку около 1 сек. (по крайней мере у меня работало только не < ~1 сек.) после отсылки команды каждому ПК. Вот тут мне про Nagle скорее всего и намекнули.


DVM ( 2010-05-13 16:25 )


> kernel © (13.05.10 14:08)


> Тут, наверное, больше интересует у кого из них (сок. компоненты
> vs WinSock API) производительность будет побольше. Или
> в этом плане разницы никакой нет?

Компоненты они же на базе WinSock построены. Разницы между ними и чистым Winsock практически нет. По крайней мере это не то место, где следует искать разницу.


DVM ( 2010-05-13 16:26 )


> kernel © (13.05.10 14:08)

Другое дело, что вокруг WinSock можно написать свои простенькие обертки, больше подходящие к конкретной задаче - в этом их удобство.


kernel ( 2010-05-14 21:46 )

Спасибо за ответы, DVM !
Сделал свой класс на основе ServerSocket, все работает быстро и замечательно [тьфу-тьфу-тьфу], контроля полностью хватает.
PS: собст-но это сообщение и отправил через пробный прокси:)


kernel ( 2010-05-18 17:36 )

Всем привет! Чтобы не создавать отдельный топик, спрошу здесь (т.к. разговор связан с этой темой).

Организовал нужный мне функционал на базе TServerSocket (режим - stThreadBlocking) с созданием отдельных потоков для каждого установленного соединения. Дошел до момента, где мне необходимо делать туннелирование между [клиент] <-> ["прокси-сервер"] <-> [запрашиваемый сервер] для работы SSL. Механизм такой: клиент посылает мне запрос CONNECT (HTTP/1.0), затем после того, как я (прокси) отправляю заголовок "200 OK - соединение установлено", в цикле, пока любое из двух соединений не разорвется (клиент или TargetHost), ловлю данные в буфер от клиента и тут же передаю запрашиваемому серверу, пока от клиента не начнет приходить ноль байт. Затем то же самое делаю, но наоборот, ловлю данные в буфер от запрашиваемого сервера и тут же передаю клиенту. И это все повторяется до тех пор, пока кто-либо из них не разорвет соединение.

...
const BufSize = 128;
...
ClientStream: TWinSocketStream;
TargetConnection: TTcpClient;
Buf: array of Byte;
...
ClientStream:= TWinSocketStream.Create(ClientSocket, 60000);
...
while (КлиентИПунктНазначенияПодключены) do begin
ClientWait:= (ClientStream.WaitForData(100));
if ClientWait then begin
while (ClientSocket.Connected) and (TargetConnection.Connected) and (RecLen > 0) do begin
TargetConnection.SendBuf(Buf, RecLen);
if (not ClientStream.WaitForData(10)) then Break;
RecLen:= ClientStream.Read(Buf, BufSize);
end;
end;

ServerWait:= TargetConnection.WaitForData(100);
if ServerWait then begin
while (ClientSocket.Connected) and TargetConnection.Connected) and (RecLen > 0) do begin
ClientStream.Write(Buf, RecLen);
if (not ClientSocket.Connected) or (not TargetConnection.Connected) then Break;
if (not TargetConnection.WaitForData(10)) then Break;
RecLen:= TargetConnection.ReceiveBuf(Buf, BufSize);
end;
end;
end;

Все бы хорошо (и даже отрабатываются несколько проходов общего цикла верно), но практически сразу общий цикл замирает (точнее, не замирает, а бесконечно крутится) и наблюдается странная картина: ClientStream.WaitForData(100) в начале цикла начинает все время возвращать True, в то время как количество принятых байт (RecLen:= ClientStream.Read ... ) с этого сокета становится все время равно нулю. При этом, оба соединения не дисконнэктятся (хотя с чего бы им разрываться, если данные "недопередались").

Вопрос: Что я делаю не так? :)


kernel ( 2010-05-18 17:40 )

То есть, проще говоря, через некоторое время цикл "крутится", а данные не хочет отдавать ни один сокет:/

Я добавил сетевую поддержку. То есть создал отдельный сервер и отдельный клиент. Смысл заключается в том, что работает сервер приложения, пользователь запускается клиент и в пользователь вводит запрос: Москва Тверская 6. Затем сервер обрабатывает запрос, получает результаты поиска из Яндекс.Карт и отправляет полученную картинку клиенту, затем уже в клиенте в компоненте TMap отображается часть данной карты, которая соответствует запросу пользователя. В итоге пользователь может ее масштабировать, сохранять и так далее.

Поэтому в данной статье я хочу рассказать, как я реализовывал клиента и сервер. Это я делал с помощью TClientSocket и TServerSocket, в данной статье мы и рассмотрим подробно те методы, которые использовал я у себя, в своем проекте.

Для начала давайте посмотрим, как эти компоненты можно установить себе в IDE. Если Вы используете IDE Delphi 7, то в ней по умолчанию данные компоненты присутствуют, но они, к сожалению, не установлены, но это не проблема. Нам достаточно открыть Delphi и установить.

Для этого выполним команду Component-Install Packages… и в появившемся окне необходимо нажать на кнопку Add. После этого необходимо указать путь к файлу dclsockets70.bpl, который обычно, по умолчанию, находится в папке BIN. После этого необходимо нажать на кнопку Ок. Все, компоненты у Вас должны появиться на вкладке Internet (TClientSocket и TServerSocket).

В проекте , я начинал всю работу, с минимальной разработке сервера. Для начала установил компонент TServerSocket на форму. И по нажатию на кнопку Запустить сервер задал первоначальные настройки, для его инициализации:

Server. Port : = FormServerSetting. SpinEditPort . Value ; //указываем порт сервера Server. Active : = True ; //активируем его Server. Open ; if Server. Active then begin //выводим сообщение что сервер запущен и работает end ; …….. //выводим ошибку, если сервер не запустился

Для инициализации сервера на своей машине, я задавал лишь только свободный порт (который не занят другими приложениями) и активировал его.

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

Для того, чтобы мне получить список клиентов, которые подключаются к серверу и дальнейшей работы с ними, я установил компонент TCheckListBox на форму и на событие OnclientConnect компонента TServerSocket, написал следующий код:

procedure TFormServer. ServerClientConnect (Sender: TObject ; Socket: TCustomWinSocket) ; begin //отслеживаем подключение клиента RichEditLog. SelAttributes . Color : = clGreen; RichEditLog. SelAttributes . Style : = [ fsBold] ; CheckListClient. Items . Add (Socket. RemoteHost ) ; RichEditLog. Lines . Add ("[" + TimeToStr (Time ) + "] Client connected: " + Socket. RemoteHost ) ; //добавляем в список клиента, который подключился RichEditLog. Perform (WM_VSCROLL, SB_BOTTOM, 0) ; end ;

То есть, я в список добавляю имена тех клиентов, которые подключаются к серверу, для дальнейшего получения информации о них.

Например, можно получить подробную информацию о клиенте:

procedure TFormInfoClient. FormShow (Sender: TObject ) ; begin //выводим ифнормацию о клиенте Caption: = "Информация о клиенте: " + FormServer. CheckListClient . Items [ FormServer. CheckListClient . ItemIndex ] ; LocalName. Caption : = FormServer. Server . Socket . Connections [ FormServer. CheckListClient . ItemIndex ] . LocalHost ; LocalHost. Caption : = FormServer. Server . Socket . Connections [ FormServer. CheckListClient . ItemIndex ] . LocalAddress ; LocalPort. Caption : = IntToStr (FormServer. Server . Socket . Connections [ FormServer. CheckListClient . ItemIndex ] . LocalPort ) ; RemoteName. Caption : = FormServer. Server . Socket . Connections [ FormServer. CheckListClient . ItemIndex ] . RemoteHost ; RemoteHost. Caption : = FormServer. Server . Socket . Connections [ FormServer. CheckListClient . ItemIndex ] . RemoteAddress ; RemotePort. Caption : = IntToStr (FormServer. Server . Socket . Connections [ FormServer. CheckListClient . ItemIndex ] . RemotePort ) ; end ;

Можно получить следующие данные:

  • Локальное имя
  • Локальный адрес
  • Локальный порт
  • Удаленное имя
  • Удаленный адрес
  • Удаленный порт

Информацию я получаю о том клиента, с помощью данного кода, которого я выделил в списке компонента TCheckListBox.

Ничего сложного, как видите, нет, для того, чтобы отправить сообщение клиенту, можно воспользоваться следующим кодом:

В квадратных скобках, я указываю, какому клиенту мы будем отправлять сообщение (оно равно выделенному клиенту в компоненте TCheckListBox), в сообщение я указываю #message# — что означает, что это обычное сообщение от сервера, которое следует просто вывести в окне.

Для того чтобы получить сообщение от клиента, серверу, нам понадобится событие OnClientRead компонента TServerSocket и текстовая переменная, в которую мы будем записывать запрос, который посылает клиент.

procedure TFormServer. ServerClientRead (Sender: TObject ; Socket: TCustomWinSocket) ; var query: String ; begin //получаем запрос от клиента на карту try query: = Socket. ReceiveText ; if pos ("query" , query) <>0 then begin //запрашиваем у Яндекса или Google координаты карты, соответсвующие запросу клиента end ; //если просто сообщение от клиента, то выводим его if pos ("#message#" , query) <>0 then begin end ; ……

Из данного кода можно увидеть, что клиент может посылать как обычное сообщение серверу, так и запрос на получение карты, вида: Москва, Тверская, 6.

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

  • #message#
  • #query#

Если вначале сообщения клиента присутствует идентификатор #message#, то сервер распознает, как обычное сообщение от клиента. Если вначале сообщения присутствует идентификатор #query#, то это означает, что клиент послал запрос на получение карты.

Также клиент, в любое время может отключить от сервера, нам это также необходимо отследить, чтобы удалить его из общего списка клиентов, подключенных к серверу. Для этого выделяем компонент TServerSocket и на событие OnClientDisconnect пишем следующий код:

procedure TFormServer. ServerClientDisconnect (Sender: TObject ; Socket: TCustomWinSocket) ; var i: integer ; begin try //отслеживаем отключение клиента RichEditLog. SelAttributes . Color : = clRed; RichEditLog. SelAttributes . Style : = [ fsBold] ; for i : = 0 to Server. Socket . ActiveConnections - 1 do begin if Server. Socket . Connections [ i] . Handle = Server. Socket . Connections [ i] . Handle then begin RichEditLog. Lines . Add ("[" + TimeToStr (Time ) + "] Client disconnected: " + Socket. RemoteHost ) ; CheckListClient. Items . Delete (i) ; RichEditLog. Perform (WM_VSCROLL, SB_BOTTOM, 0) ; end ; end ; finally //-//-//-//-//-// end ; end ;

Мы проходим по всем клиентам, которые у нас есть в списке и если какого-то ненаходим, то удаляем его из компонента TCheckListBox, это означает, что клиент в своем приложении нажал на кнопку Отключиться.

выбрать меню: Component - Install Packages… - Add., далее нужно указать файл …\bin\dclsockets70.bpl.

Перейдем непосредственно к созданию проекта клиент-сервера, для начала на примере сетевого чата.

Сетевой чат на двух пользователей

Как правило, разработка любой программы начинается с определения задач, которые она должна выполнять, и определения уже на этом этапе нужных компонентов. Наша программа представляет собой чат на двоих пользователей, каждый из которых может быть как сервером, так и клиентом, значит, кидаем в форму компоненты ServerSocket и ClientSocket . Важным параметром для обоих является порт. Только при одинаковом значении свойства Port , связь между ними установится. Кинем в форму компонент Edit , чтобы оперативно изменять порт, назовем его PortEdit . Для соединения с сервером необходимо указывать IP сервера или его имя, поэтому кинем еще один Edit , назовем его HostEdit . Так же нам понадобятся еще два Edit "а для указания ника и ввода текста сообщения, назовем их NikEdit и TextEdit , соответственно. Текст принимаемых и отправляемых сообщений будет отображаться в Memo , кинем его в форму и назовем ChatMemo . Установим сразу вертикальную полосу прокрутки: ScrollBars = ssVertical , и свойство ReadOnly = True . Добавим клавиши управления Button : ServerBtn - для создания/закрытия сервера, ClientBtn - для подключения/отключения клиента к серверу, SendBtn - для отправки сообщений. Изменим Caption этих клавиш на "Создать сервер ", "Подключиться " и "Отправить ", соответственно. Последний штрих - добавим надписи Label для предания форме надлежащего вида (это по желанию).

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

Определим, что должно происходить при создании формы. Опишем процедуру OnCreate :


begin
// предложенное значения порта
PortEdit.Text:="777";
// адрес при проверке программы на одном ПК ("сам на себя")
HostEdit.Text:="127.0.0.1";
// остальные поля просто очистим
NikEdit.Clear;
TextEdit.Clear;
ChatMemo.Lines.Clear;
end;

Будем считать, что выбран режим сервера. Перевод программы в режим сервера осуществляется клавишей "Создать сервер " (ServerBtn) . Чтобы не использовать лишних клавиш для отключения этого режима или компонентов типа RadioButton , можно использовать то же свойство Tag клавиши ServerBtn , изменяя его значения и выполняя те или иные операции, если значение соответствует указанному. Вот так выглядит процедура на нажатие клавиши ServerBtn (OnClick ):

procedure TForm1.ServerBtnClick(Sender: TObject);
begin
If ServerBtn.Tag=0 then
Begin
// клавишу ClientBtn и поля HostEdit, PortEdit заблокируем
ClientBtn.Enabled:=False;
HostEdit.Enabled:=False;
PortEdit.Enabled:=False;
// запишем указанный порт в ServerSocket
ServerSocket.Port:=StrToInt(PortEdit.Text);
// запускаем сервер
ServerSocket.Active:=True;
// добавим в ChatMemo сообщение с временем создания
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Сервер создан");
// изменяем тэг
ServerBtn.Tag:=1;
// меняем надпись клавиши
ServerBtn.Caption:="Закрыть сервер";
end
else
Begin
// клавишу ClientBtn и поля HostEdit, PortEdit разблокируем
ClientBtn.Enabled:=True;
HostEdit.Enabled:=True;
PortEdit.Enabled:=True;
// закрываем сервер
ServerSocket.Active:=False;

ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Сервер закрыт.");

ServerBtn.Tag:=0;

ServerBtn.Caption:="Создать сервер";
end;
end;

Разберемся с событиями, которые должны происходить при определенном состоянии ServerSocket "а. Напишем процедуру, когда клиент подсоединился к серверу (OnClientConnect ):

procedure TForm1.ServerSocketClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение с временем подключения клиента
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Подключился клиент.");
end;

Напишем процедуру, когда клиент отключается (OnClientDisconnect ):

procedure TForm1.ServerSocketClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение с временем отключения клиента
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Клиент отключился.");
end;

Когда на сервер приходит очередное сообщение клиента, мы должны сразу же отображать его. Напишем процедуру на чтение сообщения от клиента (OnClientRead ):


Socket: TCustomWinSocket);
begin


end;

Самое главное - отправка сообщений. У нас она осуществляется нажатием клавиши "Отправить" (SendBtn ), но необходима проверка режима программы сервер или клиент. Напишем ее процедуру (OnClick ):

procedure TForm1.SendBtnClick(Sender: TObject);
begin
// проверка, в каком режиме находится программа

// отправляем сообщение с сервера (он под номером 0, поскольку один)
ServerSocket.Socket.Connections.SendText("["+TimeToStr(Time)+"] "+NikEdit.Text+": "+TextEdit.Text)
else
// отправляем сообщение с клиента
ClientSocket.Socket.SendText("["+TimeToStr(Time)+"] "+NikEdit.Text+": "+TextEdit.Text);
// отобразим сообщение в ChatMemo
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] "+NikEdit.Text+": "+TextEdit.Text);
end;

Теперь разберемся с режимом клиента. Здесь наоборот, при нажатии клавиши "Подключиться" (ClientBtn ), блокируется ServerBtn и активируется ClientSocket . Вот процедура ClientBtn (OnClick) :

procedure TForm1.ClientBtnClick(Sender: TObject);
begin
If ClientBtn.Tag=0 then
Begin
// клавишу ServerBtn и поля HostEdit, PortEdit заблокируем
ServerBtn.Enabled:=False;
HostEdit.Enabled:=False;
PortEdit.Enabled:=False;
// запишем указанный порт в ClientSocket
ClientSocket.Port:=StrToInt(PortEdit.Text);
// запишем хост и адрес (одно значение HostEdit в оба)
ClientSocket.Host:=HostEdit.Text;
ClientSocket.Address:=HostEdit.Text;
// запускаем клиента
ClientSocket.Active:=True;
// изменяем тэг
ClientBtn.Tag:=1;
// меняем надпись клавиши
ClientBtn.Caption:="Отключиться";
end
else
Begin
// клавишу ServerBtn и поля HostEdit, PortEdit разблокируем
ServerBtn.Enabled:=True;
HostEdit.Enabled:=True;
PortEdit.Enabled:=True;
// закрываем клиента
ClientSocket.Active:=False;
// выводим сообщение в ChatMemo
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Сессия закрыта.");
// возвращаем тэгу исходное значение
ClientBtn.Tag:=0;
// возвращаем исходную надпись клавиши
ClientBtn.Caption:="Подключиться";
end;
end;

Остается прописать процедуры на OnConnect , OnDisconnect , OnRead клиента ClientSocket . Сначала на чтение сообщения с сервера (OnRead ):


Socket: TCustomWinSocket);
begin
// добавим в ChatMemo пришедшее сообщение
ChatMemo.Lines.Add(Socket.ReceiveText());
end;

procedure TForm1.ClientSocketConnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение о соединении с сервером
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Подключение к серверу.");
end;

procedure TForm1.ClientSocketDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
begin
// добавим в ChatMemo сообщение о потере связи
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Сервер не найден.");
end;

Вот тот минимум, который нужно проделать. Остается внести еще некоторый ряд алгоритмов для проверки правильности ввода некоторых значений.
Возникает вопрос: а если нужно передать данные и совсем не строковые, а какой-нибудь массив? Для этого есть специальные команды. Давайте попробуем написать алгоритм отправки массива, как команды для некоторой игры.

Отправка массива данных

Воспользуемся той же формой чата, только добавим несколько компонентов чуть ниже. Пусть задача - управлять объектом типа Shape , менять тип геометрической фигуры, цвет, размеры. Поместим в форму компонент GroupBox , а в него Shape , их имена будут такими же. Для изменения типа геометрической фигуры используем список ComboBox , назовем его ShapeCBox . Сразу заполнять не будем, это сделаем в OnCreate формы. Далее понадобится такой же ComboBox для выбора цвета, и два Edit "а для указания размера фигуры (в случае с прямоугольником имеем два значения, на круг будем использовать одно первое). Назовем их ColorCBox , Value1Edit , Value2Edit , соответственно. Последним кинем в форму компонент Button , назовем его SendBufBtn , Caption изменим на "Отправить буфер ".
Немного о том, как представить вводимые данные в виде буфера данных. Нужно сразу определиться в последовательности, какое значение, за каким следует в буфере. Пусть первым будет тип фигуры, за ним цвет, а следом оба значения размера. Для этих целей следует использовать массив длиной 4 и типом Byte . Добавим в раздел var массив:

Buf: array of Byte;

С размерами фигуры все понятно, а вот для типа и цвета нужна "таблица истинности". Представим ее следующим образом:

параметр код
прямоугольник 0
круг 1
-------------------
красный 0
зеленый 1
синий 2

Этого вполне хватит для демонстрации. По желанию круг параметров можно расширить, ввести тип заливки, тип контура, смещение, или воспользоваться примером для других целей.
Пропишем заполнение списков в OnCreate формы:

procedure TForm1.FormCreate(Sender: TObject);
begin

// ...часть чата...

// заполнение списков
ShapeCBox.Items.Add("прямоугольник");
ShapeCBox.Items.Add("круг");
ColorCBox.Items.Add("красный");
ColorCBox.Items.Add("зеленый");
ColorCBox.Items.Add("синий");
end;

При нажатии клавиши "Отправить буфер " будем собирать данные с полей и формировать массив известной длины, а затем проверять на режим сервер/клиент и отправлять. Вот процедура SendBufBtn (OnClick) :

procedure TForm1.SendBufBtnClick(Sender: TObject);
begin
// соберем данные для отправки
Buf:=ShapeCBox.ItemIndex;
Buf:=ColorCBox.ItemIndex;
Buf:=StrToInt(Value1Edit.Text);
Buf:=StrToInt(Value2Edit.Text);
// проверяем режим программы
If ServerSocket.Active=True then
// отправим буфер с сервера (длина известна - 4)
ServerSocket.Socket.Connections.SendBuf(Buf,4)
else
// отправим буфер с клиента
ClientSocket.Socket.SendBuf(Buf,4);
// добавим в ChatMemo сообщение о передачи данных
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Данные переданы.");

Shape.Height:=Buf;
Shape.Width:=Buf;

If Buf>


Case Buf of
0: Shape.Brush.Color:=clRed;

2: Shape.Brush.Color:=clBlue;
end;
// изменить данные в полях
ShapeCBox.ItemIndex:=Buf;
ColorCBox.ItemIndex:=Buf;


end;

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

procedure TForm1.ServerSocketClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var
len: Byte;
begin
// добавим в ChatMemo клиентское сообщение


len:=Socket.ReceiveLength;
Socket.ReceiveBuf(Buf,len);
// применим изменения к своему Shape
Shape.Height:=Buf;
Shape.Width:=Buf;
If Buf>0 then Shape.Shape:=stCircle {круг}
else Shape.Shape:=stRectangle; {прямоуголник}
// выбор цвета по таблице истинности
Case Buf of
0: Shape.Brush.Color:=clRed;
1: Shape.Brush.Color:=clGreen;
2: Shape.Brush.Color:=clBlue;
end;
// изменить данные в полях
ShapeCBox.ItemIndex:=Buf;
ColorCBox.ItemIndex:=Buf;
Value1Edit.Text:=IntToStr(Buf);
Value2Edit.Text:=IntToStr(Buf);


end;

Осталось аналогичным образом изменить процедуру на чтение клиентом сообщения от сервера (OnRead ):

procedure TForm1.ClientSocketRead(Sender: TObject;
Socket: TCustomWinSocket);
var
len: Byte;
begin
// добавим в ChatMemo сообщение с сервера
// ChatMemo.Lines.Add(Socket.ReceiveText());
// принимаем буфер неизвестного размера
len:=Socket.ReceiveLength;
Socket.ReceiveBuf(Buf,len);
// применим изменения к своему Shape
Shape.Height:=Buf;
Shape.Width:=Buf;
If Buf>0 then Shape.Shape:=stCircle {круг}
else Shape.Shape:=stRectangle; {прямоуголник}
// выбор цвета по таблице истинности
Case Buf of
0: Shape.Brush.Color:=clRed;
1: Shape.Brush.Color:=clGreen;
2: Shape.Brush.Color:=clBlue;
end;
// изменить данные в полях
ShapeCBox.ItemIndex:=Buf;
ColorCBox.ItemIndex:=Buf;
Value1Edit.Text:=IntToStr(Buf);
Value2Edit.Text:=IntToStr(Buf);
// добавим в ChatMemo сообщение о приходе данных
ChatMemo.Lines.Add("["+TimeToStr(Time)+"] Пришли данные.");
end;

Это и все, что нужно сделать. Обратите внимание на то, что если не отключать принятие сообщения (Socket.ReceiveText() ) и попытаться одновременно принять и сообщение и буфер, то это приведет к потере данных одной из функций. Решить эти проблемы можно за счет перевода сообщения в формат буфера вот так:

For i:=1 to Length(TextEdit.Text) do
Buf:=Copy(TextEdit.Text,i,1);

Очевидно, что массив станет на одну ячейку больше и изменит свой тип на символьный или строковый. Buf при этом будет служить меткой, чем является пришедший буфер: сообщением или данными. В процедурах получения сообщений нужно сделать условие примерно так:

len:=Socket.ReceiveLength;
Socket.ReceiveBuf(Buf,len);
If Buf="t" then
Begin
… делать операцию по соединению в строку (через цикл)
end;
If Buf="c" then
Begin
… делать операцию по изменению параметров Shape
end;

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

Напишем программу, которая сможет передавать файлы через сокеты (клиент и сервер), и кроме этого другие команды, например какое-нибудь сообщение! Клиент будет принимать файлы или команды, а сервер - отсылать. Если же клиент будет всё подряд записывать в буфер, то кроме файла, в нём будут и команды, а нам нужно сделать так, чтоб файлы и команды не в коем случае не сливались! Ещё нужно учитывать, что если файл большой, то при пересылке, он разрежется на несколько пакетов, то есть файл перешлётся не в одном пакете, а в нескольких, и событие OnClientRead будет вызываться несколько раз... В этом и заключается основная проблема передачи!

Чтоб можно было отделить команды от файла, сначала пошлём клиенту примерно такую строку: "file#file.txt#16", то есть: команда + разделитель + имя файла + разделитель + размер файла.
При получении данной команды, клиент перейдёт в режим приёма файла и всё подряд будет записывать в буфер, до тех пор пока размер файла не будет равен размеру принятых данных. Таким образом клиент отделит команды от файла!

И так приступим к написанию кода:
Начнём с сервера (он будет посылать файл):

Разместите на форму следующие компоненты: TServerSocket, TButton, TEdit, TProgressBar и TStatiusBar. Расположите их как показанно на рисунке.
Установите у компонента TServerSocket, порт (port): 1001.
Установите у компонента TStatusBar, переменную SimplePanel в true.
В строке, вводится название файла для передачи, кнопка TButton, используется для передачи файла.

Сначала добавим буфер для файла в глобальные переменные:

Var Form1: TForm1; MS: TMemoryStream; // Буфер для файла

Теперь сделаем, чтоб при создании формы, открывался сокет:

Procedure TForm1.FormCreate(Sender: TObject); begin ServerSocket1.Open; // Открываем сокет end;

При завершении приложения, нужно не забыть закрыть сокет:

Procedure TForm1.FormDestroy(Sender: TObject); begin ServerSocket1.Close; // Закрываем сокет end;

При нажатии на кнопку посылаем файл:

Procedure TForm1.Button1Click(Sender: TObject); // Передаём файл var Size: integer; P: ^Byte; begin MS:= TMemoryStream.Create; // Создаём буфер для файла MS.LoadFromFile(Edit1.Text); // Загружаем файл в буфер // Посылаем информацию о файл (команда # название # размер) ServerSocket1.Socket.Connections.SendText("file#"+Edit1.Text+"#"+IntToStr(MS.Size)+"#"); MS.Position:= 0; // Переводим каретку в начало файла P:= MS.Memory; // Загружаем в переменную "P" файл Size:= ServerSocket1.Socket.Connections.SendBuf(P^, MS.Size); // Посылаем файл // Выводим прогресс ProgressBar1.Position:= Size*100 div MS.Size; StatusBar1.SimpleText:= "Отправлено "+IntToStr(Size)+" из "+IntToStr(MS.Size)+" байт"; end;

На событие OnClientRead, компонента TServerSocket, впишите следующий код:

Procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); begin if Socket.ReceiveText = "end" then // Если клиент принял файл, то... begin StatusBar1.SimpleText:= "Клиент принял файл"; MS.Free; // Убиваем буфер end; end;

Это нужно для того, чтоб сервер убил буфер, только после того, как клиент примет файл. Если убить буфер, сразу после передачи файла, то клиент не успеет принять весь файл! Как только клиент примет файл, он пошлёт серверу команду "end", что значит файл принят, и сервер убьёт буфер.

Теперь сделаем чтоб наш сервер выводил немного информации о соединении:
На событие OnClientConnect, компонента TServerSocket впишите следующий код:

Procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); begin StatusBar1.SimpleText:= "Соединение установлено"; end;

А на событие OnClientDisconnect впишите:

Procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); begin StatusBar1.SimpleText:= "Соединение не установлено"; end;

Вот сервер и готов! Теперь перейдём к клиенту (он принимает файл) с пим возни будет побольше:

Разместите на форуму компоненты: TClientSocket, две метки TLabel, TProgressBar и TStatusBar.
Установите у компонента TClientSocket, порт (port): 1001 (как у сервера), а переменную адрес (address): 127.0.0.1 (ваш IP).
Не забудьте установить у компонента TStatusBar, переменную SimplePanel в true, чтоб было видно наш текст.
В одном TLabel"е выводится имя фала, в другой размер файла.
Должно получиться что-то похожее на это:

Объявляем переменные и оду процедуру. Запишите переменные именно в private , иначе ничего не будет работать:

Procedure Writing(Text: string); // Процедура записи в данных в буфер private { Private declarations } Name: string; // Имя файла Size: integer; // Размер файла Receive: boolean; // Режим клиента MS: TMemoryStream; // Буфер для файла

На событие создания формы, мы соединяемся с сервером и ждём передачи файла:

Procedure TForm1.FormCreate(Sender: TObject); begin ClientSocket1.Open; // Открываем сокет Receive:= false; // Режим клиента - приём команд end;

При завершении приложения, закрываем сокет:

Procedure TForm1.FormDestroy(Sender: TObject); begin ClientSocket1.Close; // Закрываем сокет end;

Так-же как и у сервера, сделаем чтоб клиент выдавал информацию о соединении:

Procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket); begin StatusBar1.SimpleText:= "Соединение установлено"; end; procedure TForm1.ClientSocket1Disconnect(Sender: TObject; Socket: TCustomWinSocket); begin StatusBar1.SimpleText:= "Соединение не установлено"; end;

Теперь нам нужно вписать код в процедуру Writing. Эта процедура нужна для того, чтоб принятые данные записывать в файл. Код процедуры:

Procedure TForm1.Writing(Text: string); begin if MS.Size < Size then // Если принято байт меньше размера файла, то... MS.Write(Text, Length(Text)); // Записываем в буфер // Выводим прогресс закачки файла ProgressBar1.Position:= MS.Size*100 div Size; StatusBar1.SimpleText:= "Принято "+IntToStr(MS.Size)+" из "+IntToStr(Size); if MS.Size = Size then // Если файл принят, то... begin Receive:= false; // Переводим клиента в нормальный режим MS.Position:= 0; // Переводим каретку в начало буфера MS.SaveToFile(Name); // Сохраняем файл ClientSocket1.Socket.SendText("end"); // Посылаем команду "end", то есть файл принят MS.Free; // Убиваем буфер StatusBar1.SimpleText:= "Файл принят"; end; end;

Теперь на событие OnClientRead компонента TClientSocket, впишите следующий код:

Procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); var Rtext: string; // Принятый текст begin Rtext:= Socket.ReceiveText; if Receive then // Если клиент в режиме приёма файла, то... Writing(RText) // Записываем данные в буфер else // Если клиент не в режиме приёма файла, то... begin if Copy(Rtext, 0, Pos("#", Rtext) -1) = "file" then // Если это файл, то... begin MS:= TMemoryStream.Create; // Создаём буфер для файла Delete(Rtext, 1, Pos("#", Rtext)); // Определяем имя файла Name:= Copy(Rtext, 0, Pos("#", Rtext) -1); // Определяем имя файла Delete(Rtext, 1, Pos("#", Rtext)); // Определяем размер файла Size:= StrToInt(Copy(Rtext, 0, Pos("#", Rtext) -1)); // Определяем размер файла Delete(Rtext, 1, Pos("#", Rtext)); // Удаляем последний разделитель Label1.Caption:= "Размер файла: "+IntToStr(Size)+" байт"; // Выводим размер файла Label2.Caption:= "Имя файла: "+Name; // Выводим имя файла Receive:= true; // Переводим сервер в режим приёма файла Writing(RText); // Записываем данные в буфер end; end; end;

Таким образом, если файл большой, и событие OnClientRead будет вызываться ни один раз, а несколько, то если клиент в режиме приёма файла, он будет записывать данные в буфер, если же нет, то клиент определит принятую команду, и если это файл, то перейдёт в режим приёма файла. Если вы чего-то не поняли, то прочитайте код программы, я там не зря всё раскоментировал:-)

Ну вот и всё...
Клиент и сервер - готовы! Сначала запустите сервер, а за тем клиента и попробуйте передать файлы, размером в несколько мегабайт:-) Я без проблем пересылал по сети файлы размером 10-12 Мб.

Удачи в программировании!