Статья: Пишем PROXY-SERVER
—[2001]—
Было время, когда мне нужно было написать простейшей одноконнектовый прокси, даже без интерфейса, но состоящий из двух половинок, которые соединяются протоколом SPX, а не TCP. Я столкнулся с тем, что в том небольшом количестве примеров работы с WinSock, что у меня были, было столько ненужного мне мусора, что это затрудняло понимание самого принципа. А примеров организации многоконнектовости у меня вообще не было. Поэтому в данной статье я постараюсь как можно проще объяснить принцип работы прокси, но я не буду объяснять все с нуля.
Если вы хотите понять принцип работы асинхронных неблокирующих сокетов в Windows и их отличия от стандартных синхронных, для начала прочтите документ “Синхронные и асинхронные сокеты в Windows”. А если вы вообще не знакомы с сетевым программированием, отложите не надолго эту статью и постигните основы. Здесь же я расскажу только о том, что действительно может быть непонятным читателю. В качестве примера рассмотрим программу, организующую прослушивание сокета и осуществляющую перенаправление данных на указанный IP:PORT. Правильнее было бы назвать это чем-то вроде “port map” или “port redirect”.
самая лучшая документация для программиста - это исходный текст программы
Для начала определимся с константами.
// Какой локальный порт будем прослушивать:
#define IN_PORT 3128
// Удаленный IP адрес.
#define OUT_IP "192.168.0.1"
// Порт к которому будем подключаться.
#define OUT_PORT 3128
// Решим, какое максимальное количество соединений мы будем поддерживать.
#define MAXCONN 1000
Объявим глобальные переменные.
// Это будет буфер для принятых данных.
char buf[MAX_DATA];
// Слушающий сокет, на который будут коннектиться клиенты.
SOCKET hListenSockTCP;
/* Массив дескрипторов сокетов, полученных при соединении нашей
программой с удаленным сервисом в ответ на подключение со стороны
клиента. Дескриптор сокета с клиентской стороны и будет индексом.
Например: к нашей программе подключается клиент. После выполнения
строки "currentsock = accept(hListenSockTCP,NULL,NULL);" в переменной
currentsock типа SOCKET будет возращен дескриптор сокета. Он,
например, может быть числом 5,6 и т.д., поэтому, сам дескриптор можно
использовать в качестве индекса в массиве. Теперь в ответ на "пятое"
соединение (в случае, когда currentsock=5) соединяемся с прокси, на
который мы делаем перенаправление, и полученный дескриптор сохраняем
sockets[5]. Это равносильно строке "sockets[5]=connect
(sockets[nofsock], ". Как вы должны понимать, это не самый лучший
метод. Но зато он самый простой и нам пока подойдет. */
SOCKET sockets[MAXCONN];
Начнем.
// Инициализация среды перед использованием WinSock:
WSADATA stWSADataTCPIP;
if(WSAStartup(0x0101, &stWSADataTCPIP)) MessageBox(hwndMain,
"WSAStartup error !","NET ERROR!!!",0);
// Заполним массив дескрипторов сокетов нулями (на всякий случай).
ZeroMemory(sockets,sizeof(sockets));
// Зарегистрируем класс и создадим окно. Получим hwndMain - дескриптор
окна.
// Создадим сокет.
hListenSockTCP = socket (AF_INET,SOCK_STREAM,0);
// Заполним структуру SOCKADDR_IN, указав тип протокола(family) и порт, к которому будем "биндиться", и "привязываем" сокет.
SOCKADDR_IN myaddrTCP;
myaddrTCP.sin_family = AF_INET;
myaddrTCP.sin_addr.s_addr = htonl (INADDR_ANY);
myaddrTCP.sin_port = htons (IN_PORT);
bind( hListenSockTCP,(LPSOCKADDR)&myaddrTCP, sizeof(struct sockaddr) );
// Запускаем сокет "на прослушку".
listen (hListenSockTCP, SOMAXCONN));
// Привязываем события FD_ACCEPT, FD_READ, FD_CLOSE сокета к главному окну программы.
WSAAsyncSelect (hListenSockTCP,hwndMain,WM_ASYNC_CLIENTEVENT,
FD_ACCEPT|FD_READ|FD_CLOSE);
/* Это значит, что при попытке клиента подключиться к прослушиваемому
сокету окну с дескриптором hwndMain будет передаваться сообщение
WM_ASYNC_CLIENTEVENT. Напомню, что функция обработки сообщений окна
выглядит так - "LRESULT CALLBACK MainWndProc(HWND hwnd,UINT
msg,WPARAM wParam,LPARAM lParam)". В переменной wParam будет передан
дескриптор сокета, в котором произошло событие. А какое именно -
узнаем из lParam. Но кроме кода события в lParam еще находится код
ошибки. Для извлечения их этого 4-х байтного числа (DWORD) двух слов
(WORD) существуют два макроопределения - WSAGETSELECTERROR(lParam) и
WSAGETSELECTEVENT(lParam). */
// Процедура обработки сообщений.
// .............
case WM_ASYNC_CLIENTEVENT:
// Сообщения о событиях подключенных к клиенту сокетов...
currentsock = wParam;
// именно так узнаем, какое событие с сокетом произошло
WSAEvent = WSAGETSELECTEVENT (lParam);
switch (WSAEvent)
{
// Это сообщение приходит тогда, когда к нам хотят подключиться.
case FD_ACCEPT:
// Разрешаем подключение клиента, и пытаемся теперь подключиться к нашему удаленному прокси.
ConnectToProxy(accept(hListenSockTCP,NULL,NULL));
/* Если это не удалось, закрываем соединение, которое только что мы
позволили установить с нами клиенту. Второй параметр - SD_SEND (у
меня - просто единица). Этим мы позволяем соединению спокойно
закрыться. После этой команды с сокетом произойдет событие
"FD_CLOSE". */
shutdown(currentsock,1);
return 0;
case FD_CLOSE :
// Клиент по какой-либо причине хочет прервать соединение. Глушим соединение с уд. прокси, которое мы установили в ответ на это соединение.
shutdown(sockets[currentsock],1);
// и закрываем сокет.
closesocket(currentsock);
return 0;
case FD_READ:
// На сокет пришли данные. Берем от клиента, посылаем на сервер.
i=recv(currentsock, buf, MAX_DATA, 0);
send(sockets[currentsock], buf, i, 0);
// и отправляем...
return 0;
}
break;
Так же поступаем и с обработкой событий на сокетах, когда сам наш прокси является клиентом другого прокси, на который мы делаем перенаправление. Проще говоря, у нас в программе будут две группы сокетов:
- Со стороны клиентов. Есть главный сокет - hListenSockTCP. К нему могут подключаться клиенты, каждый раз создавая новые виртуальные каналы, каждому из которых назначается свой дескриптор.
- Со стороны оконечного прокси сервера (или сервиса). Наша программа будет каждый раз при необходимости создавать сокет и коннектиться на заданный IP:PORT. Каждый открытый виртуальный канал будет ответом на подсоединение со стороны клиентов.
case WM_ASYNC_PROXYEVENT:
// Найдем соответствующий дескриптор в массиве.
for (i=0;i<MAXCONN;i++)
if (sockets[i] == wParam) { currentsock=i; break; }
// Теперь в currentsock - наше соединение с клиентом, а в sockets[currentsock] - соответствующее ему соединение с удаленным прокси.
WSAEvent = WSAGETSELECTEVENT (lParam);
switch (WSAEvent)
{
// Произошло подключение к удаленному хосту.
case FD_CONNECT :
i=WSAGETSELECTERROR(lParam);
if (i!=0)
// Если соединение не удалось, закроем уже установленное соединение с клиентом и сокет, который мы создали, пытаясь установить соединение с удаленным прокси.
{
shutdown(currentsock,1);
closesocket(sockets[currentsock]);
sockets[currentsock]=INVALID_SOCKET;
}
return 0;
// Сервер нас отрубает...
case FD_CLOSE :
shutdown(currentsock,1);
closesocket(sockets[currentsock]);
sockets[currentsock]=INVALID_SOCKET;
return 0;
// Перенаправление данных клиенту.
case FD_READ:
i=recv(sockets[currentsock], buf, MAX_DATA, 0);
send(currentsock,buf, i, 0);
return 0;
}
break;
А теперь рассмотрим функцию соединения с прокси.
void ConnectToProxy(SOCKET nofsock)
{
// Заполняем структуру - IP, с которым мы будем связываться, порт, тип протокола.
SOCKADDR_IN rmaddr;
rmaddr.sin_family = AF_INET;
rmaddr.sin_addr.s_addr = inet_addr(OUT_IP);
rmaddr.sin_port = htons (OUT_PORT);
// Создание сокета TCP.
sockets[nofsock] = socket (AF_INET,SOCK_STREAM,0);
/* Привязываем события FD_READ и FD_CLOSE с этим сокетом к главному
окну приложения сообщением WM_ASYNC_PROXYEVENT. Тем самым мы
переводим сокет в не блокирующий режим. */
WSAAsyncSelect (sockets[numofsock],hwndMain,WM_ASYNC_PROXYEVENT,
FD_CONNECT|FD_READ|FD_CLOSE);
// Пытаемся соединиться.
connect (sockets[nofsock], (struct sockaddr *)&rmaddr,sizeof(rmaddr));
// Результат функции connect() мы не проверяем, так как она завершится до того, как соединение будет установлено.
return;
}
Warning!
Итак, я показал основные функции прокси. Я специально в первом варианте не добавлял код для проверки ошибок, дабы упростить ядро и позволить проще понять принципы. Теперь я расскажу о тех проблемах, которые есть у нашего прокси:
После каждой функции Winsock необходимо получать код возврата и адекватно реагировать.
Функция приема данных и передача их дальше по цепочке у нас выглядит так:
Но на самом деле функция send() не гарантирует то, что данные будут посланы, а так же то, что будет послано именно это число байт, а не меньше. Если бы сокет был блокирующим, мы за это могли бы не переживать, так как программа была бы в ожидании того, когда весь объем данных будет послан. Чтобы избежать этих проблем, можно ожидать в обработчике сообщения FD_WRITE, которое говорит о том, что сокет готов к передаче данных.i=recv(sockets[currentsock], buf, MAX_DATA, 0); send(currentsock,buf, i, 0);
Массив для хранения дескрипторов сокетов с индексированием другими дескрипторами сокетов - не самый правильный вариант. Представим, например, что у нас такая запись - “SOCKET sockets[MAXCONN]“, а MAXCONN=1000. Но при большой нагрузке на сеть значение дескриптора может быть и больше 1000 - тогда будет очень большая проблема, которую тяжело будет исправить, если не догадываться о ее присутствии. Самым простым (но не лучшим) решением данной проблемы будет заведение заранее большого массива и проверкой каждого созданного сокета - кодом типа:
hsocket=accept(hListenSockTCP,NULL,NULL);
if(hsocket>MAXCONN)
{
shutdown(hsocket);
close(hsocket);
}
Код программы скачать можно здесь - Скачано 11,703 раз
© Copyright 2001. Украина, Запорожье. uinC Member [c]uinC