目次
ゴール
- SOAP通信をローカルで実行できるようにする
- サーバー・クライアントどちらもデバッグできるようにする
- ログ出力する
開発環境等
- VisualStudio2022
- C#
- .netflamework
- wsdl
サーバーサイド
プロジェクトの作成(Server)
-
プロジェクトテンプレートの選択 ASP.NETWeb アプリケーション
※出てこない場合はインストーラーでAsp.NetとWeb開発 .netflamework プロジェクトと項目テンプレートをインストール
Webサービスの作成
-
asmxの追加
以下のようなコードが生成される [WebMethod]属性をつけると外部に公開されクライアントから呼び出すことが可能になる。public class WebService1 : System.Web.Services.WebService { [WebMethod] public string HelloWorld() { return "Hello World"; } }
-
HelloWorldクリック → 起動ボタンクリック とするとレスポンスを確認できるメソッドの定義通りに「HelloWorld」が返却されている。
<string xmlns="http://tempuri.org/">Hello World</string>
-
ログ出力
以下のメソッドをasmx.csに追加しHelloWorldメソッド内で呼び出すとログがテキスト形式で出力される。WebServiceを継承しているためContextプロパティからRequestを取得することができる。void LogRequest() { var req = Context.Request; string basePath = Context.Request.PhysicalApplicationPath; string filename = basePath + "HelloWorld.txt"; using (StreamWriter sw = new StreamWriter(filename, true)) { foreach (var key in req.Headers.AllKeys) { sw.WriteLine(key + " : " + req.Headers[key]); } var bodyStream = new StreamReader(req.InputStream); bodyStream.BaseStream.Seek(0, SeekOrigin.Begin); sw.WriteLine(bodyStream.ReadToEnd()); } }
サーバーの例
この後クライアントサイドの実装に移っていくがその際に以下のサーバーを使用する。引数なしだと寂しいので引数があるメソッドを定義した。
using System.IO;
using System.Web.Services;
namespace WebApplication1
{
/// <summary>
/// WebService1 の概要の説明です
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
// この Web サービスを、スクリプトから ASP.NET AJAX を使用して呼び出せるようにするには、次の行のコメントを解除します。
// [System.Web.Script.Services.ScriptService]
public class WebService1 : System.Web.Services.WebService
{
[WebMethod]
public string GetKSyntax(int ageNow, int yearsLater)
{
LogRequest();
return $"{yearsLater}年後の自分は何歳かなと考えると{ageNow + yearsLater}歳なんですね";
}
void LogRequest()
{
var req = Context.Request;
string basePath = Context.Request.PhysicalApplicationPath;
string filename = basePath + "GetKSyntax.txt";
using (StreamWriter sw = new StreamWriter(filename, true))
{
foreach (var key in req.Headers.AllKeys)
{
sw.WriteLine(key + " : " + req.Headers[key]);
}
var bodyStream = new StreamReader(req.InputStream);
bodyStream.BaseStream.Seek(0, SeekOrigin.Begin);
sw.WriteLine(bodyStream.ReadToEnd());
}
}
}
}
クライアントサイド
プロジェクトの作成(Client)
- 何でもよい。今回はコンソールアプリケーションを使用する。
ProxyCodeの作成
-
サーバーのwsdlを取得する。Webサービスの作成の2の手順の際にブラウザのURLの末尾に「?wsdl」と入力する。以下のようにwsdlを取得できる。「ctrl + s」でファイル名.wsdlの形式で保存する。
-
1で保存したファイルを作成したプロジェクトフォルダ配下へ移動。Developer PowerShellもしくは開発者コマンドプロンプトで以下のコマンドを実行
---- ------------- ------ ---- d----- 2022/07/26 15:47 bin d----- 2022/07/26 15:47 obj d----- 2022/07/26 15:47 Properties -a---- 2022/07/26 15:47 187 App.config -a---- 2022/07/26 15:47 260 Program.cs -a---- 2022/07/26 15:04 3098 WebService1.wsdl PS C:\Users\hogehoge\Desktop\SOAPTestingDocWork\ConsoleApp1\consoleapp1> wsdl.exe Webservice1.wsdl
以下のようなコードが生成されるのでプロジェクトに追加する(一部抜粋)
/// <remarks/> [System.CodeDom.Compiler.GeneratedCodeAttribute("wsdl", "4.8.3928.0")] [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute(Name="WebService1Soap", Namespace="http://tempuri.org/")] public partial class WebService1 : System.Web.Services.Protocols.SoapHttpClientProtocol { private System.Threading.SendOrPostCallback GetKSyntaxOperationCompleted; /// <remarks/> public WebService1() { this.Url = "https://localhost:44345/WebService1.asmx"; } /// <remarks/> public event GetKSyntaxCompletedEventHandler GetKSyntaxCompleted; /// <remarks/> [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tempuri.org/GetKSyntax", RequestNamespace="http://tempuri.org/", ResponseNamespace="http://tempuri.org/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] public string GetKSyntax(int ageNow, int yearsLater) { object[] results = this.Invoke("GetKSyntax", new object[] { ageNow, yearsLater}); return ((string)(results[0])); }
リクエストを送ってみる
ProxyCodeの作成で生成されたクラスをインスタンス化しメソッドを呼ぶだけでよい。
var service = new WebService1();
Console.WriteLine(service.GetKSyntax(20,30));
// 30年後の自分は何歳かなと考えると50歳なんですねと表示される
サーバーサイドで取得したログ
Connection : Keep-Alive
Content-Length : 345
Content-Type : text/xml; charset=utf-8
Expect : 100-continue
Host : localhost:44345
User-Agent : Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol 4.0.30319.42000)
SOAPAction : "http://tempuri.org/GetKSyntax"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<GetKSyntax xmlns="http://tempuri.org/">
<ageNow>20</ageNow>
<yearsLater>30</yearsLater>
</GetKSyntax>
</soap:Body>
</soap:Envelope>
これでクライアントとサーバーが実装できたのでいくらでもSOAP通信を試すことができるようになった。
例外処理
ProxyCodeを用いたSOAP通信では以下の3つの例外が生じます。 SoapHttpClientProtocol.Invoke(String, Object[]) メソッド
- SoapException
- InvalidOperationException
- WebException
ここではSoapExceptionを主に取り扱います。
SoapException
SOAPにおいて何らかのエラーが発生した際にSoapFalutが返却されることがあります。SoapFaultはの4つの要素を持つxmlです。(詳細)
ProxyCodeを用いるとライブラリ内でxmlが解析され、そこからSoapExceptionが生成されまる。サーバーでSoapExceptionを発生させ、クライアントでその内容を取得してみる。
-
サーバーサイドに引数をチェックし例外を投げるコードを追加、GetKSyntaxメソッドで呼び出されるようにする。
private void CheckParameter(int ageNow, int yearsLater) { if (ageNow >= 0 && yearsLater >= 0) { return; } //soapfaultを投げる XmlDocument doc = new XmlDocument(); XmlNode detailNode = doc.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace); XmlNode errtxt = doc.CreateNode(XmlNodeType.Text, String.Empty, null); errtxt.Value = "パラメータはどちらも正の整数である必要があります"; detailNode.AppendChild(errtxt); throw new SoapException("引数が不正です", SoapException.ClientFaultCode, Context.Request.Url.AbsoluteUri, detailNode); }
-
クライアントサイドでSOAPFaultの内容を取得できるようにする。
static void Main(string[] args) { try { var service = new WebService1(); // 負の値をパラメータにセット Console.WriteLine(service.GetKSyntax(-1, 30)); } catch (SoapException ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.Code.Name); Console.WriteLine(ex.Actor); Console.WriteLine(ex.Detail.OuterXml); } catch (Exception ex) { Console.WriteLine(ex); } }
-
実行結果
SOAPFaultの4要素を取得できた。SOAPExceptionの仕様上detailはxml形式となるのでサーバー側の実装に合わせてパースするとなおよい。(ここでは割愛)System.Web.Services.Protocols.SoapException: メソッド呼び出しが不正です Client https://localhost:44345/WebService1.asmx <detail>パラメータはどちらも正の整数である必要があります</detail>
EX 悩んだことなど
ここではSOAPの環境を作成する中でつまずいた点およびその解決法を記載する。
サーバーが立ち上がらない
-
- 原因
サーバー側のプロジェクトにクラスファイルを追加し、それを参照するコードを書いた場合に生じる。 - 解決法(どちらか)
-
Asp.netフォルダの追加から「App_Code」フォルダを追加。その中にクラスファイル を移動する。namespaceもフォルダ構成に合わせる。
-
Web.configに「directoryBrowse enabled="true"」を追加する
<configuration> <system.webServer> <directoryBrowse enabled="true" /> </system.webServer> </configuration>
-
- 原因
Responseのステータスコード、Bodyを取得したい
上で記載した方法ではhttp通信のステータスコードを取得できない。そもそも生のレスポンスを取得することができない。サーバーの仕様を完全に把握できるのならば問題ないが、そうでない場合はどのようなレスポンスが帰ってきているのかを確認したい場合がある。
方針1 ProxyCodeを使わずに実装する
SoapHttpClientProtocolのInvokeメソッドの実装を見るとhttp通信を行っていることが分かる。ProxyCodeを用いて送信しているリクエストと同様のものを送ればサーバーは認識してくれる。
-
ProxyCodeなしSOAP通信クライアント
コード
static void Main(string[] args) { string soapBody = @"<?xml version=""1.0"" encoding=""utf-8""?> <soap:Envelope xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema""> <soap:Body> <GetKSyntax xmlns=""http://tempuri.org/""> <ageNow>45</ageNow> <yearsLater>30</yearsLater> </GetKSyntax> </soap:Body> </soap:Envelope>"; try { //バイト型配列に変換 byte[] postDataBytes = Encoding.UTF8.GetBytes(soapBody); //WebRequestの作成 WebRequest req = WebRequest.Create("https://localhost:44345/WebService1.asmx"); req.Method = "POST"; req.ContentType = "text/xml; charset=utf-8"; req.ContentLength = postDataBytes.Length; // JX手順で実行するメソッドをここで指定 req.Headers.Add("SOAPAction", @"http://tempuri.org/GetKSyntax"); // 送信データを書き込み Stream reqStream = req.GetRequestStream(); reqStream.Write(postDataBytes, 0, postDataBytes.Length); reqStream.Close(); //サーバーからの応答を受信するためのWebResponseを取得 WebResponse res = req.GetResponse(); //応答データを受信するためのStreamを取得 Stream resStream = res.GetResponseStream(); // ステータスコード Console.WriteLine(((HttpWebResponse)res).StatusDescription); Console.WriteLine(((HttpWebResponse)res).StatusCode); // 受信して表示 using (StreamReader sr = new System.IO.StreamReader(resStream, Encoding.UTF8)) { Console.WriteLine(sr.ReadToEnd()); } } catch (WebException ex) { // ProtocolErrorの場合原因を表示 if (ex.Status == System.Net.WebExceptionStatus.ProtocolError) { WebResponse errResp = ex.Response; using (Stream respStream = errResp.GetResponseStream()) { StreamReader reader = new StreamReader(respStream); string text = reader.ReadToEnd(); Console.WriteLine(text); } } else Console.WriteLine(ex); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
-
実行結果
statuscode : OK <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetKSyntaxResponse xmlns="http://tempuri.org/"> <GetKSyntaxResult>30年後の自分は何歳かなと考えると75歳なんですね</GetKSyntaxResult> </GetKSyntaxResponse> </soap:Body> </soap:Envelope>
-
問題点
- xmlパーサーを自分で書く必要がある。
- SOAPExceptionがでないのでxmlそれ用のxmlパーサーを書く必要がある。
- とにかくクライアント側で書くコードの量が増える
方針2 TraceSourceを利用する
System.netにはあらかじめロギングが用意されているためそれを利用する。ネットワークのトレースを行う
- App.config に以下を追加
<system.diagnostics>
<sources>
<source name="System.Net.Sockets" switchName="NetSwitch"/>
<source name="System.Net" switchName="NetSwitch"/>
<source name="System.Net.HttpListener" switchName="NetSwitch"/>
<source name="System.Net.Cache" switchName="NetSwitch"/>
</sources>
<switches>
<add name="NetSwitch" value="Verbose"/>
</switches>
</system.diagnostics>
- 取得できるログ(必要な個所のみ抜粋)
長いので折り畳み
System.Net Information: 0 : [21940] Connection#26239245 - 受信された状態行: Version=1.1、StatusCode=200、StatusDescription=OK
System.Net Verbose: 0 : [31664] 00000000 : 3C 3F 78 6D 6C 20 76 65-72 73 69 6F 6E 3D 22 31 : <?xml version="1
System.Net Verbose: 0 : [31664] 00000010 : 2E 30 22 20 65 6E 63 6F-64 69 6E 67 3D 22 75 74 : .0" encoding="ut
System.Net Verbose: 0 : [31664] 00000020 : 66 2D 38 22 3F 3E 3C 73-6F 61 70 3A 45 6E 76 65 : f-8"?><soap:Enve
System.Net Verbose: 0 : [31664] 00000030 : 6C 6F 70 65 20 78 6D 6C-6E 73 3A 73 6F 61 70 3D : lope xmlns:soap=
System.Net Verbose: 0 : [31664] 00000040 : 22 68 74 74 70 3A 2F 2F-73 63 68 65 6D 61 73 2E : "http://schemas.
System.Net Verbose: 0 : [31664] 00000050 : 78 6D 6C 73 6F 61 70 2E-6F 72 67 2F 73 6F 61 70 : xmlsoap.org/soap
System.Net Verbose: 0 : [31664] 00000060 : 2F 65 6E 76 65 6C 6F 70-65 2F 22 20 78 6D 6C 6E : /envelope/" xmln
System.Net Verbose: 0 : [31664] 00000070 : 73 3A 78 73 69 3D 22 68-74 74 70 3A 2F 2F 77 77 : s:xsi="http://ww
System.Net Verbose: 0 : [31664] 00000080 : 77 2E 77 33 2E 6F 72 67-2F 32 30 30 31 2F 58 4D : w.w3.org/2001/XM
System.Net Verbose: 0 : [31664] 00000090 : 4C 53 63 68 65 6D 61 2D-69 6E 73 74 61 6E 63 65 : LSchema-instance
System.Net Verbose: 0 : [31664] 000000A0 : 22 20 78 6D 6C 6E 73 3A-78 73 64 3D 22 68 74 74 : " xmlns:xsd="htt
System.Net Verbose: 0 : [31664] 000000B0 : 70 3A 2F 2F 77 77 77 2E-77 33 2E 6F 72 67 2F 32 : p://www.w3.org/2
System.Net Verbose: 0 : [31664] 000000C0 : 30 30 31 2F 58 4D 4C 53-63 68 65 6D 61 22 3E 3C : 001/XMLSchema"><
System.Net Verbose: 0 : [31664] 000000D0 : 73 6F 61 70 3A 42 6F 64-79 3E 3C 47 65 74 4B 53 : soap:Body><GetKS
System.Net Verbose: 0 : [31664] 000000E0 : 79 6E 74 61 78 52 65 73-70 6F 6E 73 65 20 78 6D : yntaxResponse xm
System.Net Verbose: 0 : [31664] 000000F0 : 6C 6E 73 3D 22 68 74 74-70 3A 2F 2F 74 65 6D 70 : lns="http://temp
System.Net Verbose: 0 : [31664] 00000100 : 75 72 69 2E 6F 72 67 2F-22 3E 3C 47 65 74 4B 53 : uri.org/"><GetKS
System.Net Verbose: 0 : [31664] 00000110 : 79 6E 74 61 78 52 65 73-75 6C 74 3E 33 30 E5 B9 : yntaxResult>30..
System.Net Verbose: 0 : [31664] 00000120 : B4 E5 BE 8C E3 81 AE E8-87 AA E5 88 86 E3 81 AF : ................
System.Net Verbose: 0 : [31664] 00000130 : E4 BD 95 E6 AD B3 E3 81-8B E3 81 AA E3 81 A8 E8 : ................
System.Net Verbose: 0 : [31664] 00000140 : 80 83 E3 81 88 E3 82 8B-E3 81 A8 37 35 E6 AD B3 : ...........75...
System.Net Verbose: 0 : [31664] 00000150 : E3 81 AA E3 82 93 E3 81-A7 E3 81 99 E3 81 AD 3C : ...............<
System.Net Verbose: 0 : [31664] 00000160 : 2F 47 65 74 4B 53 79 6E-74 61 78 52 65 73 75 6C : /GetKSyntaxResul
System.Net Verbose: 0 : [31664] 00000170 : 74 3E 3C 2F 47 65 74 4B-53 79 6E 74 61 78 52 65 : t></GetKSyntaxRe
System.Net Verbose: 0 : [31664] 00000180 : 73 70 6F 6E 73 65 3E 3C-2F 73 6F 61 70 3A 42 6F : sponse></soap:Bo
System.Net Verbose: 0 : [31664] 00000190 : 64 79 3E 3C 2F 73 6F 61-70 3A 45 6E 76 65 6C 6F : dy></soap:Envelo
System.Net Verbose: 0 : [31664] 000001A0 : 70 65 3E : pe>
マルチバイト文字が表示されていないがデコードをすればよい。ProxyCodeを用いる以上基本的には例外情報から問題を特定することになるが、それでも特定できなかった場合の保険としてなので多少解読に手間がかかってしまうログであってもそこまで問題にならない。