BinaryReader.ReadChars()の問題
-
05-07-2019 - |
質問
BinaryReader.ReadChars()メソッドの問題だと思うことに遭遇しました。生のソケットNetworkStreamの周りにBinaryReaderをラップすると、ストリームの破損が発生し、読み取り中のストリームが同期しなくなることがあります。問題のストリームには、バイナリシリアル化プロトコルのメッセージが含まれています。
これを次のように追跡しました
- Unicode文字列(Encoding.BigEndianを使用してエンコードされた)を読み取るときにのみ発生します
- 問題の文字列が2つのtcpパケットに分割されている場合にのみ発生します(wiresharkを使用して確認)
何が起こっているのかと思います(以下の例のコンテキストで)
- BinaryReader.ReadChars()が呼び出され、3文字を読み取るように要求されます(文字列の長さは文字列自体の前にエンコードされます)
- 最初のループは、ネットワークストリームから6バイト(残り3文字* 2バイト/文字)の読み取りを内部的に要求します
- ネットワークストリームには3バイトしか使用できません
- ローカルバッファに読み込まれた3バイト
- デコーダに渡されるバッファ
- デコーダーは1文字をデコードし、他のバイトを独自の内部バッファーに保持します
- 2番目のループは内部で4バイトの読み取りを要求します! (残り2文字* 2バイト/文字)
- ネットワークストリームには4バイトすべてが使用可能です
- ローカルバッファに読み込まれた4バイト
- デコーダに渡されるバッファ
- デコーダーは2文字をデコードし、残りの4番目のバイトを内部的に保持します
- 文字列のデコードが完了
-
シリアル化コードは、ストリームの破損のために次のアイテムの非整列化とクロークを試みます。
char[] buffer = new char[3]; int charIndex = 0; Decoder decoder = Encoding.BigEndianUnicode.GetDecoder(); // pretend 3 of the 6 bytes arrives in one packet byte[] b1 = new byte[] { 0, 83, 0 }; int charsRead = decoder.GetChars(b1, 0, 3, buffer, charIndex); charIndex += charsRead; // pretend the remaining 3 bytes plus a final byte, for something unrelated, // arrive next byte[] b2 = new byte[] { 71, 0, 114, 3 }; charsRead = decoder.GetChars(b2, 0, 4, buffer, charIndex); charIndex += charsRead;
ルートは.NETコードのバグで、charsRemaining * bytes / char各ループを使用して、必要な残りのバイトを計算するバグだと思います。 Decoderに隠された余分なバイトがあるため、この計算は1つずれることがあり、入力ストリームから余分なバイトが消費されます。
問題の.NET Frameworkコードは次のとおりです
while (charsRemaining>0) {
// We really want to know what the minimum number of bytes per char
// is for our encoding. Otherwise for UnicodeEncoding we'd have to
// do ~1+log(n) reads to read n characters.
numBytes = charsRemaining;
if (m_2BytesPerChar)
numBytes <<= 1;
numBytes = m_stream.Read(m_charBytes, 0, numBytes);
if (numBytes==0) {
return (count - charsRemaining);
}
charsRead = m_decoder.GetChars(m_charBytes, 0, numBytes, buffer, index);
charsRemaining -= charsRead;
index+=charsRead;
}
これがバグなのか、APIの誤用なのかは完全にはわかりません。この問題を回避するには、自分で必要なバイトを計算し、それらを読み取ってから、関連するEncoding.GetString()でbyte []を実行するだけです。ただし、UTF-8などでは機能しません。
これに関する人々の考えや、私が何か間違ったことをしているかどうかを聞いてください。そして、多分それは次の人を退屈なデバッグの数時間/日節約するでしょう。
編集:追跡アイテムに接続
解決
BinaryReader.ReadChars
で言及した問題を再現しました。
開発者は、ストリームやデコーダーなどを作成する際に常に先読みを考慮する必要がありますが、これは BinaryReader
のかなり重大なバグのようです。データ。この場合、 ReadChars
は、そのバイトを失うことを避けるために、読み取り内容をより保守的にすべきであることに同意します。
ReadChars
が舞台裏で行うことをすべて行った後、 Decoder
を直接使用する回避策に問題はありません。
Unicodeは単純なケースです。任意のエンコーディングについて考える場合、バイト数ではなく文字数を渡すときに正しいバイト数が消費されることを保証する一般的な目的の方法は本当にありません(長さの異なる文字や不正な入力を含む場合を考えてください)。このため、特定のバイト数の読み取りを優先して BinaryReader.ReadChars
を回避すると、より堅牢で一般的なソリューションが提供されます。
http://connect.microsoft.com/visualstudio <を使用して、Microsoftの注意を喚起することをお勧めします。 / a>。
他のヒント
興味深い;これは「接続」で報告できます。一時的なギャップとして、 BufferredStream
ですが、これは亀裂を突破することを期待しています(まだ発生する可能性はありますが、頻度は低くなります)。
もちろん、もう1つのアプローチは、メッセージ全体(ストリーム全体ではなく)を事前にバッファリングすることです。次に、 MemoryStream
のようなものから読み取ります-ネットワークプロトコルが 論理的な(理想的には長さのプレフィックスがあり、大きすぎない)メッセージを想定しています。その後、デコードすると、すべてのデータが利用可能になります。
これは、私自身の質問の1つを思い出させます( HttpResponseStreamからの読み取りに失敗しました) HTTP応答ストリームから読み取るときにStreamReaderがストリームの終わりに早まってしまったと思うため、パーサーが予期せず爆撃するという問題がありました。
Like Marcがあなたの問題に対して提案しました。最初に MemoryStream
でプリバッファリングを試みましたが、大きなファイルを読み込む場合は(特にネットワーク/ウェブ)を使用して、有用な操作を行うことができます。最終的に、Readメソッドをオーバーライドし、ReadBlockメソッドを使用してそれらを定義するTextReaderの独自の拡張機能を作成することに決めました(ブロック読み取りを行います。つまり、要求する文字数を正確に取得できるまで待機します)
たとえば、 BinaryReader.Read
のドキュメントを見ると、Readメソッドが要求した文字数を返すように保証されていないという事実が原因である可能性があります。 ( http://msdn.microsoft.com/en-us/library/ms143295 .aspx )メソッドが表示されます:
戻り値
タイプ:System .. ::。Int32
バッファに読み込まれた文字数。これは、そのバイト数が利用できない場合は要求されたバイト数よりも少ない場合があり、ストリームの終わりに達した場合はゼロになる場合があります。
BinaryReaderにはTextReaderのようなReadBlockメソッドがないので、自分で自分の位置を監視したり、Marcの事前キャッシュを行ったりするだけです。
Unity3D / Mono atmを使用していますが、ReadCharsメソッドにはさらにエラーが含まれている場合があります。このような文字列を作成しました:
mat.name = new string(binaryReader.ReadChars(64));
mat.name
には正しい文字列さえ含まれていましたが、文字列を前に追加するだけでした。文字列の後はすべて消えました。 String.Formatでも。これまでの私の解決策は、ReadCharsメソッドを使用せず、データをバイト配列として読み取り、文字列に変換することです。
byte[] str = binaryReader.ReadBytes(64);
int lengthOfStr = Array.IndexOf(str, (byte)0); // e.g. 4 for "clip\0"
mat.name = System.Text.ASCIIEncoding.Default.GetString(str, 0, lengthOfStr);