今天在維護一箇舊項目的時候,看到一個方法把string
轉換爲 byte[]
用的是寫入內存流的,而後ToArray()
,由於日常都是用System.Text.Encoding.UTF8.GetBytes(string)
,恰好這裏遇到一個安全的問題,就想把它重構了。html
因爲這個是已經找不到原來開發的人員,因此也無從問當時爲何要這麼作,我想就算找到應該他也不知道當時爲何要這麼作。git
因爲這個是線上跑了好久的項目,因此須要作一下測試,萬一真裏面真的是有歷史緣由呢!因而就有了這篇文章。github
byte
數組的函數(確保重構先後一致),沒找到有系統自帶,因此寫了一個BytesEquals
比較字節數組是否徹底相等,方法比較簡單,就不作介紹macos
public static bool BytesEquals(byte[] array1, byte[] array2) { if (array1 == null && array2 == null) return true; if (Array.ReferenceEquals(array1, array2)) return true; if (array1?.Length != array2?.Length) return false; for (int i = 0; i < array1.Length; i++) { if (array1[i] != array2[i]) return false; } return true; }
原始方法(使用StreamWriter)數組
public static byte[] StringToBytes(string value) { if (value == null) throw new ArgumentNullException(nameof(value)); using (var ms = new System.IO.MemoryStream()) using (var streamWriter = new System.IO.StreamWriter(ms, System.Text.Encoding.UTF8)) { streamWriter.Write(value); streamWriter.Flush(); return ms.ToArray(); } }
重構(使用Encoidng)安全
public static byte[] StringToBytes(string value) { if (value == null) throw new ArgumentNullException(nameof(value)); return System.Text.Encoding.UTF8.GetBytes(value); }
dotnet new xunit -n 'Demo.StreamWriter.UnitTests'
[Fact] public void BytesEqualsTest_Equals_ReturnTrue() { ... } [Fact] public void BytesEqualsTest_NotEquals_ReturnFalse() { ... } [Fact] public void StringToBytes_Equals_ReturnTrue() { ... }
dotnet test
StringToBytes_Equals_ReturnTrue
未能經過單元測試這個未能經過,重構後的生成的字節數組與原始不一致函數
StringToBytes_Equals_ReturnTrue
, 發現bytesWithStream
比 bytesWithEncoding
在數組頭多了三個字節(不少人都能猜到這個是UTF8的BOM)+ bytesWithStream[0] = 239 + bytesWithStream[1] = 187 + bytesWithStream[2] = 191 bytesWithStream[3] = 72 bytesWithStream[4] = 101 bytesWithEncoding[0] = 72 bytesWithEncoding[0] = 101
不瞭解BOM,能夠看看這篇文章Byte order mark性能
從文章能夠明確多出來字節就是UTF8-BOM,問題來了,爲何StreamWriter
會多出來BOM,而Encoding.UTF8
沒有,都是用同一個編碼單元測試
StreamWriter
測試
public StreamWriter(Stream stream) : this(stream, UTF8NoBOM, 1024, leaveOpen: false) { } public StreamWriter(Stream stream, Encoding encoding) : this(stream, encoding, 1024, leaveOpen: false) { }
private static Encoding UTF8NoBOM => EncodingCache.UTF8NoBOM; internal static readonly Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
能夠看到StreamWriter
, 默認是使用UTF8NoBOM
, 可是在這裏指定了System.Text.Encoding.UTF8
,根據encoderShouldEmitUTF8Identifier
這個參數決定是否寫入BOM,最終是在Flush
寫入
private void Flush(bool flushStream, bool flushEncoder) { ... if (!_haveWrittenPreamble) { _haveWrittenPreamble = true; ReadOnlySpan<byte> preamble = _encoding.Preamble; if (preamble.Length > 0) { _stream.Write(preamble); } } int bytes = _encoder.GetBytes(_charBuffer, 0, _charPos, _byteBuffer, 0, flushEncoder); _charPos = 0; if (bytes > 0) { _stream.Write(_byteBuffer, 0, bytes); } ... }
Flush
最終也是使用_encoder.GetBytes
獲取字節數組寫入流中,而System.Text.Encoding.UTF8.GetBytes()
最終也是使用這個方法。
System.Text.Encoding.UTF8.GetBytes
public virtual byte[] GetBytes(string s) { if (s == null) { throw new ArgumentNullException("s", SR.ArgumentNull_String); } int byteCount = GetByteCount(s); byte[] array = new byte[byteCount]; int bytes = GetBytes(s, 0, s.Length, array, 0); return array; }
若是要達到和原來同樣的效果,只須要在最終返回結果加上UTF8.Preamble
, 修改以下
public static byte[] StringToBytes(string value) { if (value == null) throw new ArgumentNullException(nameof(value)); - return System.Text.Encoding.UTF8.GetBytes(value); + var bytes = System.Text.Encoding.UTF8.GetBytes(value); + var result = new byte[bytes.Length + 3]; + Array.Copy(Encoding.UTF8.GetPreamble(), result, 3); + Array.Copy(bytes, 0, result, 3, bytes.Length); + return result; }
可是對於這樣修改感受是不必,由於這個最終是傳給一個對外接口,因此只能對那個接口作測試,最終結果也是不須要這個BOM
排除了StreamWriter
沒有作特殊處理,能夠用System.Text.Encoding.UTF8.GetBytes()
重構。還有就是效率問題,雖然直觀上看到使用StreamWriter
最終都是使用Encoder.GetBytes
方法,並且還多了兩次資源對申請和釋放。可是仍是用基準測試才能直觀看出其中差異。
基準測試使用BenchmarkDotNet,BenchmarkDotNet這裏以前有介紹過
BenchmarksTests
目錄並建立基準項目mkdir BenchmarksTests && cd BenchmarksTests && dotnet new benchmark -b StreamVsEncoding
dotnet add reference ../../src/Demo.StreamWriter.csproj
注意:Demo.StreamWriter須要Release編譯
[SimpleJob(launchCount: 10)] [MemoryDiagnoser] public class StreamVsEncoding { [Params("Hello Wilson!", "使用【BenchmarkDotNet】基準測試,Encoding vs Stream")] public string _stringValue; [Benchmark] public void Encoding() => StringToBytesWithEncoding.StringToBytes(_stringValue); [Benchmark] public void Stream() => StringToBytesWithStream.StringToBytes(_stringValue); }
dotnet build && sudo dotnet benchmark bin/Release/netstandard2.0/BenchmarksTests.dll --filter 'StreamVsEncoding'
注意:macos 須要sudo權限
Method | _stringValue | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|---|
Encoding | Hello Wilson! | 107.4 ns | 0.61 ns | 2.32 ns | 106.9 ns | 0.0355 | - | - | 112 B |
Stream | Hello Wilson! | 565.1 ns | 4.12 ns | 18.40 ns | 562.3 ns | 1.8196 | - | - | 5728 B |
Encoding | 使用【Be(...)tream [42] | 166.3 ns | 1.00 ns | 3.64 ns | 165.4 ns | 0.0660 | - | - | 208 B |
Stream | 使用【Be(...)tream [42] | 584.6 ns | 3.65 ns | 13.22 ns | 580.8 ns | 1.8349 | - | - | 5776 B |
執行時間相差了4~5倍, 內存使用率相差 20 ~ 50倍,差距還比較大。
StreamWriter
默認是沒有BOM,若指定System.Text.Encoding.UTF8
,會在Flush
字節數組開頭添加BOMSystem.Text.Encoding.UTF8.GetBytes
要高效System.Text.Encoding.UTF8.GetBytes
是不會本身添加BOM,提供Encoding.UTF8.GetPreamble()
獲取BOM轉發請標明出處:http://www.javashuo.com/article/p-ekhzzjvf-ne.html
示例代碼