вЫкладЫваем статьи и собственнЫе наработки :)
ЯП: С++
Статья: Пишем PROXY-SERVER
Источник: www.uinc.ru
ЯП: С++
Статья: Пишем PROXY-SERVER
Источник: www.uinc.ru
Введение
Было время, когда мне нужно было написать простейшей одноконнектовый прокси, даже без интерфейса, но состоящий из двух половинок, которые соединяются протоколом SPX, а не TCP. Я столкнулся с тем, что в том небольшом количестве примеров работы с WinSock, что у меня были, было столько ненужного мне мусора, что это затрудняло понимание самого принципа. А примеров организации многоконнектовости у меня вообще не было. Поэтому в данной статье я постараюсь как можно проще объяснить принцип работы прокси, но я не буду объяснять все с нуля. Если вы хотите понять принцип работы асинхронных неблокирующих сокетов в Windows и их отличия от стандартных синхронных, для начала прочтите документ "Синхронные и асинхронные сокеты в Windows". А если вы вообще не знакомы с сетевым программированием, отложите не надолго эту статью и постигните основы. Здесь же я расскажу только о том, что действительно может быть непонятным читателю. В качестве примера рассмотрим программу, организующую прослушивание сокета и осуществляющую перенаправление данных на указанный IP:PORT. Правильнее было бы назвать это чем-то вроде "port map" или "port redirect". Самая лучшая документация для программиста - это листинг программы.
Для начала определимся с константами.
Какой локальный порт будем прослушивать:
Удаленный IP адрес.Код:#define IN_PORT 3128
Порт к которому будем подключаться.Код:#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);
* Зарегистрируем класс и создадим окно. Получим hwndMain - дескриптор окна.Код:ZeroMemory(sockets,sizeof(sockets));
* Создадим сокет.
* Заполним структуру SOCKADDR_IN, указав тип протокола(family) и порт, к которому будем "биндиться", и "привязываем" сокет.Код:hListenSockTCP = socket (AF_INET,SOCK_STREAM,0);
* Запускаем сокет "на прослушку".Код: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) );
* Привязываем события FD_ACCEPT, FD_READ, FD_CLOSE сокета к главному окну программы.Код:listen (hListenSockTCP, SOMAXCONN));
Это значит, что при попытке клиента подключиться к прослушиваемому сокету окну с дескрипторо 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).Код:WSAAsyncSelect (hListenSockTCP,hwndMain,WM_ASYNC_CLIENTEVENT, FD_ACCEPT|FD_READ|FD_CLOSE);
* Процедура обработки сообщений.
Сообщения о событиях подключенных к клиенту сокетов...Код:............. case WM_ASYNC_CLIENTEVENT:
именно так узнаем, какое событие с сокетом произошлоКод:currentsock = wParam;
Это сообщение приходит тогда, когда к нам хотят подключиться.Код:WSAEvent = WSAGETSELECTEVENT (lParam); switch (WSAEvent) {
Разрешаем подключение клиента, и пытаемся теперь подключиться к нашему удаленному прокси.Код:case FD_ACCEPT:
Если это не удалось, закрываем соединение, которое только что мы позволили установить с нами клиенту. Второй параметр - SD_SEND (у меня - просто единица). Этим мы позволяем соединению спокойно закрыться. После этой команды с сокетом произойдет событиеКод:ConnectToProxy(accept(hListenSockTCP,NULL,NULL));
Клиент по какой-либо причине хочет прервать соединение. ГлушимКод:"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:
Теперь в currentsock - наше соединение с клиентом, а вКод:for (i=0;i<MAXCONN;i++) if (sockets[i] == wParam) { currentsock=i; break; }
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;
Заполняем структуру - IP, с которым мы будем связываться, порт, типКод:void ConnectToProxy(SOCKET nofsock) {
протокола.
Создание сокета TCP.Код:SOCKADDR_IN rmaddr; rmaddr.sin_family = AF_INET; rmaddr.sin_addr.s_addr = inet_addr(OUT_IP); rmaddr.sin_port = htons (OUT_PORT);
Привязываем события FD_READ и FD_CLOSE с этим сокетом к главному окну приложения сообщением WM_ASYNC_PROXYEVENT. Тем самым мы переводим сокет в не блокирующий режим.Код:sockets[nofsock] = socket (AF_INET,SOCK_STREAM,0);
Пытаемся соединиться.Код:WSAAsyncSelect (sockets[numofsock],hwndMain,WM_ASYNC_PROXYEVENT, FD_CONNECT|FD_READ|FD_CLOSE);
Результат функции connect() мы не проверяем, так как она завершится до того, как соединение будет установлено.Код:connect (sockets[nofsock], (struct sockaddr *)&rmaddr,sizeof(rmaddr));
Итак, я показал основные функции прокси. Я специально в первом варианте не добавлял код для проверки ошибок, дабы упростить ядро и позволить проще понять принципы. Теперь я расскажу о тех проблемах, которые есть у нашего прокси.Код:return; } Warning!
1. После каждой функции Winsock необходимо получать код возврата и адекватно реагировать.
2. Функция приема данных и передача их дальше по цепочке у нас выглядит так:
Но на самом деле функция send() не гарантирует то, что данные будут посланы, а так же то, что будет послано именно это число байт, а не меньше. Если бы сокет был блокирующим, мы за это могли бы не переживать, так как программа была бы в ожидании того, когда весь объем данных будет послан. Чтобы избежать этих проблем, можно ожидать в обработчике сообщения FD_WRITE, которое говорит о том, что сокет готов к передаче данных.Код:i=recv(sockets[currentsock], buf, MAX_DATA, 0); send(currentsock,buf, i, 0);
3. Массив для хранения дескрипторов сокетов с индексированием другими дескрипторами сокетов - не самый правильный вариант. Представим, например, что у нас такая запись - "SOCKET sockets[MAXCONN]", а MAXCONN=1000. Но при большой нагрузке на сеть значение дескриптора может быть и больше 1000 - тогда будет очень большая проблема, которую тяжело будет исправить, если не догадываться о ее присутствии. Самым простым (но не лучшим) решением данной проблемы будет заведение заранее большого массива и проверкой каждого созданного сокета - кодом типа:
4. Еще раз внимательно посмотрите на код, и, возможно, вы найдете еще что-то, что я упустил.Код:hsocket=accept(hListenSockTCP,NULL,NULL); if(hsocket>MAXCONN) { shutdown(hsocket); close(hsocket); }
Ну что, вы разобрались и готовы к реализации полноценной программы? Тогда заходите сервер UInC в раздел проектов. Там вы найдете полнофункциональную реализацию такого port-mapper-а. Вместе с ним будет исходный код. В нем вы увидите обработку всех ошибок, ведение лога, обработка командной строки и многое другое.
(c) Copyright 2001. Украина, Запорожье. KMiNT21 (mailto:kmint21@mail.ru).
uinC Member
[c]uinC