私は楽しみのために .NET XMPP ライブラリを作成していますが、他の場所で説明されているように、.NET 4.5 より前のバージョンの実装は、内部 4KB バッファーがいっぱいになるか EOF に達するまで解析を開始しないXmlReader
ため、XML からの解析には適していませんでした。 NetworkStream
.
他のライブラリは、まったく使用XmlReader
しないことでこれを回避しました。以前にリンクされた質問で述べたように、jabber-net は Java XML パーサーのポートを使用します。検索中に見つけた実装である Babel IM は、独自の単純な XML パーサーを使用しています。agsXMPP が何をするのかわかりません。
ただし、.NET 4.5 のリリースと新しい非同期機能がXmlReader
明らかにアップグレードされ、真の非同期解析を実行できるようになりました。このように、サーバーに接続してメッセージを送受信できるかなり単純な XMPP クライアントをハックするためにそれを使用しました。
ただし、問題は実際にはサーバーからの切断にあるようです。Dispose()
切断すると、通常はXmlReader
インスタンスとその下にあるストリームを削除したいだけです。ただし、Dispose()
実際にはInvalidOperationException
「非同期操作が既に進行中です」というメッセージとともにがスローされます。非同期のときにそれを呼び出すと...メッセージの内容。ただし、XMPP の性質上XmlReader
、サーバーからの XML スタンザがパイプを通過するのを待つため、基本的に常に非同期操作を実行しています。
保留中の非同期操作をキャンセルして、きれいに処理できるようにするために使用できるメソッドはありません。単に処分しようとしないよりも、この状況に対処するためのより良い方法はありますか? XMPP仕様では、サーバーが切断時に終了タグを送信することになっていると述べています。これをシグナルとして使用して、別の非同期読み取りを実行しようとしないようにすることもできますが、これは保証されません。XmlReader
Dispose()
XmlReader
</stream:stream>
以下は、再生するサンプル コードです。基本的に、EOF に到達せず、少なくとも 1 バイトが読み取れるまでブロックするという点でLongLivedTextStream
オープンをエミュレートします。NetworkStream
が喜んで解析する XML テキストを「注入」できますがXmlReader
、リーダーを破棄しようとすると、前述の例外がトリガーされます。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
namespace Example
{
class LongLivedTextStream : Stream
{
ManualResetEvent moarDatas = new ManualResetEvent(false);
List<byte> data = new List<byte>();
int pos = 0;
public void Inject(string text)
{
data.AddRange(new UTF8Encoding(false).GetBytes(text));
moarDatas.Set();
}
public override int Read(byte[] buffer, int offset, int count)
{
var bytes = GetBytes(count).ToArray();
for (int i = 0; offset + i < buffer.Length && i < bytes.Length; i++)
{
buffer[offset + i] = bytes[i];
}
return bytes.Length;
}
private IEnumerable<byte> GetBytes(int count)
{
int returned = 0;
while (returned == 0)
{
if (pos < data.Count)
{
while (pos < data.Count && returned < count)
{
yield return data[pos];
pos += 1; returned += 1;
}
}
else
{
moarDatas.Reset();
moarDatas.WaitOne();
}
}
}
#region Other Stream Members
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override void Flush() { }
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
#endregion
}
public class Program
{
public static void Main(string[] args)
{
Test();
Console.ReadLine();
}
public static async void Test()
{
var stream = new LongLivedTextStream();
var reader = XmlReader.Create(stream, new XmlReaderSettings() { Async = true });
var t = Task.Run(() =>
{
stream.Inject("<root>");
Thread.Sleep(2000);
stream.Inject("value");
Thread.Sleep(2000);
stream.Inject("</root>");
Thread.Sleep(2000);
reader.Dispose(); // InvalidOperationException: "An asynchronous operation is already in progress."
Console.WriteLine("Disposed");
});
while (await reader.ReadAsync())
{
bool kill = false;
switch (reader.NodeType)
{
case XmlNodeType.Element:
Console.WriteLine("Start: " + reader.LocalName);
break;
case XmlNodeType.EndElement:
Console.WriteLine("End: " + reader.LocalName);
//kill = true; // I could use a particular EndElement as a signal to not try another read
break;
case XmlNodeType.Text:
Console.WriteLine("Text: " + await reader.GetValueAsync());
break;
}
if (kill) { break; }
}
}
}
}
編集
この例では、actualNetworkStream
を使用しており、IClose()
またはDispose()
基になるストリームのReadAsync()
呼び出しがXmlReader
期待どおりに false を返さない場合、代わりにブロックし続けることを示しています。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
namespace Example
{
public class Program
{
public static void Main(string[] args)
{
NetworkStream stream = null;
var endpoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 50000);
var serverIsUp = new ManualResetEvent(false);
var doneWriting = new ManualResetEvent(false);
var t1 = Task.Run(() =>
{
var server = new TcpListener(endpoint);
server.Start();
serverIsUp.Set();
var client = server.AcceptTcpClient();
var writer = new StreamWriter(client.GetStream());
writer.Write("<root>"); writer.Flush();
Thread.Sleep(2000);
writer.Write("value"); writer.Flush();
Thread.Sleep(2000);
writer.Write("</root>"); writer.Flush();
Thread.Sleep(2000);
doneWriting.Set();
});
var t2 = Task.Run(() =>
{
doneWriting.WaitOne();
stream.Dispose();
Console.WriteLine("Disposed of Stream");
});
var t3 = Task.Run(async () =>
{
serverIsUp.WaitOne();
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
socket.Connect(endpoint);
stream = new NetworkStream(socket, true);
var reader = XmlReader.Create(stream, new XmlReaderSettings() { Async = true });
bool val;
while (val = await reader.ReadAsync())
{
bool kill = false;
switch (reader.NodeType)
{
case XmlNodeType.Element:
Console.WriteLine("Start: " + reader.LocalName);
break;
case XmlNodeType.EndElement:
Console.WriteLine("End: " + reader.LocalName);
//kill = true; // I could use a particular EndElement as a signal to not try another read
break;
case XmlNodeType.Text:
Console.WriteLine("Text: " + await reader.GetValueAsync());
break;
}
if (kill) { break; }
}
// Ideally once the underlying stream is closed, ReadAsync() would return false
// we would get here and could safely dispose the reader, but that's not the case
// ReadAsync() continues to block
reader.Dispose();
Console.WriteLine("Disposed of Reader");
});
Console.ReadLine();
}
}
}