Delphi. Программирование сокетов в Дельфи. Программирование серверов на основе сокетов в дельфи Компоненты для работы с сокетами delphi

Начинающие программисты (и я сам, когда начинал учить 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 Мб.

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

Введение

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

Следует сразу заметить, что для сосуществования отдельных приложений клиента и сервера не обязательно иметь несколько компьютеров. Достаточно иметь лишь один, на котором Вы одновременно запустите и сервер, и клиент. При этом нужно в качестве имени компьютера, к которому надо подключиться, использовать хост-имя localhost или IP-адрес - 127.0.0.1 .

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

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

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

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

  • Определение св-в Port и ServerType - чтобы к серверу могли нормально подключаться клиенты, нужно, чтобы порт, используемый сервером точно совпадал с портом, используемым клиентом (и наоборот). Свойство ServerType определяет тип подключения (подробнее см.ниже);
  • Открытие сокета - открытие сокета и указанного порта. Здесь выполняется автоматическое начало ожидания подсоединения клиентов (Listen );
  • Подключение клиента и обмен данными с ним - здесь подключается клиент и идет обмен данными с ним. Подробней об этом этапе можно узнать ниже в этой статье и в статье про сокеты (клиентская часть);
  • Отключение клиента - Здесь клиент отключается и закрывается его сокетное соединение с сервером;
  • Закрытие сервера и сокета - По команде администратора сервер завершает свою работу, закрывая все открытые сокетные каналы и прекращая ожидание подключений клиентов.

Следует заметить, что пункты 3-4 повторяются многократно, т.е. эти пункты выполняются для каждого нового подключения клиента.

Примечание : Документации по сокетам в Дельфи на данный момент очень мало, так что, если Вы хотите максимально глубоко изучить эту тему, то советую просмотреть литературу и электронную документацию по Unix/Linux-системам - там очень хорошо описана теория работы с сокетами. Кроме того, для этих ОС есть множество примеров сокетных приложений (правда, в основном на C/C++ и Perl).

Краткое описание компонента TServerSocket

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

Свойства Методы События
Socket - класс TServerWinSocket, через который Вы имеете доступ к открытым сокетным каналам. Далее мы рассмотрим это свойство более подробно, т.к. оно, собственно и есть одно из главных. Тип: TServerWinSocket ;
ServerType - тип сервера. Может принимать одно из двух значений: stNonBlocking - синхронная работа с клиентскими сокетами. При таком типе сервера Вы можете работать с клиентами через события OnClientRead и OnClientWrite . stThreadBlocking - асинхронный тип. Для каждого клиентского сокетного канала создается отдельный процесс (Thread). Тип: TServerType ;
ThreadCacheSize - количество клиентских процессов (Thread), которые будут кэшироваться сервером. Здесь необходимо подбирать среднее значение в зависимости от загруженности Вашего сервера. Кэширование происходит для того, чтобы не создавать каждый раз отдельный процесс и не убивать закрытый сокет, а оставить их для дальнейшего использования. Тип: Integer ;
Active - показатель того, активен в данных момент сервер, или нет. Т.е., фактически, значение True указывает на то, что сервер работает и готов к приему клиентов, а False - сервер выключен. Чтобы запустить сервер, нужно просто присвоить этому свойству значение True . Тип: Boolean ;
Port - номер порта для установления соединений с клиентами. Порт у сервера и у клиентов должны быть одинаковыми. Рекомендуются значения от 1025 до 65535, т.к. от 1 до 1024 - могут быть заняты системой. Тип: Integer ;
Service - строка, определяющая службу (ftp , http , pop , и т.д.), порт которой будет использован. Это своеобразный справочник соответствия номеров портов различным стандартным протоколам. Тип: string ;
Open - Запускает сервер. По сути, эта команда идентична присвоению значения True свойству Active ;
Close - Останавливает сервер. По сути, эта команда идентична присвоению значения False свойству Active .
OnClientConnect - возникает, когда клиент установил сокетное соединение и ждет ответа сервера (OnAccept );
OnClientDisconnect - возникает, когда клиент отсоединился от сокетного канала;
OnClientError - возникает, когда текущая операция завершилась неудачно, т.е. произошла ошибка;
OnClientRead - возникает, когда клиент передал берверу какие-либо данные. Доступ к этим данным можно получить через пеаедаваемый параметр Socket: TCustomWinSocket ;
OnClientWrite - возникает, когда сервер может отправлять данные клиенту по сокету;
OnGetSocket - в обработчике этого события Вы можете отредактировать параметр ClientSocket ;
OnGetThread - в обработчике этого события Вы можете определить уникальный процесс (Thread) для каждого отдельного клиентского канала, присвоив параметру SocketThread нужную подзадачу TServerClientThread;
OnThreadStart , OnThreadEnd - возникает, когда подзадача (процесс, Thread) запускается или останавливается, соответственно;
OnAccept - возникает, когда сервер принимает клиента или отказывает ему в соединении;
OnListen - возникает, когда сервер переходит в режим ожидания подсоединения клиентов.

TServerSocket.Socket (TServerWinSocket)

Итак, как же сервер может отсылать данные клиенту? А принимать данные? В основном, если Вы работаете через события OnClientRead и OnClientWrite , то общаться с клиентом можно через параметр ClientSocket (TCustomWinSocket). Про работу с этим классом можно прочитать в статье про клиентские сокеты, т.к. отправка/посылка данных через этот класс аналогична - методы (Send/Receive)(Text,Buffer,Stream). Также и при работе с TServerSocket.Socket. Однако, т.к. здесь мы рассматриваем сервер, то следует выделить некоторые полезные свойства и методы:

  • ActiveConnections (Integer ) - количество подключенных клиентов;
  • ActiveThreads (Integеr ) - количество работающих процессов; Connections (array ) - массив, состоящий из отдельных классов TClientWinSocket для каждого подключенного клиента. Например, такая команда:
    ServerSocket1.Socket.Connections.SendText("Hello!");
    отсылает первому подключенному клиенту сообщение "Hello!". Команды для работы с элементами этого массива - также (Send/Receive)(Text,Buffer, Stream);
  • IdleThreads (Integer ) - количество свободных процессов. Такие процессы кэшируются сервером (см. ThreadCacheSize );
  • LocalAddress , LocalHost , LocalPort - соответственно - локальный IP-адрес, хост-имя, порт;
  • RemoteAddress , RemoteHost , RemotePort - соответственно - удаленный IP-адрес, хост-имя, порт;
  • Методы Lock и UnLock - соответственно, блокировка и разблокировка сокета.

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

А теперь рассмотрим вышеприведенное на конкретном примере. Скачать уже готовые исходники можно, щелкнув .

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

Пример 1. Протоколирование и изучение работы сервера, посылка/прием сообщений через сокеты.

{... Здесь идет заголовок файла и определение формы TForm1 и ее экземпляра Form1} {Полный исходник смотри } procedure TForm1.Button1Click(Sender: TObject); begin {Определяем порт и запускаем сервер} ServerSocket1.Port:= 1025; {Метод Insert вставляет строку в массив в указанную позицию} Memo2.Lines.Insert(0,"Server starting"); ServerSocket1.Open; end; procedure TForm1.Button2Click(Sender: TObject); begin {Останавливаем сервер} ServerSocket1.Active:= False; Memo2.Lines.Insert(0,"Server stopped"); end; procedure TForm1.ServerSocket1Listen(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь сервер "прослушивает" сокет на наличие клиентов} Memo2.Lines.Insert(0,"Listening on port "+IntToStr(ServerSocket1.Port)); end; procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь сервер принимает клиента} Memo2.Lines.Insert(0,"Client connection accepted"); end; procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь клиент подсоединяется} Memo2.Lines.Insert(0,"Client connected"); end; procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь клиент отсоединяется} Memo2.Lines.Insert(0,"Client disconnected"); end; procedure TForm1.ServerSocket1ClientError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); begin {Произошла ошибка - выводим ее код} Memo2.Lines.Insert(0,"Client error. Code = "+IntToStr(ErrorCode)); end; procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); begin {От клиента получено сообщение - выводим его в Memo1} Memo2.Lines.Insert(0,"Message received from client"); Memo1.Lines.Insert(0,"> "+Socket.ReceiveText); end; procedure TForm1.ServerSocket1ClientWrite(Sender: TObject; Socket: TCustomWinSocket); begin {Теперь можно слать данные в сокет} Memo2.Lines.Insert(0,"Now can write to socket"); end; procedure TForm1.ServerSocket1GetSocket(Sender: TObject; Socket: Integer; var ClientSocket: TServerClientWinSocket); begin Memo2.Lines.Insert(0,"Get socket"); end; procedure TForm1.ServerSocket1GetThread(Sender: TObject; ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread); begin Memo2.Lines.Insert(0,"Get Thread"); end; procedure TForm1.ServerSocket1ThreadEnd(Sender: TObject; Thread: TServerClientThread); begin Memo2.Lines.Insert(0,"Thread end"); end; procedure TForm1.ServerSocket1ThreadStart(Sender: TObject; Thread: TServerClientThread); begin Memo2.Lines.Insert(0,"Thread start"); end; procedure TForm1.Button3Click(Sender: TObject); var i: Integer; begin {Посылаем ВСЕМ клиентам сообщение из Edit1} for i:= 0 to ServerSocket1.Socket.ActiveConnections-1 do begin ServerSocket1.Socket.Connections[i].SendText(Edit1.Text); end; Memo1.Lines.Insert(0,"< "+Edit1.Text); end;

Приемы работы с TServerSocket (и просто с сокетами)

Хранение уникальных данных для каждого клиента.

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

Посылка файлов через сокет.

Здесь мы рассмотрим посылку файлов через сокет (по просьбе JINX-а) :-). Итак, как же послать файл по сокету? Очень просто! Достаточно лишь открыть этот файл как файловый поток (TFileStream) и отправить его через сокет (SendStream)! Рассмотрим это на примере:

Нужно заметить, что метод SendStream используется не только сервером, но и клиентом (ClientSocket1.Socket.SendStream(srcfile) )

Почему несколько блоков при передаче могут обьединяться в один

Это тоже по просьбе JINX-а:-). За это ему огромное спасибо! Итак, во-первых, надо заметить, что посылаемые через сокет данные могут не только объединяться в один блок, но и разъединяться по нескольким блокам. Дело в том, что сокет - обычный поток, но в отличие, скажем, от файлового (TFileStream), он передает данные медленнее (сами понимаете - сеть, ограниченный трафик, и т.д.). Именно поэтому две команды:
ServerSocket1.Socket.Connections.SendText("Hello, ");
ServerSocket1.Socket.Connections.SendText("world!");
совершенно идентичны одной команде:
ServerSocket1.Socket.Connections.SendText("Hello, world!");

И именно поэтому, если Вы отправите через сокет файл, скажем, в 100 Кб, то тому, кому Вы посылали этот блок, придет несколько блоков с размерами, которые зависят от трафика и загруженности линии. Причем, размеры не обязательно будут одинаковыми. Отсюда следует, что для того, чтобы принять файл или любые другие данные большого размера, Вам следует принимать блоки данных, а затем объединять их в одно целое (и сохранять, например, в файл). Отличным решением данной задачи является тот же файловый поток - TFileStream (либо поток в памяти - TMemoryStream). Принимать частички данных из сокета можно через событие OnRead (OnClientRead), используя универсальный метод ReceiveBuf . Определить размер полученного блока можно методом ReceiveLength . Также можно воспользоваться сокетным потоком (см. статью про TClientSocket). А вот и небольшой примерчик (приблизительный):

Как следить за сокетом

Это вопрос сложный и требует долгого рассмотрения. Пока лишь замечу, что созданный Вашей программой сокет Вы можете промониторить всегда:-). Сокеты (как и большинство объектов в Windows) имеют свой дескриптор (handle), записанный в свойстве Handle. Так вот, узнав этот дескриптор Вы свободно сможете управлять любым сокетом (даже созданным чужой программой)! Однако, скорее всего, чтобы следить за чужим сокетом, Вам придется использовать исключительно функции WinAPI Sockets.

Эпилог

В этой статье отображены основные приемы работы с компонентом TServerSocket в Дельфи и несколько общих приемов для обмена данными по сокетам. Если у Вас есть вопросы - скидывайте их мне на E-mail: [email protected] , а еще лучше - пишите в конференции этого сайта (Delphi. Общие вопросы), чтобы и другие пользователи смогли увидеть Ваш вопрос и попытаться на него ответить!

Карих Николай (Nitro ). Московская область, г.Жуковский

Я добавил сетевую поддержку. То есть создал отдельный сервер и отдельный клиент. Смысл заключается в том, что работает сервер приложения, пользователь запускается клиент и в пользователь вводит запрос: Москва Тверская 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, это означает, что клиент в своем приложении нажал на кнопку Отключиться.

В этой статье я немного расскажу о сокетах и о граблях, на которые я понаступал, программируя различные клиентские и серверные приложения на протоколе TCP/IP. Постараюсь объяснить простым языком для неспециалистов. Здесь будут даны самые начальные сведения и будет попытка обобщения. В некоторых статьях есть такая фраза - "для... необходимо знать это и то, а для тех кто не знает - идите смотрите там, не знаю где". Теперь будет ясно "где"; и эти статьи, я думаю, могут быть справочником в дальнейшем. Будет рассмотренна работа с сокетами в m$ windows. Для программирования сокетов в никсах различие очень незначительны (все функции и структуры мелкософт постарался без изменений передрать) и основные из них рассмотрены в статьях, ссылки на которые приведены в конце, в разделе "Что еще почитать". Программа, использующая сокеты, может работать с одним сокетом или с множеством одноременно "открытых" сокетов (сокетный движок). Сразу стоит выделить различие между блокирующими (асинхронными) и неблокирующими (синхронными, требующими синхронизацию) сокетами.

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

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

Итак, вначале некоторый ликбез. Программы, о которых пойдет речь ниже, делятся на клиенты и серверы. Вначале необходимо рассмотреть минимум используемых функций, для работы этих программ. В m$ windows работой с сокетами "заведует" winsock.dll. Для разных языков программирования синтаксис вызова функций из этой DLL незначителен. В Delphi, например, в катологе Source находится файл winsock.pas, который всего лишь объявляет нужные функции и структуры данных. При подгрузке модуля WinSock в uses, их можно вызывать с синтаксисом паскаля, но от этого принцип их работы не изменится. Кстати, не советовал бы использовать стандартные компоненты Delphi и Builder (TServerSocket, TClientSocket) из-за их глючности. Если не очень хочется использовать стандартные winsock-функции, то можно взять набор компонент Indy. Вот функции winsock:

int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData);

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

"деинициализирует" WSAStartup.

SOCKET socket (int af, int type, int protocol);

создает сокет. Второй параметр - вид данных, третий - вид протокола. Порт и адрес задается в функции bind (сервер) или connect (клиент).

int closesocket (SOCKET s);

закрывает сокет.

int bind (SOCKET s, const struct sockaddr FAR* name, int namelen);

ассоциирует адрес с сокетом. Структура адреса содержит порт (необходимо привести функцией htons) и адрес (для сервера обычно указывается INADDR_ANY - любой).

int connect (SOCKET s, const struct sockaddr FAR* name, int namelen);

функция соединения для клиента. Структура адреса содержит порт (необходимо привести функцией htons) и адрес (для клиента необходимо привести из имени или спецификации ip4 - xxx.xxx.xxx.xxx).

int WSAAsyncSelect (SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent);

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

int select (int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);

контроль состояния сокета. Используется для неблокирующих вызовов, чтобы получить состояние сокета на данный момент, с использованием макросов FD_.

int WSAEventSelect (SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);

связывает сокет с получением сообщений операционной системы. Можно проинициализировать необходимый Event (см. документацию) и обрабатывать сообщения FD_ без использования окна m$ windows.

int send (SOCKET s, const char FAR * buf, int len, int flags);

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

int recv (SOCKET s, char FAR* buf, int len, int flags);

получение данных.

Кстати, и в блокирующих и в неблокирующих сокетах, используется так или иначе функция [...]select. Только опрос о состоянии очереди сокета мы производим сами (неблокирующий режим) или это делает операционная система (блокирующий).

Преобразование адреса

Вот пример функции на Delphi, которая преобразует адрес из имени или спецификации ip4, в четырехбайтное значение, требующееся для структуры sockaddr_in, с которой работает сокет:

Uses WinSock; function d_addr(IPaddr: string) : Cardinal; var pa: PChar; sa: TInAddr; Host: PHostEnt; begin Result:=inet_addr(PChar(IPaddr)); // Перевод если адреа не в ip4 if Result=INADDR_NONE then begin host:=GetHostByName(PChar(IPaddr)); if Host = nil then exit else begin // Преобразование pa:= Host^.h_addr_list^; sa.S_un_b.s_b1:= pa; sa.S_un_b.s_b2:= pa; sa.S_un_b.s_b3:= pa; sa.S_un_b.s_b4:= pa; with TInAddr(sa).S_un_b do Result:=inet_addr(PChar(IntToStr(Ord(s_b1)) + "." + IntToStr(Ord(s_b2)) + "." + IntToStr(Ord(s_b3)) + "." + IntToStr(Ord(s_b4)))); end; end; end;

Использование блокировки в некоторых программах:

  • Сервер. Проще говоря, задача сервера открыть порт на компьютере и "висеть", принимая команды или некоторые данные от клиента по инициализированному порту. Например, FTP-сервер открывает 21 порт и обрабатывая определенные команды от клиента, выполняет необходимые операции с файловой системой. POP3-сервер открывает 110 порт и занимается приемом электронных сообщений (e-mail). SMTP (25 порт) - отправка писем. TelNet (23), SSh (22) и т.д. Обычно используются блокирующие сокеты, потому что следующие за приемом данных операторы - это обработка принятых данных. Нет смысла делать передачу на них управления, пока эти данные не приняты. Сервер может "занять" несколько портов, принимать данные от множества клиентов, но все равно из-за постоянного опроса select инициализированных сокетов и черезчур больших требований к алгоритму корректной обработки данных, это не лучшее решение для сервера. Хотя есть исключения и все зависит от приверженности программиста к тому или иному способу.
  • Клиент. Задача клиента - соедениться с сервером и посылать ему (принимать от него) данные. Естественно, если клиент пытается соедениться по порту, на котором по указанному адресу не "висит" сервер, то будет ошибка соединения. Если сервер проинициализирован на другой протокол (UDP, ICMP, TCP,...) или вид обмена данными, а клиент на другой, то соединение скорее всего произойдет, но обмен данными станет невозможен. В случае, если клиент работает с сокетным движком (несколько сокетов), то я обычно использую блокирующие сокеты, работающие каждый на своей нити (Threads) или процессе (Process) с минимальным приоритетом. Таким образом достигается необходимая многозадачность, параллельность работы и, в то же время, удобно обрабатывать принятые/отправленные данные в несинхронном режиме работы. Таким образом достигается упрощение алгоритма и минимальное количество выполняемых операций из-за того, что нет необходимости постоянно вызывать select.
  • Различные сканеры, брутфорсеры. Задача - создать максимальное количество сокетов, поддерживаемое операционной системой для достижения максимальной скорости сканирования или перебора. Здесь надо очень хорошо продумывать сокетный движок. Предыдущий способ создания клиента хорош для относительно небольшого количества одновременно проинициализированных сокетов. Иначе, из-за очень большого количества однновременно запущенных процессов, повышается нагрузка на ядро системы. Здесь все зависит от скорости соединения и компьютерного "железа". Можно использовать как блокирующие, так и неблокирующие сокеты. При современном развитии компьютерного "железа" и относительно небольшой скорости соединения (DialUp, выделенка с низкой скоростью), я бы порекомендовал использовать способ, рассмотренный выше. Количество нитей 255 вполне "потянет" практически любой компьютер, а большее количество из-за скорости соединения использовать нет смысла. При очень хорошем канале (можно создать практически неограниченное количество сокетов) или не очень хорошем компьютере (всего один процесс обработки), надо все-таки использовать неблокирующие сокеты. Несмотря на эти преимущества, кроме рассмотренных недостатков синхронных сокетов, будет рассмотрено еще несколько в примерах, во второй статье.

Создание отдельной нити, процесса (Threads)

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

Uses WinSock; //Описываем процесс как класс, типа TThread type TScaner = class(TThread) Sock: TSocket; private protected procedure Execute; override; procedure Run; end; //забиваем место в памяти под процесс var Scaner: TScaner; //фунцция, вызываемая при создании процесса procedure TScaner.Execute; var <переменные> begin <инициализация сокета и операции с ним> //запуск дополнительной фунцции процесса Synchronize(Run); //закрытие сокета closesocket(Sock) //Прервать процесс Terminate; end; //дополнительная фунцция procedure TScaner.Run; var <переменные> begin <какие либо действия> end; //программа begin <какие-либо действия> //создать процесс, но пока не запускать Scaner:=TScaner.Create(true); //Освободить память при прерывании процесса Scaner.FreeOnTerminate:=true; //установить приоритет Scaner.Priority:=tpLowest; //запустить процесс Scaner.Resume; <какие-либо действия> end.

Этот прием очень удобен, например, для вызова обработки сокета и прерывания по какой-либо клавише. На клавишу "Start" цепляем создание процесса, а на клавишу "Stop" - Scaner.Terminate. Можно также описать процедуру Terminate процесса, где будет closesocket. Этот процесс будет работать независимо от основной программы. Правда для синхронизации его с VCL главного окна, его необходимо немного дописать. Этот прием одинаково удобен и для неблокирующего (создается процесс, в котором уже идет цикл по select по множеству сокетов) и для блокирующего сокета (создается много процессов и для каждого свой сокет) при написании различных клиентов и сканеров. В частности, он использовался мной для написания моей многонитевой программы "DScan", которая включает универсальный клиент, сканер, брутфорсер и предоставленна со всеми исходниками.

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

P.S. Статья и программа предоставлена в целях обучения и вся ответственность за использование ложится на твои хилые плечи.


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

Следует сразу заметить, что для сосуществования отдельных приложений клиента и сервера не обязательно иметь несколько компьютеров. Достаточно иметь лишь один, на котором Вы одновременно запустите и сервер, и клиент. При этом нужно в качестве имени компьютера, к которому надо подключиться, использовать хост-имя localhost или IP-адрес - 127.0.0.1.

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

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

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

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

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

  1. Определение св-в Port и ServerType - чтобы к серверу могли нормально подключаться клиенты, нужно, чтобы порт, используемый сервером точно совпадал с портом, используемым клиентом (и наоборот). Свойство ServerType определяет тип подключения (подробнее см.ниже);
  2. Открытие сокета - открытие сокета и указанного порта. Здесь выполняется автоматическое начало ожидания подсоединения клиентов (Listen);
  3. Подключение клиента и обмен данными с ним - здесь подключается клиент и идет обмен данными с ним. Подробней об этом этапе можно узнать ниже в этой статье и в статье про сокеты (клиентская часть);
  4. Отключение клиента - Здесь клиент отключается и закрывается его сокетное соединение с сервером;
  5. Закрытие сервера и сокета - По команде администратора сервер завершает свою работу, закрывая все открытые сокетные каналы и прекращая ожидание подключений клиентов.
  6. Следует заметить, что пункты 3-4 повторяются многократно, т.е. эти пункты выполняются для каждого нового подключения клиента.

Примечание:

Документации по сокетам в Дельфи на данный момент очень мало, так что, если Вы хотите максимально глубоко изучить эту тему, то советую просмотреть литературу и электронную документацию по Unix/Linux-системам - там очень хорошо описана теория работы с сокетами. Кроме того, для этих ОС есть множество примеров сокетных приложений (правда, в основном на C/C++ и Perl).

Краткое описание компонента TServerSocket

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

Свойства Методы События:

Socket класс TServerWinSocket, через который Вы имеете доступ к открытым сокетным каналам. Далее мы рассмотрим это свойство более подробно, т.к. оно, собственно и есть одно из главных. Тип: TServerWinSocket; ServerType тип сервера. Может принимать одно из двух значений: stNonBlocking - синхронная работа с клиентскими сокетами. При таком типе сервера Вы можете работать с клиентами через события OnClientRead и OnClientWrite. stThreadBlocking - асинхронный тип. Для каждого клиентского сокетного канала создается отдельный процесс (Thread). Тип: TServerType; ThreadCacheSize количество клиентских процессов (Thread), которые будут кэшироваться сервером. Здесь необходимо подбирать среднее значение в зависимости от загруженности Вашего сервера. Кэширование происходит для того, чтобы не создавать каждый раз отдельный процесс и не убивать закрытый сокет, а оставить их для дальнейшего использования. Тип: Integer; Active показатель того, активен в данных момент сервер, или нет. Т.е., фактически, значение True указывает на то, что сервер работает и готов к приему клиентов, а False - сервер выключен. Чтобы запустить сервер, нужно просто присвоить этому свойству значение True. Тип: Boolean; Port номер порта для установления соединений с клиентами. Порт у сервера и у клиентов должны быть одинаковыми. Рекомендуются значения от 1025 до 65535, т.к. от 1 до 1024 - могут быть заняты системой. Тип: Integer; Service строка, определяющая службу (ftp, http, pop, и т.д.), порт которой будет использован. Это своеобразный справочник соответствия номеров портов различным стандартным протоколам. Тип: string; Open Запускает сервер. По сути, эта команда идентична присвоению значения True свойству Active; Close Останавливает сервер. По сути, эта команда идентична присвоению значения False свойству Active. OnClientConnect возникает, когда клиент установил сокетное соединение и ждет ответа сервера (OnAccept); OnClientDisconnect возникает, когда клиент отсоединился от сокетного канала; OnClientError возникает, когда текущая операция завершилась неудачно, т.е. произошла ошибка; OnClientRead возникает, когда клиент передал берверу какие-либо данные. Доступ к этим данным можно получить через пеаедаваемый параметр Socket: TCustomWinSocket; OnClientWrite возникает, когда сервер может отправлять данные клиенту по сокету; OnGetSocket в обработчике этого события Вы можете отредактировать параметр ClientSocket; OnGetThread в обработчике этого события Вы можете определить уникальный процесс (Thread) для каждого отдельного клиентского канала, присвоив параметру SocketThread нужную подзадачу TServerClientThread; OnThreadStart, OnThreadEnd возникает, когда подзадача (процесс, Thread) запускается или останавливается, соответственно; OnAccept возникает, когда сервер принимает клиента или отказывает ему в соединении; OnListen возникает, когда сервер переходит в режим ожидания подсоединения клиентов.

TServerSocket.Socket (TServerWinSocket)

Итак, как же сервер может отсылать данные клиенту? А принимать данные? В основном, если Вы работаете через события OnClientRead и OnClientWrite, то общаться с клиентом можно через параметр ClientSocket (TCustomWinSocket). Про работу с этим классом можно прочитать в статье про клиентские сокеты, т.к. отправка/посылка данных через этот класс аналогична - методы (Send/Receive)(Text,Buffer,Stream). Также и при работе с TServerSocket.Socket. Однако, т.к. здесь мы рассматриваем сервер, то следует выделить некоторые полезные свойства и методы:

ActiveConnections (Integer) количество подключенных клиентов; ActiveThreads (Integеr) количество работающих процессов; Connections (array) - массив, состоящий из отдельных классов TClientWinSocket для каждого подключенного клиента. Например, такая команда:


ServerSocket1.Socket.Connections.SendText("Hello!");

отсылает первому подключенному клиенту сообщение "Hello!". Команды для работы с элементами этого массива - также (Send/Receive)(Text,Buffer, Stream); IdleThreads (Integer) количество свободных процессов. Такие процессы кэшируются сервером (см. ThreadCacheSize); LocalAddress, LocalHost, LocalPort соответственно - локальный IP-адрес, хост-имя, порт; RemoteAddress, RemoteHost, RemotePort соответственно - удаленный IP-адрес, хост-имя, порт; Методы Lock и UnLock соответственно, блокировка и разблокировка сокета.

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

А теперь рассмотрим вышеприведенное на конкретном примере. Скачать уже готовые исходники можно, щелкнув здесь.

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

Пример 1. Протоколирование и изучение работы сервера, посылка/прием сообщений через сокеты.


{... Здесь идет заголовок файла и определение формы TForm1 и ее экземпляра Form1} procedure TForm1.Button1Click(Sender: TObject); begin {Определяем порт и запускаем сервер} ServerSocket1.Port:= 1025; {Метод Insert вставляет строку в массив в указанную позицию} Memo2.Lines.Insert(0, "Server starting"); ServerSocket1.Open; end ; procedure TForm1.Button2Click(Sender: TObject); begin {Останавливаем сервер} ServerSocket1.Active:= False; Memo2.Lines.Insert(0, "Server stopped"); end ; procedure TForm1.ServerSocket1Listen(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь сервер "прослушивает" сокет на наличие клиентов} Memo2.Lines.Insert(0, "Listening on port " + IntToStr(ServerSocket1.Port)); end ; procedure TForm1.ServerSocket1Accept(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь сервер принимает клиента} Memo2.Lines.Insert(0, "Client connection accepted"); end ; procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь клиент подсоединяется} Memo2.Lines.Insert(0, "Client connected"); end ; procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); begin {Здесь клиент отсоединяется} Memo2.Lines.Insert(0, "Client disconnected"); end ; procedure TForm1.ServerSocket1ClientError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); begin {Произошла ошибка - выводим ее код} Memo2.Lines.Insert(0, "Client error. Code = " + IntToStr(ErrorCode)); end ; procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); begin {От клиента получено сообщение - выводим его в Memo1} Memo2.Lines.Insert(0, "Message received from client"); Memo1.Lines.Insert(0, "> " + Socket.ReceiveText); end ; procedure TForm1.ServerSocket1ClientWrite(Sender: TObject; Socket: TCustomWinSocket); begin {Теперь можно слать данные в сокет} Memo2.Lines.Insert(0, "Now can write to socket"); end ; procedure TForm1.ServerSocket1GetSocket(Sender: TObject; Socket: Integer; var ClientSocket: TServerClientWinSocket); begin Memo2.Lines.Insert(0, "Get socket"); end ; procedure TForm1.ServerSocket1GetThread(Sender: TObject; ClientSocket: TServerClientWinSocket; var SocketThread: TServerClientThread); begin Memo2.Lines.Insert(0, "Get Thread"); end ; procedure TForm1.ServerSocket1ThreadEnd(Sender: TObject; Thread: TServerClientThread); begin Memo2.Lines.Insert(0, "Thread end"); end ; procedure TForm1.ServerSocket1ThreadStart(Sender: TObject; Thread: TServerClientThread); begin Memo2.Lines.Insert(0, "Thread start"); end ; procedure TForm1.Button3Click(Sender: TObject); var i: Integer; begin {Посылаем ВСЕМ клиентам сообщение из Edit1} for i:= 0 to ServerSocket1.Socket.ActiveConnections - 1 do ServerSocket1.Socket.Connections[i].SendText(Edit1.Text); Memo1.Lines.Insert(0, "end;

Приемы работы с TServerSocket (и просто с сокетами)

Хранение уникальных данных для каждого клиента.

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

Посылка файлов через сокет

Здесь мы рассмотрим посылку файлов через сокет (по просьбе JINX-а) :-). Итак, как же послать файл по сокету? Очень просто! Достаточно лишь открыть этот файл как файловый поток (TFileStream) и отправить его через сокет (SendStream)! Рассмотрим это на примере:


Нужно заметить, что метод SendStream используется не только сервером, но и клиентом (ClientSocket1.Socket.SendStream(srcfile))

Почему несколько блоков при передаче могут обьединяться в один

Это тоже по просьбе JINX-а:-). За это ему огромное спасибо! Итак, во-первых, надо заметить, что посылаемые через сокет данные могут не только объединяться в один блок, но и разъединяться по нескольким блокам. Дело в том, что сокет - обычный поток, но в отличие, скажем, от файлового (TFileStream), он передает данные медленнее (сами понимаете - сеть, ограниченный трафик, и т.д.). Именно поэтому две команды:


ServerSocket1.Socket.Connections.SendText("Hello, "); ServerSocket1.Socket.Connections.SendText("world!");

совершенно идентичны одной команде:


ServerSocket1.Socket.Connections.SendText("Hello, world!");

И именно поэтому, если Вы отправите через сокет файл, скажем, в 100 Кб, то тому, кому Вы посылали этот блок, придет несколько блоков с размерами, которые зависят от трафика и загруженности линии. Причем, размеры не обязательно будут одинаковыми. Отсюда следует, что для того, чтобы принять файл или любые другие данные большого размера, Вам следует принимать блоки данных, а затем объединять их в одно целое (и сохранять, например, в файл). Отличным решением данной задачи является тот же файловый поток - TFileStream (либо поток в памяти - TMemoryStream). Принимать частички данных из сокета можно через событие OnRead (OnClientRead), используя универсальный метод ReceiveBuf. Определить размер полученного блока можно методом ReceiveLength. Также можно воспользоваться сокетным потоком (см. статью про TClientSocket). А вот и небольшой примерчик (приблизительный):


Как следить за сокетом

Это вопрос сложный и требует долгого рассмотрения. Пока лишь замечу, что созданный Вашей программой сокет Вы можете промониторить всегда:-). Сокеты (как и большинство объектов в Windows) имеют свой дескриптор (handle), записанный в свойстве Handle. Так вот, узнав этот дескриптор Вы свободно сможете управлять любым сокетом (даже созданным чужой программой)! Однако, скорее всего, чтобы следить за чужим сокетом, Вам придется использовать исключительно функции WinAPI Sockets.

Эпилог

В этой статье отображены основные приемы работы с компонентом TServerSocket в Дельфи и несколько общих приемов для обмена данными по сокетам. Если у Вас есть вопросы - скидывайте их мне на E-mail: [email protected], а еще лучше - пишите в конференции этого сайта (Delphi. Общие вопросы), чтобы и другие пользователи смогли увидеть Ваш вопрос и попытаться на него ответить!

Понравилось? Лайкни нас на Facebook