VBScriptでWebサーバーのAPIを同時並列に呼び出す

#はじめに

スタートトゥデイ工務店 Advent Calendar 2016 13日目の記事です。

本記事では実行可能形式ファイルを作るほどでもない、
ちょっとした処理を書きたいときに便利なVBScriptについて書きます。

#VBScriptとは

WindowsScriptHostを利用したWindows上の標準スクリプト言語で、
拡張子が[vbs]のテキストファイルを作成すればダブルクリックするだけでも実行できます。
実行ファイルを作成する必要もなく、テキストエディタで記述、修正できるのでどんなWindowsPCでも
環境を準備することなく始めることができます。（2016年12月現在）
そんな手軽なVBScriptですが、本稿では少し深く使い込んでみたいと思います。

#WebAPIに対して、非同期で通信する

WinHTTPの機能を利用することができるので、WebAPIに対して非同期での通信を行うこともできます。

もちろん、通信中のオブジェクトを配列などに格納することでWebAPIサーバー上の処理を並列に呼び出すこともできます。

以下は複数台のWebAPIサーバーに対してリクエストを行うサンプルです。

```vbnet:webapi.vbs

Const sServerList = "server1,server2,server3" 'サーバー一覧

Dim aServerList
aServerList = Split(sServerList, ",") '配列化

Dim alServerName '確認サーバー名ArrayList（消込）
Set alServerName = CreateObject("System.Collections.ArrayList")

Dim dicXmlHttp '各サーバーXmlHttpの配列
Set dicXmlHttp = CreateObject("Scripting.Dictionary")

'送信処理
Dim oXmlHttp
Dim sServerName
For Each sServerName In aServerList

	Set oXmlHttp = CreateObject("MSXML2.ServerXMLHTTP")
	Call oXmlHttp.SetTimeouts(30000, 30000, 30000, 30000)
	Call oXmlHttp.Open("post", "http://" & sServerName & "/Setting/", True, "username", "password") '非同期処理：True
	Call oXmlHttp.SetRequestHeader("Content-Type", "Application/X-WWW-Form-UrlEncoded;Charset=UTF-8")
	Call oXmlHttp.Send("param1=a&param2=b")

	Call dicXmlHttp.Add(sServerName, oXmlHttp) '送信済みXmlHttpを配列に追加
	Call alServerName.Add(sServerName) '配列を消込用ArrayListに変換

Next

'通信待機
Do Until alServerName.Count = 0 'サーバーリストすべて消し込むまでループ
	For Each sServerName In alServerName.ToArray()
		If dicXmlHttp(sServerName).ReadyState <> 4 Then 'ReadyState:4(complete)
			Call dicXmlHttp(sServerName).WaitForResponse(1) '待機
		Else
			Call alServerName.Remove(sServerName)
		End If
	Next
Loop

'結果処理
For Each sServerName In dicXmlHttp.Keys
	If dicXmlHttp(sServerName).Status <> 200 Then
		Call WScript.Echo("Error HttpStatus")
	ElseIf dicXmlHttp(sServerName).ResponseXml.ParseError.ErrorCode <> 0 Then
		Call WScript.Echo("Error XmlParseError")
	Else
		Call dicXmlHttp(sServerName).ResponseXml.SetProperty("SelectionLanguage", "XPath")
		Call WScript.Echo(dicXmlHttp(sServerName).ResponseXml.DocumentElement.SelectSingleNode("/result/status").Text)
	End If
Next
```


#解説

部分ごとに分けて解説していきます。

```vbnet
Dim alServerName '確認サーバー名ArrayList
Set alServerName = CreateObject("System.Collections.ArrayList")
```
通信完了消込用のサーバー名文字列ArrayListオブジェクトを宣言します。
一部の.NET FrameworkのクラスオブジェクトはVBScriptからも利用できます。
特にVBScriptでは配列の取り扱いが不便なため、
ArrayListが利用できることはとてもありがたいです。

```vbnet
Dim dicXmlHttp '各サーバーXmlHttpオブジェクトのDictionary
Set dicXmlHttp = CreateObject("Scripting.Dictionary")
```
通信を開始したXmlHttpオブジェクトを格納します。
HashTableの様に利用します。

```vbnet
'送信処理
Dim oXmlHttp
Dim sServerName
For Each sServerName In aServerList

	Set oXmlHttp = CreateObject("MSXML2.ServerXMLHTTP")
	Call oXmlHttp.SetTimeouts(30000, 30000, 30000, 30000)
	Call oXmlHttp.Open("post", "http://" & sServerName & "/Setting/", True, "username", "password") '非同期処理：True
	Call oXmlHttp.SetRequestHeader("Content-Type", "Application/X-WWW-Form-UrlEncoded;Charset=UTF-8")
	Call oXmlHttp.Send("param1=a&param2=b")

	Call dicXmlHttp.Add(sServerName, oXmlHttp) '送信済みXmlHttpを配列に追加
	Call alServerName.Add(sServerName) '配列を消込用ArrayListに変換

Next
```
サーバー名リストすべてに対してHTTP通信を開始し、送信済みとなったXmlHttpオブジェクトをDictionaryオブジェクトに格納します。
非同期通信を行うにはOpenメソッドの第三引数にTrueを設定します。
うっかりど忘れしてしまい、苦労しやすいのですが、第四引数にユーザー名を、第五引数にパスワードを設定することによってBasic認証を通過することができます。

```vbnet
'通信待機
Do Until alServerName.Count = 0 'サーバーリストすべて消し込むまでループ
	For Each sServerName In alServerName.ToArray()
		If dicXmlHttp(sServerName).ReadyState <> 4 Then 'ReadyState:4(complete)
			Call dicXmlHttp(sServerName).WaitForResponse(1) '待機
		Else
			Call alServerName.Remove(sServerName)
		End If
	Next
Loop
```

サーバー全台の通信が完了するのを待機します。
通信状態ステータス(ReadyState)が4(Complete)でなければさらにレスポンス待機、
通信が完了していればサーバー名リストから消して確認対象から除き、
すべてのサーバーが完了するまで繰り返します。

```vbnet
'結果処理
For Each sServerName In dicXmlHttp.Keys
	If dicXmlHttp(sServerName).Status <> 200 Then
		Call WScript.Echo("Error HttpStatus")
	ElseIf dicXmlHttp(sServerName).ResponseXml.ParseError.ErrorCode <> 0 Then
		Call WScript.Echo("Error XmlParseError")
	Else
		Call dicXmlHttp(sServerName).ResponseXml.SetProperty("SelectionLanguage", "XPath")
		Call WScript.Echo(dicXmlHttp(sServerName).ResponseXml.DocumentElement.SelectSingleNode("/result/status").Text)
	End If
Next
```
ここの時点でエラー等なければすべてのサーバーのレスポンス結果を得ることができます。
XmlHttpオブジェクトを利用しているので、ResponseXmlプロパティから
Domオブジェクトを取得することができ、DomDocument同様に扱うことができます。

#エラー処理
レスポンス待機時にエラーが起きることがありますので、処理を中断せず継続したいときは
以下のようなエラー処理を加えるとよいかもしれません。

```vbnet
'通信待機
Do Until alServerName.Count = 0 'サーバーリストすべて消し込むまでループ
	For Each sServerName In alServerName.ToArray()
		If dicXmlHttp(sServerName).ReadyState <> 4 Then 'ReadyState:4(complete)
			On Error Resume Next                           '■追加■
			Call dicXmlHttp(sServerName).WaitForResponse(1) '待機
			If Err.Number <> 0 Then                        '■追加■
				Call alServerName.Remove(sServerName)      '■追加■
				Call WScript.Echo("Error WaitForResponse") '■追加■
			End If                                         '■追加■
			On Error GoTo 0                                '■追加■
		Else
			Call alServerName.Remove(sServerName)
		End If
	Next
Loop
```
このエラーが発生しても処理を継続するステートメント`On Error Resume Next`は条件文判定の際にエラーが生じた場合、正の条件の方に分岐させてしまうため、分岐判定部（`If`から`Then`の間など）には関数などの処理を利用せず、早めに`On Error GoTo 0`でエラー通過処理を無効にしたいところです。

#おわりに

VBScriptの標準機能でもWebAPIサーバーへの非同期通信は行うことができ、
同期通信では80秒もかかってしまうような通信作業でも非同期通信にすることで3秒程度で完了できたりすることもあります。

文法もモダンではなく、配列機能、クラス機能などに制限も多いですが、手軽に利用することができ、
ActiveServerPage、Excelマクロ(VBA)、HTAアプリケーションなどへの応用も可能ですので、とにかく速やかに作りたいときの瞬発力は他の言語にはない魅力です。

多数のサーバーを管理する方や、Windows環境を利用する方に少しでも役に立てば幸いです。