筆者在編寫一個上網計費軟件時,涉及到如何對局域網中各工作站上網計費問題。一般來講,這些工作站通過代理服務器上網,而采用現成的代理服務器軟件時,由于代理服務器軟件是封閉的系統,很難編寫程序獲取實時的上網計時信息。因此,考慮是否能編寫自己的代理服務器,一方面解決群體上網,另一方面又解決上網的計費問題呢? 經過實驗性編程,終于圓滿地解決了該問題。現寫出來,與各位同行分享。
1、 思路 當前流行的瀏覽器的系統選項中有一個參數,即“通過代理服務器連接”,經過編程測 試,當局域網中一臺工作站指定了該屬性,再發出Internet請求時,請求數據將發送到所指定的代理服務器上,以下為請求數據包示例: GET http://home.microsoft.com/intl/cn/ HTTP/1.0 Accept: */* Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT) Host: home.microsoft.com Proxy-Connection: Keep-Alive 其中第一行為目標URL及相關方法、協議,“Host”行指定了目標主機的地址。 由此知道了代理服務的過程:接收被代理端的請求、連接真正的主機、接收主機返回的數據、將接收數據發送到被代理端。 為此可編寫一個簡單的程序,完成上述網絡通信重定向問題。 用Delphi設計時,選用ServerSocket作為與被代理工作站通信的套接字控件,選用ClientSocket動態數組作為與遠程主機通信的套接字控件。 編程時應解決的一個重要問題是多重連接處理問題,為了加快代理服務的速度和被代理端的響應速度,套接字控件的屬性應設為非阻塞型;各通信會話與套接字動態綁定,用套接字的SocketHandle屬性值確定屬于哪一個會話。 通信的銜接過程如下圖所示:
代理服務器 Serversocket (1) 接 收 被代理端 發 送 遠程主機 (6) (2) (5) Browser ClientSocket (4) Web Server 接 收 發 送 (3)
(1)、被代理端瀏覽器發出Web請求,代理服務器的Serversocket接收到請求。 (2)、代理服務器程序自動創建一個ClientSocket,并設置主機地址、端口等屬性,然后連接遠程主機。 (3)、遠程連通后激發發送事件,將Serversocket接收到的Web請求數據包發送到遠程主機。 (4)、當遠程主機返回頁面數據時,激發ClientSocket的讀事件,讀取頁面數據。 (5)、代理服務器程序根據綁定信息確定屬于ServerSocket控件中的哪一個Socket應該將從主機接收的頁面信息發送到被代理端。 (6)、ServerSocket中的對應Socket將頁面數據發送到被代理端。
2、 程序編寫 使用Delphi設計以上通信過程非常簡單,主要是ServerSocket、ClientSocket的相關事 件驅動程序的程序編寫。下面給出作者編寫的實驗用代理服務器界面與源程序清單,內含簡要功能說明:
unit main;
interface
uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, ScktComp, TrayIcon, Menus, StdCtrls;
type session_record=record Used: boolean; {會話記錄是否可用} SS_Handle: integer; {代理服務器套接字句柄} CSocket: TClientSocket; {用于連接遠程的套接字} Lookingup: boolean; {是否正在查找服務器} LookupTime: integer; {查找服務器時間} Request: boolean; {是否有請求} request_str: string; {請求數據塊} client_connected: boolean; {客戶機聯機標志} remote_connected: boolean; {遠程服務器連接標志} end;
type TForm1 = class(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; var ErrorCode: 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); procedure 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); private { Private declarations } public Service_Enabled: boolean; {代理服務是否開啟} session: array of session_record; {會話數組} sessions: integer; {會話數} LookUpTimeOut: integer; {連接超時值} InvalidRequests: integer; {無效請求數} end;
var Form1: TForm1;
implementation
{$R *.DFM}
file://系統啟動定時器,啟動窗顯示完成后,縮小到System Tray… procedure TForm1.Timer2Timer(Sender: TObject); begin timer2.Enabled:=false; {關閉定時器} sessions:=0; {會話數=0} Application.OnException := AppException; {為了屏蔽代理服務器出現的異常} invalidRequests:=0; {0錯誤} LookUpTimeOut:=60000; {超時值=1分鐘} timer1.Enabled:=true; {打開定時器} n11.Enabled:=false; {開啟服務菜單項失效} n21.Enabled:=true; {關閉服務菜單項有效} serversocket1.Port:=988; {代理服務器端口=988} serversocket1.Active:=true; {開啟服務} form1.hide; {隱藏界面,縮小到System Tray上} end;
file://開啟服務菜單項… procedure TForm1.N11Click(Sender: TObject); begin serversocket1.Active:=true; {開啟服務} end;
file://停止服務菜單項… procedure TForm1.N21Click(Sender: TObject); begin serversocket1.Active:=false; {停止服務} N11.Enabled:=True; N21.Enabled:=False; Service_Enabled:=false; {標志清零} end;
file://主窗口建立… procedure TForm1.FormCreate(Sender: TObject); begin Service_Enabled:=false; timer2.Enabled:=true; {窗口建立時,打開定時器} end;
file://窗口關閉時… procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin timer1.Enabled:=false; {關閉定時器} if Service_Enabled then serversocket1.Active:=false; {退出程序時關閉服務} end;
file://退出程序按鈕… procedure TForm1.N01Click(Sender: TObject); begin form1.Close; {退出程序} end;
file://開啟代理服務后… procedure TForm1.ServerSocket1Listen(Sender: TObject; Socket: TCustomWinSocket); begin Service_Enabled:=true; {置正在服務標志} N11.Enabled:=false; N21.Enabled:=true; end;
file://被代理端連接到代理服務器后,建立一個會話,并與套接字綁定… procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); var i,j: integer; begin j:=-1; for i:=1 to sessions do {查找是否有空白項} if not session[i-1].Used and not session[i-1].CSocket.active then begin j:=i-1; {有,分配它} session[j].Used:=true; {置為在用} break; end else if not session[i-1].Used and session[i-1].CSocket.active then session[i-1].CSocket.active:=false; if j=-1 then begin {無,新增一個} j:=sessions; inc(sessions); setlength(session,sessions); 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; session[j].Lookingup:=false; end; session[j].SS_Handle:=socket.socketHandle; {保存句柄,實現綁定} session[j].Request:=false; {無請求} session[j].client_connected:=true; {客戶機已連接} session[j].remote_connected:=false; {遠程未連接} edit1.text:=inttostr(sessions); end;
file://被代理端斷開時… procedure TForm1.ServerSocket1ClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); var i,j,k: integer; begin for i:=1 to sessions do if (session[i-1].SS_Handle=socket.SocketHandle) and session[i-1].Used then begin session[i-1].client_connected:=false; {客戶機未連接} if session[i-1].remote_connected then session[i-1].CSocket.active:=false {假如遠程尚連接,斷開它} else session[i-1].Used:=false; {假如兩者都斷開,則置釋放資源標志} break; end; j:=sessions; k:=0; for i:=1 to j do {統計會話數組尾部有幾個未用項} begin if session[j-i].Used then break; inc(k); end; if k>0 then {修正會話數組,釋放尾部未用項} begin sessions:=sessions-k; setlength(session,sessions); end; edit1.text:=inttostr(sessions); end;
file://通信錯誤出現時… procedure TForm1.ServerSocket1ClientError(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); var i,j,k: integer; begin for i:=1 to sessions do if (session[i-1].SS_Handle=socket.SocketHandle) and session[i-1].Used then begin session[i-1].client_connected:=false; {客戶機未連接} if session[i-1].remote_connected then session[i-1].CSocket.active:=false {假如遠程尚連接,斷開它} else session[i-1].Used:=false; {假如兩者都斷開,則置釋放資源標志} break; end; j:=sessions; k:=0; for i:=1 to j do begin if session[j-i].Used then break; inc(k); end; if k>0 then begin sessions:=sessions-k; setlength(session,sessions); end; edit1.text:=inttostr(sessions); errorcode:=0; end;
file://被代理端發送來頁面請求時… procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); var tmp,line,host: string; i,j,port: integer; begin for i:=1 to sessions do {判斷是哪一個會話} if session[i-1].Used and (session[i-1].SS_Handle=socket.sockethandle) then begin 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 {逐行掃描請求文本,查找主機地址} begin line:=copy(tmp,1,j-1); {取一行} delete(tmp,1,j+1); {刪除一行} j:=pos('Host',line); {主機地址標志} if j>0 then begin delete(line,1,j+5); {刪除前面的無效字符} j:=pos(':',line); if j>0 then begin host:=copy(line,1,j-1); delete(line,1,j); try port:=strtoint(line); except port:=80; end; end else begin host:=trim(line); {獲取主機地址} port:=80; end; if not session[i-1].remote_connected then {假如遠征尚未連接} begin session[i-1].Request:=true; {置請求數據就緒標志} session[i-1].CSocket.host:=host; {設置遠程主機地址} session[i-1].CSocket.port:=port; {設置端口} session[i-1].CSocket.active:=true; {連接遠程主機} session[i-1].Lookingup:=true; {置標志} session[i-1].LookupTime:=0; {從0開始計時} end else {假如遠程已連接,直接發送請求} session[i-1].CSocket.socket.sendtext(session[i-1].request_str); break; {停止掃描請求文本} end; j:=pos(char(13)+char(10),tmp); {指向下一行} end; break; {停止循環} end; end;
file://當連接遠程主機成功時… procedure TForm1.ClientSocket1Connect(Sender: TObject; Socket: TCustomWinSocket); var i: integer; begin for i:=1 to sessions do if (session[i-1].CSocket.socket.sockethandle=socket.SocketHandle) and session[i-1].Used then begin session[i-1].CSocket.tag:=socket.SocketHandle; session[i-1].remote_connected:=true; {置遠程主機已連通標志} session[i-1].Lookingup:=false; {清標志} break; end; end;
file://當遠程主機斷開時… procedure TForm1.ClientSocket1Disconnect(Sender: TObject; Socket: TCustomWinSocket); var i,j,k: integer; begin for i:=1 to sessions do if (session[i-1].CSocket.tag=socket.SocketHandle) and session[i-1].Used then begin session[i-1].remote_connected:=false; {置為未連接} if not session[i-1].client_connected then session[i-1].Used:=false {假如客戶機已斷開,則置釋放資源標志} else for k:=1 to serversocket1.Socket.ActiveConnections do if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) and session[i-1].used then begin serversocket1.Socket.Connections[k-1].Close; break; end; break; end; j:=sessions; k:=0; for i:=1 to j do begin if session[j-i].Used then break; inc(k); end; if k>0 then {修正會話數組} begin sessions:=sessions-k; setlength(session,sessions); end; edit1.text:=inttostr(sessions); end;
file://當與遠程主機通信發生錯誤時… procedure TForm1.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket; ErrorEvent: TErrorEvent; var ErrorCode: Integer); var i,j,k: integer; begin for i:=1 to sessions do if (session[i-1].CSocket.tag=socket.SocketHandle) and session[i-1].Used then begin socket.close; session[i-1].remote_connected:=false; {置為未連接} if not session[i-1].client_connected then session[i-1].Used:=false {假如客戶機已斷開,則置釋放資源標志} else for k:=1 to serversocket1.Socket.ActiveConnections do if (serversocket1.Socket.Connections[k-1].SocketHandle=session[i-1].SS_Handle) and session[i-1].used then begin serversocket1.Socket.Connections[k-1].Close; break; end; break; end; j:=sessions; k:=0; for i:=1 to j do begin if session[j-i].Used then break; inc(k); end; errorcode:=0; if k>0 then {修正會話數組} begin sessions:=sessions-k; setlength(session,sessions); end; edit1.text:=inttostr(sessions); end;
file://向遠程主機發送頁面請求… procedure TForm1.ClientSocket1Write(Sender: TObject; Socket: TCustomWinSocket); var i: integer; begin for i:=1 to sessions do if (session[i-1].CSocket.tag=socket.SocketHandle) and session[i-1].Used then begin if session[i-1].Request then begin socket.SendText(session[i-1].request_str); {假如有請求,發送} session[i-1].Request:=false; {清標志} end; break; end; end;
file://遠程主機發來頁面數據時… procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); var i,j: integer; rec_bytes: integer; {傳回的數據塊長度} rec_Buffer: array[0..2047] of char; {傳回的數據塊緩沖區} begin for i:=1 to sessions do if (session[i-1].CSocket.tag=socket.SocketHandle) and session[i-1].Used then begin rec_bytes:=socket.ReceiveBuf(rec_buffer,2048); {接收數據} for j:=1 to serversocket1.Socket.ActiveConnections do if serversocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle then begin serversocket1.Socket.Connections[j-1].SendBuf(rec_buffer,rec_bytes); {發送數據} break; end; break; end; end;
file://“頁面找不到”等錯誤信息出現時… procedure TForm1.AppException(Sender: TObject; E: Exception); begin inc(invalidrequests); end;
file://查找遠程主機定時… procedure TForm1.Timer1Timer(Sender: TObject); var i,j: integer; begin for i:=1 to sessions do if session[i-1].Used and session[i-1].Lookingup then {假如正在連接} begin inc(session[i-1].LookupTime); if session[i-1].LookupTime>lookuptimeout then {假如超時} begin session[i-1].Lookingup:=false; session[i-1].CSocket.active:=false; {停止查找} for j:=1 to serversocket1.Socket.ActiveConnections do if serversocket1.Socket.Connections[j-1].SocketHandle=session[i-1].SS_Handle then begin serversocket1.Socket.Connections[j-1].Close; {斷開客戶機} break; end; end; end; end; end.
3、 后記 由于這種設計思路僅僅在被代理端和遠程主機之間增加了一個重定向功能,被代理端原 有的緩存技術等特點均保留,因此效率較高。經過測試,利用1個33.6K的Modem上網時,三到十個被代理工作站同時上網,仍有較好的響應速度。由于被代理工作站和代理服務器工作站之間的連接一般通過高速鏈路,因此瓶頸主要出現在代理服務器的上網方式上。 通過上述方法,作者成功開發了一套完善的代理服務器軟件并與機房計費系統完全集 成,實現了利用一臺工作站完成上網代理、上網計費、用機計費等功能。 有編程經驗的朋友完全可以另行增加代理服務器功能,如設定禁止訪問站點、統計客戶流量、Web訪問列表等等。
|