|
● 前言
1.公司 POS 專案需要用 WebSoxket 通訊協定連接一家手機點餐的系統, 以便獲取訂單通知資訊
2.找不到好用的 for Lazarus 之 WebSoxket 元件, 不然就是不支援 SSL (Server 端要求使用 wss 協定)
3.知道 WebSocket 是 http 的 Upgrade 版本, 只好土法煉鋼, 用 https 來實作 WebSoxket SSL (wss) 協定
4.本範例只是實作證明, 用 Indy/TIdHTTP 可以模擬出 WebSoxket 通訊協定; 實用上要把相關功能放在另一個 Thread 內
不然主執行緒會被 hold 住 (因為 http 通訊內用了一個 while do 的無窮迴圈)
● Server 端要求的連線方式
連線方式
wsocket_protocol=wss
wsocket_host=wsocket-test.mydomain.shop
wsocket_port=443
登入驗證:連線時帶入Header「Authorization」,值為HTTP基本認證格式。
1.編碼方式:將「帳號:密碼」進行Base64編碼。
2.範例:當帳號為「username」,密碼為「password」時,對應Header的值為 「Basic dXNlcm5hbWU6cGFzc3dvcmQ=」。
3.驗證失敗時會回傳特定HTTP狀態碼並中止連線:
(1) 401:缺少Header「Authorization」或是header內容錯誤
(2) 403:帳號或密碼錯誤
心跳機制:每隔30秒伺服器端會發送訊息,客戶端需回傳對應訊息以保持連線建立。
1.伺服器端傳送格式:「"primus::ping::<timestamp>"」,其中<timestamp> 為目前時間戳記。
2.客戶端回傳格式:「"primus::pong::<timestamp>"」,其中<timestamp>為 「伺服器端傳來的時間戳記」。*注意:包含「"」
3.範例:伺服器端傳送「"primus::ping::1523519829084"」時,客戶端需回傳訊 息「"primus::pong::1523519829084"」
● 實作 (程式碼中有 ★★ 者為重點)
//使用 HTTPS 來模擬實作 WSS (WebSocket over SSL)
procedure TForm1.Button1Click(Sender: TObject);
var RequestStr: string;
ResponseStr: string;
RequestBody: TStringStream;
IdHTTP1: TIdHTTP;
IdSSLIOHandlerSocketOpenSSL1: TIdSSLIOHandlerSocketOpenSSL;
i: integer;
Ib: TIdBytes;
begin
WebSocket_stop:=0;
IdHTTP1:=TIdHTTP.Create(nil);
IdSSLIOHandlerSocketOpenSSL1:=TIdSSLIOHandlerSocketOpenSSL.Create(nil);
try
try
//---------------------------------
//打包 JSON 或一般字串
//---------------------------------
RequestStr:=''; //走 IdHTTP1.Get() 方法, 不需用到
RequestBody := TStringStream.Create(RequestStr);
//---------------------------------
//設定通訊元件
//---------------------------------
IdHTTP1.HandleRedirects:=true;
IdHTTP1.ReadTimeout:=40000;
IdHTTP1.ConnectTimeout:=40000;
//走 https 通信協定
IdSSLIOHandlerSocketOpenSSL1.SSLOptions.Method:=sslvTLSv1_2; //LinePay 要用到 TLS V1.2
IdHTTP1.IOHandler:=IdSSLIOHandlerSocketOpenSSL1;
//有 Proxy 時
//IdHTTP1.ProxyParams.ProxyServer:=ProxyServer;
//IdHTTP1.ProxyParams.ProxyPort:=ProxyPort;
//參數中文不要自動 ENCODEING
//IdHTTP1.HTTPOptions := IdHTTP1.HTTPOptions + [hoKeepOrigProtocol];
IdHTTP1.HTTPOptions:=IdHTTP1.HTTPOptions - [hoForceEncodeParams]; //(同上功能)
//-----------------------------------------------------------------
//製作 Request Header (★★ 把 HTTP Upgrade 為 WEBSocket 通訊協定 )
//-----------------------------------------------------------------
IdHTTP1.Request.Connection := 'keep-alive';
IdHTTP1.Request.ContentType := 'text/plain; charset=UTF-8'; //UTF-8
IdHTTP1.Request.Connection := 'Upgrade';
IdHTTP1.Request.CustomHeaders.Clear;
IdHTTP1.Request.CustomHeaders.Add('Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
//沒加這段, 會有 => (錯誤)HTTP/1.1 426 Upgrade Required
IdHTTP1.Request.CustomHeaders.Add('GET / HTTP/1.1');
IdHTTP1.Request.CustomHeaders.Add('Host: wsocket-test.mydomain.shop:443');
IdHTTP1.Request.CustomHeaders.Add('Connection: Upgrade');
IdHTTP1.Request.CustomHeaders.Add('Upgrade: websocket');
IdHTTP1.Request.CustomHeaders.Add('Sec-WebSocket-Version: 13');
IdHTTP1.Request.CustomHeaders.Add('Sec-WebSocket-Key: dXNlcm5hbWU6cGFzc3dvcmQ='); //與 "後面服務端" 響應首部的Sec-WebSocket-Accept 是配套的,提供基本的防護,比如惡意的連接,或者無意的連接。
//---------------------------------
//呼叫 IdHTTP1.Get()
//---------------------------------
ResponseStr:=IdHTTP1.Get('https://wsocket-test.mydomain.shop:443');
if IdHTTP1.Response.ResponseCode = 101 then begin
Memo2.Lines.Add('Upgrade was accepted, use IdHTTP1.IOHandler to process WebSocket packets as needed ...'); //監看
//★★ 建立持續的連線, 以及處理心跳包機制
while true do begin
Application.ProcessMessages;
if WebSocket_stop=1 then break; //讓 while 迴圈停止的機制
//收信
SetLength(Ib, 0);
IdHTTP1.IOHandler.ReadBytes(Ib, -1);
Memo2.Lines.Add(BytesToString(Ib)); //監看
if Pos('ping', BytesToString(Ib))>0 then begin
//收到 ping, 回傳 pong
Ib[12] := $6f; //'o' ...//把收到的 "primus::ping::1604037836730" 改成 "primus::pong::1604037836730"
Memo2.Lines.Add(BytesToString(Ib)); //監看
IdHTTP1.IOHandler.Write(Ib, -1);
else else begin
Memo2.Lines.Add(BytesToString(Ib)); //監看其他資料
end;
end;
Memo2.Lines.Add('WebSocket Stop!!'); //監看
end else begin
Memo2.Lines.Add('Upgrade was not accepted ...'); //監看
end;
RequestBody.Free;
except
on E: Exception do begin
Memo2.Lines.Add('(錯誤)'+E.Message); //監看
end;
end;
finally
IdHTTP1.Disconnect;
IdHTTP1.Free;
IdSSLIOHandlerSocketOpenSSL1.Free;
end;
end; |
|