Если к серверу подключаются некорректно работающие клиенты, которые не присылают данные после подключения, операция может не завершаться очень долго, что будет мешать подключению новых клиентов. MSDN рекомендует при ожидании время от времени с помощью функции getsockopt для сокета sAcceptSocket узнавать значение целочисленного параметра SO_CONNECT_TIME уровня SOL_SOCKET. Этот параметр показывает время в секундах, прошедшее с момента подключения клиента к данному сокету (или -1, если подключения не было). Если подключившийся клиент слишком долго не присылает данных, сокет sAcceptSocket следует закрыть, что приведет к завершению операции, начатой AcceptEx, с ошибкой. После этого можно снова вызывать AcceptEx для приема новых клиентов.
Функция AcceptEx реализована таким образом, чтобы обеспечивать максимальную скорость подключения. Ранее мы говорили, что сокеты, созданные функциями accept и WSAAccept, наследуют параметры слушающего сокета (например, свойства асинхронного режима). Для повышения производительности сокет sAcceptSocket по умолчанию не получает свойств сокета sListenSocket. Но он может унаследовать их после завершения операции с помощью следующей установки параметра сокета SO_UPDATE_ACCEPT_CONTEXT:
setsockopt(sAcceptSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, PChar(@sListenSocket), SizeOf(sListenSocket));
Ha сокет sAcceptedSocket после его подключения к клиенту накладываются ограничения: он может использоваться не во всех функциях WinSock, а только в следующих: send, WSASend, recv, WSARecv, ReadFile, WriteFile, TransmitFile, closesocket и setsockopt, причем в последней — только для установки параметра SO_UPDATE_ACCEPT_CONTEXT.
В WinSock не документируется, в какую именно часть буфера помещаются адрес клиента и принявшего его сокета. Вместо этого предоставляется функция GetAcceptExSockAddrs, прототип которой приведен в листинге 2.83.
Листинг 2.83. Функция GetAcceptExSockAddrs
procedure GetAcceptExSockAddrs(lpOutputBuffer: Pointer; dwReceiveDataLength: DWORD; dwLocalAddressLength: DWORD; dwRemoteAddressLength: DWORD; var LocalSockaddr: PSockAddr; var LocalSockaddrLength: Integer; var RemoteSockaddr: PSockAddr; var RemoteSockaddrLength: Integer);
Примечание
В Delphi до 7-й версии включительно модуль WinSock содержит ошибку — параметры LocalSockaddr и RemoteSockaddr функции GetAcceptExSockAddrs имеют в нем тип TSockAddr вместо PSockAddr. Из-за этой ошибки функцию GetAcceptExSockAddrs в этих версиях Delphi необходимо самостоятельно импортировать. Следует заметить, что во многих модулях для WinSock 2 от независимых разработчиков объявление этой функции скопировано из стандартного модуля вместе с ошибкой.
Первые четыре параметра функции GetAcceptExSockAddrs определяют буфер, в котором в результате вызова AcceptEx оказались данные от клиента и адреса, и размеры частей буфера, зарезервированных для данных и для адресов. Значения этих параметров должны совпадать со значениями аналогичных параметров в соответствующем вызове AcceptEx. Через параметр LocalSockaddrs возвращается указатель на то место в буфере, в котором хранится адрес привязки сокета sAcceptSocket, а через параметр LocalSockaddrsLength — длина адреса (16 в случае TCP). Адрес клиента и его длина возвращаются через параметры RemoteSockaddrs и RemoteSockaddrsLength. Следует особенно подчеркнуть, что указатели LocalSockaddrs и RemoteSockaddrs указывают именно на соответствующие части буфера: память для них специально не выделяется и, следовательно, не должна освобождаться, а свою актуальность они теряют при освобождении буфера.
Последняя из дополнительных функций, TransmitFile, служит для передачи файлов по сети. Ее прототип приведен в листинге 2.84.
Листинг 2.84. Функция TransmitFile
function TransmitFile(hSocket: TSocket; hFile: THandle; nNumberOfBytesToWrite, nNumberOfBytesPerSend: DWORD; lpOverlapped: POverlapped; lpTransmitBuffers: PTransmitFileBuffers; dwReserved: DWORD): BOOL;
Функция TransmitFile отправляет содержимое указанного файла через указанный сокет. При этом допускаются только протоколы, поддерживающие соединение, т.е. использовать данную функцию с UDP-сокетом нельзя. Сокет задается параметром hSocket, файл — параметром hFile. Дескриптор файла обычно получается с помощью функции стандартного API CreateFile. Файл рекомендуется открывать с флагом FILE_FLAG_SEQUENTIAL_SCAN, т.к. это повышает производительность.
Параметр nNumberOfBytesToWrite определяет, сколько байтов должно быть передано (позволяя, тем самым, передавать не весь файл, а только его часть). Если этот параметр равен нулю, передается весь файл.
Функция TransmitFile кладет данные из файла в буфер сокета по частям. Параметр nNumberOfBytesPerSend определяет размер одной порции данных. Он может быть равен нулю — в этом случае система сама определяет размер порции. Этот параметр критичен только в случае дейтаграммных протоколов, потому что при этом размер порции определяет размер дейтаграммы. Для TCP данные, хранящиеся в буфере, передаются в сеть целиком или по частям в зависимости от загрузки сети, готовности принимающей стороны и т.п., а какими порциями они попали в буфер, на размер пакета почти не влияет. Поэтому для TCP-сокета параметр nNumberOfBytesPerSend лучше установить равным нулю.
Параметр lpOverlapped указывает на запись TOverlapped, использующуюся для перекрытого ввода-вывода. Эту структуру мы обсуждали при описании функции AcceptEx. В отличие от AcceptEx, в TransmitFile этот параметр добыть равным nil, и тогда операция передачи файла не будет перекрытой.
Если параметр lpOverlapped равен nil, передача файла начинается с той позиции, на которую указывает файловый указатель (для только что открытого файла этот указатель указывает на его начало, а переместить его можно, например, с помощью функции SetFilePointer; также он перемещается при чтении файла с помощью ReadFile). Если же параметр lpOverlapped задан, то передача файла начинается с позиции, заданной значениями полей Offset и OffsetHigh, которые должны содержать соответственно младшую и старшую часть 64-битного смещения стартовой позиции от начала файла.
Параметр lpTransmitBuffers является указателем на запись TTransmitFileBuffers, объявленную так, как показано в листинге 2.85.
Листинг 2.85. Тип TTransmitFileBuffers
PTransmitFileBuffers = ^TTransmitFileBuffers;
_TRANSMIT_FILE_BUFFERS = record
Head: Pointer;
HeadLength: DWORD;
Tail: Pointer;
TailLength: DWORD;
end;
TTransmitFileBuffers = _TRANSMIT_FILE_BUFFERS;
С ее помощью можно указывать буферы, содержащие данные, которые должны быть отправлены перед передачей самого файла и после него. Поле Head содержит указатель на буфер, содержащий данные, предназначенные для отправки перед файлом, HeadLength — размер этих данных. Аналогично Tail и TailLength определяют начало и длину буфера с данными, которые передаются после передачи файла. Если передача дополнительных данных не нужна, параметр lpTransmitBuffer может быть равен nil.
Допускается и обратная ситуация: параметр hFile может быть равен нулю, тогда передаются только данные, определяемые параметром lpTransmitBuffer.
Последний параметр функции TransmitFile в модуле WinSock имеет имя Reserved. В WinSock 1 он и в самом деле был зарезервирован и не имел смысла, но в WinSock 2 через него передаются флаги, управляющие операцией передачи файла. Мы не будем приводить здесь полный список возможных флагов (он есть в MSDN), а ограничимся лишь самыми важными. Указание флага TF_USE_DEFAULT_WORKER или TF_USE_SYSTEM_THREAD позволяет повысить производительность при передаче больших файлов, a TF_USE_KERNEL_APC — при передаче маленьких файлов. Вообще, при работе с функцией TransmitFile чтение файла и передачу данных в сеть осуществляет ядро операционной системы, что приводит к повышению быстродействия по сравнению с использованием ReadFile и send самой программой.
Функция TransmitFile реализована по-разному в серверных версиях Windows NT/2000 и в остальных системах: в серверных версиях она оптимизирована по быстродействию, а в остальных — по количеству необходимых ресурсов.
Данные, переданные функцией TransmitFile, удаленная сторона должна принимать обычным образом, с помощью функций recv/WSARecv.
На этом мы заканчиваем рассмотрение WinSock. Многие возможности этого стандарта остались не рассмотренными и даже не упомянутыми. Но для этого существуют книги, подобные [3]. Нашей же основной задачей было последовательное знакомство с базовыми возможностями WinSock API и способам их применения в Delphi.
Следует отметить, что в Delphi не обязательно напрямую использовать WinSock API, чтобы работать с сокетами, т.к. VCL содержит компоненты для этого. Прежде всего это TServerSocket и TClientSocket, использующие асинхронные сокеты, основанные на оконных сообщениях. Начиная с Delphi 7, к ним добавились компоненты TTCPServer, TTCPClient и TUDPSocket, использующие блокирующие или неблокирующие сокеты. Кроме того, с Delphi поставляется библиотека Indy, которая тоже содержит компоненты для работы с сокетами. Но практика показывает, что освоить эти компоненты без знания особенностей WinSock API очень сложно, так что даже если вы никогда не будете вызывать функции WinSock API явно, а ограничитесь компонентами. информация, изложенная в этой главе, вам все равно пригодится.