Статья: Пишем 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!

Итак, я показал основные функции прокси. Я специально в первом варианте не добавлял код для проверки ошибок, дабы упростить ядро и позволить проще понять принципы. Теперь я расскажу о тех проблемах, которые есть у нашего прокси:

  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);
}

Код программы скачать можно здесь - Скачано 11,703 раз

© Copyright 2001. Украина, Запорожье. uinC Member [c]uinC