我們在編寫網絡程序的時候,經常會進行如下操作:
-
申請一個緩沖區
-
從數據源中讀入數據至緩沖區
-
解析緩沖區的數據
-
重復第2步
表面上看來這是一個很常規而簡單的操作,但實際使用過程中往往存在如下痛點:
可能不能在一次read操作中讀入所有需要的數據,因此需要在緩沖區中維護一個游標,記錄下次讀取操作的起始位置,這個游標帶了了不小的復雜度:
由於緩沖區有限,可能申請的緩沖區不夠用,需要引入動態緩沖區。這也大幅加大了代碼的復雜度。
我們的業務本身只關心使用操作,但讀和用操作沒有分離,復雜的都操作導致用操作也變得復雜,並且嚴重干擾業務邏輯。
今天介紹微軟新推出的一個庫:System.IO.Pipelines(需要在Nuget上安裝),用於解決這些痛點。它主要包含一個Pipe對象,它有一個Writer屬性和Reader屬性。
var pipe = new Pipe();
var writer = pipe.Writer;
var reader = pipe.Reader;
Writer對象用於從數據源讀取數據,將數據寫入管道中;它對應業務中的"讀"操作。
var content = Encoding.Default.GetBytes("hello world");
var data = new Memory<byte>(content);
var result = await writer.WriteAsync(data);
var buffer = writer.GetMemory(512);
content.CopyTo(buffer);
writer.Advance(content.Length);
var result = await writer.FlushAsync();
Reader對象用於從管道中獲取數據源,它對應業務中的"用"操作。
var result = await reader.ReadAsync();
var buffer = result.Buffer;
這個Buffer是一個ReadOnlySequence<byte>對象,它是一個相當好的動態內存對象,並且相當高效。它本身由多段Memory<byte>組成,查看Memory段的方法有:
它從邏輯上也可以看成一段連續的Memory<byte>,也有類似的方法:
另外,它還有一個類似游標的位置對象SequencePosition,可以從其Position相關函數中使用,這里就不多介紹了。
這個緩沖區解決了"數據讀不夠"的問題,一次讀取的不夠下次可以接着讀,不用緩沖區的動態分配,高效的內存管理方式帶來了良好的性能,好用的接口是我們能更關注業務。
使用完后,告訴PIPE當前使用了多少數據,下次接着從結束位置后讀起
reader.AdvanceTo(buffer.GetPosition(4));
這是一個相當實用的設計,它解決了"讀了就得用"的問題,不僅可以將不用的數據下次再使用,還可以實現Peek的操作,只讀但不改變游標。
Reader和Writer都有一個Complete函數,用於通知結束:
reader.Complete();
writer.Complete();
FlushResult result = await writer.FlushAsync();
ReadResult result = await reader.ReadAsync();
它們都有一個IsComplete屬性,可以根據它是否為true判斷是否已經結束了讀和寫的操作。
在寫入和讀取的時候,也可以傳入一個CancellationToken,用於取消相應的操作。
writer.FlushAsync(CancellationToken.None);
reader.ReadAsync(CancellationToken.None);