StreamReaderのReadLine/ReadLineAsyncメソッド、まぁ便利です。
改行コードがLF/CR/CR+LFどれであっても、いい感じに読み込んでくれます。
が、…このメソッド、CRが来るだけでは処理を返してくれません!!!!
CRを読み込んだ後、他の文字が来るか、もしくはもうEOFだということがわかるまで、なんと処理を返さず待機するのです!
なんでやねん!CR読み込んだ時点で1行確定でしょ!!すぐに返してよ!!!!
この動作、テキストファイル等、すでにすべてのデータが揃っているストリームに対しては問題無いですね。
ですが、例えばネットワークなどでCRを区切りとしている系のAPIだと、応答が返ってきているはずなのに何も来ない…とか、1回分遅れて応答が帰ってくるように見える…ということが発生します。
CRの次の文字が来るまで返さないので、CRが来ただけでは、最新の1行分の応答がStreamReaderにバッファされたままになるのです。
先日、PJLinkのC#ラッパーを書いていてこの問題にぶち当たりました。
PJLinkとは、プロジェクタ等をLAN経由で操作するための標準仕様ですが、コマンドや応答の終端をCRとすることになっているのです。
つまり、
→コマンド1を送る
(←コマンド1の応答が返ってくるけどStreamReaderにバッファされるだけ)
→コマンド2を送る
←コマンド1の応答がやっと返される(そしてコマンド2の応答はStreamReaderにバッファされる)
みたいな。
しかたがないので、改行コードがCRな1行を読み込んだら、すぐに返すクラスを書いてみた。
長いけどこんなん。
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace ShComp
{
/// <summary>
/// 改行コードがCRの場合に、1行を読み込んですぐに応答を返すメソッドを提供します。
/// </summary>
class CRLineReader : IDisposable
{
private StreamReader _reader;
private bool _isEof;
private int _bufferStart;
private int _bufferEnd;
private char[] _buffer;
public CRLineReader(Stream stream, int capacity = 136)
{
_reader = new StreamReader(stream);
_buffer = new char[capacity];
}
/// <summary>
/// CR区切りの1行を非同期に読み込みます。CR以外の改行コードは、改行コードとみなしません。
/// </summary>
public async Task<string> ReadLineAsync()
{
if (_isEof) return null;
var sb = new StringBuilder();
if (_bufferEnd > _bufferStart)
{
if (AppendFromBuffer(sb))
{
return sb.ToString();
}
}
_bufferStart = 0;
while (true)
{
_bufferEnd = await _reader.ReadAsync(_buffer, 0, _buffer.Length);
if (_bufferEnd == 0)
{
_isEof = true;
return sb.ToString();
}
if (AppendFromBuffer(sb))
{
return sb.ToString();
}
}
}
private bool AppendFromBuffer(StringBuilder sb)
{
for (int i = _bufferStart; i < _bufferEnd; i++)
{
if (_buffer[i] == 'r')
{
sb.Append(_buffer, _bufferStart, i - _bufferStart);
_bufferStart = i + 1;
return true;
}
}
sb.Append(_buffer, _bufferStart, _bufferEnd - _bufferStart);
return false;
}
public void Dispose()
{
_reader.Dispose();
}
}
}
バッファサイズが136なのは、PJLinkの応答の最大長が136だから。
PJLink、ASCIIな文字しか使わないから、StreamReader介さず直接Streamから読み込んでもいい気がするけど、まぁ。