2011年9月8日 星期四

Powerbuilder inet.PostURL用法

目前PB的開發我都是把它當成一個 Remote Client UI,

所以實際上它沒有連接資料庫,而完全是透過Coldfusion的服務頁面完成資料操作,

因此很多時候需要傳遞大量資料時,簡單的 inet.GetURL 函數就變得不好用了,

因此特地研究了inet.PostURL的用法,它的用法在Help上有兩個範例(我有修改過):

第一種是簡單的呼叫一個頁面,並取得頁面回傳的HTML內容:
(請注意 linet 和 n_internetresult 都是必須先建立的物件與類別,可參考這裡)

Blob lblb_args
String ls_headers , ls_url
Long ll_length
n_internetresult lir_html
inet linet

linet = Create inet
lir_html = CREATE n_internetresult
ls_url = "http://www.mydoamin.com/testpage.cfm"
lblb_args = blob("")
ll_length = Len(lblb_args)
ls_headers = "Content-Length: " + String(ll_length) + "~n~n"
linet.PostURL (ls_url, lblb_args, ls_headers, 8080, lir_html)

destroy lir_html
destroy linet

上面這種方式並沒有傳遞參數,其實效果如同:
linet.GetURL("http://www.mydoamin.com:8080/testpage.cfm",lir_html)



不過上述方式實際在執行時是有點問題,那就是Cache,如果你已經執行過一次這個方式,其回傳內容會被Cache在PBVM內,當下次再執行時,它並不會重新發出請求,反而直接從記憶體Cache中的內容重新取出並回傳。

解決的方式可以在要傳遞的內容不要放空值,隨便填一個東西,只是這樣子做真的有點多此一舉,不如直接使用 GetURL 還來得直覺些:
lblb_args = blob(String(today()))
ll_length = Len(lblb_args)

第二種方式帶有傳遞參數

Blob lblb_args
String ls_headers , ls_url , ls_args
Long ll_length
n_internetresult lir_html
inet linet

linet = Create inet
lir_html = CREATE n_internetresult
ls_url = "http://www.mydoamin.com/testpage.cfm"
ls_args = "user=John&pwd=123456"
lblb_args = blob(ls_args)
ll_length = Len(lblb_args)
ls_headers = "Content-Type: application/x-www-form-urlencoded~n" + &
                    "Content-Length: " + String(ll_length) + "~n~n"
linet.PostURL (ls_url, lblb_args, ls_headers, 8080, lir_html)

destroy lir_html
destroy linet

其實上面這種效果也同等於下面的方式
linet.GetURL("http://www.mydoamin.com:8080/testpage.cfm?user=John&pwd=123456",lir_html)

但若要傳遞檔案就要使用HTML表單的 HTTP multipart/form-data

方式來處理會比較妥當,因此在PB Help說明不甚清楚的情況下就必須研究一下

HTTP的相關協定了。

在HTTP的協定中有訂製 multipart/form-data 傳輸的方式為 rfc1867協定
這個協定的基本結構如下:

Content-Type: multipart/form-data; boundary=****20110908123520****~r~n
Content-Length: 299~r~n
~r~n
--****20110908123520****~r~n
Content-Disposition: form-data; name="user"~r~n
~r~n
John~r~n
--****20110908123520****~r~n
Content-Disposition: form-data; name="pwd"~r~n
~r~n
123456~r~n
--****20110908123520****~r~n
Content-Disposition: form-data; name="txtfile"; filename="我的訂單.txt"~r~n
Content-Type: text/plain~r~n
~r~n
...我的訂單.txt的內容...~r~n
--****20110908123520****~r~n
Content-Disposition: form-data; name="imgfile"; filename="我的照片.jpg"~r~n
Content-Type: image/jpeg~r~n
Content-Transfer-Encoding: binary~r~n
~r~n
...我的照片.jpg的內容...~r~n
--****20110908123520****--~r~n

可以看到淺藍色部份就是它的 Header,粉色部份就是 Arguments


  1. 在 Header 的地方 Content-Type:設定為 multipart/form-data;
  2. boundary 是用來定義分隔線的樣式,其內容可以自訂,一般而言都是亂數產生的文字,盡量避開會與內容發生相同的可能性。
  3. Content-Length: 是 Arguments (粉色部份全部)內容的總長度。
  4. Header 與 Arguments 中間由一個換行分隔(白色部份)
  5. 粉色部份則是 Arguments的內容,而裡面每一個 argument 都由一個 boundary 分開,
  6. 每一個作為分隔的 boundary 前面會再加上 --
  7. argument內容都會先定義其來源變數名稱:Content-Disposition: form-data; name="欄位名"
  8. 然後會加個換行表示此欄位對應值的資料開始,直到遇到下一個 boundary 結束
  9. 如果 argument 內容值不是單純的文字資料,則會視情況加上 Content-Type 或是 Content-Transfer-Encoding
  10. 最後,整個 Arguments 的結束由 boundary 前後各加上 -- 作為結束符號


由於 PostURL 的 Header 與 Arguments 分開傳輸,所以淺藍色與粉色的部份就必須分開處理。

下面展示一個上傳圖片的 Script:

Blob lblb_args , lblb_pic
String ls_headers , ls_url , ls_args , ls_bndy
Long ll_length
n_internetresult lir_html
inet linet
integer li_file

//讀取圖片
li_file = FileOpen("C:\myPic.jpg",StreamMode!)
FileReadEx(li_file,lblb_pic)
FileClose(li_file)
//建立網路物件
linet = Create inet
lir_html = CREATE n_internetresult
ls_url = "http://www.mydoamin.com/uploadpic.cfm"
//建立分隔字串
ls_bndy = "****" + String(cpu()) + "****"
//先產生其它argument資料
ls_args = "--" + ls_bndy + "~r~n" + &
              "Content-Disposition: form-data; name=~"user~"~r~n~r~n" + &
              "John~r~n" + &
              "--" + ls_bndy + "~r~n" + &
              "Content-Disposition: form-data; name=~"pwd~"~r~n~r~n" + &
              "123456~r~n" + &
              "--" + ls_bndy + "~r~n" + &
              "Content-Disposition: form-data; name=~"pic~"; filename=~"MyPic.jpg~"~r~n" +&
              "Content-Type: image/jpeg~r~n" + &
              "Content-Transfer-Encoding: binary~r~n~r~n"
//轉成binary後加上圖片資料與結束符號
lblb_args = blob(ls_args) + lblb_pic + blob("~r~n--" + ls_bndy + "--~r~n")
//計算Argumments長度
ll_length = Len(lblb_args)
ls_headers = "Content-Type: multipart/form-data; boundary=" + ls_bndy + "~r~n" + &
                    "Content-Length: " + String(ll_length) + "~r~n~r~n"
//Post方式上傳檔案
linet.PostURL (ls_url, lblb_args, ls_headers, 8080, lir_html)

destroy lir_html
destroy linet


注意:展示程式碼沒有檢查機制所以正常情況下該有的檢查還是要做,比如檔案開啟失敗等等,雖然你可以用PowerBuilder Exception來處理,但實際上不太建議。

附上 PowerBuilder 發送 http 請求的基本 Header 給大家參考:
POST /test/hello.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Content-Length: 16
User-Agent: PowerBuilder
Host: 192.168.0.155
Cache-Control: no-cache
Cookie: CFID=7835; CFTOKEN=15357209

2.0.1.1./.9./.9.

16 個回應:

egg 提到...

謝謝!您的範例很詳細! 想請教您另外一個問題
我使用pb post 的方式取到回傳的session id , 但是想要繼續用這個session id 傳一些參數, 但是還到瓶頸, 不知道要如何處理, 可否賜教

Spencer Lee 提到...

請教PB dynamicly pass two parameter value to WebService(calc)by httpgost protocol(web.config had been added )
for example-url...http://localhost/WebSite/WebService.asmx/calc?x=3&y=6...OK
(this page had gotten the resault that I wish)
but
in PB
...
ls_url = "http://localhost/WebSite/WebService.asmx/calc"
ls_args = "x=3&y=6"
...
inet.posturl(...)
PB can not correctly pass two parameter value x,y into this WebService(calc).
Why? was something missing?
please,Can you help me to ptch this issue?

Spencer Lee 提到...

Cintinue..last letter
my WebService(function calc(int x, int y)) http://localhost/WebSite/WebService.asmx/calc?x=3&y=6
the web.comfig had already added httpget,httppost,httpsoap protocol.
PB and WebService are both on a server.
PB inet.geturl to call WebService by httpget...tested OK,
But inet.posturl to call WS by httppost protocol....tested NG,
the error message is "lost parameter x value"
why? Are something wrong?
Can you help me to debug this issue?

WILDOX 提到...

Re: Spencer Lee <2093322091581199941>

Hi, As far as i know , It's different retrieve arguments from asp.net (.asmx) when use GET/POST method , because arguments will be store in different array .

// GET method
String data1 = this.Context.Request.QueryString["x"]

// POST method
String data1 = this.Context.Form["x"]

WILDOX 提到...

Re: Spencer Lee <9063488261866878955>
Hi , can you show your server script of how get the pass arguments ?

Spencer Lee 提到...

Hi,my powerScript code
>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Blob lblb_args
String ls_headers , ls_url , ls_args
Long ll_length
nv_internetresult lir_html
inet linet
integer li_rtn

linet = Create inet
lir_html = CREATE nv_internetresult
ls_url = "http://localhost/website2/WebService.asmx/calc"
ls_args = "y=7&x=5"
lblb_args = blob(ls_args)
ll_length = Len(lblb_args)
ls_headers = "Content-Type: application/x-www-form-urlencoded~n" + &
"Content-Length: " + String(ll_length) + "~n~n"
li_rtn=linet.PostURL (ls_url, lblb_args, ls_headers, 80, lir_html)
messagebox(string(li_rtn),lir_html.is_text )
//....internetdata function is_textstring(blob)
destroy lir_html
destroy linet

WILDOX 提到...

Re: Spencer Lee <3422784346226239578>

Your PB script seems to be no problem, May the problem occur in the server script ?

匿名 提到...

Hi,
我需要從PB的應用程式裡去判斷另一個網頁應用程式是否已經登入.
請教二個問題:
1.PB9 + IE瀏覽器, 採用Get_URL(), 但似乎有PBVM cache問題, 一定要退出PB應用程式後再登入才得以正確取得資訊. 請問有何手法? Post_URL()則無法抓到已登入狀態資訊.
2.當網頁應用程式採Chrome或Friefox瀏覽器登入, Get_URL()則完全無作用?
以上, 感謝先!

Charles

////Get_URL程式碼
integer li_rc
inet linet_main
n_cst_internet luo_data // as defined above

linet_main = CREATE inet
luo_data = CREATE n_cst_internet


SetPointer(HourGlass!)
erLogin.aspx?employeeid=00004725")
li_rc = linet_main.GetURL("http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725", luo_data)
SetPointer(Arrow!)
IF li_rc = 1 THEN
MessageBox("Data from Real's HowTo1", luo_data.is_data)
ELSE
MessageBox("Data from Real's HowTo2", "Oops rc:" + string(li_rc))
END IF

DESTROY luo_data
DESTROY linet_main


////Post_URL程式碼
Blob lblb_args
String ls_headers , ls_url
Long ll_length
n_cst_internet lir_html
inet linet

linet = Create inet
lir_html = CREATE n_cst_internet
ls_url = "http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725"
lblb_args = blob(string(f_sysdate()))
ll_length = Len(lblb_args)
ls_headers = "Content-Length: " + String(ll_length) + "~n~n"
linet.PostURL (ls_url, lblb_args, ls_headers, 80, lir_html)

MessageBox("Data from Real's HowTo1", lir_html.is_data)

destroy lir_html
destroy linet

WILDOX 提到...

Re: 匿名 <3325206192694071570>
PB的getURL我有發現它好像是基於IE底層的元件,所以有可能受到IE的設定影響,可以嘗試2種方式去解決:
1.IE->設定->網際網路選項->一般->瀏覽歷程記錄(設定)->檢查儲存的畫面是否有新版本->(選擇)每次造訪網頁時。

2.在GetURL的參數放入假的變動參數,例如:
http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725&dummy=052015221
其中dummy的值是隨機產生,借此破壞inet元件在本機上的cache條件,當然server通常不會處理自己不接受的參數。

3.先瞭解http的架構下所有Client和Server是Request/Response(離線式)不是Connection(連線式)型式,在server上無法直覺的取得各個請求的來源是否原自同一支程式?因此Server的判讀上一般都會採用IP或是Browser的token,或是COOKIE傳遞的值,或是傳遞的帳號來記錄連線狀態。

4.PB與各瀏覽器一樣都是Client,在WEB架構下除非Server會回傳給Client目前的登入帳號數量,否則Client無法判斷其他程式是否登入,因此從您的程式碼上我無法得知您如何去判斷『其他的網頁應用程式是否登入』?

所以,要判斷其他的網頁應用程式是否登入應該要從Server解決,例如您只希望只有同一支程式可以登入一個帳號,那您在傳遞參數時除了employeeid之外還要有一個ApplicationID或稱為Application token,而這個ApplicationID必須依照每部電腦特徵去產生(例如:電腦名稱、IP等等),然後Server上要對這個ApplicationID與employeeid去組合記錄,並且拒絕同一個employeeid和不同ApplicationID的請求,並返回拒絕訊息。

當你的應用程式關閉前必須進行『Logout』通知Server解除employeeid和ApplicationID的配對關係。

WILDOX 提到...

Re: 匿名 <3325206192694071570>
您可以從我的文章後面看到 PowerBuilder 發送 http 請求的基本 Header
其實是很簡單的,其中user-agent只有PowerBuilder而已,如果你用FF或是chrome或是IE,你會發現它是像這樣:
Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13

而我的Server能夠認出由哪個瀏覽器來的部分是下面這個meta:
Cookie: CFID=7835; CFTOKEN=15357209
因為我用的Server是Coldfusion,它會要求瀏覽端記錄這組Cookie已便認出是哪個瀏覽器來的,由於每種Server會發出的要求不太一樣所以這段僅能參考。

因此,若您使用GetURL便無法再增加Meta的數量,所以只能用傳遞參數去變化了。


WILDOX 提到...

Re: egg <4692556331809936404>
比較大的問題是powerbuilder的inet元件不支援cookie的設定,由於session id的使用必須加在cookie裡面,如果一定要用到,網路上也有人建議使用webbrowser control元件,把webbrowser control崁入程式,隱藏,然後利用它的Method發送/接收請求,這樣它本身就可以記錄session id 到cookie裡面了。

匿名 提到...

容再將我們規劃的應用面陳述如下:
我們的ERP系統是PB9開發, 需要與簽核系統密切整合. 於ERP裡的某些表單作業裡, 擬佈下Hyper-link, 讓user可以直接click開啟簽核系統去瀏覽該單據的簽核履歷網頁(前題是該user已經在同一台電腦登入在簽核系統裡-即認證過); 然而, 當click觸發時, 若該user並未登入在簽核系統裡, 基於安全考量, 系統是不允許開發簽核履歷頁面的. "http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725" 此URL即在判斷00004725帳號是否已登入? 若user的瀏覽器已登入在簽核系統, 上述URL會傳回"Y"; 反之, 則會提示user必須先登入簽核系統. 上述URL在任何Browser上測試均成功, 但在PB作業裡則有前PO文的種狀況.
故, 針對您第1點之回覆, 似乎並無相關性, 且應只適用於IE? 經實測結果為NG. 而採用dummy變數li_rc = linet_main.GetURL("http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725&dummy=" + string(f_sysdate()), luo_data),此作法亦告失敗.

誠如您所言, 若Get_URL真基於IE底層的元件, 那真的就沒法子了, 畢竟限制user的瀏覽器是不智之舉!

感謝您如此神速的回覆.

Charles

匿名 提到...

謝謝您神速的回覆:
容我再詳細說明一下我想實的功能:
我們的ERP是以PB9所開發, ERP表單作業會發起簽核流程, 並由.Net的綱頁簽核系統接手後續的簽核作業. 故我們需要於ERP的表單作業裡, 透過click url即可開啟該表單於簽核系統的最新或歷史簽核紀錄, 而非再客製一pb window/datawindow來呈現簽核紀錄.
"http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725"此URL即是在判斷該電腦, 帳號00004725是否已登入簽核系統? 若是, return 'Y', 因已認證過, 故ERP即可開啟簽核紀錄頁面; 若否, 則會開啟簽核系統的登入頁面. 上述作法, 很明顯地是基於資安考量.

針對您回覆的第一點並IE設定, 似乎與我所遭遇的問題點關係不大, 經實測後, 並無任何幫助.
至於加上dummy參數的作法, 結果也依舊.

或許, 誠如您所言, 或許INET元件是基於ie所開發, 故無法套用於Chrome及FF等其他browser. 由於我們不可能限制user只能使用IE, 故只能再尋求其他手法了.
總之, 感謝您的分享.

Charles

WILDOX 提到...

Re: 匿名 <6874230841920263572>
您有測試過是否為ASPX的Cache造成的,而不是瀏覽器的問題?

WILDOX 提到...

Re: 匿名 <7165436113921152208>
這個以前我也做過,但我不會再用URL去認證使用者ID。
我的做法是當程式(使用者端)正常登入後,會記錄到一個LOGIN記錄,併產生一組隨機編號做為認證ID,此記錄檔會記錄使用者ID、電腦IP、登入時間、最後存取時間等,並將認證ID返回給程式做為URL查詢的代替身分。

1.程式會定時更新存取時間,保持ID有效性。
2.所有從程式發出URL請求一律帶此認證ID做為識別。
3.當URL查詢要限定查尋來源的電腦時,只要從ASPX取得存取電腦的IP,去比對認證ID的登錄電腦IP即可進行限制。
4.當URL查詢時,也會去檢查該認證ID『最後存取時間』,如果在一定時間內有保持更新,表示程式還在使用中,屬於有效,反之代表程式已關閉,則此認證ID屬於失效。
5.當查詢條件需要使用者ID,只需利用認證ID反查記錄檔可得之。
6.如果URL只是返回查詢結果(畫面),則只需利用RUN外部程式呼叫iexplorer.exe yoururlstring方式,毋須辛苦自己處理HTML的內容。
7.看到您使用li_rc = linet_main.GetURL("http://10.190.2.122/Workflow/WhetherLogin.aspx?employeeid=00004725&dummy=" + string(f_sysdate()), luo_data),比較好奇的是f_sysdate()只有到日期嗎?如果是的話,那同一日的查詢條件亦是『沒變化』啊...我還比較建議Rand的方式耶

WILDOX 提到...

Re: 匿名 <7165436113921152208>
如果要用外部瀏覽器submit複雜的查詢條件,我會在程式路徑下設計一個要submit用的HTML表單,這個表單的method="POST" target="_self",然後HTML尾部寫上javascript自動去submit表單的程式碼。

畢竟HTML是文字檔,所以input條件/值就用PB去處理填好後,再以外部瀏覽器呼叫就可以完成複雜的POST查詢動作囉。

這個方法提供給您參考