Delphi Studio » Delphi Статьи » Использование WinSock в Delphi. Часть 2

    "Уважаемые посетители, если вы хотите задать вопросы и получить ответы, то регистрируйтесь и добавляйте свои вопросы тут: " Задать вопрос.
      Delphi Studio Использование WinSock в Delphi. Часть 2
      FeniXElite | 18-08-2010, 06:59 | Delphi Статьи

      УЧИМСЯ ИСПОЛЬЗОВАТЬ WINSOCK. ЧАСТЬ II.




      УЧИМСЯ ИСПОЛЬЗОВАТЬ WINSOCK. ЧАСТЬ II.


      (подключение, передача и прием данных. многопоточность.)




      ПРЕДИСЛОВИЕ

      Как и обещал в первой статье, сейчас будут рассмотрены:

      - Функции подключения к удаленному серверу и ожидание входящих подключений

      - Передачи и приема данных.

      - Организация многопоточности клиента и сервера.

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



      ПОДКЛЮЧЕНИЕ

      Предположим что у нас уже был создан сокет sock:TSOCKET и настроены все параметры caddr:sockaddr_in. (читайте первую статью)

      Для осуществления подключения к другому компьютеру, необходимо использовать функцию connect

      function connect( const s: TSocket; const name: PSockAddr; namelen: Integer): Integer; stdcall;


      Первым параметром передается созданный сокет

      Вторым – адрес структуры sockaddr_in

      Третьим – размер структуры sockaddr_in в байтах.

      При удачном подключении будет возвращено число 0.

      Для распознания кода ошибки необходимо использовать функцию WSAGetLastError которая возвращает код ошибки. Коды ошибок можно посмотреть в MSDN


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

      WSAECONNREFUSED – сервер отверг попытку подключения

      WSAENETUNREACH – сеть недоступна

      WSAETIMEDOUT – не удалось подключить к серверу в установленный промежуток времени(банальный timeout).

      На практике это выглядит как



      Код:


      if connect(sock, @caddr, SizeOf(caddr))=0 then
      begin
      удачно подключились
      end
      else
      begin
      обработка ошибки
      end;





      Важно заметить, что по умолчанию в Windows XP/2003 время таймаута для коннекта стоит 20-45 секунд. В некоторых случаях это очень неудобно когда, к примеру используется работа со списками прокси серверов и при попадании на мертвый сервер идет большая пауза, по этому для того, чтобы учитывать такие ситуации (когда они критически важны) необходимо применять различные способы. Каждый из этих способов имеет свои плюсы и минусы. В основе этого лежат 3 способа

      1) предварительное пингование сервера – очень удобно чтобы сразу определить доступен сервак или нет. Но есть минусы – не все серваки отвечают на пинг, а также пинг не может свидетельствовать об открытости нужного порта.

      2) запуск отдельного потока и в нем выполнение действий, и при истечении таймаута закроется сокет и прибьется поток. – отнимает ресурсы очень сильно, но зато в его помощью можно задать таймаут на выполнения блока функций. К примеру, функция которая получает данные от клиентов или от сервера может завершить свою работу по истечению таймаута, независимо от того, на чем остановилась работа.


      Алгоритм выглядит примерно так:

      procedure f():stdcall;
      begin
      //создает сокет и помещает его в глобальную переменную, далее идет  работа с  сетью, а затем закрытие сокета
      end;


      В основной программе создается поток через CreateThread

      И затем дескриптор потока передается функции WaitForSingleObject, в которой как раз и задается таймаут. Если функция завершилась с кодом WAIT_TIMEOUT, значит, сработал таймаут, и тогда необходимо самому закрыть сокет (CloseSocket) и завершить поток через TerminateThread


      Также аналогом данного метода может быть применение таймеров. В таком случае делается наоборот всё. Т.е. перед выполнение работы с сетью запускается таймер – settimer. И в его обработчике уже делаются все действия связанные с таймаутом. Но как видно – метод очень сильно тратит ресурсы.

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



      Рассмотрим 1 и 3 способ более подробно т.к. они более актуальны сейчас.



      ВРЕМЕННЫЙ ПЕРЕВОД В НЕБЛОКИРУЕМЫЙ РЕЖИМ


      Код:



      var
      sock:TSOCKET;
      block:bool;
      timeout:ttimeval;
      fds:TFDSet;
      rc:integer;
      .-.-.-.-.-.-.-.-.--
      begin
      .-.-.-.-.-.-.-.-.--
      block:=true;
      ioctlsocket(sock, FIONBIO, cardinal(block)); // переводим сокет в неблокируемый режим
      if connect(sock, @caddr, SizeOf(caddr))=SOCKET_ERROR then // пытаемся подключиться
      begin
      if WSAGetLastError=WSAEWOULDBLOCK then // проверяем что сокет перешел в неблокируемый режим
      begin
      FD_ZERO(fds);
      FD_SET(sock,fds);
      timeout.tv_sec:=10; // таймаут 10 секунд
      timeout.tv_usec:=0;
      rc:=select(0, nil, @fds, nil, @timeout); // ожидаем
      end;
      end;
      block:=false;
      ioctlsocket(sock, FIONBIO, cardinal(block)); // переводим сокет обратно в блокируемый режим
      if rc=0 then
      begin
      // сработал таймаут
      end
      else
      begin
      // удачно подключились
      end;





      ПРЕДВАРИТЕЛЬНОЕ ПИНГОВАНИЕ СЕРВЕРА

      Пингование будем осуществлять через функции icmp.dll


      Код:



      function IcmpCreateFile : THandle; stdcall; external 'icmp.dll';
      function IcmpSendEcho (IcmpHandle : THandle; DestinationAddress : TInAddr;
      RequestData : Pointer; RequestSize : Smallint;
      RequestOptions : pointer;
      ReplyBuffer : Pointer;
      ReplySize : DWORD;
      Timeout : DWORD) : DWORD; stdcall; external 'icmp.dll';


      Type // структуры кторые нам понадобятся
      ip_option_information = record
      Ttl : byte;
      Tos : byte;
      Flags : byte;
      OptionsSize : byte;
      OptionsData : pointer;
      end;

      type
      ICMP_ECHO_REPLY =
      record
      Address : TInAddr;
      Status : ULONG;
      RoundTripTime : ULONG;
      DataSize : Word;
      Reserved : Word;
      Data : Pointer;
      Options : IP_OPTION_INFORMATION;
      PingBuf: array[0..31] of char;
      end;

      function Ping(ip:string;timeout:cardinal):boolean;
      var
      Handle:THandle;// дискрипт icmp
      InAddr:TInAddr;// адрес который пинговать будем
      DW:DWORD;//результат пингования
      Reply:ICMP_ECHO_REPLY;// структура echo запроса
      PingBuf: array[0..31] of char;// буфер с данными для пинга
      begin
      result:=false;
      Handle:=IcmpCreateFile;// открываем ICMP
      if Handle=INVALID_HANDLE_VALUE then exit; // если неудалось открыть
      InAddr.S_addr:=inet_addr(Pansichar(ip)); // кого будем пинговать
      Reply.data:[email protected];// буфер для пингования
      Reply.DataSize:=32;// размер буфера
      DW:=IcmpSendEcho(Handle, InAddr, @PingBuf, 32, nil, @reply, SizeOf(icmp_echo_reply)+32,timeout);// посылаем пинг
      if DW<>0 then result:=true else result:=false;
      end;



      Теперь если нам необходимо проверить жив сервак или нет мы может использовать эту функцию так:


      Код:


      if Ping(‘xxx.xxx.xxx.xxx’,3000) then
      begin
      пингуется
      end
      else
      begin
      НЕ пингуется
      end;



      первым параметром задается IP адрес сервера. (доменное имя нельзя)


      вторым – время ожидания ответа на пинг в миллисекундах.

      т.е. 3000 – 3 секунды.



      ОЖИДАНИЕ ВХОДЯЩЕГО ПОДКЛЮЧЕНИЯ

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

      1) bind – ассоциирование локального адреса с сокетом.

      function bind( const s: TSocket; const addr: PSockAddr; const namelen: Integer ): Integer; stdcall;

      первый параметр – сокет


      второй – ссылка на структуру sockaddr_in в которой необходимо указать порт на котором мы будем ожидать подключение и IP адрес (для того чтобы привязать к определенному интерфейсу)

      третий – размер структуры sockaddr_in

      При удачном выполнении функция вернет 0

      Как видно это функция очень похожа connect.

      Также при ошибке можно воспользоваться функцией WSAGetLastError

      Самой распространенной ошибкой является WSAEADDRINUSE.


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



      2) listen – переводит сокет в режим ожидания входящих подключений.

      function listen(s: TSocket; backlog: Integer): Integer; stdcall;

      первый параметр – сокет

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



      3) accept - извлекает из очереди ожидающих подключений первое, затем создает новый сокет и возвращает его дескриптор и некоторое описание клиента. Размер этой очереди как раз и устанавливается в функции listen.




      function accept( const s: TSocket; var addr: TSockAddr; var addrlen: Integer ): TSocket; stdcall;

      первый параметр – сокет

      второй – адрес структуры sockaddr_in куда будет помещена информация о клиенте.

      третий – размер структуры sockaddr_in

      При удачно завершении функция возвращает дескриптор сокета. При неудачно - INVALID_SOCKET




      В сокет, который возвращает accept, можно использовать для приёма и передачи информации.

      Как я уже говорил ранее в addr будет возвращена информация о клиенте. Нам будет важна только:

      addr.sin_port – порт с которого было подключение

      addr.sin_addr – ip клиента в двоичном формате. Для преобразования в строковой формат используется функция inet_ntoa. Т.е. ipИспользование WinSock в Delphi. Часть 2char; ip:=inet_ntoa(addr.sin_addr);




      т.е. основная структура будет выглядеть так:


      Код:


      WSAStartup($202, ws);
      lsocket:=socket(AF_INET, SOCK_STREAM, 0);
      laddr.sin_family:=AF_INET;
      laddr.sin_port:=htons(1080);
      laddr.sin_addr.s_addr:=INADDR_ANY; // для всех интерфейсов
      if bind(lsocket,@laddr, sizeof(laddr))<>0 then
      begin
      writeln('[-] Bind');
      exit;
      end;
      if listen(lsocket, $100)<>0 then // максимум очередь - 256
      begin
      writeln('[-] Listen');
      exit;
      end;
      writeln('[+] Waiting connection');
      while true do // будем обрабатывать бесконечно все подключения
      begin
      csocket:=accept(lsocket,caddr,size_caddr);
      if csocket<> INVALID_SOCKET then
      begin
      writeln('[+] Client connected. IP=',inet_ntoa(caddr.sin_addr));
      работаем с сокетом. Читаем и принимаем данные
      closesocket(csocket); // закрываем сокет
      end;
      end;
      end;




      Хотя на деле лучше всего конструкцию while true do заменить на

      while переменаня do чтобы можно было прерывать бесконечный цикл



      ПОЛУЧЕНИЕ ДАННЫХ

      Вот мы и добрались до приема данных с сокета.

      Это самое простое что может быть. Для получения данных используется функция

      recv. Функция может применять и для TCP и для UDP соединение. Но всё же желательно использовать её только для TCP


      function recv(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;

      первый параметр – сокет

      второй – адрес буфера куда будут закинуты данные

      третий – размер этого буфера.

      Flags – специально опции чтения. Нам они особо не важны, по этому будем ставить его = 0

      При неудаче функция возвращает значение SOCKET_ERROR

      Если функция вернула значение = 0 – это свидетельствует о том, что клиент/сервер разорвал соединение и дальнейшее чтении уже ненужно и требуется лишь закрыть сокет.


      Все остальные значение – это размер считанных данных. Но в любом случае это кол-во не может быть больше чем размер буфера указанный в len.





      Для чтение данные передаваемых по UDP протоколу используется функция recvfrom

      function recvfrom(s: TSocket; var Buf; len, flags: Integer; var from: TSockAddr; var fromlen: Integer): Integer; stdcall;

      Она ничем не отличается от recv за исключением последних двух параметров.


      from: TSockAddr – адрес структуры в которую будет помещена информация о том, кто прислал данные (PORT и IP).

      fromlen: Integer – размер этой структуры.



      ПОСЫЛКА ДАННЫХ

      Для посылке данных используется функция send и sendto.Подробно описывать данные функции я не буду, потому что они полностью аналогичны recv и recvfrom за одним 2-мя лишь исключениями:


      - функции возвращают кол-во посланных, а не принятых данных

      - в sendto в поле addrto (аналог from из recvfrom) указывается адрес и порт того компьютера кому предназначаются эти данные.



      P.S. Если вы клиент и посылаете данные по UDP протоколу, то следует учесть следующее:

      1) Вы можете не делать connect а сразу после создания сокета слать данные через sendto указывая в нем адрес того, кому предназначаются данные


      2) Если вы используете connect то для посылке данных можно использовать функцию send. И тогда ip и порт сервера будут браться из структуры, которая была передана функции connect



      МНОГОПОТОЧНОСТЬ КЛИЕНТА

      Организация многопоточной работы для отправки данных особо ничем не отличается об однопоточной работы, просто весь код работы с сетью оформляется в виде отдельной процедуры(за исключением функции WSAStartup потому как её нужно делать только 1 раз), которая запускается в потоке через функцию CreateThread. Единственное о чем хочется упомянуть так это только о том, что при работе с глобальными переменными желательно делать простейшую синхронизацию. Выглядеть это будет примерно так:



      Код:


      Wait:Boolean=false; // глобальная переменная
      Procedure threadproc(param:pointer);stdcall;
      Begin
      .-.-.-.-.-.-.-.-.
      While (wait) do sleep(1); // ждем если идет работа с глобальными переменными
      Wait:=true;// ставим флаг что начали работать с глобальными переменными
      Работаем с глобальными переменными
      Wait:=false; // разрешаем другим потокам работать с глобальными переменными
      .-.-.-.-.-.-.-.-.-.-
      ExitThread(0); // ОБРАТИТЕ ВНИМАНИЕ 1
      End;
      Запуск потоков
      Wait:=false;
      For x:=1 to кол-во_потоков do
      Begin
      Thread[x]:=CreateThread(nil, 0, @ threadproc, param, 0, Thread[x]);
      End;



      Thread – это массив в который мы будем записывать дескрипторы созданных потоков, чтобы потом при необходимости можно было завершить их через TerminateThread

      Для более сложной синхронизации лучше применять критические секции.

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




      МНОГОПОТОЧНОСТЬ СЕРВЕРА

      Показанный выше код серверной части одновременно может обрабатывать только 1 запрос клиента. А это очень сильно снижает скорость работы. Для того чтобы организовать многотопочную обработку клиентов необходимо после получения сокета от функции accept, передать его созданному потоку. Программно это будет выглядеть так:




      Код:



      procedure ThreadProc(sock:tsocket);stdcall;
      begin
      работа с сокетом (recv/send)
      closesocket(sock);
      ExitThread(0); // ОБРАТИТЕ ВНИМАНИЕ 1
      end;


      Var
      ThID:dword;
      while true do // будем обрабатывать бесконечно все подключения
      begin
      csocket:=accept(lsocket,caddr,size_caddr);
      if csocket<> INVALID_SOCKET then
      begin
      writeln('[+] Client connected. IP=',inet_ntoa(caddr.sin_addr));
      ThID:=CreateThread(nil, 0, @ThreadProc, pointer(csocket), 0, ThID);
      CloseHandle(ThID); // ОБРАТИТЕ ВНИМАНИЕ 2
      end;
      end;




      Как видно из кода, после получения дескриптора сокета мы запускаем поток, и передаем в виде параметра, этот дескриптор.



      ОБРАТИТЕ ВНИМАНИЕ 1 – поток должен быть завершен командой ExitThread(0) т.к. это является более корректным завершением.

      ОБРАТИТЕ ВНИМАНИЕ 2 – очень важная деталь, о котором многие забывают. После создания потока мы получаем дескриптор потока. Но даже при завершении потока через ExitThread, всё равно дескриптор остается открытым. Со временем это приводит в утечки памяти, а также разного рода тормозам, когда невозможно запустить новые потоки.


      Т.к. мы не собираемся завершать поток насильно, то нам этот дескриптор не нужен и по этому мы его сразу закроем через CloseHandle.



      Ну вот всё ))

      (С) SLESH 2009
      • 0
       (голосов: 0)
      28 | 0

        Информация
        Информация

          Посетители, находящиеся в группе Гости, не могут оставлять комментарии в данной новости.

            Лучшее на Delphi Studio

            Лучшие книги по Delphi

            • Книга Delphi 2010 Handbook with Source Code
              Книга Delphi 2010 Handbook with Source Code - Книга Delphi 2010 Handbook with Source Code посвящена CodeGear Delphi 2010
            • Книга Программирование в Delphi глазами хакера. Фленов
              Книга Программирование в Delphi глазами хакера. Фленов - В книге вы найдете множество нестандартных приемов программирования на языке Delphi, его недокументированные функции и возможности. Вы узнаете, как создавать маленькие шуточные программы. Большая часть книги посвящена программированию сетей
            • Книга Delphi в шутку и всерьез что умеют хакеры М.Флёнов
              Книга Delphi в шутку и всерьез что умеют хакеры М.Флёнов - Электронная книга о профессиональных приемах программирования в Delphi. В легкой и доступной форме с использованием большого количества профессиональных примеров рассмотрены вопросы корректного написания кода, оптимизации программ, работы с системным окружением, создания сетевых приложений
            • Книга Библия Delphi Михаил Фленов (2-е издание) + CD
              Книга Библия Delphi Михаил Фленов (2-е издание) + CD - Книга посвящена программированию на языке Delphi от самых основ до примеров построения конкретных приложений. Подробно описывается логика выполнения каждого участка кода, чтобы читатель смог использовать эти знания при решении собственных задач. Книга содержит большое количество примеров практического программирования
            • Книга О чем не пишут в книгах по Delphi + CD Григорьев А.Б
              Книга О чем не пишут в книгах по Delphi + CD Григорьев А.Б - Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP
            • Книга Delphi 7 Учебный курс С.Бобровский
              Книга Delphi 7 Учебный курс С.Бобровский - Электронная книга является руководством по программированию в среде Delplii 7. Описывается весь процесс разработки программы: от конструирования диалогового окна до организации справочной системы и создания установочного CD-ROM
            • Книга Delphi Быстрый Старт
              Книга Delphi Быстрый Старт - В книге описываются интерфейс системы визуального программирования Delphi на основе 6-й версии, состав и характеристика элементов проекта приложения, приемы программирования на языке Object Pascal
            • Книга Indy in Depth. Глубины Indy
              Книга Indy in Depth. Глубины Indy - Книга Indy in Depth Глубины Indy будет интересно для тех, кто интересуется хакингом, вирусописанием, а значит и для тех, кто занимается защитой сетей, программ, информации. Эта книга не только по Indy, она про Интернет, про протоколы, термины, методы работы, а к Indy относятся только примеры
            • Книга OpenGL - Графика в проектах Delphi + CD. М.В.Краснов
              Книга OpenGL - Графика в проектах Delphi + CD. М.В.Краснов - Книга посвящена использованию стандартной графической библиотеки OpenGL в проектах Delphi. Начиная с самой минимальной программы, последовательно и подробно рассматриваются все основные принципы программирования компьютерной графики: двухмерные итрехмерные построения, анимация, работа с текстурой, визуальные эффекты
            • Книга Delphi 7 для профессионалов. Марко Кэнту
              Книга Delphi 7 для профессионалов. Марко Кэнту - Книга, которую должен прочитать каждый, кто хочет стать профессиональным программистом на Delphi. Книга не предназначена для начинающих. Требуются хорошие знания Delphi. Предназначена для тех, кто хочет стать именно профессиональным программистом

            Друзья сайта

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


            Поиск на Delphi Studio




            На Delphi Studio нашли

            delphi компоненты для работы в трей delphi компоненты для работы в трей
            delphi свернуть в трей delphi свернуть в трей
            Delphi вывести случайное число Delphi вывести случайное число
            обновить indy обновить indy
            права доступа к файлам delphi права доступа к файлам delphi
            скачать учебник delphi 7 скачать учебник delphi 7
            как скопировать в tstringlist из memo как скопировать в tstringlist из memo
            StringReplace delphi2010 StringReplace delphi2010
            delphi как нажать кнопку Button delphi как нажать кнопку Button
            Как добавить прграмму в Total Commander Как добавить прграмму в Total Commander
            delphi stringgrid нажатие на заголовок delphi stringgrid нажатие на заголовок
            как на delphi свернуть программу в трей как на delphi свернуть программу в трей
            загрузить файл на ftp вудзрш загрузить файл на ftp вудзрш
            delphi компонент анимация gif delphi компонент анимация gif
            vcl, определить размер файла vcl, определить размер файла
            delphi как проверить существует ли фаил delphi как проверить существует ли фаил
            учебник delphi 2010 учебник delphi 2010
            как скопировать первые 10 знаков string delphi как скопировать первые 10 знаков string delphi
            icq база данных исходник icq база данных исходник
            скачать бесплатно исходники для delphi 7 скачать бесплатно исходники для delphi 7
            создать twebbrowser программно создать twebbrowser программно
            delphi bitblt jgbcfybt delphi bitblt jgbcfybt
            архангельский delphi 7 скачать архангельский delphi 7 скачать
            форму панель задач Delphi форму панель задач Delphi
            язык Delphe.net язык Delphe.net
            открыть форму дельфи открыть форму дельфи
            программно перезагрузить программно перезагрузить
            компонент CoolTrayIcon компонент CoolTrayIcon
            delphi скачать картинку delphi скачать картинку
            Delphi считать случайную строку из файла Delphi считать случайную строку из файла
            ClientSocket отправка файлов delphi
ClientSocket отправка файлов delphi
            создание webbrowser на Delphi 7
создание webbrowser на Delphi 7
            idhttp размер
idhttp размер
            работа со строками в паскале примеры
работа со строками в паскале примеры
            развернуть строку в delphi
развернуть строку в delphi
            Tform delphi расположить окна поверх
Tform delphi расположить окна поверх
            задачи по паскалю с решениями
задачи по паскалю с решениями


            Информация

            Сайт Delphi Studio рассчитан для начинающих, новичков, чайников, которые решили программировать на Delphi :)
            Добавляйте свои примеры, исходники, компоненты, статьи и тогда на сайте будет много полезной информации, что поможет друг другу находить нужный материал.


            Случайные новости

            Книга Delphi 7 для профессионалов. Марко Кэнту
            Книга Delphi 7 для профессионалов. Марко Кэнту - Книга, которую должен прочитать каждый, кто хочет стать профессиональным программистом на Delphi. Книга не предназначена для начинающих. Требуются хорошие знания Delphi. Предназначена для тех, кто хочет стать именно профессиональным программистом
            Книга Delphi в шутку и всерьез что умеют хакеры М.Флёнов
            Книга Delphi в шутку и всерьез что умеют хакеры М.Флёнов - Электронная книга о профессиональных приемах программирования в Delphi. В легкой и доступной форме с использованием большого количества профессиональных примеров рассмотрены вопросы корректного написания кода, оптимизации программ, работы с системным окружением, создания сетевых приложений
            Как открыть и закрыть CD-ROM в Windows (MMSystem). Пример на Delphi
            Как открыть и закрыть CD-ROM в Windows (MMSystem). Пример на Delphi - Пример на Delphi показывающий как можно открыть и закрыть CD-ROM в Windows. В uses нужно дабвить MMSystem


            Опрос

            Что вы хотите больше на Delphi Studio?
            Исходники
            Книги, Учебники
            Компоненты
            Статьи
            Примеры



            Информация

            А знаете ли вы что такие известные программы как AIMP, Skype, QIP, QIP Infium, R&Q, The Bat!, FL Studio, Guitar Pro, Game Maker, Total Commander, PowerArchiver, Download Master написаны на Delphi? И это далеко не весь список программ чем может похвастаться продукция Borland Delphi!


            rss