筆者在前一段的工作中,需要開發一套簡單的網絡數據傳輸程序。由於平時常用Delphi做點開發,故此次也不例外。Delphi 7中帶有兩套TCP Socket組件:Indy Socket組件(IdTCPClient和IdTCPServer)和Delphi原生的TCP Socket組件(ClientSocket和ServerSocket)。但是,Borland已宣稱ClientSocket和ServerSocket組件即將被廢棄,建議用相應的Indy組件來代替。因此,筆者使用了Indy。本文在對Indy進行簡要介紹的基礎上,創建了一組簡單的TCP Socket數據傳輸應用來演示了Indy的使用方法。
開放源代碼的Internet組件集——Internet Direct(Indy)Internet Direct(Indy)是一組開放源代碼的Internet組件,涵蓋了幾乎所有流行的Internet協議。Indy用Delphi編寫,被包含在Delphi 6,Kylix 1和C++ Builder 6及以上各個版本的Borland開發環境中。Indy曾經叫做WinShoes(雙關於WinSock——Windows的Socket庫),是由Chad Z. Hower領導的一群開發者構建的,可以從Indy的站點www.nevrona.com/indy上找到更多的信息並下載其新版本。到筆者撰寫本文時為止,Indy的最新穩定版是9.0.14,Indy 10也進入了Beta測試階段。
Delphi 7中所帶的是Indy 9。在其的組件面板上,一共安裝有100多個Indy組件。使用這些組件你可以開發基於各種協議的TCP客戶和服務器應用程序,並處理相關的編碼和安全問題。你可以通過前綴Id來識別Indy組件。
Indy是阻塞式(Blocking)的
當你使用Winsock開發網絡應用程序時,從Socket中讀取數據或者向Socket寫入數據都是異步發生的,這樣就不會阻斷程序中其它代碼的執行。在收到數據時,Winsock會向應用程序發送相應的消息。這種訪問方式被稱作非阻塞式連接,它要求你對事件作出響應,設置狀態機,並通常還需要一個等待循環。
與通常的Winsock編程方法不同的是,Indy使用了阻塞式Socket調用方式。阻塞式訪問更像是文件存取。當你讀取數據,或是寫入數據時,讀取和寫入函數將一直等到相應的操作完成後才返回。比如說,發起網絡連接只需調用Connect方法並等待它返回,如果該方法執行成功,在結束時就直接返回,如果未能成功執行,則會拋出相應的異常。同文件訪問不同的是,Socket調用可能會需要更長的時間,因為要讀寫的數據可能不會立即就能准備好(在很大程度上依賴於網絡帶寬)。
阻塞式Socket並非惡魔(Evil)
長期以來,阻塞式Socket都遭到了毫無理由的攻擊。其實阻塞式Socket並非如通常所說的那樣可怕。這還要從Winsock的發展說起。
當Socket被從Unix移植到Windows時,一個嚴重的問題立即就出現了。Unix支持fork,客戶程序和服務器都能夠fork新的進程,並啟動這些進程,從而能夠很方便地使用阻塞式Socket。而Windows 3.x既不支持fork也不支持多線程,當使用阻塞式Socket時,用戶界面就會被“鎖住”而無法響應用戶輸入。
為克服Windows 3.x的這一缺陷,微軟在Winsock中加入了異步擴展,以使Winsock不會“鎖住”應用程序的主線程(也是唯一的線程)。然而,這需要了一種完全不同的編程方式。於是有些人為了掩飾這一弱點,就開始強烈地誹謗阻塞式Socket。
當Win32出現的時候,它能夠很好地支持線程。但是既成的觀念已經很難更改,並且說出去的話也無法收回,因此對阻塞式Socket的誹謗繼續存在著。
事實上,阻塞式Socket仍然是Unix實現Socket的唯一方式,並且它工作得很好。
阻塞式Socket的優點
歸結起來,在Windows上使用阻塞式Socket開發應用程序具有如下優點:
編程簡單——阻塞式Socket應用程序很容易編寫。所有的用戶代碼都寫在同一個地方,並且順序執行。
容易向Unix移植——由於Unix也使用阻塞式Socket,編寫可移植的代碼就變得比較容易。Indy就是利用這一點來實現其多平台支持而又單一源代碼的設計。
很好地利用了線程技術——阻塞式Socket是順序執行的,其固有的封裝特性使得它能夠很容易地使用到線程中。
阻塞式Socket的缺點
事物都具有兩面性,阻塞式Socket也不例外。它的一個主要的缺點就是使客戶程序的用戶界面“凍結”。當在程序的主線程中進行阻塞式Socket調用時,由於要等待Socket調用完成並返回,這段時間就不能處理用戶界面消息,使得Update、Repaint以及其它消息得不到及時響應,從而導致用戶界面被“凍結”。
使用TIdAntiFreeze對抗“凍結”
Indy使用一個特殊的組件TIdAntiFreeze來透明地解決客戶程序用戶界面“凍結”的問題。TIdAntiFreeze在Indy內部定時中斷對棧的調用,並在中斷期間調用Application.ProcessMessages方法處理消息,而外部的Indy調用繼續保存阻塞狀態,就好像TIdAntiFreeze對象不存在一樣。你只要在程序中的任意地方添加一個TIdAntiFreeze對象,就能在客戶程序中利用到阻塞式Socket的所有優點而避開它的一些顯著缺點。
Indy使用了線程技術
阻塞式Socekt通常都采用線程技術,Indy也是如此。從最底層開始,Indy的設計都是線程化的。因此用Indy創建服務器和客戶程序跟在Unix下十分相似,並且Delphi的快速開發環境和Indy對WinSock的良好封裝使得應用程序創建更加容易。
Indy服務器模型
一個典型的Unix服務器有一個或多個監聽進程,它們不停地監聽進入的客戶連接請求。對於每一個需要服務的客戶,都fork一個新進程來處理該客戶的所有事務。這樣一個進程只處理一個客戶連接,編程就變得十分容易。
Indy服務器工作原理同Unix服務器十分類似,只是Windows不像Unix那樣支持fork,而是支持線程,因此Indy服務器為每一個客戶連接分配一個線程。
圖1顯示了Indy服務器的工作原理。Indy服務器組件創建一個同應用程序主線程分離的監聽線程來監聽客戶連接請求,對於接受的每一個客戶,都創建一個新的線程來為該客戶提供服務,所有與這一客戶相關的事務都由該線程來處理。
圖1 Indy服務器工作原理
使用組件TIdThreadMgrPool,Indy還支持線程池。
線程與Indy客戶程序
Indy客戶端組件並未使用線程。但是在一些高級的客戶程序中,程序員可以在自定義的線程中使用Indy客戶端組件,以使用戶界面更加友好。
簡單的Indy應用示例
下面將創建一個簡單的TCP客戶程序和一個簡單的TCP服務器來演示Indy的基本使用方法。客戶程序使用TCP協議同服務器連接,並向服務器發送用戶所輸入數據。服務器支持兩條命令:DATA和QUIT。在DATA命令後跟隨要發送的數據,並用空格將命令字DATA和數據分隔開。
表單布局
建立一個項目組,添加一個客戶程序項目和一個服務器項目。客戶程序和服務器程序的表單布局如同2和圖3所示。客戶程序表單上放置了TIdTCPClient組件,服務器程序表單上放置了TIdTCPServer組件。為防止客戶程序“凍結”,還在其表單上放置TIdAntiFreeze組件。
圖2 簡單的TCP客戶程序表單
圖3 簡單的TCP服務器程序表單
客戶程序和服務器程序的表單上都放置有TListBox組件,用來顯示通信記錄。
客戶程序代碼
客戶程序片斷如代碼列表1所示。
代碼列表1
procedure TFormMain.BtnConnectClick(Sender: TObject);
begin
IdTCPClient.Host := EdtHost.Text;
IdTCPClient.Port := StrToInt(EdtPort.Text);
LbLog.Items.Add('正在連接 ' + EdtHost.Text + '...');
with IdTCPClient do
begin
try
Connect(5000);
try
LbLog.Items.Add(ReadLn());
BtnConnect.Enabled := False;
BtnSend.Enabled := True;
BtnDisconnect.Enabled := True;
except
LbLog.Items.Add('遠程主機無響應!');
IdTCPClient.Disconnect();
end;//end try
except
LbLog.Items.Add('無法建立到' + EdtHost.Text + '的連接!');
end;//end try
end;//end with
end;
procedure TFormMain.BtnSendClick(Sender: TObject);
begin
LbLog.Items.Add('DATA ' + EdtData.Text);
with IdTCPClient do
begin
try
WriteLn('DATA ' + EdtData.Text);
LbLog.Items.Add(ReadLn())
except
LbLog.Items.Add('發送數據失敗!');
IdTCPClient.Disconnect();
LbLog.Items.Add('同主機 ' + EdtHost.Text + ' 的連接已斷開!');
BtnConnect.Enabled := True;
BtnSend.Enabled := False;
BtnDisconnect.Enabled := False;
end;//end try
end;//end with
end;
procedure TFormMain.BtnDisconnectClick(Sender: TObject);
var
Received: string;
begin
LbLog.Items.Add('QUIT');
try
IdTCPClient.WriteLn('QUIT');
finally
IdTCPClient.Disconnect();
LbLog.Items.Add('同主機 ' + EdtHost.Text + ' 的連接已斷開!');
BtnConnect.Enabled := True;
BtnSend.Enabled := False;
BtnDisconnect.Enabled := False;
end;//end try
end;
在“連接”按鈕事件響應過程中,首先根據用戶輸入設置IdTCPClient的主機和端口,並調用IdTCPClient的Connect方法向服務器發出連接請求。然後調用ReadLn方法讀取服務器應答數據。
在“發送”按鈕事件響應過程中,調用WriteLn方法寫DATA命令,向服務器發送數據。
在“斷開”按鈕事件響應過程中,向服務器發送QUIT命令,並調用Disconnect方法斷開連接。
程序中還包含有通信信息記錄和異常處理的代碼。
服務器程序代碼
服務器程序片斷如代碼列表2所示。
代碼列表2
procedure TFormMain.BtnStartClick(Sender: TObject);
begin
IdTCPServer.DefaultPort := StrToInt(EdtPort.Text);
IdTCPServer.Active := True;
BtnStart.Enabled := False;
BtnStop.Enabled := True;
LbLog.Items.Add('服務器已成功啟動!');
end;
procedure TFormMain.BtnStopClick(Sender: TObject);
begin
IdTCPServer.Active := False;
BtnStart.Enabled := True;
BtnStop.Enabled := False;
LbLog.Items.Add('服務器已成功停止!');
end;
procedure TFormMain.IdTCPServerConnect(AThread: TIdPeerThread);
begin
LbLog.Items.Add('來自主機 '
+ AThread.Connection.Socket.Binding.PeerIP
+ ' 的連接請求已被接納!');
AThread.Connection.WriteLn('100: 歡迎連接到簡單TCP服務器!');
end;
procedure TFormMain.IdTCPServerExecute(AThread: TIdPeerThread);
var
sCommand: string;
begin
with AThread.Connection do
begin
sCommand := ReadLn();
FLogEntry := sCommand + ' 來自於主機 '
+ AThread.Connection.Socket.Binding.PeerIP;
AThread.Synchronize(AddLogEntry);
if AnsiStartsText('DATA ', sCommand) then
begin
FReceived := RightStr(sCommand, Length(sCommand)-5);
WriteLn('200: 數據接收成功!');
AThread.Synchronize(DisplayData);
end
else if SameText(sCommand, 'QUIT') then begin
FLogEntry := '斷開同主機 '
+ AThread.Connection.Socket.Binding.PeerIP
+ ' 的連接!';
AThread.Synchronize(AddLogEntry);
Disconnect;
end
else begin
WriteLn('500: 無法識別的命令!');
FLogEntry := '無法識別命令:' + sCommand;
AThread.Synchronize(AddLogEntry);
end;//endif
end;
end;
procedure TFormMain.DisplayData();
begin
EdtData.Text := FReceived;
end;
procedure TFormMain.AddLogEntry();
begin
LbLog.Items.Add(FLogEntry);
end;
“啟動”按鈕設置IdTCPServer 的Active屬性為True來啟動服務器,“停止”按鈕設置Active屬性為False來關閉服務器。
IdTCPServerConnect方法作為IdTCPServer 的OnCorrect事件響應過程,向客戶端發送歡迎信息。OnCorrect事件在一個客戶連接請求被接受時發生,為該連接創建的線程AThread被作為參數傳遞給IdTCPServerConnect方法。
IdTCPServerExecute方法是IdTCPServer 的OnExecute事件響應過程。OnExecute事件在TIdPeerThread對象試圖執行其Run方法時發生。OnExecute事件與通常的事件有所不同,其響應過程是在某個線程上下文中執行的,參數AThread就是調用它的線程。這一點很重要,它意味著可能有多個OnExecute事件響應過程被同時執行。在連接被斷開或中斷前,OnExecute事件響應過程會被反復執行。
在IdTCPServerExecute方法中,首先讀入一條指令,然後對指令進行判別。如果是DATA指令,就解出數據並顯示它。如果收到的是QUIT指令,則斷開連接。需要特別指出的是,由於IdTCPServerExecute方法在某一線程上下文中執行,因此顯示數據和添加事件記錄都是將相應的方法傳遞給Synchronize調用來完成的。
運行程序
運行客戶端和服務器程序,按如下流程進行操作:
1. 按服務器程序的“啟動”按鈕啟動服務器;
2. 按客戶程序的“連接”按鈕,建立同服務器的連接;
3. 在客戶程序的待發送數據編輯框中輸入“Hello, Indy!”,並按“發送”按鈕發送數據;
4. 按客戶程序的“斷開”按鈕,斷開同服務器的連接;
5. 按服務器程序的“停止”按鈕停止服務器。
程序運行的結果如圖4和圖5所示。
圖4 簡單的TCP客戶
圖5 簡單的TCP服務器