【VBA】Seleniumなし、WebDriverなし、VBA-JSONなしでクローム操作にチャレンジしてみた③



https://qiita.com/kabkabkab/items/e4243694cfc474c4c950

の続きです。

前回の記事で、以下実装概要の３をとりあげました。
今回の記事では実装概要４を取り上げます。

***実装概要***
1. WSHオブジェクトを使用して、クローム起動
2. XMLHttpオブジェクトを使用して「localhost:9222/json」Http通信(Get)を行う
   VBAの文字列操作関数を使用して、レスポンス（JSON）から必要情報取得
3. WindowsAPI(WinHttp関連のAPI)を使用して「WebSocketDebuggerURL」にHttp通信を行う。
4. WindowsAPI(WinHttp関連のAPI)を使用して、HTTP通信をWebSockt通信にUpGradeさせる。
5. WindowsAPI(WebSocket関連のAPI)を使用して、CDPメソッドの送信、レスポンスの受信を行う。


<h2>※注意※

<h5>
 以下コードは本来、全てクラスで実装しますが、わかりやすさのため、以下のような記述をしています。<br>

<h6>

- 該当箇所のみを標準モジュールに記述したような形式でコード例を記載
- リテラルは本来は定数宣言しますが、そのままリテラルで記載
- グローバル変数なんてもちろん本来は使用しませんが、コード例では使用。
（クラス内でメンバ変数宣言、およびプロパティ設定するものとお考え下さい）

- WinAPIのコメントでの説明内容はMSのレファレンスをDeepLで翻訳し、
わかりづらいところは意訳し、全体的にかなり端折って抜粋したものです。
あくまで大枠をつかむこと、および今回の実装に焦点をあてた説明です。
そのため正確な情報はMSのレファレンスをご参照ください。
<br>

<h2>ここからが続き

***＜実装4. HTTP通信をWebSockt通信にUpGradeさせる＞***

※前回と同じく、まず全体コードを掲載し、それに続けて個別箇所を説明します。
※各所でWinAPIを使用していますが、API宣言については個別箇所の説明時に掲載します。

~~~vb:vba：実装4全体
Public hwebsockethandle_ As LongPtr

Public Function UpgradeToWebSocket() As Boolean
    '1 プロトコルをhttpからwebsocketにUpGradeするためのoptionをリクエストハンドルに設定）
    Dim result As Long
    result = WinHttpSetOption(hRequestHandle_, _
                        WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, 0, 0)
    If (result = 0) Then GoTo quit

    '2 サーバーにリクエスト送信
    result = WinHttpSendRequest(hRequestHandle_, _
                WINHTTP_NO_ADDITIONAL_HEADERS, _
                0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)
    If (result = 0) Then GoTo quit
    '3 サーバーからのレスポンス受信
    result = WinHttpReceiveResponse(hRequestHandle_, 0)
    If (result = 0) Then GoTo quit
    '4 レスポンスのステータスコード確認。101でアップグレード確認。
    If IsResponseStatus(101) = False Then GoTo quit

    '5 WebSocketハンドル取得
    hwebsockethandle_ = WinHttpWebSocketCompleteUpgrade(hRequestHandle_, 0)
    If hwebsockethandle_ = 0 Then GoTo quit
    
'    Debug.Print "Upgrade成功"
    UpgradeToWebSocket = True

Exit Function
quit:
    UpgradeToWebSocket = False
End Function
~~~
***概要***
このコードでは、ChromeのWebDebuggingURLとのHTTP接続について、
プロトコルをHTTP→WebSoketにUpgradeする処理を行っています。
(その処理をUpgradeToWebSocketという自作関数にまとめています。)

補足ですが、プロトコルをHTTP→WebSocketにUpgradeするためには
以下処理が必要です。
- HTTPリクエストにUpgradeするのに必要なヘッダー情報を追加する
- HTTPリクエストを投げる
- HTTPレスポンスでステータスコードが101になっている事を確認する
- Upgradeを完了させる

そのため処理内容としては、以下の順でWinHTTP関連のAPIを呼び出す内容となっています。

`WinHttpSetOption` 
（HTTPリクエストにHTTP→WebSocketへUpgradeするのに必要なヘッダー情報を追加）
→`WinHttpSendRequest` （HTTPリクエスト送信）
→`WinHttpReceiveResponse` （HTTPレスポンス受信）
→`WinHttpQueryHeaders` （レスポンスのヘッダーを確認し、ステータスコードが101であるか確認）
→`WinHttpWebSocketCompleteUpgrade`(Upgradeを完了させ、今後のWebSocket通信に必要なウェブソケットハンドルを取得）

上記が全て成功すればHTTP→WebSocketへのアップグレードは完了なので、処理を進め次の実装5に進みます。

※もし途中で失敗した場合は、UpgradeToWebSocketをFalseで抜け、
＜実装3.（前回の記事参照）＞同様、呼び出し側で、CloseHInternetHandles()という
ハンドル解放処理をまとめた自作関数を呼び、
取得済みのハンドルをまとめて解放してから、接続失敗として処理を終了します。
<br>
*******
<br>

***＜以下、コード内容の個別箇所説明＞***
ここからWinAPIの宣言も一緒に掲載します。
コメントで、そのAPI関数の機能、戻り値、引数の説明も記載します。

***コメント箇所１：プロトコルをhttpからwebsocketにUpgradeするためのoptionをリクエストハンドルに設定）***
~~~vb:vba：コメント箇所１で使用されるWinAPIの宣言
'説明:インターネットオプションを設定
'戻り値:成功した場合TRUE、失敗した場合FALSEを返す。
Public Declare PtrSafe Function WinHttpSetOption Lib "WinHttp" ( _
   ByVal HINTERNET As LongPtr, _
   ByVal dwOption As Long, _
   ByVal lpBuffer As LongPtr, _
   ByVal dwBufferLength As Long _
   ) As Long
'引数1：データを設定するハンドルを指定。設定するオプションによって、
'セッションハンドルまたはリクエストハンドルのいずれかを指定する｡
'→今回はリクエストハンドルにオプション設定を行う。=リクエストハンドルを指定。
'引数2：設定するオプションを示す定数。
'プロトコルをHTTP→WebSoketにUpgradeさせるためには以下の定数を指定する。
Public Const WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114
'引数3：オプションに関する設定情報を格納するバッファへのポインタ。
'→WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKETは別途の設定情報は不要=0を指定。
'引数4：引数3のバッファーのサイズを指定。引数2で指定するオプションにより
'文字数で指定するか、バイト数で指定するかが異なる。
'→今回はそもそも引数3のバッファ指定していないので0を指定。
~~~
~~~vb:vba：コメント箇所１
Public hwebsockethandle_ As LongPtr

Public Function UpgradeToWebSocket() As Boolean
    '1 プロトコルをhttpからwebsocketにUpgradeするためのoptionをリクエストハンドルに設定
    Dim result As Long
    result = WinHttpSetOption(hRequestHandle_, _
                        WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET, 0, 0)
    If (result = 0) Then GoTo quit
~~~
***コメント箇所１の説明***
ここでは、「HTTPリクエストに必要なヘッダー情報を追加する」ために
`WinHttpSetOption`関数を呼び出しています。

この関数の第二引数に
WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET(114)
という定数を指定するだけで、Upgradeに必要なヘッダーは全て自動で追加されます。

そのため、`WinHttpSetOption`関数の引数に与えているのは、
＜実装.3（前回の記事参照）＞で取得済みのリクエストハンドルとWINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET定数だけです。
(この定数は引数3,4が不要)

成功するとTrue、失敗するとFalseが戻ります。

<br>

***参考：HTTP→WebSocketにUpgradeするために必要なヘッダー***

- Host: 例）example.com 
- Upgrade: websocket
- Connection: Upgrade 
- Sec-WebSocket-Version: 13　 
※現時点では「13」を指定
- Sec-WebSocket-Key: 例）AQIDBAUGBwgJCgsMDQ4PEA==
※値は一回限り有効な長さ16のランダムなバイト列をbase64に符号化した値。
  　復号した結果は長さ16バイトになるもの。

※RFC（4. opening ハンドシェイクー4.1. クライアントに課される要件ー５の「サーバへの接続が確立されたなら～次に挙げるすべての要件を満たさなければならない」という箇所の後のくだり ）を読むと上記５つが必須ヘッダで、他にも存在するWebSocket関連のヘッダーは任意だと思われます。
　https://triple-underscore.github.io/RFC6455-ja.html#section-7.1.2

※WinHttpSetOptionの第二引数に
　WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKETを指定すれば
　上記５つのヘッダがリクエストに自動で追加されます。
　MSのレファレンスではそういった情報が見つけられず、
　最初は「自分で追加する必要あるのだろうか？」と悩んだのですが、　
　パケットキャプチャしてみたら、追加されていることが確認できました。

****
<br>

***コメント箇所２,３,４：<br>サーバーにリクエスト送信<br>サーバーからのレスポンス受信<br>レスポンスのステータスコード確認。101でアップグレード確認。***
~~~vb:vba：API宣言
'使用しているAPIは、<実装3.>で宣言済みのため省略
~~~
~~~vb:vba：コメント箇所２,３,４
Public hwebsockethandle_ As LongPtr

Public Function UpgradeToWebSocket() As Boolean

・・・
　　'2 サーバーにリクエスト送信、websocketハンドシェイクを行う。
    result = WinHttpSendRequest(hRequestHandle_, _
                WINHTTP_NO_ADDITIONAL_HEADERS, _
                0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0)
    If (result = 0) Then GoTo quit
    '3 サーバーからのレスポンス受信
    result = WinHttpReceiveResponse(hRequestHandle_, 0)
    If (result = 0) Then GoTo quit
    '4 レスポンスのステータスコード確認。101でアップグレード確認。
    If IsResponseStatus(101) = False Then GoTo quit
~~~
***コメント箇所２,３,４の説明***
ここではHTTPリクエストの送信→レスポンス受信→ステータスコード確認を行っています。
使用するWinAPIおよび引数に与えている内容は＜実装3.（前回の記事参照）＞の内容と同じであるため、説明は省略します。

一点だけ補足として、HTTP→WebSocketにUpgradeするためのリクエストを送信し、サーバー側がUpgradeに応じると、サーバーはレスポンスのステータスコードを101に設定して返してきます。
ステータスコード200が返ってきてもUpgradeはできないのでご注意ください。

******

<br>

***コメント箇所５：WebSocketハンドル取得***
~~~vb:vba：コメント箇所５で使用されるWinAPIの宣言
'説明:WinHttpSendRequestによって開始されたWebSocketハンドシェイクを完了させ、
'WebSocket通信に必要なWebSocketハンドルを取得。
'この関数は、HTTPレスポンスのステータスコードが101であることを確認した上で
'呼び出すこと｡それ以外のステータスコードで呼び出すと失敗する｡
'戻り値:成功の場合、WebSocketハンドル。失敗の場合0。
Public Declare PtrSafe Function WinHttpWebSocketCompleteUpgrade Lib "WinHttp" ( _
   ByVal hRequest As LongPtr, _
   ByVal pContext As LongPtr _
   ) As LongPtr
'引数1：Http→WebSocketへのUpgradeに使用したリクエストハンドル。
'引数2：新しいハンドルに関連付けられるコンテキスト。
'※この引数は正直理解しきれていません。
'MSのレファレンスにもこの一文しか説明がありません。
'もし「コンテキスト」＝「APサーバのWebアプリのこと、もしくはWebアプリの配置場所や呼び出し方」
'のようなことを指しているのであれば、今回のクローム操作には関係ないだろうと考えたこと、
'及びレファレンスではoptional扱いの引数のため、一旦0を設定しています。
'もし正確にわかる方がいれば教えてほしいです・・・。
~~~
~~~vb:vba：コメント箇所５
Public hwebsockethandle_ As LongPtr

Public Function UpgradeToWebSocket() As Boolean

・・・
    '5 WebSocketハンドル取得
    hwebsockethandle_ = WinHttpWebSocketCompleteUpgrade(hRequestHandle_, 0)
    If hwebsockethandle_ = 0 Then GoTo quit
~~~
***コメント箇所５の説明***
最後にUpgradeを完了させ、今後のWebSocket通信で使用するウェブソケットハンドルを取得するために、`WinHttpWebSocketCompleteUpgrade`関数を呼び出しています。

引数の設定は、リクエストハンドルのみです。

成功するとWebSocketハンドル、失敗の場合0が戻ります。

******

<h4>実装４は以上です

*********************************


***また記事が長くなってきたので、いったんここで切ります。***

<br><br>

***続きはこちらです。***

https://qiita.com/kabkabkab/items/d65aef219b14ca44c25c