VB.NETでどんな沢山のXML Web サービスでもサクっとHTTP圧縮化する


XML Web サービスってモダンなネットワークインフラが前提。

特にDataSetのXMLシリアライズは結構なサイズになるようで、MBサイズのレスポンスはザラです;


でも、実際には実効回線速度が数Mbpsなんてこともありますよね?*1

たとえば、実効回線速度が5Mbpsで、WebMethodレスポンスのContent-Lengthが1MBだとすると、レスポンスタイムは1.6秒になってしまうことになります。*2



そこで、HTTP圧縮です。

XMLと圧縮は相性がいいので、だいたい10%以下のサイズになります。

遅い回線では効果絶大です。


でわ、その方法を。。


どんな沢山のXML Web サービスでもサクっとHTTP圧縮化する方法

今回は、サクっとがテーマなのでサクっと紹介しますw

SoapExtension作る
Option Explicit On
Option Strict On

Imports System.IO
Imports System.Web.Services.Protocols

Public Class WsClientExtension
    Inherits SoapExtension

    Public Overrides Function ChainStream(ByVal stream As Stream) As Stream
        Return stream
    End Function

    Public Overloads Overrides Function GetInitializer(
            ByVal methodInfo As LogicalMethodInfo,
            ByVal attribute As SoapExtensionAttribute) As Object
        Return Nothing
    End Function

    Public Overloads Overrides Function GetInitializer(ByValwebserviceType As Type) As Object
        Return Nothing
    End Function

    Public Overrides Sub Initialize(ByVal initializer As Object)
    End Sub

    Public Overrides Sub ProcessMessage(ByVal message As SoapMessage)
        Select Case message.Stage
            Case SoapMessageStage.BeforeSerialize
                With DirectCast(message, SoapClientMessage)
                    Dim req As System.Net.HttpWebRequest =
                        DirectCast(
                            .Client.GetType.InvokeMember("PendingSyncRequest",
                                Reflection.BindingFlags.GetProperty Or
                                Reflection.BindingFlags.Instance Or
                                Reflection.BindingFlags.NonPublic, Nothing, .Client, Nothing), 
                            System.Net.HttpWebRequest)
                    req.AutomaticDecompression = Net.DecompressionMethods.GZip
                End With
        End Select
    End Sub
End Class
app.configにsoapExtensionTypes要素を追加する
  <system.web>
    <webServices>
      <soapExtensionTypes>
        <!-- クラス名とアセンブリ名を指定します。-->
        <add type="hoge.WsClientExtension, hoge_s_assembly_name" />
      </soapExtensionTypes>
    </webServices>
  </system.web>


これだけです。

サクっとしてたでしょ?


そこらじゅうで

Dim ws As New HogeWS
ws.EnableDecompression = True

なんてしなくっていいんですw


Wiresharkなんかでパケットキャプチャして確認してください。

リクエストヘッダに"Accept-Encoding: gzip"が追加されて、レスポンスヘッダに"Content-Encoding: gzip"が追加されて、Content-Lengthが10%位になっているはずです。


おまけ

SoapExtensionでレスポンスを検証する場合

HttpWebResponseのGetResponseStreamがSystem.Net.GZipWrapperStreamであるか調べます。

    Public Overrides Sub ProcessMessage(ByVal message As SoapMessage)
        Select Case message.Stage
            Case SoapMessageStage.BeforeSerialize
                With DirectCast(message, SoapClientMessage)
                    Dim req As System.Net.HttpWebRequest =
                        DirectCast(
                                .Client.GetType.InvokeMember("PendingSyncRequest",
                                    Reflection.BindingFlags.GetProperty Or
                                    Reflection.BindingFlags.Instance Or
                                    Reflection.BindingFlags.NonPublic, Nothing, .Client, Nothing), 
                            System.Net.HttpWebRequest)
                    req.AutomaticDecompression = Net.DecompressionMethods.GZip
                End With
            Case SoapMessageStage.BeforeDeserialize
                With DirectCast(message, SoapClientMessage)
                    If .Client.GetType.Module.Equals(Me.GetType.Module) Then
                        Dim req As System.Net.HttpWebRequest =
                            DirectCast(
                                    .Client.GetType.InvokeMember("PendingSyncRequest",
                                        Reflection.BindingFlags.GetProperty Or
                                        Reflection.BindingFlags.Instance Or
                                        Reflection.BindingFlags.NonPublic, Nothing, .Client, Nothing), 
                                System.Net.HttpWebRequest)
                        Dim res As System.Net.HttpWebResponse =
                            DirectCast(
                                    req.GetType.InvokeMember("_HttpResponse",
                                        Reflection.BindingFlags.GetField Or
                                        Reflection.BindingFlags.Instance Or
                                        Reflection.BindingFlags.NonPublic, Nothing, req, Nothing), 
                                System.Net.HttpWebResponse)
                        If Not res.GetResponseStream.GetType.ToString.Equals("System.Net.GZipWrapperStream") Then
                            Throw New SystemException("http圧縮が有効化されていません。")
                        End If
                    End If
                End With
        End Select
    End Sub


ちょっと解説すると、HTTP上は"Content-Encoding: gzip"となっているのに、HttpWebResponseのレスポンスヘッダ要素はそうならかったのでGetResponseStreamで判定しています。


SQL ServerのReportingServices帳票を実装している場合*3、クライアントのコントロールがReportingServicesとの認証などにSOAPを利用していて、このSoapExtensionに引っかかってしまいます。

ReportingServicesのHTTPサービスは圧縮に対応していません。

だから、自分のモジュール(ユーザプログラムモジュール)以外(Microsoft.ReportViewer.WinForms.dllとか)からの呼び出し時は、チェックをスルーしています。


あと、SoapMessageStage.BeforeSerializeで、リフレクションしてreq.AutomaticDecompression = Net.DecompressionMethods.GZipなんてやってるのは、DirectCast(message, SoapClientMessage).EnableDecompression = Trueだと呼び出すWebサービスによって、レスポンスヘッダに"Accept-Encoding: gzip"が付加されないケースがあったからです。

SoapExtensionのProcessMessageのコンテキストでは、SoapClientMessageはきっと特異な状態にあるのだと思います。


IISについて

"静的コンテンツ圧縮"と"動的なコンテンツ圧縮"が有効になっている必要があります。

Windows 2008 Serverのデフォは有効でしたが、Windows 7IISはデフォで無効でした。



Work! Enjoy it!

*1:そんなインフラでXML Web サービスを選択したアーキテクトの方がどうかしてると思うんだけど。。

*2:クラサバをスマートクライアントに移植したら極端に遅くなったというケースがあります。

*3:Microsoft.Reporting.WinForms.ReportViewerコントロールの利用など。