Delphi를 사용하여 자체 프록시 서버 설계
저자가 인터넷 요금 청구 소프트웨어를 작성할 때 근거리 통신망의 각 워크스테이션에 대한 인터넷 액세스 요금을 청구하는 방법에 대한 문제가 있었습니다. 일반적으로 이러한 워크스테이션은 기성 프록시 서버 소프트웨어를 사용하여 인터넷에 액세스합니다. 프록시 서버 소프트웨어는 폐쇄형 시스템이므로 실시간 인터넷 액세스 타이밍 정보를 얻는 프로그램을 작성하기가 어렵습니다. 따라서 한편으로는 그룹 인터넷 액세스 문제와 다른 한편으로는 과금 문제를 해결하기 위해 자체 프록시 서버를 작성할 수 있는지 생각해 보십시오.
실험적인 프로그래밍 후에 문제는 마침내 만족스럽게 해결되었습니다. 지금 바로 적어서 동료들과 공유해보세요.
1. 아이디어
현재 널리 사용되는 브라우저의 시스템 옵션에는 "프록시 서버를 통해 연결"이라는 매개변수가 있습니다.
로컬 네트워크의 워크스테이션이 이 속성을 지정한 다음 인터넷 요청을 발행하면 요청 데이터가 지정된 프록시 서버로 전송됩니다. 다음은 요청 패킷의 예입니다.
http://home.microsoft.com/intl/cn/ HTTP/1.0 받기
수용하다: */*
허용 언어: zh-cn
Accept-Encoding: gzip, deflate
사용자 에이전트: Mozilla/4.0(호환 가능, MSIE 5.0, Windows NT)
호스트: home.microsoft.com
PRoxy 연결: 연결 유지
첫 번째 줄은 대상 URL과 관련 방법 및 프로토콜이고 "Host" 줄은 대상 호스트의 주소를 지정합니다.
이를 통해 우리는 프록시 서비스의 프로세스를 알 수 있습니다. 즉, 프록시로부터 요청을 수신하고, 실제 호스트에 연결하고, 호스트에서 반환된 데이터를 수신하고, 수신된 데이터를 프록시로 보냅니다.
이를 위해 위의 네트워크 통신 리디렉션 문제를 완료하는 간단한 프로그램을 작성할 수 있습니다.
Delphi로 디자인할 때 프록시 워크스테이션과 통신하기 위한 소켓 컨트롤로 ServerSocket을 선택하고 원격 호스트와 통신하기 위한 소켓 컨트롤로 ClientSocket 동적 배열을 선택합니다.
프로그래밍 시 해결해야 할 중요한 문제는 다중 연결 처리 문제입니다. 프록시 서비스 및 에이전트의 응답 속도를 높이기 위해서는 각 통신 세션마다 소켓 제어 속성을 비차단으로 설정해야 합니다. 동적으로 소켓에 바인딩된 경우 소켓의 SocketHandle 속성 값을 사용하여 해당 소켓이 속한 세션을 확인합니다.
통신 연결 과정은 아래 그림과 같습니다.
프록시 서버
서버소켓
(1) 받다
에이전트가 원격 호스트로 보냄
(6) (2) (5)
브라우저 ClientSocket(4) 웹 서버
인수하다
보내기(3)
(1) 프록시 브라우저는 웹 요청을 보내고, 프록시 서버의 Serversocket은 요청을 받습니다.
(2) 프록시 서버 프로그램은 자동으로 ClientSocket을 생성하고 호스트 주소, 포트 및 기타 속성을 설정한 후 원격 호스트에 연결합니다.
(3) 원격 연결 후 send 이벤트가 발생하고 Serversocket이 수신한 웹 요청 패킷이 원격 호스트로 전송됩니다.
(4) 원격 호스트가 페이지 데이터를 반환하면 ClientSocket의 읽기 이벤트가 트리거되어 페이지 데이터를 읽습니다.
(5) 프록시 서버 프로그램은 바인딩 정보를 기반으로 호스트로부터 받은 페이지 정보를 프록시 대상으로 보내야 하는 ServerSocket 컨트롤의 어느 소켓을 결정합니다.
(6) ServerSocket의 해당 소켓은 페이지 데이터를 에이전트로 보냅니다.
2. 프로그래밍
주로 ServerSocket 및 ClientSocket과 관련된 위의 통신 프로세스를 Delphi를 사용하여 설계하는 것은 매우 간단합니다.
소프트웨어 드라이버 프로그래밍. 다음은 간단한 기능 설명을 포함하여 저자가 작성한 실험용 프록시 서버 인터페이스 및 소스 프로그램 목록입니다.
유닛 메인;
인터페이스
용도
Windows, 메시지, SysUtils, 클래스, 그래픽, 컨트롤, 양식, 대화 상자,
ExtCtrls, ScktComp, TrayIcon, 메뉴, StdCtrls;
유형
session_record=기록
사용됨: 부울 {세션 녹음 가능 여부}
SS_Handle: 정수; {프록시 서버 소켓 핸들}
CSocket: TClientSocket; {원격 연결에 사용되는 소켓}
조회: 부울 {서버를 찾고 있는지 여부}
LookupTime: 정수; {조회 서버 시간}
요청: 부울 {요청이 있는지 여부}
request_str: 문자열 {요청 데이터 블록}
client_connected: 부울; {클라이언트 온라인 플래그}
remote_connected: 부울; {원격 서버 연결 플래그}
끝;
유형
TForm1 = 클래스(TForm)
서버소켓1: TServerSocket;
ClientSocket1: TClientSocket;
타이머2: T타이머;
TrayIcon1: TTrayIcon;
PopupMenu1: TPopupMenu;
N11: TMenu항목;
N21: TMenu항목;
N1: TMenuItem;
N01: TMenu항목;
메모1: TMemo;
편집1: T편집;
라벨1: TLabel;
타이머1: T타이머;
절차 Timer2Timer(Sender: TObject);
절차 N11Click(보내는 사람: TObject);
절차 FormCreate(보내는 사람: TObject);
절차 FormClose(Sender: TObject; var Action: TCloseAction);
절차 N21Click(보내는 사람: TObject);
절차 N01Click(보내는 사람: TObject);
프로시저 ServerSocket1ClientConnect(Sender: TObject;
소켓: TCustomWinSocket);
프로시저 ServerSocket1ClientDisconnect(Sender: TObject;
소켓: TCustomWinSocket);
프로시저 ServerSocket1ClientError(Sender: TObject;
소켓: TCustomWinSocket: TErrorEvent;
varErrorCode: 정수);
프로시저 ServerSocket1ClientRead(Sender: TObject;
소켓: TCustomWinSocket);
프로시저 ClientSocket1Connect(Sender: TObject;
소켓: TCustomWinSocket);
프로시저 ClientSocket1Disconnect(Sender: TObject;
소켓: TCustomWinSocket);
절차 ClientSocket1Error(발신자: TObject; 소켓: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: 정수);
프로시저 ClientSocket1Write(Sender: TObject;
소켓: TCustomWinSocket);
절차 ClientSocket1Read(Sender: TObject; 소켓: TCustomWinSocket);
프로시저 ServerSocket1Listen(Sender: TObject;
소켓: TCustomWinSocket);
절차 AppException(Sender: TObject; E: 예외);
절차 Timer1Timer(Sender: TObject);
사적인
{비공개 선언}
공공의
Service_Enabled: 부울; {프록시 서비스 활성화 여부}
세션: session_record의 배열 {세션 배열}
세션: 정수; {세션 수}
LookUpTimeOut: 정수 {연결 시간 초과 값}
InvalidRequests: 정수; {잘못된 요청 수}
끝;
var
Form1: TForm1;
구현
{$R *.DFM}
file://시스템 시작 타이머, 시작 창이 표시된 후 시스템 트레이로 축소...
절차 TForm1.Timer2Timer(Sender: TObject);
시작하다
타이머2.Enabled:=false; {타이머 끄기}
세션:=0; {세션 수=0}
application.OnException := AppException; {프록시 서버에서 발생하는 예외를 보호하기 위해}
잘못된 요청:=0; {0 오류}
LookUpTimeOut:=60000; {시간 초과 값=1분}
타이머1.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(Sender: TObject);
시작하다
서비스_활성화:=false;
timer2.Enabled:=true; {창이 생성되면 타이머를 엽니다.}
끝;
file://창을 닫을 때...
절차 TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
시작하다
타이머1.Enabled:=false; {타이머 끄기}
Service_Enabled이면
serverocket1.Active:=false; {프로그램 종료 시 서비스 종료}
끝;
파일://프로그램 종료 버튼…
절차 TForm1.N01Click(보내는 사람: TObject);
시작하다
form1.Close;{프로그램 종료}
끝;
file://프록시 서비스를 활성화한 후...
프로시저 TForm1.ServerSocket1Listen(Sender: TObject;
소켓: TCustomWinSocket);
시작하다
Service_Enabled:=true; {서비스 플래그 설정}
N11.활성화:=false;
N21.활성화:=true;
끝;
file://이 프록시에 의해 프록시 서버에 연결된 후 세션이 설정되고 소켓에 바인딩됩니다.
프로시저 TForm1.ServerSocket1ClientConnect(Sender: TObject;
소켓: TCustomWinSocket);
var
i,j: 정수;
시작하다
j:=-1;
i:=1에서 세션 수행 {빈 항목이 있는지 확인}
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:=세션;
inc(세션);
setlength(세션, 세션);
session[j].Used:=true; {사용 중인 것으로 설정}
세션[j].CSocket:=TClientSocket.Create(nil);
세션[j].CSocket.OnConnect:=ClientSocket1Connect;
세션[j].CSocket.OnDisconnect:=ClientSocket1Disconnect;
세션[j].CSocket.OnError:=ClientSocket1Error;
세션[j].CSocket.OnRead:=ClientSocket1Read;
세션[j].CSocket.OnWrite:=ClientSocket1Write;
세션[j].Lookingup:=false;
끝;
session[j].SS_Handle:=socket.socketHandle; {핸들 저장 및 바인딩 구현}
session[j].Request:=false; {요청 없음}
session[j].client_connected:=true; {클라이언트가 연결되었습니다.}
session[j].remote_connected:=false; {원격 연결되지 않음}
edit1.text:=inttostr(세션);
끝;
file://이 에이전트에 의해 연결이 끊어지면...
절차 TForm1.ServerSocket1ClientDisconnect(Sender: TObject;
소켓: TCustomWinSocket);
var
i,j,k: 정수;
시작하다
i:=1에서 세션 수행
if (session[i-1].SS_Handle=socket.SocketHandle) 및 session[i-1].Used then
시작하다
session[i-1].client_connected:=false; {클라이언트가 연결되지 않았습니다.}
session[i-1].remote_connected인 경우
session[i-1].CSocket.active:=false {원격 연결이 아직 연결되어 있으면 연결을 끊습니다.}
또 다른
session[i-1].Used:=false; {둘 다 연결이 끊어진 경우 릴리스 리소스 플래그를 설정합니다.}
부서지다;
끝;
j:=세션;
k:=0;
for i:=1 to j do {통계 세션 배열 끝에 사용되지 않은 항목이 여러 개 있습니다}
시작하다
if session[ji].Used then
부서지다;
Inc(k);
끝;
k>0이면 {세션 배열을 수정하고 마지막에 사용하지 않은 항목을 해제}
시작하다
세션:=세션-k;
setlength(세션, 세션);
끝;
edit1.text:=inttostr(세션);
끝;
file://communication 오류가 발생하는 경우...
프로시저 TForm1.ServerSocket1ClientError(Sender: TObject;
소켓: TCustomWinSocket: TErrorEvent;
varErrorCode: 정수);
var
i,j,k: 정수;
시작하다
i:=1에서 세션 수행
if (session[i-1].SS_Handle=socket.SocketHandle) 및 session[i-1].Used then
시작하다
session[i-1].client_connected:=false; {클라이언트가 연결되지 않았습니다.}
session[i-1].remote_connected인 경우
session[i-1].CSocket.active:=false {원격 연결이 아직 연결되어 있으면 연결을 끊습니다.}
또 다른
session[i-1].Used:=false; {둘 다 연결이 끊어진 경우 릴리스 리소스 플래그를 설정합니다.}
부서지다;
끝;
j:=세션;
k:=0;
i:=1 ~ j do
시작하다
if session[ji].Used then
부서지다;
Inc(k);
끝;
k>0이면
시작하다
세션:=세션-k;
setlength(세션, 세션);
끝;
edit1.text:=inttostr(세션);
오류코드:=0;
끝;
페이지를 요청하기 위해 프록시가 file://을 전송하면...
프로시저 TForm1.ServerSocket1ClientRead(Sender: TObject;
소켓: TCustomWinSocket);
var
tmp,라인,호스트: 문자열;
i,j,포트: 정수;
시작하다
i:=1에서 세션은 {어떤 세션인지 결정}합니다.
세션[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('호스트',line) {호스트 주소 플래그}
j>0이면
시작하다
delete(line,1,j+5); {이전의 잘못된 문자를 삭제합니다.}
j:=pos(':',line);
j>0이면
시작하다
호스트:=copy(line,1,j-1);
삭제(라인,1,j);
노력하다
포트:=strtoint(라인);
제외하고
포트:=80;
끝;
끝
또 다른
시작하다
호스트:=trim(line); {호스트 주소 가져오기}
포트:=80;
끝;
session[i-1].remote_connected가 아닌 경우 {탐사가 아직 연결되지 않은 경우}
시작하다
session[i-1].Request:=true; {요청 데이터 준비 플래그 설정}
session[i-1].CSocket.host:=host; {원격 호스트 주소 설정}
세션[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(session[i-1].request_str);
break; {요청 텍스트 스캔 중지}
끝;
j:=pos(char(13)+char(10),tmp) {다음 줄을 가리킴}
끝;
중단; {루프 중지}
끝;
끝;
file://원격 호스트에 접속에 성공하면...
절차 TForm1.ClientSocket1Connect(Sender: TObject;
소켓: TCustomWinSocket);
var
나는: 정수;
시작하다
i:=1에서 세션 수행
if (session[i-1].CSocket.socket.sockethandle=socket.SocketHandle) 및 session[i-1].Used then
시작하다
세션[i-1].CSocket.tag:=socket.SocketHandle;
session[i-1].remote_connected:=true; {원격 호스트 연결 플래그 설정}
세션[i-1].Lookingup:=false; {플래그 지우기}
부서지다;
끝;
끝;
file://원격 호스트의 연결이 끊어지면...
절차 TForm1.ClientSocket1Disconnect(Sender: TObject;
소켓: TCustomWinSocket);
var
i,j,k: 정수;
시작하다
i:=1에서 세션 수행
if (session[i-1].CSocket.tag=socket.SocketHandle) 및 session[i-1].Used then
시작하다
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].used then
시작하다
serverocket1.Socket.Connections[k-1].Close;
부서지다;
끝;
부서지다;
끝;
j:=세션;
k:=0;
i:=1 ~ j do
시작하다
if session[ji].Used then
부서지다;
Inc(k);
끝;
k>0이면 {세션 배열 수정}
시작하다
세션:=세션-k;
setlength(세션, 세션);
끝;
edit1.text:=inttostr(세션);
끝;
file://원격 호스트와 통신 중 오류가 발생하는 경우...
프로시저 TForm1.ClientSocket1Error(Sender: TObject;
소켓: TCustomWinSocket: TErrorEvent;
varErrorCode: 정수);
var
i,j,k: 정수;
시작하다
i:=1에서 세션 수행
if (session[i-1].CSocket.tag=socket.SocketHandle) 및 session[i-1].Used then
시작하다
소켓.닫기;
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].used then
시작하다
serverocket1.Socket.Connections[k-1].Close;
부서지다;
끝;
부서지다;
끝;
j:=세션;
k:=0;
i:=1 ~ j do
시작하다
if session[ji].Used then
부서지다;
Inc(k);
끝;
오류코드:=0;
k>0이면 {세션 배열 수정}
시작하다
세션:=세션-k;
setlength(세션, 세션);
끝;
edit1.text:=inttostr(세션);
끝;
file://원격 호스트에 페이지 요청을 보냅니다…
절차 TForm1.ClientSocket1Write(Sender: TObject;
소켓: TCustomWinSocket);
var
나는: 정수;
시작하다
i:=1에서 세션 수행
if (session[i-1].CSocket.tag=socket.SocketHandle) 및 session[i-1].Used then
시작하다
세션[i-1].요청인 경우
시작하다
소켓.SendText(session[i-1].request_str) {요청이 있으면 전송}
세션[i-1].Request:=false; {플래그 지우기}
끝;
부서지다;
끝;
끝;
file://원격 호스트가 페이지 데이터를 보낼 때...
프로시저 TForm1.ClientSocket1Read(Sender: TObject;
소켓: TCustomWinSocket);
var
i,j: 정수;
Rec_bytes: 정수; {반환된 데이터 블록 길이}
rec_Buffer: {반환된 데이터 블록 버퍼}의 배열[0..2047];
시작하다
i:=1에서 세션 수행
if (session[i-1].CSocket.tag=socket.SocketHandle) 및 session[i-1].Used then
시작하다
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(Sender: TObject; E: 예외);
시작하다
inc(잘못된 요청);
끝;
file://원격 호스트 타이밍 찾기...
절차 TForm1.Timer1Timer(Sender: TObject);
var
i,j: 정수;
시작하다
i:=1에서 세션 수행
if session[i-1].Used 및 session[i-1].Lookingup then {연결 중인 경우}
시작하다
inc(세션[i-1].LookupTime);
if session[i-1].LookupTime>lookuptimeout then {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.6K 모뎀을 사용하여 인터넷에 액세스하면 3~10개의 프록시 워크스테이션이 동시에 인터넷에 액세스할 수 있으며 여전히 좋은 응답 속도가 있습니다. 프록시 워크스테이션과 프록시 서버 워크스테이션 간의 연결은 일반적으로 고속 링크를 통과하므로 병목 현상은 주로 프록시 서버의 인터넷 접속 방식에서 발생합니다.
위의 방법을 통해 저자는 완전한 프록시 서버 소프트웨어 세트를 성공적으로 개발하고 이를 컴퓨터실 청구 시스템과 완전히 통합했습니다.
성공적으로 하나의 워크스테이션을 사용하여 인터넷 프록시, 인터넷 요금 청구 및 컴퓨터 사용 요금 청구와 같은 기능을 완료하는 것이 가능합니다. 프로그래밍 경험이 있는 친구는 액세스 금지 사이트 설정, 고객 트래픽 계산, 웹 액세스 목록 등과 같은 추가 프록시 서버 기능을 추가할 수 있습니다.
작성자 블로그: http://blog.csdn.net/BExpress/