KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программирование » А. Григорьев - О чём не пишут в книгах по Delphi

А. Григорьев - О чём не пишут в книгах по Delphi

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн А. Григорьев, "О чём не пишут в книгах по Delphi" бесплатно, без регистрации.
Перейти на страницу:

 bind(ServSock, Addr, SizeOf(Addr));

 listen(ServSock, SOMAXCONN);

 // Перевод сокета в асинхронный режим. Кроме события FD_ACCEPT

 // указаны также события FD_READ и FD_CLOSE, которые никогда не

 // возникают на сокете, установленном в режим прослушивания.

 // Это сделано потому, что сокеты, созданные с помощью функции

 // accept, наследуют асинхронный режим, установленный для

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

 // функцию WSAAsyncSelect для этих сокетов - для них сразу

 // будет назначен обработчик событий FD_READ и FD_CLOSE.

 WSAAsyncSelect(ServSock, Handle, WM_SOCKETEVENT, FD_READ or FD_ACCEPT or FD_CLOSE);

end;


procedure TForm1.FormDestroy(Sender: TObject);

begin

 closesocket(ServSock);

 WSACleanup;

end;


procedure TForm1.WMSocketEvent(var Msg: TMessage);

var

 Sock: TSocket;

 SockError: Integer;

begin

 Sock := TSocket(Msg.WParam);

 SockError := WSAGetSelectError(Msg.lParam);

 if SockError <> 0 then

 begin

  // Здесь должен быть анализ ошибки

  closesocket(Sock);

  Exit;

 end;

 case WSAGetSelectEvent(Msg.lParam) of

 FD_READ: begin

  // Пришел запрос от клиента. Необходимо прочитать данные,

  // сформировать ответ и отправить его.

 end;

 FD_АССЕРТ: begin

  // Просто вызываем функция accept. Ее результат нигде не

  // сохраняется, потому что вновь созданный сокет автоматически

  // начинает работать в асинхронном режиме, и его дескриптор

  // при необходимости будет передан через Msg.wParam при

  // возникновение события

  accept(Sock, nil, nil);

 end;

 FD_CLOSE:

 begin

  // Получив от клиента сигнал завершения, сервер, в принципе,

  // может попытаться отправить ему данные. После этого сервер

  // также должен закрыть соединение со своей стороны

  shutdown(Sock, SD_SEND);

  closesocket(Sock);

 end;

 end;

end;


end.

Преимущество такого сервера по сравнению с сервером, основанным на функции select, заключается в том, что он не должен постоянно проверять наличие полученных данных — когда данные поступят, он без дополнительных усилий получит уведомление об этом. Кроме того, этот сервер не имеет проблем, связанных с количеством сокетов в множестве типа TFDSet. Впрочем, последнее несущественно, т.к. при таком количестве клиентов сервер обычно реализует другие, более производительные способы взаимодействия с клиентами.

2.2.6. Пример сервера, основанного на сообщениях

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

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

Примечание

Вообще говоря, ситуация, когда сервер не отправит данные за один раз, и их отправка растянется на несколько итераций петли сообщений, настолько редка, что при разработке сервера, к которому не предъявляются повышенные требования по надежности, ее можно было бы вообще не учитывать. Соответственно, возможность получения сервером нового уведомления о поступлении данных до того, как на старое сообщение будет дан ответ, возможна только тогда, когда клиент не соблюдает принятый протокол и посылает несколько сообщений подряд, не дожидаясь ответа. Наш пример призван продемонстрировать наиболее надежный к подобным действиям клиента сервер, поэтому мы его напишем "по всем правилам".

Как обычно, работа сервера начинается с инициализации слушающего сокета, выполняющейся при нажатии кнопки Запустить (листинг 2.50).

Листинг 2.50. Инициализация сервера, основанного на сообщениях

procedure TServerForm.BtnStartServerClick(Sender: TObject);

var

 // Адрес, к которому привязывается слушающий сокет

 ServerAddr: TSockAddr;

begin

 // Формируем адрес для привязки.

 FillChar(ServerAddr.sin_zero, SizeOf(ServerAddr.sin_zero), 0);

 ServerAddr.sin_family := AF_INET;

 ServerAddr.sin_addr.S_addr := INADDR_ANY;

 try

  ServerAddr.sin_port := htons(StrToInt(EditPortNumber.Text));

  if ServerAddr.sin_port = 0 then

  begin

   MessageDlg('Номер порта должен находиться в диапазоне 1-65535',

    mtError, [mbOK], 0);

   Exit;

  end;

  // Создание сокета

  FServerSocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

  if FServerSocket = INVALID_SOCKET then

  begin

   MessageDlg('Ошибка при создании сокета:'#13#10 +

    GetErrorString, mtError, [mbOK], 0);

   Exit;

  end;

  // Привязка сокета к адресу

  if bind(FServerSocket, ServerAddr, SizeOf(ServerAddr)) = SOCKET_ERROR then

  begin

   MessageDlg('Ошибка при привязке сокета к адресу:'#13#10 +

    GetErrorString, mtError, [mbOK], 0);

   closesocket(FServerSocket);

   Exit;

  end;

  // Перевод сокета в режим прослушивания

  if listen(FServerSocket, SOMAXCONN) = SOCKET_ERROR then

  begin

   MessageDlg('Ошибка при переводе сокета в режим прослушивания:'#13#10 +

    GetErrorString, mtError, [mbOK], 0);

   closesocket(FServerSocket);

   Exit;

  end;

  // Связь слушающего сокета с событием FD_ACCEPT

  if WSAAsyncSelect(FServerSocket, Handle,

WM_ACCEPTMESSAGE, FD_ACCEPT) = SOCKET_ERROR then

  begin

   MessageDlg('Ошибка при установке асинхронного режима ' +

    'cлушающего сокета:'#13#10 + GetErrorString, mtError, [mbOK], 0);

   closesocket(FServerSocket);

   Exit;

  end;

  // Перевод элементов управления в состояние "Сервер работает"

  LabelPortNumber.Enabled := False;

  EditPortNumber.Enabled := False;

  BtnStartServer.Enabled := False;

  LabelServerState.Caption := 'Сервер работает';

 except

  on EConvertError do

   // Это исключение может возникнуть только в одном месте -

   // при вызове StrToInt(EditPortNumber.Text)

   MessageDlg('"' + EditPortNumber.Text +

    '" не является целый числом', mtError, [mbOK], 0);

  on ERangeError do

   // Это исключение может возникнуть только в одном месте -

   // при присваивании значения номеру порта

   MessageDlg('Номер порта должен находиться в диапазоне 1-65535',

    mtError, [mbOK], 0);

 end;

end;

Этот код мало чем отличается от того, что мы уже видели (сравните, например, с листингами 2.19 и 2.30). Единственное существенное отличие здесь — вызов функции WSAAsyncSelect после перевода сокета в режим прослушивания. Этот вызов связывает событие FD_ACCEPT с сообщением WM_ACCEPTMESSAGE.

Сообщение WM_ACCEPTMESSAGE нестандартное, мы должны сами определить его. Использовать это сообщение сервер будет только для определения момента подключения нового клиента, определять момент прихода данных мы будем с помощью другого сообщения — WM_SOCKETMESSAGE, которое тоже нужно определить. И, чтобы легче было писать обработчики для этих сообщений, объявим тип TWMSocketMessage, "совместимый" с типом TMessage (листинг 2.51).

Листинг 2.51. Сообщения, связанные с сокетами, и тип TWMSocketMessage

const

 WM_ACCEPTMESSAGE = WM_USER + 1;

 WM_SOCKETMESSAGE = WM_USER + 2;

type

 TWMSocketMessage = packed record

  Msg: Cardinal;

  Socket: TSocket;

  SockEvent: Word;

  SockError: Word;

 end;

Прежде чем реализовывать реакцию на эти сообщения, нужно позаботиться об обработке ошибок. Функция GetErrorString (см. листинг 2.6), столько времени служившая нам верой и правдой, нуждается в некоторых изменениях. Это связано с тем, что теперь код ошибки может быть получен не только в результате вызова функции WSAGetLastError, но и через параметр SockError сообщения. Новый вариант функции GetErrorString иллюстрирует листинг 2.52.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*