Entwerfen Sie Ihren eigenen Proxyserver mit Delphi
Als der Autor eine Internet-Abrechnungssoftware schrieb, ging es um die Frage, wie der Internetzugang für jeden Arbeitsplatz im lokalen Netzwerk abgerechnet werden soll. Im Allgemeinen greifen diese Workstations über einen Proxyserver auf das Internet zu. Da es sich bei der Proxyserversoftware um ein geschlossenes System handelt, ist es schwierig, ein Programm zu schreiben, um Echtzeitinformationen zum Internetzugang zu erhalten. Überlegen Sie daher, ob Sie einen eigenen Proxyserver schreiben können, um einerseits das Problem des Gruppen-Internetzugangs und andererseits das Abrechnungsproblem zu lösen?
Nach experimenteller Programmierung konnte das Problem schließlich zufriedenstellend gelöst werden. Schreiben Sie es jetzt auf und teilen Sie es mit Ihren Kollegen.
1. Ideen
In den Systemoptionen aktuell gängiger Browser gibt es einen Parameter, nämlich „Verbindung über einen Proxyserver herstellen“.
Versuchen Sie, wenn eine Workstation im lokalen Netzwerk dieses Attribut angibt und dann eine Internetanforderung ausgibt, werden die Anforderungsdaten an den angegebenen Proxyserver gesendet. Das Folgende ist ein Beispiel für ein Anforderungspaket:
GET http://home.microsoft.com/intl/cn/ HTTP/1.0
Akzeptieren: */*
Akzeptierte Sprache: zh-cn
Akzeptierte Kodierung: gzip, deflate
Benutzeragent: Mozilla/4.0 (kompatibel; MSIE 5.0; Windows NT)
Host: home.microsoft.com
Proxy-Verbindung: Keep-Alive
Die erste Zeile enthält die Ziel-URL sowie zugehörige Methoden und Protokolle, und die Zeile „Host“ gibt die Adresse des Zielhosts an.
Daraus kennen wir den Prozess des Proxy-Dienstes: Empfangen der Anfrage vom Proxy, Herstellen einer Verbindung zum realen Host, Empfangen der vom Host zurückgegebenen Daten und Senden der empfangenen Daten an den Proxy.
Zu diesem Zweck kann ein einfaches Programm geschrieben werden, um das oben genannte Problem der Netzwerkkommunikationsumleitung zu lösen.
Wählen Sie beim Entwerfen mit Delphi ServerSocket als Socket-Steuerung für die Kommunikation mit der Proxy-Workstation und das dynamische Array ClientSocket als Socket-Steuerung für die Kommunikation mit dem Remote-Host.
Ein wichtiges Problem, das während der Programmierung gelöst werden sollte, ist das Problem der Mehrfachverbindungsverarbeitung. Um den Proxy-Dienst und die Antwortgeschwindigkeit des Agenten zu beschleunigen, sollten die Eigenschaften der Socket-Steuerung auf nicht blockierend eingestellt werden ist dynamisch an den Socket gebunden. Verwenden Sie den SocketHandle-Attributwert des Sockets, um zu bestimmen, zu welcher Sitzung er gehört.
Der Kommunikationsverbindungsprozess ist in der folgenden Abbildung dargestellt:
Proxyserver
Serversocket
(1) erhalten
Wird vom Agenten an den Remote-Host gesendet
(6) (2) (5)
Browser ClientSocket (4) Webserver
übernehmen
Senden(3)
(1) Der Proxy-Browser sendet eine Web-Anfrage und der Serversocket des Proxy-Servers empfängt die Anfrage.
(2) Das Proxyserverprogramm erstellt automatisch einen ClientSocket, legt die Hostadresse, den Port und andere Attribute fest und stellt dann eine Verbindung zum Remote-Host her.
(3) Nach der Remote-Verbindung wird das Sendeereignis ausgelöst und das von Serversocket empfangene Webanforderungspaket wird an den Remote-Host gesendet.
(4) Wenn der Remote-Host Seitendaten zurückgibt, wird das Leseereignis von ClientSocket ausgelöst, um die Seitendaten zu lesen.
(5) Das Proxy-Server-Programm bestimmt anhand der Bindungsinformationen, welcher Socket im ServerSocket-Steuerelement die vom Host empfangenen Seiteninformationen an das Proxy-Ende senden soll.
(6) Der entsprechende Socket in ServerSocket sendet die Seitendaten an den Agenten.
2. Programmierung
Es ist sehr einfach, den oben genannten Kommunikationsprozess mit Delphi zu entwerfen, der sich hauptsächlich auf ServerSocket und ClientSocket bezieht.
Programmierung von Softwaretreibern. Das Folgende ist eine Liste der vom Autor geschriebenen experimentellen Proxy-Server-Schnittstelle und des Quellprogramms, einschließlich einer kurzen Funktionsbeschreibung:
Haupteinheit;
Schnittstelle
verwendet
Windows, Nachrichten, SysUtils, Klassen, Grafiken, Steuerelemente, Formulare, Dialoge,
ExtCtrls, ScktComp, TrayIcon, Menus, StdCtrls;
Typ
session_record=Datensatz
Verwendet: boolean; {ob Sitzungsaufzeichnung verfügbar ist}
SS_Handle: Ganzzahl; {Proxy-Server-Socket-Handle}
CSocket: TClientSocket; {Socket zur Verbindung mit der Fernbedienung}
Lookingup: boolean; {ob nach dem Server gesucht wird}
LookupTime: integer; {Lookup-Serverzeit}
Anfrage: boolean; {ob es eine Anfrage gibt}
request_str: string; {Anforderungsdatenblock}
client_connected: boolean; {Client-Online-Flagge}
remote_connected: boolean; {Remote-Server-Verbindungsflag}
Ende;
Typ
TForm1 = Klasse(TForm)
ServerSocket1: TServerSocket;
ClientSocket1: TClientSocket;
Timer2: TTimer;
TrayIcon1: TTrayIcon;
PopupMenu1: TPopupMenu;
N11: TMenuItem;
N21: TMenuItem;
N1: TMenuItem;
N01: TMenuItem;
Memo1: TMemo;
Edit1: TEdit;
Label1: TLabel;
Timer1: TTimer;
procedure Timer2Timer(Sender: TObject);
procedure N11Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure N21Click(Sender: TObject);
procedure N01Click(Sender: TObject);
procedure ServerSocket1ClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ServerSocket1ClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ServerSocket1ClientError(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
varErrorCode: Integer);
procedure ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
procedure ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ClientSocket1Disconnect(Sender: TObject;
Socket: TCustomWinSocket);
procedure ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var ErrorCode: Integer);
Prozedur ClientSocket1Write(Sender: TObject;
Socket: TCustomWinSocket);
procedure ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket);
procedure ServerSocket1Listen(Sender: TObject;
Socket: TCustomWinSocket);
procedure AppException(Sender: TObject; E: Exception);
procedure Timer1Timer(Sender: TObject);
Privat
{Private Erklärungen}
öffentlich
Service_Enabled: boolean; {ob der Proxy-Dienst aktiviert ist}
session: Array von session_record; {session array}
Sitzungen: Ganzzahl; {Anzahl der Sitzungen}
LookUpTimeOut: integer; {Verbindungszeitüberschreitungswert}
InvalidRequests: Ganzzahl; {Anzahl ungültiger Anfragen}
Ende;
var
Form1: TForm1;
Durchführung
{$R *.DFM}
file://Systemstart-Timer, nach Anzeige des Startfensters in die Taskleiste verkleinern...
Prozedur TForm1.Timer2Timer(Sender: TObject);
beginnen
timer2.Enabled:=false; {Timer ausschalten}
Sitzungen:=0; {Anzahl der Sitzungen=0}
application.OnException := AppException; {Um Ausnahmen abzuschirmen, die auf dem Proxyserver auftreten}
invalidRequests:=0; {0 Fehler}
LookUpTimeOut:=60000; {timeout value=1 minute}
timer1.Enabled:=true; {Timer einschalten}
n11.Enabled:=false; {Dienstmenüelement aktivieren ungültig}
n21.Enabled:=true; {Menüelement „Service schließen“ ist gültig}
serverocket1.Port:=988; {Proxyserver-Port=988}
serverocket1.Active:=true; {Dienst starten}
form1.hide; {Schnittstelle ausblenden, in die Taskleiste verkleinern}
Ende;
file://Service-Menüpunkt öffnen…
procedure TForm1.N11Click(Sender: TObject);
beginnen
serverocket1.Active:=true; {Dienst starten}
Ende;
file://Menüpunkt Dienst stoppen…
procedure TForm1.N21Click(Sender: TObject);
beginnen
serverocket1.Active:=false; {Dienst stoppen}
N11.Enabled:=True;
N21.Enabled:=False;
Service_Enabled:=false; {Flag gelöscht}
Ende;
file://Erstellung des Hauptfensters…
procedure TForm1.FormCreate(Sender: TObject);
beginnen
Service_Enabled:=false;
timer2.Enabled:=true; {Wenn das Fenster erstellt wird, öffne den Timer}
Ende;
file://Wenn das Fenster geschlossen ist...
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
beginnen
timer1.Enabled:=false; {Timer ausschalten}
wenn Service_Enabled dann
serverocket1.Active:=false; {Dienst beim Beenden des Programms schließen}
Ende;
Datei://Schaltfläche „Programm beenden“…
procedure TForm1.N01Click(Sender: TObject);
beginnen
form1.Close; {Programm beenden}
Ende;
file://Nach dem Einschalten des Proxy-Dienstes...
procedure TForm1.ServerSocket1Listen(Sender: TObject;
Socket: TCustomWinSocket);
beginnen
Service_Enabled:=true; {Service-Flag setzen}
N11.Enabled:=false;
N21.Enabled:=true;
Ende;
Nachdem file:// über den Proxy mit dem Proxyserver verbunden wurde, wird eine Sitzung aufgebaut und an den Socket gebunden ...
procedure TForm1.ServerSocket1ClientConnect(Sender: TObject;
Socket: TCustomWinSocket);
var
i,j: ganze Zahl;
beginnen
j:=-1;
for i:=1 to session do {find if there are blank items}
wenn nicht session[i-1].Used und nicht session[i-1].CSocket.active dann
beginnen
j:=i-1; {Ja, weise es zu}
session[j].Used:=true; {set as in use}
brechen;
Ende
anders
wenn nicht session[i-1].Used und session[i-1].CSocket.active dann
session[i-1].CSocket.active:=false;
wenn j=-1 dann
begin {keine, füge eins hinzu}
j:=Sitzungen;
inc(Sitzungen);
setlength(session,sessions);
session[j].Used:=true; {set as in use}
session[j].CSocket:=TClientSocket.Create(nil);
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;
session[j].Lookingup:=false;
Ende;
session[j].SS_Handle:=socket.socketHandle; {Handle speichern und Bindung implementieren}
session[j].Request:=false; {Keine Anfrage}
session[j].client_connected:=true; {Client ist verbunden}
session[j].remote_connected:=false; {remote nicht verbunden}
edit1.text:=inttostr(sessions);
Ende;
Wenn file:// vom Agenten getrennt wird...
procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject;
Socket: TCustomWinSocket);
var
i,j,k: ganze Zahl;
beginnen
für i:=1 bis Sitzungen tun
if (session[i-1].SS_Handle=socket.SocketHandle) und session[i-1].Used then
beginnen
session[i-1].client_connected:=false; {Client ist nicht verbunden}
if session[i-1].remote_connected dann
session[i-1].CSocket.active:=false {Wenn die Remote-Verbindung noch besteht, trennen Sie sie.}
anders
session[i-1].Used:=false; {Wenn beide getrennt sind, setzen Sie das Release-Ressourcen-Flag}
brechen;
Ende;
j:=Sitzungen;
k:=0;
for i:=1 to j do {Am Ende des Statistiksitzungsarrays befinden sich mehrere nicht verwendete Elemente}
beginnen
if session[ji].Wird dann verwendet
brechen;
inc(k);
Ende;
Wenn k>0, dann {Ändern Sie das Sitzungsarray und geben Sie nicht verwendete Elemente am Ende frei}
beginnen
Sitzungen:=Sitzungen-k;
setlength(session,sessions);
Ende;
edit1.text:=inttostr(sessions);
Ende;
Wenn ein Datei://Kommunikationsfehler auftritt...
procedure TForm1.ServerSocket1ClientError(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
varErrorCode: Integer);
var
i,j,k: ganze Zahl;
beginnen
für i:=1 bis Sitzungen tun
if (session[i-1].SS_Handle=socket.SocketHandle) und session[i-1].Used then
beginnen
session[i-1].client_connected:=false; {Client ist nicht verbunden}
if session[i-1].remote_connected dann
session[i-1].CSocket.active:=false {Wenn die Remote-Verbindung noch besteht, trennen Sie sie.}
anders
session[i-1].Used:=false; {Wenn beide getrennt sind, setzen Sie das Release-Ressourcen-Flag}
brechen;
Ende;
j:=Sitzungen;
k:=0;
für i:=1 bis j tun
beginnen
if session[ji].Wird dann verwendet
brechen;
inc(k);
Ende;
wenn k>0 dann
beginnen
Sitzungen:=Sitzungen-k;
setlength(session,sessions);
Ende;
edit1.text:=inttostr(sessions);
Fehlercode:=0;
Ende;
Wenn file:// vom Proxy gesendet wird, um die Seite anzufordern ...
procedure TForm1.ServerSocket1ClientRead(Sender: TObject;
Socket: TCustomWinSocket);
var
tmp,line,host: string;
i,j,port: ganze Zahl;
beginnen
for i:=1 to session do {Bestimmen Sie, um welche Sitzung es sich handelt}
if session[i-1].Used and (session[i-1].SS_Handle=socket.sockethandle) then
beginnen
session[i-1].request_str:=socket.ReceiveText; {Anfragedaten speichern}
tmp:=session[i-1].request_str; {in temporärer Variable gespeichert}
memo1.lines.add(tmp);
j:=pos(char(13)+char(10),tmp); {one line mark}
while j>0 do {den Anforderungstext Zeile für Zeile scannen und nach der Hostadresse suchen}
beginnen
line:=copy(tmp,1,j-1); {take a line}
delete(tmp,1,j+1); {eine Zeile löschen}
j:=pos('Host',line); {Host-Adress-Flag}
wenn j>0 dann
beginnen
delete(line,1,j+5); {das vorherige ungültige Zeichen löschen}
j:=pos(':',line);
wenn j>0 dann
beginnen
host:=copy(line,1,j-1);
delete(line,1,j);
versuchen
port:=strtoint(line);
außer
Port:=80;
Ende;
Ende
anders
beginnen
host:=trim(line); {Hostadresse abrufen}
Port:=80;
Ende;
if not session[i-1].remote_connected then {Wenn die Expedition noch nicht verbunden ist}
beginnen
session[i-1].Request:=true; {Flag für Anforderungsdaten bereitsetzen}
session[i-1].CSocket.host:=host; {Legen Sie die Remote-Host-Adresse fest}
session[i-1].CSocket.port:=port; {set port}
session[i-1].CSocket.active:=true; {Mit Remote-Host verbinden}
session[i-1].Lookingup:=true; {Flag setzen}
session[i-1].LookupTime:=0; {beginnt mit dem Zählen bei 0}
Ende
anders
{Wenn die Fernbedienung verbunden ist, senden Sie die Anfrage direkt}
session[i-1].CSocket.socket.sendtext(session[i-1].request_str);
break; {Scannen des Anforderungstexts stoppen}
Ende;
j:=pos(char(13)+char(10),tmp); {zeigt auf die nächste Zeile}
Ende;
Pause; {Stoppschleife}
Ende;
Ende;
file://Wenn die Verbindung zum Remote-Host erfolgreich ist...
procedure TForm1.ClientSocket1Connect(Sender: TObject;
Socket: TCustomWinSocket);
var
i: ganze Zahl;
beginnen
for i:=1 to session do
if (session[i-1].CSocket.socket.sockethandle=socket.SocketHandle) und session[i-1].Used then
beginnen
session[i-1].CSocket.tag:=socket.SocketHandle;
session[i-1].remote_connected:=true; {Setzt das Remote-Host-Verbindungsflag}
session[i-1].Lookingup:=false; {Flag löschen}
brechen;
Ende;
Ende;
file://Wenn der Remote-Host die Verbindung trennt...
procedure TForm1.ClientSocket1Disconnect(Sender: TObject;
Socket: TCustomWinSocket);
var
i,j,k: ganze Zahl;
beginnen
für i:=1 bis Sitzungen tun
if (session[i-1].CSocket.tag=socket.SocketHandle) und session[i-1].Used then
beginnen
session[i-1].remote_connected:=false; {auf nicht verbunden gesetzt}
Wenn nicht session[i-1].client_connected dann
session[i-1].Used:=false {Wenn die Verbindung zum Client getrennt ist, setzen Sie das Release-Ressourcen-Flag}
anders
für k:=1 bis serverocket1.Socket.ActiveConnections tun
if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) und session[i-1].used then
beginnen
serverocket1.Socket.Connections[k-1].Close;
brechen;
Ende;
brechen;
Ende;
j:=Sitzungen;
k:=0;
für i:=1 bis j tun
beginnen
if session[ji].Wird dann verwendet
brechen;
inc(k);
Ende;
Wenn k>0, dann {Sitzungsarray reparieren}
beginnen
Sitzungen:=Sitzungen-k;
setlength(session,sessions);
Ende;
edit1.text:=inttostr(sessions);
Ende;
file://Wenn bei der Kommunikation mit dem Remote-Host ein Fehler auftritt...
procedure TForm1.ClientSocket1Error(Sender: TObject;
Socket: TCustomWinSocket; ErrorEvent: TErrorEvent;
varErrorCode: Integer);
var
i,j,k: ganze Zahl;
beginnen
für i:=1 bis Sitzungen tun
if (session[i-1].CSocket.tag=socket.SocketHandle) und session[i-1].Used then
beginnen
socket.close;
session[i-1].remote_connected:=false; {auf nicht verbunden gesetzt}
Wenn nicht session[i-1].client_connected dann
session[i-1].Used:=false {Wenn die Verbindung zum Client getrennt ist, setzen Sie das Release-Ressourcen-Flag}
anders
für k:=1 bis serverocket1.Socket.ActiveConnections tun
if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) und session[i-1].used then
beginnen
serverocket1.Socket.Connections[k-1].Close;
brechen;
Ende;
brechen;
Ende;
j:=Sitzungen;
k:=0;
für i:=1 bis j tun
beginnen
if session[ji].Wird dann verwendet
brechen;
inc(k);
Ende;
Fehlercode:=0;
Wenn k>0, dann {Sitzungsarray reparieren}
beginnen
Sitzungen:=Sitzungen-k;
setlength(session,sessions);
Ende;
edit1.text:=inttostr(sessions);
Ende;
file://Sendet eine Seitenanforderung an den Remote-Host…
procedure TForm1.ClientSocket1Write(Sender: TObject;
Socket: TCustomWinSocket);
var
i: ganze Zahl;
beginnen
für i:=1 bis Sitzungen tun
if (session[i-1].CSocket.tag=socket.SocketHandle) und session[i-1].Used then
beginnen
if session[i-1].Anfrage dann
beginnen
socket.SendText(session[i-1].request_str); {Wenn eine Anfrage vorliegt, senden Sie}
session[i-1].Request:=false; {Flag löschen}
Ende;
brechen;
Ende;
Ende;
file://Wenn der Remote-Host Seitendaten sendet ...
procedure TForm1.ClientSocket1Read(Sender: TObject;
Socket: TCustomWinSocket);
var
i,j: ganze Zahl;
rec_bytes: integer; {zurückgegebene Datenblocklänge}
rec_Buffer: array[0..2047] of char; {zurückgegebener Datenblockpuffer}
beginnen
für i:=1 bis Sitzungen tun
if (session[i-1].CSocket.tag=socket.SocketHandle) und session[i-1].Used then
beginnen
rec_bytes:=socket.ReceiveBuf(rec_buffer,2048);
für j:=1 bis serverocket1.Socket.ActiveConnections tun
Wenn ServerSocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle dann
beginnen
serverocket1.Socket.Connections[j-1].SendBuf(rec_buffer,rec_bytes); {Daten senden}
brechen;
Ende;
brechen;
Ende;
Ende;
Datei:// „Seite nicht gefunden“ und andere Fehlermeldungen werden angezeigt...
procedure TForm1.AppException(Sender: TObject; E: Exception);
beginnen
inc(invalidrequests);
Ende;
file://Finden Sie das Timing des Remote-Hosts...
procedure TForm1.Timer1Timer(Sender: TObject);
var
i,j: ganze Zahl;
beginnen
für i:=1 bis Sitzungen tun
if session[i-1].Verwendet und session[i-1].Lookingup then {if connected}
beginnen
inc(session[i-1].LookupTime);
if session[i-1].LookupTime>lookuptimeout then {if timeout}
beginnen
session[i-1].Lookingup:=false;
session[i-1].CSocket.active:=false; {Suche stoppen}
für j:=1 bis serverocket1.Socket.ActiveConnections tun
Wenn ServerSocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle dann
beginnen
serverocket1.Socket.Connections[j-1].Close; {disconnect client}
brechen;
Ende;
Ende;
Ende;
Ende;
Ende.
3. Nachwort
Da diese Designidee nur eine Umleitungsfunktion zwischen dem Proxy-Ende und dem Remote-Host, dem ursprünglichen Proxy-Ende, hinzufügt
Einige Funktionen wie die Caching-Technologie bleiben erhalten, sodass die Effizienz hoch ist. Nach dem Test können bei Verwendung eines 33,6-K-Modems für den Zugriff auf das Internet drei bis zehn Proxy-Workstations gleichzeitig auf das Internet zugreifen, und die Reaktionsgeschwindigkeit ist immer noch gut. Da die Verbindung zwischen der Proxy-Workstation und der Proxy-Server-Workstation im Allgemeinen über eine Hochgeschwindigkeitsverbindung erfolgt, tritt der Engpass hauptsächlich in der Internetzugriffsmethode des Proxy-Servers auf.
Mit der oben beschriebenen Methode hat der Autor erfolgreich einen kompletten Satz Proxy-Server-Software entwickelt und ihn vollständig in das Abrechnungssystem des Computerraums integriert.
Mit Erfolg ist es möglich, mit einer Workstation Funktionen wie Internet-Proxy, Internet-Abrechnung und Maschinennutzungsabrechnung auszuführen. Freunde mit Programmiererfahrung können zusätzliche Proxyserverfunktionen hinzufügen, z. B. das Festlegen verbotener Zugriffsseiten, das Zählen des Kundenverkehrs, Webzugriffslisten usw.
Blog des Autors: http://blog.csdn.net/BExpress/