Сокеты в Delphi

Автор статьи: Danil
Сайт Автора: danil.dp.ua
E-mail Автора: post@danil.dp.ua
Дата публикации: 20.12.2005

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

int WSACleanup;

"деинициализирует" 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[0];
      sa.S_un_b.s_b2 := pa[1];
      sa.S_un_b.s_b3 := pa[2];
      sa.S_un_b.s_b4 := pa[3];
      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. Статья и программа предоставлена в целях обучения и вся ответственность за использование ложится на твои хилые плечи.