Delphi組件indy 10中IdTCPServer修正及SSL使用心得

indy 10終於隨着Delphi2005發佈了,不過indy套件在我的印象中總是複雜並且BUG不斷,說實話,不是看在他一整套組件的面子上,我還是喜歡VCL原生的Socket組件,簡潔,清晰。Indy9發展到了indy10幾乎完全不兼容,可嘆啊。言歸正傳。在使用IdTCPServer組件的時候發現了他的漏洞,他的OnConnecOnExecuteOnDisconnect等事件是在其他線程中執行的,通常情況下這沒有問題,但是在特殊的情況下會造成問題,如果其他部分的程序寫得有問題就會出現漏洞。

我發現的漏洞是這樣的,我在OnDisconnect事件中釋放一個ListView的一個對應的Item,也就是,一個客戶端離開的時候,界面上的ListView對應的項目刪除。正常情況沒有任何問題,但是,如果不斷開連接直接關閉程序就會死掉,事實上我在程序中FormCloseQuery事件中作了處理,在這個事件中我關閉連接,但是,沒有效果,只有在程序中手動的點鼠標關閉纔不會死掉,問題出在哪裏?

問題出在這裏,ListView是一個Windows的標準組件,釋放他的一個Item是通過消息完成的,也就是說,我在OnDisconnect中在一個非主線程的線程中向主線程的窗口發送消息並且等待返回。而此時主線程在幹嘛呢?因爲是主線程觸發的DisConnect,所以他在等待這個端口的服務線程掛起,這樣就發生死鎖,問題在於,主線程的等待服務線程掛起的處理代碼不當,它理論上應該在等待同時處理消息。原代碼如下所示

procedure TIdSchedulerOfThread.TerminateYarn(AYarn: TIdYarn);
var
  LYarn: TIdYarnOfThread;
begin
  LYarn := TIdYarnOfThread(AYarn);
  if LYarn.Thread.Suspended then begin                             //
判斷是否掛起了,掛起了才釋放線程
    // If suspended, was created but never started
    // ie waiting on connection accept
    LYarn.Thread.Free;
    FreeAndNil(LYarn);
  end else begin
    // Is already running and will free itself
    LYarn.Thread.Stop;                                                          //
沒完沒了的調用Stop過程,卻不處理任何消息和同步事件
  
    // Dont free the yarn. The thread frees it (IdThread.pas)
  end;
end;

它的上一級調用者,沒完沒了的判斷服務線程數量,然後沒完沒了地調用上面這個函數,調用者原代碼如下

procedure TIdTCPServer.TerminateAllThreads;
var
  i: Integer;
begin
  // TODO:  reimplement support for TerminateWaitTimeout

  //BGO: find out why TerminateAllThreads is sometimes called multiple times
  //Kudzu: Its because of notifications. It calls shutdown when the Scheduler is
  // set to nil and then again on destroy.
  if Contexts <> nil then begin
    with Contexts.LockList do try
      for i := 0 to Count - 1 do begin
        // Dont call disconnect with true. Otheriwse it frees the IOHandler and the thread
        // is still running which often causes AVs and other.
        TIdContext(Items[i]).Connection.Disconnect(False);
      end;
    finally Contexts.UnLockList; end;
  end;

  // Scheduler may be nil during destroy which calls TerminateAllThreads
  // This happens with explicit schedulers
  if Scheduler <> nil then begin
    Scheduler.TerminateAllYarns;
  end;
end;

說實話,我很不理解indy線程對象又是stop又是start的複雜模型意義何在,而且非常容易出問題,簡單的線程模型更加可靠和實用。

修改的方法很簡單,但是考慮到兼容Linux和其他平臺的問題,還必須進行隔離分解層次,所以稍微複雜了一點點。就是在idThread類中增加一個公開的方法ProcessMessages,然後在TerminateYarn中調用。代碼如下

procedure TIdThread.ProcessMessages;
begin
{$IFDEF MSWINDOWS}
  if GetCurrentThreadID = MainThreadID then
  begin
    CheckSynchronize;
    Application.ProcessMessages;
  end;
{$ENDIF}
{$IFDEF LINUX}
  if GetCurrentThreadID = MainThreadID then
  begin
    CheckSynchronize(1000);
  end;
{$ENDIF}
end;

procedure TIdSchedulerOfThread.TerminateYarn(AYarn: TIdYarn);
var
  LYarn: TIdYarnOfThread;
begin
  LYarn := TIdYarnOfThread(AYarn);
  if LYarn.Thread.Suspended then begin
    // If suspended, was created but never started
    // ie waiting on connection accept
    LYarn.Thread.Free;
    FreeAndNil(LYarn);
  end else begin
    // Is already running and will free itself
    LYarn.Thread.Stop;
    LYarn.Thread.ProcessMessages;                                       //
此處增加了處理。
    // Dont free the yarn. The thread frees it (IdThread.pas)
  end;
end;

TidThread所在單元增加uses

{$IFDEF MSWINDOWS}
  Windows, Forms,
{$ENDIF}
至此程序正常。

雖然理論上,在其他線程中訪問VCL組件應當使用線程同步方法,但是在這個例子中由於工作線程包裝太深已經無法使用線程同步方法了,其實在這個例子中即使使用了線程同步方法來訪問VCL也無濟於事,因爲在等待線程結束的程序中根本沒有檢查主線程的未結消息和線程同步事件(Delphi7以後版本線程同步方法使用事件event作爲同步方法,Delphi7之前使用消息同步)一樣會死鎖。

其實VCL並沒有強制主線程訪問,他的說明是這樣的,它要求在任何時候應當只有一個線程訪問VCL的相關代碼(不同的線程同時訪問不相關的VCL代碼段是沒有問題的),所以,不使用線程同步方法訪問VCL也是可以的,但是要使用其他方法保證訪問的唯一,比如可以使用臨界區。

下面是使用SSL的心得。

開源給我的印象真的是越來越糟糕,早些時候處理MySQL的版本兼容性就覺得受不了,文檔不全,版本差異顯著,不同版本的外部接口不一致,甚至都不知道要保持向下兼容性,範例少,沒事就是叫你去看源程序,說實話大多數時候做開發哪有那麼多時間去人家的源程序啊,一會兒pascal,一會兒C++,一會兒在Pascal中不打;結尾,一會兒在C++寫起了begin end,累啊。

indy中的SSL組件是IdServerIOHandlerSSLOpenSSLIdSSLIOHandlerSocketOpenSSL,前一個用在服務器,後一個用在客戶端。

剛開始用,找不到DLL,我想大家都遇到,看了半天E文,哦,他用OpenSSL的,哪個是什麼玩藝?下載一個吧,原來是開源項目,還是多平臺的,我日,要我安裝perl生成mack文件,還要安裝VC6才能編譯成二進制代碼,暈,我只要2DLL,不至於這麼折騰我吧。算了,再去找,哦?有編譯好的下載,不錯。有個最新版的好像是0.9.7c不過比我那個源代碼的0.9.8版本低了一點,算了不管了,下載完畢,拷貝DLL過去,我日,版本不對,DLL外部接口函數不正確。indy究竟用哪個版本的呢?在indy官網上看了半天他也沒說他的indy10基於哪個版本的,可見這種組件供應商的素質。沒轍,下載了一DemoDemo中包含了這兩個DLL,好,拷貝過去,能夠用,總算開工了。

開工了,它不工作,看幫助,indy的幫助啥也沒有,有的簡陋到居然只有一句話,這是個string類型的屬性,這要她說嗎?沒轍,只好看Demo去(我現在有點理解開源的含義了),這麼多年養成的好習慣都費了,以前做開發,用到新組件,首先就是看官方幫助,微軟的那個幫助叫做詳細啊,完了,現在第一件事情是去看Demo,還不知道是不是標準的範例,不知道是不是能夠涵蓋所有的內容。看了半天範例,原來要證書文件,我也夠蠢的,SSL沒證書怎麼玩?證書?怎麼辦?看了半天,我應該需要一個自簽名的根證書,然後需要一個二級證書,最後是key文件。其實我只需要一個自簽名的證書就可以了。怎麼搞?windows下沒一個工具可以產生新證書的,IIS有,但是證書文件不知道給他藏在什麼地方了。只好用openSSL生成新證書。

搞了一整天,終於看明白了OpenSLL的關鍵指令,大致結構有個模糊的印象,先產生一個key文件,然後用這個Key文件產生一個自簽名的證書,這個證書可以作爲根證書使用,更多的方法就不知道了,key文件要用des加密,用其他的加密,indy不能識別。crt要用x509協議。具體的openSSL指令如下,十多年沒用DOS界面了,看樣子要練練指法了。

產生一個key,當然要先啓動openSSL,一個Dos界面,專業點吧,命令行界面,或者叫做XXXXXX

openssl>genrsa -des3 -out -voiceService.key 1024
下面是OpenSSL的反應

Loading screen into random state - done
Generating RSA private key, 1024 bit long modulus
............................................................................++++++
........++++++
e is 65537 (0x10001)
Enter pass phrase for -ca.key:123456
Verifying - Enter pass phrase for -ca.key:123456

123456是你輸入的密碼,高版本的OpenSSL不會顯示的。

產生自簽名證書

req -new -x509 -days 3650 -key voiceService.key -out voiceService.crt -config openssl.cnf

注意openssl.cnf文件是配置文件,可以自己編輯用edit編輯,不要用記事本。不過一般用自帶的,比較混蛋的是你下載的編譯好的openssl裏面沒這個文件,只有下載源代碼的纔有,拷貝到openssl一起的目錄,或者你喜歡的其他目錄。

openssl的反應

Enter pass phrase for ca.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter ., the field will be left blank.
-----
Country Name (2 letter code) [AU]:cn
State or Province Name (full name) [Some-State]:zj
Locality Name (eg, city) []:hz
Organization Name (eg, company) [Internet Widgits Pty Ltd]:voiceService
Organizational Unit Name (eg, section) []:voiceService
Common Name (eg, YOUR name) []:voiceService

Email Address []:[email protected]

很多都是你填寫的內容,記住中國的國別縮寫是cn,我查了半天,我夠苯啊,省份和城市縮寫隨便寫,下面的也是隨便寫。

弄好以後產生2個文件

VoiceService.keyVoiceService.crt

VoiceService.crt這個文件複製一份修改後綴爲VoiceService.pem,其實不修改也沒事。

然後拷貝到服務器程序所在目錄,在indy中加載的程序段,我這裏在FormCreate事件中修改

var
  appDir: string;
begin
  Users:=TUsers.Create;
  appDir:= extractFilePath(application.exename);
   IdServerIOHandlerSSLOpenSSL1.SSLOptions.KeyFile:= appDir + VoiceService.key;
  IdServerIOHandlerSSLOpenSSL1.SSLOptions.CertFile:= appDir + VoiceService.crt;
  IdServerIOHandlerSSLOpenSSL1.SSLOptions.RootCertFile:= appDir + VoiceService.pem;
這些文件路徑還不能用相對路徑,只能用絕對路徑,夠操蛋吧。

IdServerIOHandlerSSLOpenSSLOnGetPassword事件中填寫密碼

  Password:=123456;


然後就可以工作了。

對開源的感覺,在開源社區中最容易找到當老大的感覺,因爲沒有官方文檔,沒有範例,有的只是對源代碼的熟悉程度。

從工業化的角度來說,完備的文檔和成熟的範例纔是技術進步的基石,而不是源代碼。

開源社區有英雄,但是沒有工程師,那裏有思想,卻沒有規範。

商業軟件看樣子不可能消失,同樣,開源社區也不能沒有。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章