![]() |
||
В повседневной хакерской жизни часто случается необходимость включить в свой софт какой-нибудь быстрый сервер - будь то прокси, 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 © |