標題: 以 Lazarus Indy/TIdHTTP 實作 WebSocket Client 端通訊機制
無頭像
canonpapago

帖子 28
註冊 2013-4-18
用戶註冊天數 4047
發表於 2021-3-14 22:03 
115.43.252.58
分享  私人訊息  頂部
● 前言

1.公司 POS 專案需要用 WebSoxket 通訊協定連接一家手機點餐的系統, 以便獲取訂單通知資訊
2.找不到好用的 for Lazarus 之 WebSoxket 元件, 不然就是不支援 SSL (Server 端要求使用 wss 協定)
3.知道 WebSocket 是 http  的 Upgrade 版本, 只好土法煉鋼, 用 https 來實作 WebSoxket SSL (wss) 協定
4.本範例只是實作證明, 用 Indy/TIdHTTP 可以模擬出 WebSoxket 通訊協定;
5.實用上要把相關功能放在另一個 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;