А. Григорьев - О чём не пишут в книгах по Delphi
Несмотря на изменение протокола, новый сервер был бы вполне совместим со старым клиентом SimpleClient (см. разд. 2.1.11), если бы не отправлял сообщения по своей инициативе. Действительно, прочие изменения в протоколе разрешают клиенту отправлять новые сообщения до получения ответа сервера, но не обязывают его делать это. В класс TClientThread добавлено логическое поле FServerMsg. Если оно равно False, то сервер не посылает клиентам сообщений по собственной инициативе, т.е. работает в режиме совместимости со старым клиентом. Поле FServerMsg инициализируется в соответствии с параметром, переданным в конструктор, т.е. в соответствии с состоянием галочки Сообщения от сервера, расположенной на главной форме. Если перед запуском сервера она снята, сервер не будет сам посылать сообщения, и старый клиент сможет обмениваться данными с ним.
Запуск сервера практически не отличается от запуска сервера MultithreadedServer (см. листинг 2.19), только теперь объект, созданный конструктором, запоминается классом главной формы, чтобы потом можно было сервер остановить. Остановка осуществляется методом StopServer (листинг 2.65).
Листинг 2.65. Метод StopServer// Остановка сервера
procedure TServerForm.StopServer;
begin
// Запрещаем кнопку, чтобы пользователь не мог нажать ее
// еще раз, пока сервер не остановится.
BtnStopServer.Enabled := False;
// Ожидаем завершения слушавшей нити. Так как вывод сообщений
// эта нить осуществляет через Synchronize, выполняемый главной
// нитью в петле сообщений, вызов метода WaitFor мог бы привести
// к взаимной блокировке: главная нить ждала бы, когда завершится
// нить TListenThread, а та, в свою очередь - когда главная нить
// выполнит Synchronize. Чтобы этого не происходило, организуется
// ожидание с локальной петлей сообщений.
if Assigned(FListenThread) then
begin
FListenThread.StopServer;
while Assigned(FListenThread) do
begin
Application.ProcessMessages;
Sleep(10);
end;
end;
end;
Данный метод вызывается в обработчике нажатия кнопки Остановить и при завершении приложения. Сервер можно многократно останавливать и запуска вновь, не завершая приложение.
Чтобы увидеть все возможности сервера, потребуется новый клиент. На компакт-диске он называется EventSelectClient, но "EventSelect" в данном случае означает только то, что клиент является парным к серверу EventSelectServer. Сам клиент функцию WSAEventSelect не использует, поскольку она неудобна, когда нужно работать только с одним сокетом. Поэтому клиент работает в асинхронном режиме, основанном на сообщениях, т.е. посредством функции WSAAsyncSelect.
Клиент может получать от сервера сообщения двух видов: те. которые сервер посылает в ответ на запросы клиента, и те, которые он посылает по собственной инициативе. Но различить эти сообщения клиент не может: например, если клиент отправляет запрос серверу, а потом получает от него сообщение, он не может определить, то ли это сервер ответил на его запрос, то ли именно в этот момент сервер сам отправил клиенту свое сообщение. Соответственно, сообщения обоих типов читает один и тот же код.
ПримечаниеВ принципе, протокол мог бы быть определен таким образом, что ответы на запросы клиента и сообщения, посылаемые сервером по собственной инициативе, имели бы разный формат, по которому их можно было бы различить и читать по-разному. Но даже при этом форматы нельзя различить, пока сообщение не будет прочитано хотя бы частично, так что начало чтения будет выполняться единообразно в любом случае.
Подключение клиента к серверу выполняется точно так же, как в листинге 2.16, за исключением того, что после выполнения функции connect сокет переводится в асинхронный режим, и его события FD_READ и FD_CLOSE связываются с сообщением WM_SOCKETMESSAGE. Обработчик этого сообщения приведен в листинге 2.66.
Листинг 2.66. Получение данных клиентомprocedure TESClientForm.WMSocketMessage(var Msg: TWMSocketMessage);
const
// Размер буфера для получения данных
RecvBufSize = 4096;
var
// Буфер для получения данных
RecvBuf: array[0..RecvBufSize - 1] of Byte;
RecvRes: Integer;
P: Integer;
begin
// Защита от "тупой" ошибки
if Msg.Socket <> FSocket then
begin
MessageDlg('Внутренняя ошибка программы — неверный сокет',
mtError, [mbOK], 0);
Exit;
end;
if Msg.SockError <> 0 then
begin
MessageDlg('Ошибка при взаимодействии с сервером'#13#10 +
GetErrorString(Msg.SockError), mtError, [mbOK], 0);
OnDisconnect;
Exit;
end;
case Msg.SockEvent of
FD_READ:
// Получено сообщение от сервера
begin
// Читаем столько, сколько можем
RecvRes := recv(FSocket, RecvBuf, RecvBufSize, 0);
if RecvRes > 0 then
begin
// Увеличиваем строку на размер прочитанных данных
P := Length(FRecvStr);
SetLength(FRecvStr, P + RecvRes);
// Копируем в строку полученные данные
Move(RecvBuf, FRecvStr[Р + 1], RecvRes);
// В строке может оказаться несколько строк от сервера,
// причем последняя может прийти не целиком.
// Ищем в строке символы #0, которые, согласно протоколу,
// являются разделителями строк.
P := Pos(#0, FRecvStr));
while P > 0 do
begin
AddMessageToRecvMemo('Сообщение от сервера: ' +
Copy(FRecvStr, 1, P - 1));
// Удаляем из строкового буфера выведенную строку
Delete(FRecvStr, 1, P);
P := Pos(#0, FRecvStr);
end;
end
else if RecvRes = 0 then
begin
MessageDlg('Сервер закрыл соединение'#13#10 +
GetErrorString, mtError, [mbOK], 0);
OnDisconnect;
end
else
begin
if WSAGetLastError <> WSAEWOULDBLOCK then
begin
MessageDlg('Ошибка при получении данных от клиента'#13#10 +
GetErrorString, mtError, [mbOK], 0);
OnDisconnect;
end;
end;
end;
FD_CLOSE: begin
MessageDlg('Сервер закрыл соединение', mtError, [mbOK], 0);
shutdown(FSocket, SD_BOTH);
OnDisconnect;
end;
else begin
MessageDlg('Внутренняя ошибка программы — неизвестное событие ' +
IntToStr(Msg.SockEvent), mtError, [mbOK], 0);
OnDisconnect;
end;
end;
end;
Здесь мы используем новый способ чтения данных. Он во многом похож на тот, который применен в сервере. Функция recv вызывается один раз за один вызов обработчика значений и передаст данные в буфер фиксированного размера RecvBuf. Затем в буфере ищутся границы отдельных строк (символы #0), строки, полученные целиком, выводятся. Если строка получена частично (а такое может случиться не только из-за того, что она передана по частям, но и из-за того, что в буфере просто не хватило место для приема ее целиком), её начало следует сохранить в отдельном буфере, чтобы добавить к тому, что будет прочитано при следующем событии FD_READ. Этот буфер реализуется полем FRecvStr типа string. После чтения к содержимому этой строки добавляется содержимое буфера RecvBuf, а затем из строки выделяются все подстроки, заканчивающиеся на #0. То, что остается в строке FRecvStr после этого, — это начало строки, прочитанной частично. Оно будет учтено при обработке следующего события FD_READ.
ПримечаниеОписанный алгоритм разбора буфера прост, но неэффективен с точки зрения нагрузки на процессор и использования динамической памяти, особенно в тех случаях, когда в буфере RecvBuf оказывается сразу несколько строк. Это связано с тем, что при добавлении содержимого RecvBuf к FRecvStr и последующем поочередном удалении строк из FRecvStr происходит многократное перераспределение памяти, выделенной для строки. Алгоритм можно оптимизировать: все строки, которые поместились в RecvBuf целиком, выделять непосредственно из этого буфера, не помещая в FRecvStr, а помещать туда только то, что действительно нужно сохранить между обработкой разных событий FD_READ. Реализацию такого алгоритма рекомендуем выполнить в качестве самостоятельного упражнения.
При отправке данных вероятность того, что функция send не сможет быть выполнена сразу, достаточно мала. Кроме того, как мы уже говорили, блокировка клиента при отправке данных часто бывает вполне приемлема из-за редкости и непродолжительности. Таким образом, блокирующий режим из-за своей простоты наиболее удобен при отправке данных серверу клиентом. Но мы не можем перевести сокет, работающий в асинхронном режиме, в блокирующий режим на время отправки, зато можем этот режим имитировать. Занимается этим метод SendString (листинг 2.67).