談談 hbnetio 這玩意(讓你的程式透過 TCP/IP 存取 DBF檔案)

xBase/clipper
回覆文章
admin
Site Admin
文章: 50
註冊時間: 2014-09-23, 10:58

談談 hbnetio 這玩意(讓你的程式透過 TCP/IP 存取 DBF檔案)

文章 admin »

早期想存取網路版的 DBF 檔案,只有透過微軟的網路芳鄰、Novell 網路、Linux 的 Samba 系統,使用量最大的,莫過微軟的網路芳鄰,但是,他的存取效率不好卻也是眾所皆知!

而今網際網路盛行的 TCP/IP 協定,能將網路效能發揮到最大極限,但,無奈傳統的 DBF 存取卻不支援!

但,在一群 Harbour 的開發者努力之下,卻也看到了希望!
想透過 TCP/IP 協定存取 DBF,不只是單單的檔案存取而已,還能做到 C/S 架構的存取,神奇吧!

我們可以單獨寫一支主程式在主機上跑,專門處理工作端送來的命令,
這樣子有什麼好處呢?試想,如果工作站有 Win7/Vista/XP/2000/98/DOS ...
自己透過檔案鎖住機制去存取 DBF/CDX, 不同 OS 有不同機制,
索引檔常常損壞,往往是這種情況下造成的,而且,DBF 的檔案鎖住機制
也非完美,所以才會常常發生 DBF/CDX 資料錯亂問題發生。

如果我們可以改用 C/S 架構跑我們的管理系統,有一支專門處理工作站的資料工作,
那對於 DBF/CDX 的穩定性,就會大大提高了!

但是,也非完美無缺的,至少在我的開發環境 Harbour + FWH 之下,
測試透過網際網路進來存取,也許是 TcBrowse 寫的不好,三不五時就要更新畫面資料,
導致這樣子的連線方式,畫面卡卡的,至於純 Console 下能否跑得順暢?
我沒這樣子的案子可以測試.
另外,使用 xBrowse 能否改善?因為在下也沒在用 xBrowse ,所以這方面也無法提供測試結果.
若是單純在內部網路(100MB網卡)下跑,效能不錯喔!

話說回來,想要讓你的 DBF 跑 C/S 架構,是有兩項產品可以作看看,
而在下我,目前只有測試 Harbour 的 hbnetio, 另外一套也是免費的 letoDB,
在下沒測試過,不知道情況好壞,不予置評... (未完,待續...)

說到這 hbnetio 玩意,是打從 harbour 2.0 開始才有的東西,比起 LetoDB 要來得晚些,
但,好歹也是 Harbour 核心開發者搞出來的玩意,穩定性上來說,也要來的強!

在下目前已經利用此功能,開發出兩套依附此項功能開發而成的軟體,
客戶反應上來說,已經很少出現索引檔壞掉問題了。

要介紹 hbnetio 時,得先介紹主機上執行的主程式,由於在下使用的是 nightly CVS Harbour(就是每天晚上會自動產生的新版本啦),
所以,在程式目錄下: %hbdir%\bin\ 下就有一支 hbnetio.exe,但是,不建議直接使用這支程式來執行,
因為少了很多函數支援功能,建議還是依照自己需求,重新編譯一支來用.

就來說說我個人開發這兩套系統所遇到的問題一一來解說,主機端的 hbnetio.exe 需具備哪些功能?
哪些功能是辦不到?哪些需要特別處理的?且聽我一一道來...

基本上,想要擁有完整的 harbour 函數功能支援的話,首先,必須必須先修改 netiosrv.prg(檔案位於 %hbdir%\contrib\hbnetio\utils\裡面),
找到:

代碼: 選擇全部

#ifdef HB_EXTERN 
: 
#endif
將此兩行關掉,打開 hbextern 功能:

代碼: 選擇全部

// #ifdef HB_EXTERN 
: 
// #endif 
重新編譯,然後執行 hbnetio.exe,需加上參數 '-rpc',這樣子才能呼叫主機端的函數功能,
hbnetio 還支援一些參數,在下沒仔細研究,有興趣的自行研究吧!

代碼: 選擇全部

c:\>hbnetio.exe -rpc 
如此一來,幾乎所有 harbour 所具備的函數、命令功能,差不多都具備了!
主機端其他一些注意事項,後面再詳述,先來說說 client 端程式如何寫!!

Client 端也很簡單,參考如下程式碼:

代碼: 選擇全部

func main() 
  if .not. hbnetio_connect( '192.168.1.100', 2941) 
     alert('Can not connect to server!!') 
     quit 
  else 
     alert('Connect success!!') 
  endif 
執行上面程式碼,就知道能否成功連結到主機了!
要使用 hbnetio, 於連結時,需加上一些 library,
因為在下使用 bcc55, 所以需要連結:

代碼: 選擇全部

   \hb21\lib\hbzlib.lib         + 
   \hb21\lib\hbnetio.lib        + 
   \bcc55\lib\psdk\ws2_32.lib   + 
再來我們測試能否呼叫主機端的函數功能?在上面程式碼下方再加入:

代碼: 選擇全部

  cPath := netio_funcexec( 'hb_argv', 0 ) // 取得主機端 hbnetio.exe 執行檔路徑 
  alert(cPath) 
你將會看到主機端 hbnetio.exe 執行的完整路徑名稱,如果是空字串,
那就代表你的主機程式 hbnetio.exe 執行時忘了加上參數 '-rpc' 囉!

未完,待續....

說說 hbnetio Client 端支援的函數功能吧!
不知道這些功能,還真不知道該怎麼執行遠端函數、命令功能哩!

上面範例提到的: NETIO_FUNCEXEC( <cFuncName> [, <params,...>] ) -> <xFuncRetVal>
我們可以直接執行主機上的函數功能,例如上例:
cPath := netio_funcExec( 'hb_argv', 0 )
他的意思就等同於在主機端執行了 hb_argv(0) 的意思!
並將結果回傳至變數 cPath 去,所以,我們可以藉此得到主機端
執行 hbnetio.exe 時的所在路徑與檔案名稱.

另外,還提供了:
NETIO_PROCEXISTS( <cProcName> ) -> <lExists>
讓你可以在執行函數/命令/程序之前,先判斷主機端是否具備執行的能力,
才不會造成執行過程中,產生錯誤訊息回應.
雖然名稱稱之為 "ProcExists",但是,是支援 Function/Procedure/Command.

NETIO_PROCEXEC( <cProcName> [, <params,...>] ) -> <lSent>
NETIO_PROCEXECW( <cProcName> [, <params,...>] ) -> <lExecuted>
這兩個函數的差異向在於是否等待主機端回應.

hbnetio 支援所有的 RDD 功能,所以,只要是 RDD 的操作函數與命令,
都可以執行,例如: dbSkip()/dbGoTo()/dbSeek()/Eof()/Bof().... 等等.
這些功能測試,可以直接參考 %hbdir%\contrib\hbnetio\tests\ 裡面的 .prg,
這邊就不再累贅敘述了.

再來談談如何將一般的 file server 架構,修改為 C/S 架構的程式?
雖然稱之為 C/S 架構,但是,也只是半套的 C/S 罷了!怎麼說呢?
正常的 C/S 架構,Server Side 是要具備「運算」功能的,而 hbnetio 卻只提供
RDD 方面的操作功能而已,說穿了,是幫你作 Lock 動作,資料編輯,操作,Unlock...
一般我們所知的 C/S 架構中的 Server Side, 可是還要能作複雜加減與搜索運算,
所以我才會稱之為半套的 C/S 架構!

修改之前,先確認你想用哪種方式來修改?一般來說有兩種方式:
1. 直接將程式全部修改為 netio 方式,不再支援 file server 舊架構,這種方式,
那就全部將要在主機端運作的函數全面性的直接加入 netio 資料.
2. 將程式修改為同時支援 file server 與 c/s 架構,這樣子的方式,就得作一些判斷上的修改了,
在下是採用這項,因為有套程式,不是每個人都要更換,所以就採用第二種方式來處理,以下的說明都是針對這第二種方式.
這種方式也有個好處,當我要啟動 netio 功能時,我只要設定一個變數 '_NETIO'
包含了主機 ip/port/路徑等資料,如果不開啟時,直接將 '_NETIO' 設為空字串,
那麼,一些函數在執行上,我可以直接採用例如: dbExist( _NETIO+'abc.dbf' )
啟動 netio 時,先組合 '_NETIO' 變數為: 'net:192.168.1.1:2941::/',
所以上面的函數就自動變成了 dbExist( 'net:192.168.1.1:2941::/'+'abc.dbf' )
要關閉 netio 功能時,只要將變數 '_NETIO' 設為空字串,那上面函數就自動變成
dbExist( ''+'abc.dbf' ) 了,如此方便,也可以提供客戶在整個程式尚未修改完成之前
繼續使用舊的架構執行!

修改時的注意事項:
1. 檢查 dbf 檔案是否存在?
在以前,我們可能習慣用 File() 來檢查,在此要修改為 dbExists(),

2. 修改 dbPack() 為 hb_dbPack(),
dbZap() 為 hb_dbZap(),
另外: dbSkipper() 是 Harbour 從 2.0 到 2.1 的變化,原本的定義要修改為

代碼: 選擇全部

#xtranslate   _DbSkipper => __dbSkipper    // Harbour 2.1.x 
舊的定義為:

代碼: 選擇全部

#xtranslate   _DbSkipper => DbSkipper 
3. 在下使用 FWH, 所以,一些函數也要作調整,所謂的調整,是針對是否啟動 netio 作調整,而非全部替換掉,
lIsDir() / lMkDir() 這兩個函數,一個是判斷目錄是否存在,一個用來建立目錄,
我們需要作「適當」的調整如下:

代碼: 選擇全部

If Empty( _NETIO ) 
   // 一般網路協定 
   If .Not. lIsDir( _cStkDir ) 
      If .Not. lMkDir( _cStkDir ) 
         Tone( 1000, 1 ) 
         MsgStop( "資料檔案目錄 "+_cStkDir+" 無法建立,請檢查原?]!", "警告!" ) 
         Exit 
      EndIf 
   EndIf 
Else 
   // NETIO 網路 
   If ! netio_funcexec( 'hb_dirExists', _cStkDir ) 
      If netio_funcexec( 'MakeDir', _cStkDir ) <> 0 
         Tone( 1000, 1 ) 
         MsgStop( "資料檔案目錄 "+_cStkDir+" 無法建立,請檢查原?]!", "警告!" ) 
         Exit 
      EndIf 
   EndIf 
EndIf 
_cStkDir += "\" 
ps. 上面這個例子是我懶,沒有將 FWH code 替換成 harbour code,否則不需要這麼麻煩!!
這個方法就是我上面說的,可以讓程式同時支援舊的 file server 與新的 c/s 架構同時存在方法.

4. Directory() 函數也要調整,如果是要檢查本地端的目錄,就不用修改,如果要檢查遠端目錄,
那就要修改為: lExist := hbnetio_funcExec( 'Directory', '*.*', 'D' ) 來判斷目錄是否存在了.

5. 再來針對主機端上的程式再加強功能,低階檔案的修改。
hbnetio 目前不支援遠端低階檔案功能,其實問題在於 fRead() 這個函數,只要自己重寫一個替代函數就可以了!!
針對此項問題,在下於主機端程式加上了幾個函數來替代:
(請修改 %hbdir\contrib\hbnetio\utils\netiosrv.prg 於尾端加上)

代碼: 選擇全部

// 為了相容 fRead(), 所以 cBuffer 保留 
FUNC    ssfRead(nH, cBuffer, nReadBytes) 
Local   cH      := PadL(nH,5,'0') 
        afBuffer[cH] := Space(nReadBytes) 
RETURN  fRead( nH, @afBuffer[cH], nReadBytes) 

// 取回 Buffer 資料 
FUNC    ssfBuffer(nH) 
RETURN  afBuffer[PadL(nH,5,'0')] 

// 關閉檔案 
FUNC  ssfClose(nH) 
Local cH    := PadL( nH, 5, '0' ) 
Local nRet  := fClose( nH ) 
      // 清除 afBuffer[cH] 所佔的記憶體 
      If hb_HHasKey( afBuffer, cH ) 
         If afBuffer[cH] <> '' 
           afBuffer[cH] := '' 
         EndIf 
         afBuffer[cH] := NIL // 將 Hash() 所佔空間?]為 NIL 
         hb_HDel( afBuffer, cH ) // 刪除 hash() 所佔空間 
      EndIf 
      hb_gcall() 
RETURN  nRet 
同時要在 procedure Main(...) 之前加上變數宣告:

代碼: 選擇全部

// add:WenSheng 
// 為了改變 fRead() 問題 
STATIC  afBuffer // , afHandle 
// end:WenSheng 


PROCEDURE Main( ... ) 
   LOCAL netiosrv[ _NETIOSRV_MAX_ ] 

   LOCAL cParam 
   LOCAL cCommand 
   LOCAL cPassword 
: 
: 
還需要在 HB_logo() 之後加上初始化工作:

代碼: 選擇全部

   HB_Logo() 

// add:WenSheng 
   afBuffer := hb_Hash() 
// end:WenSheng 
fOpen() 這個函數不需要修改,可以直接沿用,因為他只是開檔,傳回一個檔案的 handle,
而最大問題在於 fRead(), 因為此函數同時回傳兩個資料,而 hbnetio 一次只能接收一個參數,
所以在下重寫了一個 ssfRead() 來替代原有的 fRead() 函數,先傳回此函數讀取到的 Bytes 數,
並將資料存入變數之中,然後再利用在下另外新增的 ssfBuffer() 來取回剛剛的資料.
如此一來,就可以解決 fRead() Buffer 問題了!
因為採用了 hash() 來寫,所以,fClose() 也被改寫了,用來釋放 buffer 變數記憶體,
當這個低階檔案問題解決之後,接下來就可以利用這個低階檔案功能幹一些勾當了!
例如:在下有個 fCopy() 函數功能,用來複製檔案,並且加了複製進度表顯示,
fCopy( cSource, cTarget ), 我就可以修改此函數,想要複製遠端檔案到本地端來,
我就可以寫成: fCopy( _NETIO+cSource, cTarget ),
如果想將本地端檔案複製到遠端主機去,
我也可以寫成: fCopy( cSource, _NETIO+cTarget ),
我只要修改 fCopy() 這個函數,判斷 cSource/cTarget 哪個變數含有 _NETIO 字串,
有此字串的,代表這個是遠端,沒有的是本地端,範例程式如下(包含了 FWH code):

代碼: 選擇全部

Func    fCopy() 
Para    cSource, cTarget 
Priv    nRetByte     := 00,;          && RETURN COPY BYTE 
        nReadByte    := 00,; 
        nWriteByte   := 00,; 
        nTolByte     := 00,; 
        nSHandle     := 00,;          && SOURCE FILE HANDLE 
        nTHandle     := 00,;          && TARGET FILE HANDLE 
        nBuffer      := 00,; 
        cBuffer      := '',; 
        nNowCopyByte := 00,; 
        nTolCopyByte := 00,; 
        xUN          ,; 
        lSource      := .F.,; 
        lTarget      := .F. 
        * 
        If ValType( cSource ) # 'C' .Or. ValType( cTarget ) # 'C' 
           Return nRetByte 
        EndIf 
        // 
        If Empty(_NETIO) 
           // 一般網路,不用管哪個為遠端或近端 
           lSource := .F. 
           lTarget := .F. 
        Else 
           // NETIO: 檢查哪一端是 NETIO 
           If _NETIO $ cSource 
              lSource := .T. 
              cSource := Substr( cSource, Len(_NETIO)+1 ) // 去除 _NETIO 
           EndIf 
           If _NETIO $ cTarget 
              lTarget := .T. 
              cTarget := Substr( cTarget, Len(_NETIO)+1 ) // 去除 _NETIO 
           EndIf 
        EndIf 
        // 
        MsgMeter( {|oM,oT,oD,lEnd|_FCopy(oM,oT,oD,@lEnd)}, "資料拷貝中....", cSource+" 檔案拷貝中", .F. ) 
        * 
Return  nRetByte 
* 進度表 
Func    _FCopy(oM,oT,oD,lEnd) 
        * 
        Do While .T. 
           // ?}啟原始檔案 
           If Empty(_NETIO) .Or. ! lSource 
              // 一般網路 
              nSHandle := fOpen( cSource, 66 ) 
           Else 
              // NETIO 網路 
              nSHandle := netio_funcexec( 'fOpen', cSource, 66 ) 
           EndIf 
           If nSHandle == -1         && SOURCE ?}檔錯誤 
              Exit 
           EndIf 
           // 計算原始檔案大小 
           If Empty(_NETIO) .Or. ! lSource 
              // 一般網路 
              oM:nTotal := nTolCopyByte := fSeek( nSHandle, 0, 2 )  && 移至檔尾,取得TOLBYTE 
              fSeek( nSHandle, 0 )                                  && 移回檔頭 
           Else 
              // NETIO 網路 
              oM:nTotal := nTolCopyByte := netio_funcexec( 'fSeek', nSHandle, 0, 2 )  && 移至檔尾,取得TOLBYTE 
              netio_funcexec( 'fSeek', nSHandle, 0 )                                  && 移回檔頭 
           EndIf 

           // 建立目的檔 
           If Empty(_NETIO) .Or. ! lTarget 
              // 一般網路 
              nTHandle := fCreate( cTarget ) 
           Else 
              // NETIO 網路 
              nTHandle := netio_funcexec( 'fCreate', cTarget ) 
           EndIf 
           If nTHandle == -1         && TARGET ?}檔錯誤 
              Exit 
           EndIf 

           // ?]定緩衝區大小 
           nBuffer := 30 * 1024 
           cBuffer := Space( nBuffer ) 

           // 讀取 Source 端資料 
           If Empty(_NETIO) .Or. ! lSource 
              // 一般網路 
              nReadByte := fRead( nSHandle, @cBuffer, nBuffer ) 
           Else 
              // NETIO 網路 
              nReadByte := netio_funcexec( 'ssfRead', nSHandle, @cBuffer, nBuffer ) 
           EndIf 
           If nReadByte == 0 
              Exit      // 讀不到東西,離?} 
           EndIf 

           If Empty(_NETIO) .Or. ! lSource 
              // 一般網路 
           Else 
              // NETIO 網路 
              cBuffer := netio_funcexec( 'ssfBuffer', nSHandle ) // 取回 Buffer 資料 
           EndIf 
           // 
           Do While .T. 
              // 
              oM:Set( nTolByte ) 
              SysRefresh()// oD:UpDate() 

              // 將資料寫入目的檔 
              If Empty(_NETIO) .Or. ! lTarget 
                 // 一般網路 
                 nWriteByte := fWrite( nTHandle, cBuffer, nReadByte ) 
              Else 
                 // 一般網路 
                 nWriteByte := netio_funcexec( 'fWrite', nTHandle, cBuffer, nReadByte ) 
              EndIf 
              If nWriteByte <> nReadByte 
                 Exit   // 寫入錯誤 
              Else 
                 nTolByte  += nWriteByte        // 已經寫入多少 Bytes 資料 
              EndIf 

              // 讀取原始檔資料 
              If Empty(_NETIO) .Or. ! lSource 
                 // 一般網路 
                 nReadByte  := fRead( nSHandle, @cBuffer, nBuffer ) 
              Else 
                 // NETIO 網路 
                 nReadByte  := netio_funcexec( 'ssfRead', nSHandle, @cBuffer, nBuffer ) 
              EndIf 
              If nReadByte == 0 
                 Exit 
              EndIf 

              If Empty(_NETIO) .Or. ! lSource 
                 // 一般網路 
              Else 
                 // NETIO 網路 
                 cBuffer := netio_funcexec( 'ssfBuffer', nSHandle ) // 取回 Buffer 資料 
              EndIf 
              // 
           EndDo 

           // 
           oM:Set( nTolCopyByte ) 
           oD:UpDate() 
           // 
           nRetByte := nTolByte 
           Exit 
           // 
        EndDo 
        // 
        If nTHandle <> -1 
           If Empty(_NETIO) .Or. ! lTarget 
              // 一般網路 
              fClose( nTHandle ) 
           Else 
              // NETIO 網路 
              netio_funcexec( 'ssfClose', nTHandle ) 
           EndIf 
        EndIf 
        If nSHandle <> -1 
           If Empty(_NETIO) .Or. ! lSource 
              // 一般網路 
              fClose( nSHandle ) 
           Else 
              // NETIO 網路 
              netio_funcexec( 'ssfClose', nSHandle ) 
           EndIf 
        EndIf 
        // 
        lEnd := .T. 
        oD:End() 
        // 
Return  NIL 
範例參考、參考就好,你不一定要學我這麼做!

未完,待續....

6. 針對 .ini 讀寫問題作修改:
因為在下用了 FWH, 所以,直接用 FWH 提供的 GetPvProfString/WritePProString
就算你直接用 harbour 的 ini 功能,還是得面對遠端檔案讀寫問題!
在下的解決方法,是利用轉換命令方式來處理,為每一支程式碼加上一個函數檔:

代碼: 選擇全部

#include "xxx.ch" 
透過自訂命令重新定義:

代碼: 選擇全部

#xtranslate GetPvProfString([<x, ...>]) => ssWinIni( 'GetPvProfString', <x>) 
#xtranslate WritePProString([<x, ...>]) => ssWinIni( 'WritePProString', <x>) 
將讀/寫 .ini 的功能轉到我自己重新撰寫的函數功能去,依照 .ini 檔案名稱與路徑來判斷
是要從遠端存取?還是本地端存取?

代碼: 選擇全部

Func    ssWinIni( cFuncName, x1, x2, x3, x4 ) 
Local   cPath   := x4                           // 檔案名稱+路徑 
Local   nAt     := At( _NETIO, cPath)           // 第五個為 ini 檔案名稱+路徑(第一個函數名稱) 
Local   cFile   := '' 
        If ! Empty( _NETIO ) .And. nAt > 0 
           cFile := Substr( cPath, Len(_NETIO)+1 ) // 去掉 _NETIO 字串,後面的資料才是我們要的「主機路徑」 
           Return netio_funcexec( cFuncName, x1, x2, x3, cFile ) 
        Else 
           Return &cFuncName(x1, x2, x3, x4) 
        EndIf 
我們可以透過第四個參數 x4 來判斷,如果含有 _NETIO 字串,那代表這個 .ini 檔是放在遠端,
如果不包含 _NETIO 字串,就代表這個檔案是本地端檔案!就這麼簡單,不是嗎?Very Happy
如果你要存取遠端 .ini 中的資料,那就可以寫成:

代碼: 選擇全部

WritePProString( 'SYSVAR', 'IP', "127.0.0.1", _NETIO+'\abc.ini' ) 
如果你要存取本地端 .ini 中的資料,那就可以寫成:

代碼: 選擇全部

WritePProString( 'SYSVAR', 'IP', "127.0.0.1", '\abc.ini' ) 
記得!本項功能需修改主機端程式,加入 FWH 的 profile.c 檔案重新連結,否則無法執行!

7. 再來玩一個好玩的,在遠端執行壓縮程式備份功能,

代碼: 選擇全部

cCmpStr := 'WinRAR.EXE -a -y test.rar *.dbf' 
netio_funcexec( 'hb_run', cCmpStr ) 
直接在主機端執行 winrar 的壓縮程式,備份所有 .dbf 檔案,然後再利用上面第五點說的
fCopy() 函數功能,就可以將遠端的 .DBF 給壓縮、複製到本地端了!

附上我所修改的 netiosrv.prg
netiosrv.prg
記得加上 FWH 的 profile.c, 否則無法存取 .ini

另外再附上已經編譯好的 hbnetio.exe ,可以直接執行 hbnetio.exe -rpc 就好了.
ps. 不放心的人請重新編譯吧!!
hbnetio.exe

[完]... 有想到再補上來!若是你有任何好的建議也可以提供出來大家一起研究!
[End]
line ID: ssbbstw
WeChat ID: ssbbstw
回覆文章