Создайте свой собственный прокси-сервер с помощью Delphi
Когда автор писал программу для биллинга через Интернет, он задавался вопросом, как выставлять счета за доступ в Интернет для каждой рабочей станции в локальной сети. Вообще говоря, эти рабочие станции получают доступ к Интернету через прокси-сервер. При использовании готового программного обеспечения прокси-сервера, поскольку программное обеспечение прокси-сервера представляет собой закрытую систему, сложно написать программу для получения информации о времени доступа в Интернет в реальном времени. Поэтому подумайте, можно ли написать свой прокси-сервер, чтобы решить проблему группового доступа в Интернет с одной стороны и проблему биллинга с другой?
После экспериментального программирования проблема наконец была решена удовлетворительно. Запишите это сейчас и поделитесь с коллегами.
1. Идеи
В системных настройках популярных сейчас браузеров есть параметр, а именно «Подключаться через прокси-сервер». После тестирования программирования.
Попробуйте, когда рабочая станция в локальной сети укажет этот атрибут и затем выдаст интернет-запрос, данные запроса будут отправлены на указанный прокси-сервер. Ниже приведен пример пакета запроса:
ПОЛУЧИТЬ http://home.microsoft.com/intl/cn/ HTTP/1.0
Принимать: */*
Принимаемый язык: zh-cn
Принять-кодирование: gzip, deflate
Пользовательский агент: Mozilla/4.0 (совместимый; MSIE 5.0; Windows NT)
Хост: home.microsoft.com.
Прокси-соединение: поддержка активности
Первая строка — это целевой URL-адрес и связанные с ним методы и протоколы, а строка «Хост» указывает адрес целевого хоста.
Отсюда мы знаем процесс прокси-сервиса: получение запроса от прокси, подключение к реальному хосту, получение данных, возвращаемых хостом, и отправка полученных данных прокси.
Для этой цели можно написать простую программу, решающую описанную выше проблему перенаправления сетевых коммуникаций.
При проектировании с помощью Delphi выберите ServerSocket в качестве элемента управления сокетом для связи с прокси-рабочей станцией и выберите динамический массив ClientSocket в качестве элемента управления сокетом для связи с удаленным хостом.
Важным вопросом, который необходимо решить при программировании, является проблема обработки множественных соединений. Чтобы ускорить работу прокси-сервиса и скорость ответа агента, в свойствах управления сокетами необходимо установить неблокируемость каждого сеанса связи; динамически привязан к сокету, используйте значение атрибута SocketHandle сокета, чтобы определить, к какому сеансу он принадлежит.
Процесс подключения связи показан на рисунке ниже:
прокси-сервер
Серверный сокет
(1) получить
Отправлено агентом на удаленный хост
(6) (2) (5)
Браузер ClientSocket (4) Веб-сервер
перенимать
Отправить(3)
(1) Прокси-браузер отправляет веб-запрос, а Serversocket прокси-сервера получает запрос.
(2) Программа прокси-сервера автоматически создает ClientSocket, устанавливает адрес хоста, порт и другие атрибуты, а затем подключается к удаленному хосту.
(3) После удаленного подключения запускается событие отправки, и пакет веб-запроса, полученный Serversocket, отправляется на удаленный хост.
(4) Когда удаленный хост возвращает данные страницы, запускается событие чтения ClientSocket для чтения данных страницы.
(5) Программа прокси-сервера определяет, какой сокет в элементе управления ServerSocket должен отправлять информацию о странице, полученную от хоста, на прокси-конец на основе информации о привязке.
(6) Соответствующий сокет в ServerSocket отправляет данные страницы агенту.
2. Программирование
Спроектировать описанный выше процесс связи с помощью Delphi очень просто, в основном он связан с ServerSocket и ClientSocket.
Программирование драйверов. Ниже приводится список экспериментального интерфейса прокси-сервера и исходной программы, написанных автором, включая краткое описание функций:
основной блок;
интерфейс
использует
Windows, сообщения, SysUtils, классы, графика, элементы управления, формы, диалоги,
ExtCtrls, ScktComp, TrayIcon, Меню, StdCtrls;
тип
session_record = запись
Используется: boolean {доступна ли запись сеанса}
SS_Handle: целое число; {дескриптор сокета прокси-сервера}
CSocket: TClientSocket; {сокет, используемый для подключения к пульту}
Поиск: boolean {ищет ли сервер}
LookupTime: целое число; {время сервера поиска}
Запрос: boolean {есть ли запрос}
request_str: строка; {блок данных запроса}
client_connected: boolean; {флаг клиента онлайн}
Remote_connected: boolean; {флаг подключения к удаленному серверу}
конец;
тип
ТФорм1 = класс (ТФорма)
СерверСокет1: ТСерверСокет;
КлиентСокет1: ТКлиентСокет;
Таймер2: ТТаймер;
ТрейИкон1: TTrayIcon;
ПопупМеню1: ТПопупМеню;
N11: ТМенюитем;
N21: ТМенюитем;
N1: ТМенюитем;
N01: Тменуитем;
Памятка1: TMemo;
Edit1: TEdit;
Метка1: TLabel;
Таймер1: ТТаймер;
процедура Timer2Timer (Отправитель: TObject);
процедура N11Click (Отправитель: TObject);
процедура FormCreate (Отправитель: TObject);
процедура FormClose (Отправитель: TObject; вар Действие: TCloseAction);
процедура N21Click (Отправитель: TObject);
процедура N01Click (Отправитель: TObject);
процедура ServerSocket1ClientConnect (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура ServerSocket1ClientDisconnect (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура ServerSocket1ClientError (Отправитель: TObject;
Сокет: TCustomWinSocket;ErrorEvent: TErrorEvent;
varErrorCode: Целое число);
процедура ServerSocket1ClientRead (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура ClientSocket1Connect (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура ClientSocket1Disconnect (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура ClientSocket1Error (Отправитель: TObject; Сокет: TCustomWinSocket;
ErrorEvent: TErrorEvent; вар ErrorCode: Integer);
процедура ClientSocket1Write (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура ClientSocket1Read (Отправитель: TObject; Сокет: TCustomWinSocket);
процедура ServerSocket1Listen (Отправитель: TObject;
Сокет: TCustomWinSocket);
процедура AppException (Отправитель: TObject; E: Exception);
процедура Timer1Timer (Отправитель: TObject);
частный
{Частные заявления}
общественный
Service_Enabled: boolean {включен ли прокси-сервис}
сеанс: массив session_record {массив сеанса};
сеансы: целое число; {количество сеансов}
LookUpTimeOut: целое число {значение времени ожидания соединения}
InvalidRequests: целое число; {количество недействительных запросов}
конец;
вар
Форма1: ТФорм1;
выполнение
{$R *.DFM}
file://Таймер запуска системы, после отображения окна запуска сжать до системного трея...
процедура TForm1.Timer2Timer(Отправитель: TObject);
начинать
timer2.Enabled:=false {таймер выключения}
сеансы:=0; {количество сеансов=0}
application.OnException := AppException {Для защиты исключений, возникающих на прокси-сервере}
validRequests:=0; {0 ошибка}
LookUpTimeOut:=60000; {значение таймаута=1 минута}
timer1.Enabled:=true; {таймер включения}
n11.Enabled:=false; {Недействительный пункт меню «Включить сервис»}
n21.Enabled:=true; {Пункт меню «Закрыть сервис» действителен}
serverocket1.Port:=988; {порт прокси-сервера=988};
serverocket1.Active:=true {Запустить службу}
form1.hide {скрыть интерфейс, свернуть в системный трей};
конец;
file://Открыть пункт сервисного меню…
процедура TForm1.N11Click(Отправитель: TObject);
начинать
serverocket1.Active:=true {Запустить службу}
конец;
file://Остановить пункт меню службы…
процедура TForm1.N21Click(Отправитель: TObject);
начинать
serverocket1.Active:=false; {остановить службу}
N11.Включено:=Истина;
N21.Включено:=Ложь;
Service_Enabled:=false; {флаг снят}
конец;
file://Создание главного окна…
процедура TForm1.FormCreate(Отправитель: TObject);
начинать
Service_Enabled: = ложь;
timer2.Enabled:=true; {Когда окно будет создано, откройте таймер}
конец;
file://Когда окно закрыто...
процедура TForm1.FormClose(Отправитель: TObject; var Action: TCloseAction);
начинать
timer1.Enabled:=false; {таймер выключения}
если Service_Enabled тогда
serverocket1.Active:=false; {Закрыть службу при выходе из программы}
конец;
файл://Кнопка выхода из программы…
процедура TForm1.N01Click(Отправитель: TObject);
начинать
form1.Close {Выход из программы}
конец;
file://После включения прокси-сервиса...
процедура TForm1.ServerSocket1Listen (Отправитель: TObject;
Сокет: TCustomWinSocket);
начинать
Service_Enabled:=true; {установить флаг службы}
N11.Включено:=ложь;
N21.Включено:=истина;
конец;
После того, как file:// подключается к прокси-серверу через прокси, сеанс устанавливается и привязывается к сокету...
процедура TForm1.ServerSocket1ClientConnect(Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
я, j: целое число;
начинать
j:=-1;
for i:=1 для сеансов do {найти, есть ли пустые элементы}
если не session[i-1].Used и не session[i-1].CSocket.active, тогда
начинать
j:=i-1; {Да, назначьте его}
session[j].Used:=true; {установлен как используется}
перерыв;
конец
еще
если не session[i-1].Used и session[i-1].CSocket.active, то
сеанс[i-1].CSocket.active:=false;
если j=-1 тогда
начать {нет, добавить один}
j:=сессии;
вкл (сессии);
setlength(сессия,сессии);
session[j].Used:=true; {установлен как используется}
session[j].CSocket:=TClientSocket.Create(ноль);
session[j].CSocket.OnConnect:=ClientSocket1Connect;
session[j].CSocket.OnDisconnect:=ClientSocket1Disconnect;
session[j].CSocket.OnError:=ClientSocket1Error;
session[j].CSocket.OnRead:=ClientSocket1Read;
session[j].CSocket.OnWrite:=ClientSocket1Write;
сеанс[j].Lookingup:=false;
конец;
session[j].SS_Handle:=socket.socketHandle {Сохраните дескриптор и реализуйте привязку}
сеанс[j].Request:=false {Нет запроса}
session[j].client_connected:=true; {клиент подключен}
session[j].remote_connected:=false; {удаленный пульт не подключен}
edit1.text:=inttostr(сессии);
конец;
Когда file:// отключен агентом...
процедура TForm1.ServerSocket1ClientDisconnect (Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
i,j,k: целое число;
начинать
для i:=1 для сеансов сделать
if (session[i-1].SS_Handle=socket.SocketHandle) и session[i-1].Used тогда
начинать
session[i-1].client_connected:=false; {клиент не подключен}
если сеанс[i-1].remote_connected, то
session[i-1].CSocket.active:=false {Если удаленное соединение все еще подключено, отключите его}
еще
session[i-1].Used:=false; {Если оба отключены, установите флаг ресурса освобождения}
перерыв;
конец;
j:=сессии;
к:=0;
for i:=1 to j do {В конце массива сеанса статистики есть несколько неиспользуемых элементов}
начинать
если сессия[ji].Используется тогда
перерыв;
вкл (к);
конец;
если k>0, то {Измените массив сеанса и освободите неиспользуемые элементы в конце}
начинать
сеансы: = сеансы-k;
setlength(сессия,сессии);
конец;
edit1.text:=inttostr(сессии);
конец;
Когда возникает ошибка связи file://...
процедура TForm1.ServerSocket1ClientError(Отправитель: TObject;
Сокет: TCustomWinSocket;ErrorEvent: TErrorEvent;
varErrorCode: Целое число);
вар
i,j,k: целое число;
начинать
для i:=1 для сеансов сделать
if (session[i-1].SS_Handle=socket.SocketHandle) и session[i-1].Used тогда
начинать
session[i-1].client_connected:=false; {клиент не подключен}
если сеанс[i-1].remote_connected, то
session[i-1].CSocket.active:=false {Если удаленное соединение все еще подключено, отключите его}
еще
session[i-1].Used:=false; {Если оба отключены, установите флаг ресурса освобождения}
перерыв;
конец;
j:=сессии;
к:=0;
для i:= от 1 до j сделать
начинать
если сессия[ji].Используется тогда
перерыв;
вкл (к);
конец;
если к>0, то
начинать
сеансы: = сеансы-k;
setlength(сессия,сессии);
конец;
edit1.text:=inttostr(сессии);
код ошибки:=0;
конец;
Когда прокси-сервер отправляет file:// для запроса страницы...
процедура TForm1.ServerSocket1ClientRead(Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
tmp, линия, хост: строка;
я,j,порт: целое число;
начинать
for i:=1 для сеансов do {определить, какой это сеанс}
если session[i-1].Used и (session[i-1].SS_Handle=socket.sockethandle), то
начинать
session[i-1].request_str:=socket.ReceiveText; {сохранить данные запроса}
tmp:=session[i-1].request_str; {хранится во временной переменной}
memo1.lines.add(tmp);
j:=pos(char(13)+char(10),tmp); {знак одной строки}
while j>0 do {сканируем текст запроса построчно в поисках адреса хоста}
начинать
line:=copy(tmp,1,j-1); {взять строку};
delete(tmp,1,j+1); {удалить строку}
j:=pos('Host',line); {флаг адреса хоста}
если j>0, то
начинать
delete(line,1,j+5); {удалить предыдущий недопустимый символ}
j:=pos(':',line);
если j>0, то
начинать
хост:=копия(строка,1,j-1);
удалить (строка, 1, j);
пытаться
порт: = strtoint (строка);
кроме
порт:=80;
конец;
конец
еще
начинать
хост:=trim(строка); {получить адрес хоста}
порт:=80;
конец;
if not session[i-1].remote_connected then {Если экспедиция еще не подключена}
начинать
session[i-1].Request:=true; {установить флаг готовности данных запроса}
session[i-1].CSocket.host:=host; {Установить адрес удаленного хоста}
session[i-1].CSocket.port:=порт {установить порт}
session[i-1].CSocket.active:=true {Подключиться к удаленному хосту}
сеанс[i-1].Lookingup:=true {установить флаг}
session[i-1].LookupTime:=0 {начинает отсчет с 0}
конец
еще
{Если пульт подключен, отправьте запрос напрямую}
сеанс[i-1].CSocket.socket.sendtext(сессия[i-1].request_str);
перерыв {остановить сканирование текста запроса};
конец;
j:=pos(char(13)+char(10),tmp); {указывает на следующую строку}
конец;
перерыв {остановка цикла}
конец;
конец;
file://При успешном подключении к удаленному хосту...
процедура TForm1.ClientSocket1Connect(Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
я: целое число;
начинать
для i:=1 для сеансов сделать
if (session[i-1].CSocket.socket.sockethandle=socket.SocketHandle) и session[i-1].Used тогда
начинать
session[i-1].CSocket.tag:=socket.SocketHandle;
session[i-1].remote_connected:=true {Установить флаг подключения к удаленному хосту}
сеанс[i-1].Lookingup:=false {очистить флаг};
перерыв;
конец;
конец;
file://Когда удаленный хост отключается...
процедура TForm1.ClientSocket1Disconnect(Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
i,j,k: целое число;
начинать
для i:=1 для сеансов сделать
if (session[i-1].CSocket.tag=socket.SocketHandle) и session[i-1].Used тогда
начинать
session[i-1].remote_connected:=false; {установлено как не подключено}
если не session[i-1].client_connected, то
session[i-1].Used:=false {Если клиент отключен, установите флаг ресурса освобождения}
еще
для k:=1 для serverocket1.Socket.ActiveConnections выполните
if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) и session[i-1].используется тогда
начинать
serverocket1.Socket.Connections[k-1].Close;
перерыв;
конец;
перерыв;
конец;
j:=сессии;
к:=0;
для i:= от 1 до j сделать
начинать
если сессия[ji].Используется тогда
перерыв;
вкл (к);
конец;
если k>0, то {исправить массив сеансов}
начинать
сеансы: = сеансы-k;
setlength(сессия,сессии);
конец;
edit1.text:=inttostr(сессии);
конец;
file://При возникновении ошибки связи с удаленным хостом...
процедура TForm1.ClientSocket1Error(Отправитель: TObject;
Сокет: TCustomWinSocket;ErrorEvent: TErrorEvent;
varErrorCode: Целое число);
вар
i,j,k: целое число;
начинать
для i:=1 для сеансов сделать
if (session[i-1].CSocket.tag=socket.SocketHandle) и session[i-1].Used тогда
начинать
сокет.закрыть;
session[i-1].remote_connected:=false; {установлено как не подключено}
если не session[i-1].client_connected, то
session[i-1].Used:=false {Если клиент отключен, установите флаг ресурса освобождения}
еще
для k:=1 для serverocket1.Socket.ActiveConnections выполните
if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) и session[i-1].используется тогда
начинать
serverocket1.Socket.Connections[k-1].Close;
перерыв;
конец;
перерыв;
конец;
j:=сессии;
к:=0;
для i:= от 1 до j сделать
начинать
если сессия[ji].Используется тогда
перерыв;
вкл (к);
конец;
код ошибки:=0;
если k>0, то {исправить массив сеансов}
начинать
сеансы: = сеансы-k;
setlength(сессия,сессии);
конец;
edit1.text:=inttostr(сессии);
конец;
file://Отправляет запрос страницы на удаленный хост…
процедура TForm1.ClientSocket1Write(Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
я: целое число;
начинать
для i:=1 для сеансов сделать
if (session[i-1].CSocket.tag=socket.SocketHandle) и session[i-1].Used тогда
начинать
если сеанс[i-1].Запрос тогда
начинать
сокет.SendText(session[i-1].request_str); {Если есть запрос, отправьте}
сеанс[i-1].Request:=false; {очистить флаг}
конец;
перерыв;
конец;
конец;
file://Когда удаленный хост отправляет данные страницы...
процедура TForm1.ClientSocket1Read(Отправитель: TObject;
Сокет: TCustomWinSocket);
вар
я, j: целое число;
Rec_bytes: целое число; {длина возвращаемого блока данных}
Rec_Buffer: массив [0..2047] char {буфер блока возвращаемых данных};
начинать
для i:=1 для сеансов сделать
if (session[i-1].CSocket.tag=socket.SocketHandle) и session[i-1].Used тогда
начинать
Rec_bytes:=socket.ReceiveBuf(rec_buffer,2048); {получить данные};
для j:=1 для serverocket1.Socket.ActiveConnections сделайте
если serverocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle, то
начинать
serverocket1.Socket.Connections[j-1].SendBuf(rec_buffer,rec_bytes); {Отправить данные};
перерыв;
конец;
перерыв;
конец;
конец;
File:// «Страница не найдена» и другие сообщения об ошибках появляются...
процедура TForm1.AppException (Отправитель: TObject; E: Exception);
начинать
вкл (недействительные запросы);
конец;
file://Найти время удаленного хоста...
процедура TForm1.Timer1Timer(Отправитель: TObject);
вар
я, j: целое число;
начинать
для i:=1 для сеансов сделать
если сеанс[i-1].Используется и сеанс[i-1].Поиск, то {при подключении}
начинать
inc(сессия[i-1].LookupTime);
если session[i-1].LookupTime>lookuptimeout, то {if timeout}
начинать
сеанс[i-1].Lookingup:=false;
session[i-1].CSocket.active:=false; {прекратить поиск}
для j:=1 для serverocket1.Socket.ActiveConnections сделайте
если serverocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle, то
начинать
serverocket1.Socket.Connections[j-1].Close {отключить клиент};
перерыв;
конец;
конец;
конец;
конец;
конец.
3. Постскриптум
Поскольку эта идея дизайна добавляет только функцию перенаправления между прокси-концом и удаленным хостом, исходный прокси-конец
Некоторые функции, такие как технология кэширования, сохраняются, поэтому эффективность высока. После тестирования при использовании модема 33,6К для доступа в Интернет от трех до десяти прокси-рабочих станций могут одновременно получить доступ к Интернету, при этом сохраняется хорошая скорость отклика. Поскольку соединение между рабочей станцией прокси и рабочей станцией прокси-сервера обычно осуществляется через высокоскоростное соединение, узкое место в основном возникает в методе доступа прокси-сервера в Интернет.
С помощью описанного выше метода автор успешно разработал полный набор программного обеспечения прокси-сервера и полностью интегрировал его с биллинговой системой компьютерного зала.
С успехом можно использовать одну рабочую станцию для выполнения таких функций, как интернет-прокси, интернет-биллинг и биллинг за использование оборудования. Друзья с опытом программирования могут добавить дополнительные функции прокси-сервера, такие как настройка запрещенных к доступу сайтов, подсчет трафика клиентов, списки веб-доступа и т. д.