![]() |
||
В повседневной хакерской жизни часто случается необходимость включить в свой софт какой-нибудь быстрый сервер - будь то прокси, ftp, mail или master-server управляющий ботами... а может вам просто необходим сврех быстрый эхо сервер (чем мы сегодня и будем заниматься =)). Так или иначе если вы хотите добиться от Winsock максимальных скоростей при большом количестве подключений- то этот туториал для вас ;) Предпологается что читатель знаком с концепцией Windows sockets. Main theme HANDLE CreateIoCompletionPort ( HANDLE FileHandle, // file handle to associate with // the I/O completion port HANDLE ExistingCompletionPort, // handle to the I/O completion port DWORD CompletionKey, // per-file completion key for I/O // completion packets DWORD NumberOfConcurrentThreads // number of threads allowed to // execute concurrently );С помощью данной функции мы и создадим порт завершения. Пока что наиболее важный для нас параметр NumberOfConcurrentThreads - определяет число потоков, которые могут одновременно выполняться на порту. Для достижения наибольшей производительности каждый порт должен обслуживаться только одним потоком на процессор, для избежания переключения контекстов. Если задать значение 0 мы получим число потоков, равное числу процессоров, те создавать порт завершения мы будем таким образом: CompletionPort = CreateIoCompletionPort (INVALID_HANDLE_VALUE, NULL, 0, 0);После создания порта завершения нам необходимо создать несколько рабочих потоков для обслуживания порта. Вот тут можно остановиться и предаться теоретическим измышлениям - количество потоков будет зависеть только от вашей программы - параметр NumberOfConcurrentThreads и реальное количество рабочих потоков могут (и в реальных условиях скорее всего будут) отличаться друг от друга. Остановимся подробнее на параметре NumberOfConcurrentThreads - он разрешает только определённому количеству потоков работать с портом завершения одновременно (даже если вы создадите гораздо больше потоков*). Однако некоторые потоки могут приостанавливать своё выполнение (например proxy сервер - ждёт ответа с помощью WaitForSingleObject) - и тогда в это время другой поток сможет работать с портом завершения вместо него. Те если ваша программа будет блокировать поток - тогда лучше создать рабочих потоков несколько больше чем NumberOfConcurrentThreads, с другой стороны - если в вашей программе не будет блокировки - тогда не стоит создавать лишних потоков. Примечание: * - иногда в силу каких-то внутренних глюков число потоков может превысить значение NumberOfConcurrentThread на какой-то маленький промежуток бесконечности, но потом всё возвращается на круги своя - так что по большому счёту можно считать NumberOfConcurrentThread константой.Когда мы наконец определимся с количеством потоков необходимым для нормальной работы программы необходимо связать между собой сокеты и порты завершения - это делается повторным вызовом CreateIoCompletionPort, при этом параметры принимают следующие значения:
//рабочий поток порта завершения: DWORD WINAPI ServerThread (LPVOID CompletionPortID){ } ........ // somewhere inside main function // //создаём порт завершения: CompletionPort = CreateIoCompletionPort (INVALID_HANDLE_VALUE, NULL, 0, 0); //создание рабочих потоков - мы создаём два потока на процессор: GetSystemInfo (&SysInfo); for (i = 0; i < SysInfo.dwNumberOfProcessors * 2; i ++){ // создаём рабочий поток - serverThread, в качестве параметра передаём ей порт завершения Thread = CreateThread (NULL, 0, ServerThread, CompletionPort, 0, &ThreadId); CloseHandle (Thread); } //создание сокета: Socket = WSASocket (AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); server.sin_family = AF_INET; server.sin_addr.s_addr = htonl (INADDR_ANY); server.sin_port = htons (31337); bind (Socket, (PSOCKADDR) &server, sizeof (server)); listen (Socket, 5); while (TRUE){ //приём соединения Accept = WSAAccept (Socket, NULL, NULL, NULL, 0); // Заполнение PerHandleData ..... // привязка сокета к порту завершения - мы передаём какие-нибудь данные о соединение через PerHandleData: CreateIoCompletionPort ((HANDLE)Accept, CompletionPort, (DWORD)PerHandleData, 0); //начинаем обрабатывать ввод-вывод на сокете - отправляем что-нить или получаем инфу: // WSARecv / WSASend .... }Учтите что функции WSASend/WSARecv будут возвращать ошибку SOCKET_ERROR со статусом WSA_IO_PENDING, которую необходимо игнорировать. Теперь пришло время обратится к реализации функции ServerThread - рабочего потока порта завершения. Так как модель портов завершения использует перекрытый ввод-вывод то все функции WinSock будут завершаться сразу после вызова. Наша задача извлечь результаты из структуры OVERLAPPED. Для этого мы должны поставить в очередь ожидания на порту завершения рабочие потоки. Достигается это с использованием функции GetQueuedCompletionStatus: BOOL GetQueuedCompletionStatus( HANDLE CompletionPort, // the I/O completion port of interest LPDWORD lpNumberOfBytesTransferred, // to receive number of bytes // transferred during I/O LPDWORD lpCompletionKey, // to receive file's completion key LPOVERLAPPED *lpOverlapped, // to receive pointer to OVERLAPPED // structure DWORD dwMilliseconds // optional timeout value );Параметры этой функции следующие:
typedef struct { OVERLAPPED Overlapped; //любые полезные нам данные: WSABUF DataBuf; CHAR Buffer[DATA_BUFSIZE]; DWORD BytesSend; DWORD BytesRecv; DWORD OperationType; DWORD TotalBytes; ..... } PER_IO_OPERATION_DATA;После обязательной структуры OVERLAPPED мы размещаем буффер для хранения данных данных, количество переданных/принятых байт, тип операции - отправка/приём, адрес и порт клиента... любой параметр который может быть полезен вам, исходя из требований к серверу. Таким образом при вызове WinSock функций мы должны передавать нашу структуру. Например таким образом: PER_IO_OPERATION_DATA PerIoData; ... WSARecv (...., (OVERLAPPED *)&PerIoData);И наконец поговорим о том как коректно завершить работу порта завершения - главное не освобождать память со структурой OVERLAPPED, пока выполняется какая-нибудь операция на сокете. После того как вы закроете все сокеты - необходимо завершить все рабочие потоки порта завершения. Для этого воспользуемся функцией PostQueuedCompletionStatus - которая отправит потоку пакет, заставляющий прекратить работу: BOOL PostQueuedCompletionStatus( HANDLE CompletionPort, // handle to an I/O completion port DWORD dwNumberOfBytesTransferred, // return via GetQueuedCompletionStatus DWORD dwCompletionKey, // return via GetQueuedCompletionStatus LPOVERLAPPED lpOverlapped // return via GetQueuedCompletionStatus );Параметр CompletionPort - задаёт порт завершения, а остальные параметры задают значения, которые поток получит из функции GetQueuedCompletionStatus - те мы можем задать непосредственно тип операции для завершения работы - когда поток получит это значение и интерпретирует его соответвующим образом мы можем освободить какие-то ресурсы, сделать какую-то работу, Etc... После закрытия всех рабочих потоков надо закрыть порт завершения через CloseHandle и завершить программу. Суммировав все полученные выше знания давайте теперь напишем эхо сервер: // ech0.cpp : Defines the entry point for the console application. // ///////////////////////////////////////////////////// // *Пример* простого эхо сервера, // использующего модель портов завершения /////////////////////////////////////////////////// #include <winsock2.h> #include <windows.h> #include <stdio.h> #define PORT 31337 #define DATA_BUFSIZE 1024 #define WELCOME_MSG "Hello, welcome to MaZaFaKa.Ru ech0 server!\r\n" typedef struct{ OVERLAPPED Overlapped; WSABUF DataBuf; CHAR Buffer[DATA_BUFSIZE]; DWORD BytesSend; DWORD BytesRecv; DWORD TotalBytes; SOCKADDR_IN client; } PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA; typedef struct{ SOCKET Socket; } PER_HANDLE_DATA, * LPPER_HANDLE_DATA; DWORD WINAPI ServerThread (LPVOID CompletionPortID); int main (int argc, char* argv[]) { SOCKADDR_IN server; SOCKADDR_IN client; SOCKET Socket; SOCKET Accept; HANDLE CompletionPort; SYSTEM_INFO SysInfo; HANDLE Thread; LPPER_HANDLE_DATA PerHandleData; LPPER_IO_OPERATION_DATA PerIoData; int i; DWORD SendBytes; DWORD Flags; DWORD ThreadID; WSADATA wsaData; DWORD Ret; // инициализируем WinSock: if ((Ret = WSAStartup (0x0202, &wsaData)) != 0){ printf ("WSAStartup failed with error %d\n", Ret); return 1; } // Создаём порт завершения: if ((CompletionPort = CreateIoCompletionPort (INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL){ printf ( "CreateIoCompletionPort failed with error: %d\n", GetLastError ()); return 1; } // Получаем информацию о системы: GetSystemInfo (&SysInfo); // создаём два потока на процессор:. for (i = 0; i < SysInfo.dwNumberOfProcessors * 2; i++){ // создаём рабочий поток, в качестве параметра передаём ей порт завершения if ((Thread = CreateThread (NULL, 0, ServerThread, CompletionPort, 0, &ThreadID)) == NULL){ printf ("CreateThread() failed with error %d\n", GetLastError ()); return 1; } CloseHandle(Thread); } // Создаём слушающий сокет: if ((Socket = WSASocket (AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET){ printf ("WSASocket() failed with error %d\n", WSAGetLastError ()); return 1; } server.sin_family = AF_INET; server.sin_addr.s_addr = htonl (INADDR_ANY); server.sin_port = htons (PORT); if (bind (Socket, (PSOCKADDR) &server, sizeof(server)) == SOCKET_ERROR){ printf ("bind() failed with error %d\n", WSAGetLastError ()); return 1; } if (listen (Socket, 5) == SOCKET_ERROR){ printf ("listen() failed with error %d\n", WSAGetLastError ()); return 1; } // принимаем соединения и передаём их порту завершения: while (TRUE){ // принимаем соединение: if ((Accept = WSAAccept (Socket, (PSOCKADDR)&client, NULL, NULL, 0)) == SOCKET_ERROR){ printf ("WSAAccept() failed with error %d\n", WSAGetLastError ()); continue; } // Выделяем память под структуру, которая будет хранить информацию о сокете: if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc (GPTR, sizeof (PER_HANDLE_DATA))) == NULL){ printf ("GlobalAlloc() failed with error %d\n", GetLastError ()); return 1; } printf ("Socket %d connected\n", Accept); PerHandleData->Socket = Accept; // сохраняем описатель сокета //привязываем сокет к порту завершения: if (CreateIoCompletionPort ((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData, 0) == NULL){ printf ("CreateIoCompletionPort failed with error %d\n", GetLastError()); return 1; } // выделяем память под данные операции ввода вывода: if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc (GPTR, sizeof (PER_IO_OPERATION_DATA))) == NULL){ printf ("GlobalAlloc() failed with error %d\n", GetLastError ()); return 1; } ZeroMemory (&(PerIoData->Overlapped), sizeof (OVERLAPPED)); // задаём изначальные данные для операции ввода вывода: PerIoData->BytesSend = 0; PerIoData->BytesRecv = 0; PerIoData->DataBuf.len = strlen (WELCOME_MSG); PerIoData->DataBuf.buf = WELCOME_MSG; PerIoData->client = client; PerIoData->TotalBytes = 0; Flags = 0; // отправляем welcome message // остальные операции будут выполняться в рабочем потоке if (WSASend (Accept, &(PerIoData->DataBuf), 1, &SendBytes, 0,&(PerIoData->Overlapped), NULL) == SOCKET_ERROR){ if (WSAGetLastError() != ERROR_IO_PENDING){ printf("WSASend() failed with error %d\n", WSAGetLastError()); return 1; } } } return 0; } // рабочий поток порта завершения: /* в нашем эхо сервере мы не грузимся типом операции - тк надо просто отправлять и принимать символы. В более серьёзных программах - рекомендую использовать параметр OperationType в структуре PER_IO_OPERATION_DATA, для того что бы точно понимать, что же происходит с сервером. Те ваш поток завершения станет чем-то похож на оконную функцию: switch (PerIoData->OperationType){ case SRV_NEW_CONNECTION: // это первое подключение: ... case SRV_DATA_SEND: // посылаем данные ... case SRV_DATA_RECV: // принимаем данные ... case SRV_DISCONNECT: // отсоединение ....... } */ DWORD WINAPI ServerThread(LPVOID CompletionPortID){ HANDLE CompletionPort = (HANDLE) CompletionPortID; DWORD BytesTransferred; LPOVERLAPPED Overlapped; LPPER_HANDLE_DATA PerHandleData; LPPER_IO_OPERATION_DATA PerIoData; DWORD SendBytes, RecvBytes; DWORD Flags; while (TRUE){ // ожидание завершения ввода-вывода на любом из сокетов // которые связанны с портом завершения: if (GetQueuedCompletionStatus (CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0){ printf ("GetQueuedCompletionStatus failed with error %d\n", GetLastError ()); return 0; } // проверяем на ошибки. Если была - значит надо закрыть сокет и очистить память за собой: if (BytesTransferred == 0){ // тк не было переданно ни одного байта - значит сокет закрыли на той стороне // мы должны сделать то же самое: printf ("Closing socket %d\nTotal bytes:%d\n", PerHandleData->Socket, PerIoData->TotalBytes); // закрываем сокет: if (closesocket (PerHandleData->Socket) == SOCKET_ERROR){ printf ("closesocket() failed with error %d\n", WSAGetLastError ()); return 0; } // очищаем память: GlobalFree (PerHandleData); GlobalFree (PerIoData); // погнали дальше - ждём следующую операцию continue; } PerIoData->TotalBytes += BytesTransferred; // Проверим значение BytesRecv - если оно равно нулю - значит мы получили данные от клиента: if (PerIoData->BytesRecv == 0){ PerIoData->BytesRecv = BytesTransferred; PerIoData->BytesSend = 0; } else{ PerIoData->BytesSend += BytesTransferred; } // мы должны отослать все принятые байты назад: if (PerIoData->BytesRecv > PerIoData->BytesSend){ // Шлём данные через WSASend - тк всё сразу может не отослаться // необходимо слать до упора. // в нашем случае это вряд ли произойдёт - но тем не менее, не забывайте // что за один вызов WSASend все данные могут и не отправится ! ZeroMemory (&(PerIoData->Overlapped), sizeof (OVERLAPPED)); PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSend; PerIoData->DataBuf.len = PerIoData->BytesRecv - PerIoData->BytesSend; if (WSASend (PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0, &(PerIoData->Overlapped), NULL) == SOCKET_ERROR){ if (WSAGetLastError () != ERROR_IO_PENDING){ printf ("WSASend() failed with error %d\n", WSAGetLastError()); return 0; } } } else{ PerIoData->BytesRecv = 0; // ожидаем ещё данные от пользователя: Flags = 0; ZeroMemory(&(PerIoData->Overlapped), sizeof (OVERLAPPED)); PerIoData->DataBuf.len = DATA_BUFSIZE; PerIoData->DataBuf.buf = PerIoData->Buffer; if (WSARecv (PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags, &(PerIoData->Overlapped), NULL) == SOCKET_ERROR){ if (WSAGetLastError () != ERROR_IO_PENDING){ printf ("WSARecv() failed with error %d\n", WSAGetLastError ()); return 0; } } } } }Outro В завершении хочется сказать ещё несколько слов о том каким образом возможно увеличить скорость работы своей сетевой программы в общем случае (применимо ко всем моделям ввода-вывода). Во первых это конечно же функция TransmitFile: BOOL TransmitFile( SOCKET hSocket, HANDLE hFile, DWORD nNumberOfBytesToWrite, DWORD nNumberOfBytesPerSend, LPOVERLAPPED lpOverlapped, LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, DWORD dwFlags );Данная функция позволяет быстро передать по сети данные из файла - если программа использует связку WSASend/ReadFile - тогда происходит многократное переключение между режимами ядра и пользовательским - функция TransmitFile позволяет избежать этих переключения за счёт того что весь процесс чтения и отправки происходит целиком в режиме ядра. Параметры функции:
BOOL AcceptEx ( SOCKET sListenSocket, SOCKET sAcceptSocket, PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPDWORD lpdwBytesReceived, LPOVERLAPPED lpOverlapped );
VOID GetAcceptExSockaddrs ( PVOID lpOutputBuffer, DWORD dwReceiveDataLength, DWORD dwLocalAddressLength, DWORD dwRemoteAddressLength, LPSOCKADDR *LocalSockaddr, LPINT LocalSockaddrLength, LPSOCKADDR *RemoteSockaddr, LPINT RemoteSockaddrLength );Функцию AcceptEx логично использовать только если сервер будет обрабатывать небольшое количество запросов ввода-вывода при одном соединении - например это Web или прокси сервер. Если же требуется многократная передача различных данных, тогда увеличения производительности естесвенно не произойдёт. Кроме того в Windows XP/2003 появился ещё более раширенный набор "быстрых" WinSock функций - типа TransmitPackets, однако учитывая большую распространённость серверов под управлением Win2k переходить целиком на эти функции ещё рановато, хотя присмотреться к ним всё же стоит. |
||
![]() ![]() ![]() ![]() |
||
Автор:
said /20.02.05/ © Mazafaka.Ru - E-Zine - 2005 © |