Пишем PROXY-SERVER
Источник:
www.uinc.ru
Введение
Было время, когда мне нужно было написать
простейшей одноконнектовый прокси, даже без
интерфейса, но состоящий из двух половинок,
которые соединяются протоколом 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;
Так же поступаем и с обработкой событий на
сокетах, когда сам наш прокси является клиентом
другого прокси, на который мы делаем
перенаправление. Проще говоря, у нас в программе
будут две группы сокетов.
1. Со стороны клиентов. Есть главный сокет -
hListenSockTCP. К нему могут подключаться клиенты,
каждый раз создавая новые виртуальные каналы,
каждому из которых назначается свой дескриптор.
2. Со стороны оконечного прокси сервера (или
сервиса). Наша программа будет каждый раз при
необходимости создавать сокет и коннектиться на
заданный 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!
Итак, я показал основные функции прокси. Я
специально в первом варианте не добавлял код для
проверки ошибок, дабы упростить ядро и позволить
проще понять принципы. Теперь я расскажу о тех
проблемах, которые есть у нашего прокси.
1. После каждой функции Winsock необходимо получать
код возврата и адекватно реагировать.
2. Функция приема данных и передача их дальше по
цепочке у нас выглядит так:
i=recv(sockets[currentsock], buf, MAX_DATA, 0);
send(currentsock,buf, i, 0);
Но на самом деле функция send() не гарантирует то,
что данные будут посланы, а так же то, что будет
послано именно это число байт, а не меньше. Если
бы сокет был блокирующим, мы за это могли бы не
переживать, так как программа была бы в ожидании
того, когда весь объем данных будет послан. Чтобы
избежать этих проблем, можно ожидать в
обработчике сообщения FD_WRITE, которое говорит о
том, что сокет готов к передаче данных.
3. Массив для хранения дескрипторов сокетов с
индексированием другими дескрипторами сокетов -
не самый правильный вариант. Представим,
например, что у нас такая запись - "SOCKET
sockets[MAXCONN]", а MAXCONN=1000. Но при большой нагрузке на
сеть значение дескриптора может быть и больше 1000
- тогда будет очень большая проблема, которую
тяжело будет исправить, если не догадываться о ее
присутствии. Самым простым (но не лучшим)
решением данной проблемы будет заведение
заранее большого массива и проверкой каждого
созданного сокета - кодом типа:
hsocket=accept(hListenSockTCP,NULL,NULL);
if(hsocket>MAXCONN)
{
shutdown(hsocket);
close(hsocket);
}
4. Еще раз внимательно посмотрите на код, и,
возможно, вы найдете еще что-то, что я упустил.
Ну что, вы разобрались и готовы к реализации полноценной программы? Тогда
заходите сервер UInC в раздел проектов. Там вы
найдете полнофункциональную реализацию такого
port-mapper-а. Вместе с ним будет исходный код. В нем вы
увидите обработку всех ошибок, ведение лога, обработка командной строки и многое другое.
(c) Copyright 2001. Украина,
Запорожье. KMiNT21 (mailto:kmint21@mail.ru).
uinC Member [c]uinC
Статья написана специально для UInC (http://www.uinc.ru/).
Любые комментарии, поправки, пожелания или
дополнения можно посылать сюда: kmint21@mail.ru
|