Delphi を使用して独自のプロキシ サーバーを設計する
著者がインターネット請求ソフトウェアを作成していたとき、ローカル エリア ネットワーク内の各ワークステーションのインターネット アクセスに請求する方法が問題になりました。一般に、これらのワークステーションはプロキシ サーバーを介してインターネットにアクセスしますが、既製のプロキシ サーバー ソフトウェアを使用する場合、プロキシ サーバー ソフトウェアはクローズド システムであるため、リアルタイムのインターネット アクセス タイミング情報を取得するプログラムを作成することが困難です。したがって、グループのインターネット アクセスの問題と請求の問題を解決するために、独自のプロキシ サーバーを作成できるかどうかを検討してください。
実験的なプログラミングの後、この問題は最終的に満足のいく解決が得られました。今すぐ書き留めて同僚と共有してください。
1. アイデア
現在普及しているブラウザのシステム オプションには、「プログラミング テスト後、プロキシ サーバーを介して接続する」というパラメータがあります。
試してみてください。ローカル ネットワーク内のワークステーションがこの属性を指定してインターネット リクエストを発行すると、リクエスト データは指定されたプロキシ サーバーに送信されます。リクエスト パケットの例は次のとおりです。
GET 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
プロキシ接続: キープアライブ
最初の行はターゲット URL と関連するメソッドとプロトコルで、「Host」行はターゲット ホストのアドレスを指定します。
このことから、プロキシ サービスのプロセスがわかります。プロキシからリクエストを受信し、実際のホストに接続し、ホストから返されたデータを受信し、受信したデータをプロキシに送信します。
この目的のために、上記のネットワーク通信リダイレクトの問題を完了する簡単なプログラムを作成できます。
Delphi で設計する場合、プロキシ ワークステーションと通信するソケット コントロールとして ServerSocket を選択し、リモート ホストと通信するソケット コントロールとして ClientSocket 動的配列を選択します。
プログラミング中に解決すべき重要な問題は、プロキシ サービスとエージェントの応答速度を高速化するために、各通信セッションのプロパティをノンブロッキングに設定する必要があることです。がソケットに動的にバインドされている場合は、ソケットの SocketHandle 属性値を使用して、それがどのセッションに属しているかを判断します。
通信接続プロセスを次の図に示します。
プロキシサーバー
サーバーソケット
(1) 受け取る
エージェントによってリモートホストに送信される
(6) (2) (5)
ブラウザ ClientSocket (4) Web サーバー
引き継ぐ
送信(3)
(1) プロキシ ブラウザが Web リクエストを送信し、プロキシ サーバーの Serversocket がリクエストを受信します。
(2) プロキシ サーバー プログラムは、ClientSocket を自動的に作成し、ホスト アドレス、ポートなどの属性を設定して、リモート ホストに接続します。
(3) リモート接続後、send イベントが発生し、Serversocket で受信した Web リクエストパケットがリモートホストに送信されます。
(4) リモートホストがページデータを返すと、ClientSocket の read イベントがトリガーされ、ページデータが読み取られます。
(5) プロキシ サーバー プログラムは、バインディング情報に基づいて、ServerSocket コントロール内のどのソケットがホストから受信したページ情報をプロキシ側に送信するかを決定します。
(6) ServerSocket の対応する Socket がページ データをエージェントに送信します。
2. プログラミング
Delphi を使用して、主に ServerSocket と ClientSocket に関連する上記の通信プロセスを設計するのは非常に簡単です。
ソフトウェアドライバープログラミング。以下は、実験用プロキシ サーバー インターフェイスと、著者が作成したソース プログラムのリストであり、簡単な機能説明も含まれています。
ユニットメイン。
インタフェース
用途
ウィンドウ、メッセージ、SysUtils、クラス、グラフィックス、コントロール、フォーム、ダイアログ、
ExtCtrls、ScktComp、TrayIcon、メニュー、StdCtrls;
タイプ
session_record=レコード
使用: ブール値 {セッション記録が利用可能かどうか}
SS_Handle: 整数 {プロキシ サーバー ソケット ハンドル}
CSocket: TClientSocket {リモートへの接続に使用されるソケット}
検索: ブール値 {サーバーを検索するかどうか}
LookupTime: 整数 {ルックアップサーバー時間}
リクエスト: ブール値 {リクエストがあるかどうか}
request_str: 文字列 {リクエストデータブロック}
client_connected: ブール値 {クライアントオンラインフラグ}
Remote_connected: ブール値; {リモートサーバー接続フラグ}
終わり;
タイプ
TForm1 = クラス(TForm)
ServerSocket1: TServerSocket;
ClientSocket1: TClientSocket;
タイマー 2: T タイマー;
TrayIcon1: TTrayIcon;
ポップアップメニュー 1: TPopupMenu;
N11: TMenuItem;
N21: TMenuItem;
N1: TMenuItem;
N01: TMenuItem;
メモ1: Tメモ;
編集1: TEdit;
ラベル 1: T ラベル;
タイマー 1: T タイマー;
プロシージャ Timer2Timer(送信者: TObject);
プロシージャ N11Click(送信者: TObject);
プロシージャ FormCreate(Sender: TObject);
プロシージャ FormClose(Sender: TObject; var Action: TCloseAction);
プロシージャ N21Click(送信者: TObject);
プロシージャ N01Click(送信者: TObject);
プロシージャ ServerSocket1ClientConnect(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ ServerSocket1ClientDisconnect(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ ServerSocket1ClientError(送信者: TObject;
ソケット: TCustomWinSocket; エラーイベント: TErrorEvent;
varErrorCode: 整数);
プロシージャ ServerSocket1ClientRead(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ ClientSocket1Connect(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ ClientSocket1Disconnect(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ ClientSocket1Error(送信者: TObject; ソケット: TCustomWinSocket;
エラーイベント: TErrorEvent; var エラーコード: 整数);
プロシージャ ClientSocket1Write(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ ClientSocket1Read(送信者: TObject; ソケット: TCustomWinSocket);
プロシージャ ServerSocket1Listen(送信者: TObject;
ソケット: TCustomWinSocket);
プロシージャ AppException(送信者: TObject; E: 例外);
プロシージャ Timer1Timer(送信者: TObject);
プライベート
{プライベート宣言}
公共
Service_Enabled: ブール値 {プロキシ サービスが有効かどうか}
セッション: session_record の配列 {セッション配列};
セッション: 整数 {セッション数}
LookUpTimeOut: 整数; {接続タイムアウト値}
InvalidRequests: 整数 {無効なリクエストの数}
終わり;
変数
フォーム1: TForm1;
実装
{$R *.DFM}
file://システム起動タイマー、起動ウィンドウが表示された後、システム トレイに縮小します...
プロシージャ TForm1.Timer2Timer(送信者: TObject);
始める
timer2.Enabled:=false; {タイマーをオフにする}
セッション:=0; {セッション数=0}
application.OnException := AppException {プロキシ サーバーで発生する例外をシールドするため}
無効なリクエスト:=0; {0 エラー}
LookUpTimeOut:=60000; {タイムアウト値=1 分}
timer1.Enabled:=true; {タイマーをオンにする}
n11.Enabled:=false; {有効化サービス メニュー項目が無効です}
n21.Enabled:=true; {サービスメニュー項目を閉じるが有効です}
サーバーソケット1.ポート:=988; {プロキシサーバーポート=988}
serversocket1.Active:=true {サービスの開始}
form1.hide; {インターフェイスを非表示、システム トレイに縮小}
終わり;
file://サービスメニュー項目を開く…
プロシージャ TForm1.N11Click(送信者: TObject);
始める
serversocket1.Active:=true {サービスの開始}
終わり;
file://サービスメニュー項目の停止…
プロシージャ TForm1.N21Click(送信者: TObject);
始める
サーバーソケット1.Active:=false {サービスを停止}
N11.有効:=True;
N21.有効:=False;
Service_Enabled:=false; {フラグがクリアされました}
終わり;
file://メインウィンドウの作成…
プロシージャ TForm1.FormCreate(送信者: TObject);
始める
Service_Enabled:=false;
timer2.Enabled:=true; {ウィンドウが作成されたら、タイマーを開きます}
終わり;
file://ウィンドウを閉じると...
プロシージャ TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
始める
timer1.Enabled:=false {タイマーをオフにする}
Service_Enabled の場合
serversocket1.Active:=false; {プログラムを終了するときはサービスを終了します}
終わり;
file://プログラムの終了ボタン…
プロシージャ TForm1.N01Click(送信者: TObject);
始める
form1.Close {プログラムを終了}
終わり;
file://プロキシ サービスをオンにした後...
プロシージャ TForm1.ServerSocket1Listen(送信者: TObject;
ソケット: TCustomWinSocket);
始める
Service_Enabled:=true; {サービスフラグを設定}
N11.有効:=false;
N21.有効:= true;
終わり;
file:// がプロキシによってプロキシ サーバーに接続された後、セッションが確立され、ソケットにバインドされます。
プロシージャ TForm1.ServerSocket1ClientConnect(送信者: TObject;
ソケット: TCustomWinSocket);
変数
i,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 でない場合は、
session[i-1].CSocket.active:=false;
j=-1 の場合
開始 {なし、1 つ追加}
j:=セッション;
inc(セッション);
setlength(セッション,セッション);
session[j].used:=true {使用中として設定}
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;
セッション[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(送信者: TObject;
ソケット: TCustomWinSocket);
変数
i、j、k: 整数。
始める
for i:=1 からセッションが実行される
if (session[i-1].SS_Handle=socket.SocketHandle) および session[i-1].used then
始める
session[i-1].client_connected:=false; {クライアントは接続されていません}
セッション[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);
終わり;
if k>0 then {セッション配列を変更し、最後に未使用の項目を解放します}
始める
セッション:=セッション-k;
setlength(セッション,セッション);
終わり;
edit1.text:=inttostr(セッション);
終わり;
file://通信エラーが発生した場合...
プロシージャ TForm1.ServerSocket1ClientError(送信者: TObject;
ソケット: TCustomWinSocket; エラーイベント: TErrorEvent;
varErrorCode: 整数);
変数
i、j、k: 整数。
始める
for 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 を行う
始める
if session[ji].Used then
壊す;
Inc(k);
終わり;
k>0の場合
始める
セッション:=セッション-k;
setlength(セッション,セッション);
終わり;
edit1.text:=inttostr(セッション);
エラーコード:=0;
終わり;
ページをリクエストするために file:// がプロキシによって送信されると...
プロシージャ TForm1.ServerSocket1ClientRead(送信者: TObject;
ソケット: TCustomWinSocket);
変数
tmp、ライン、ホスト: 文字列;
i、j、ポート: 整数;
始める
for i:=1 to session 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 {リクエスト テキストを 1 行ずつスキャンし、ホスト アドレスを探します}
始める
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 の場合
始める
ホスト:=コピー(行,1,j-1);
削除(行,1,j);
試す
ポート:=strtoint(行);
を除外する
ポート:=80;
終わり;
終わり
それ以外
始める
host:=trim(line) {ホストアドレスを取得}
ポート:=80;
終わり;
session[i-1].remote_connected ではない場合、{遠征がまだ接続されていない場合}
始める
session[i-1].Request:=true; {リクエストデータ準備完了フラグを設定}
session[i-1].CSocket.host:=host; {リモートホストアドレスを設定します}
session[i-1].CSocket.port:=ポート {セットポート}
session[i-1].CSocket.active:=true; {リモートホストに接続}
session[i-1].Lookingup:=true; {フラグを設定}
session[i-1].LookupTime:=0; {0 からカウントを開始します}
終わり
それ以外
{リモートが接続されている場合は、リクエストを直接送信します}
session[i-1].CSocket.socket.sendtext(session[i-1].request_str);
ブレーク; {リクエストテキストのスキャンを停止}
終わり;
j:=pos(char(13)+char(10),tmp); {次の行を指す}
終わり;
ブレーク; {ループを停止}
終わり;
終わり;
file://リモートホストへの接続に成功すると...
プロシージャ TForm1.ClientSocket1Connect(送信者: TObject;
ソケット: TCustomWinSocket);
変数
i: 整数;
始める
for i:=1 からセッションが実行される
if (session[i-1].CSocket.socket.sockethandle=socket.SocketHandle) および session[i-1].used then
始める
session[i-1].CSocket.tag:=socket.SocketHandle;
session[i-1].remote_connected:=true; {リモートホスト接続フラグを設定します}
session[i-1].Lookingup:=false {クリアフラグ}
壊す;
終わり;
終わり;
file://リモートホストが切断されたとき...
プロシージャ TForm1.ClientSocket1Disconnect(送信者: TObject;
ソケット: TCustomWinSocket);
変数
i、j、k: 整数。
始める
for 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 {クライアントが切断されている場合、リソース解放フラグを設定します}
それ以外
for k:=1 からserversocket1.Socket.ActiveConnections への実行
if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) および session[i-1].used then
始める
サーバーソケット1.Socket.Connections[k-1].Close;
壊す;
終わり;
壊す;
終わり;
j:=セッション;
k:=0;
for i:=1 to j を行う
始める
if session[ji].Used then
壊す;
Inc(k);
終わり;
k>0 の場合、{セッション配列を修正}
始める
セッション:=セッション-k;
setlength(セッション,セッション);
終わり;
edit1.text:=inttostr(セッション);
終わり;
file://リモートホストとの通信でエラーが発生した場合...
プロシージャ TForm1.ClientSocket1Error(送信者: TObject;
ソケット: TCustomWinSocket; エラーイベント: TErrorEvent;
varErrorCode: 整数);
変数
i、j、k: 整数。
始める
for 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 {クライアントが切断されている場合、リソース解放フラグを設定します}
それ以外
for k:=1 からserversocket1.Socket.ActiveConnections への実行
if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) および session[i-1].used then
始める
サーバーソケット1.Socket.Connections[k-1].Close;
壊す;
終わり;
壊す;
終わり;
j:=セッション;
k:=0;
for i:=1 to j を行う
始める
if session[ji].Used then
壊す;
Inc(k);
終わり;
エラーコード:=0;
k>0 の場合、{セッション配列を修正}
始める
セッション:=セッション-k;
setlength(セッション,セッション);
終わり;
edit1.text:=inttostr(セッション);
終わり;
file://ページリクエストをリモートホストに送信します…
プロシージャ TForm1.ClientSocket1Write(送信者: TObject;
ソケット: TCustomWinSocket);
変数
i: 整数;
始める
for i:=1 からセッションが実行される
if (session[i-1].CSocket.tag=socket.SocketHandle) および session[i-1].used then
始める
if session[i-1].Request then
始める
socket.SendText(session[i-1].request_str); {リクエストがあれば送信}
session[i-1].Request:=false {クリアフラグ}
終わり;
壊す;
終わり;
終わり;
file://リモートホストがページデータを送信するとき...
プロシージャ TForm1.ClientSocket1Read(送信者: TObject;
ソケット: TCustomWinSocket);
変数
i,j: 整数;
rec_bytes: 整数; {返されたデータ ブロックの長さ}
rec_Buffer: char の配列 [0..2047] {返されたデータ ブロック バッファ}
始める
for i:=1 からセッションが実行される
if (session[i-1].CSocket.tag=socket.SocketHandle) および session[i-1].used then
始める
rec_bytes:=socket.ReceiveBuf(rec_buffer,2048); {データを受信}
for j:=1 からserversocket1.Socket.ActiveConnections への実行
ifserversocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle then
始める
serversocket1.Socket.Connections[j-1].SendBuf(rec_buffer,rec_bytes); {データの送信}
壊す;
終わり;
壊す;
終わり;
終わり;
File:// 「ページが見つかりません」などのエラー メッセージが表示されます...
プロシージャ TForm1.AppException(送信者: TObject; E: 例外);
始める
inc(無効なリクエスト);
終わり;
file://リモート ホストのタイミングを検索...
プロシージャ TForm1.Timer1Timer(送信者: TObject);
変数
i,j: 整数;
始める
for i:=1 からセッションが実行される
session[i-1].used および session[i-1].Lookingup の場合、{if 接続中}
始める
inc(セッション[i-1].LookupTime);
if session[i-1].LookupTime>lookuptimeout then {if timeout}
始める
セッション[i-1].Lookingup:=false;
session[i-1].CSocket.active:=false; {検索を停止}
for j:=1 からserversocket1.Socket.ActiveConnections への実行
ifserversocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle then
始める
サーバーソケット1.Socket.Connections[j-1].Close {クライアントを切断}
壊す;
終わり;
終わり;
終わり;
終わり;
終わり。
3. 追記
この設計アイデアはプロキシ エンドとリモート ホスト間のリダイレクト機能を追加するだけであるため、元のプロキシ エンドは
キャッシュ技術などの一部の機能は維持されるため、効率が高くなります。テスト後、33.6K モデムを使用してインターネットにアクセスすると、3 ~ 10 台のプロキシ ワークステーションが同時にインターネットにアクセスでき、依然として良好な応答速度が得られました。プロキシ ワークステーションとプロキシ サーバー ワークステーション間の接続は一般に高速リンクを経由するため、ボトルネックは主にプロキシ サーバーのインターネット アクセス方法で発生します。
上記の方法により、著者はプロキシ サーバー ソフトウェアの完全なセットを開発し、それをコンピュータ ルームの請求システムと完全に統合することに成功しました。
1 台のワークステーションを使用して、インターネット プロキシ、インターネット請求、マシン使用量請求などの機能を完了することができます。 プログラミング経験のある友人は、アクセス禁止サイトの設定、顧客トラフィックのカウント、Web アクセス リストなどのプロキシ サーバー機能を追加できます。
著者のブログ: http://blog.csdn.net/BExpress/