IOHelper主要是针对数据流Stream和字节数组Byte[]的扩展。
X组件在二进制协议(包括通信协议和文件协议)上有极为深厚的积累和支持,完全依托于该扩展。各种协议包括但不限于:Http/Redis/Zip/SRMP/RocketMQ/MySql/Thrift/Hive
Nuget包:NewLife.Core
源码:https://github.com/NewLifeX/X/blob/master/NewLife.Core/IO/IOHelper.cs
视频:https://www.bilibili.com/video/BV1gq4y1g78w
视频:https://www.bilibili.com/video/BV1gm4y1A7h8
压缩与解压缩
Compress/Decompress 支持对数据流和字节数据进行压缩和解压缩,内置Deflate压缩算法;
CompressGZip/DecompressGZip 则是GZip压缩算法,适用于http压缩,或者单文件压缩;
数据流读写
ReadBytes
这是最常用的数据流扩展方法,从数据流中读取指定大小的字节数组,-1表示读取全部。
Byte[] ReadBytes(this Stream stream, Int64 length = -1)
ToStr
数据流或字节数组转为字符串,支持指定编码,默认utf-8,常用于打日志
String ToStr(this Stream stream, Encoding encoding = null); String ToStr(this Byte[] buf, Encoding encoding = null, Int32 offset = 0, Int32 count = -1);
WriteArray/ReadArray
从数据流中读写字节数组,约定以7位压缩编码表示的长度为开头。这是通信协议中实现变长数据的常用手段。
Stream WriteArray(this Stream des, params Byte[] src); Byte[] ReadArray(this Stream des);
在通信协议中,字符串的传输也是先转为字节数组,然后采用该方法进行序列化。
WriteDateTime/ReadDateTime
从数据流中读写时间日期,约定4字节的Unix秒,也即是1970年以来秒数。
Stream WriteDateTime(this Stream stream, DateTime dt); DateTime ReadDateTime(this Stream stream);
字节数组与整数互转
这是二进制序列化基础,整数是最常用的一种数据类型
UInt16 ToUInt16(this Byte[] data, Int32 offset = 0, Boolean isLittleEndian = true); UInt32 ToUInt32(this Byte[] data, Int32 offset = 0, Boolean isLittleEndian = true); UInt64 ToUInt64(this Byte[] data, Int32 offset = 0, Boolean isLittleEndian = true); Byte[] Write(this Byte[] data, UInt16 n, Int32 offset = 0, Boolean isLittleEndian = true); Byte[] Write(this Byte[] data, UInt32 n, Int32 offset = 0, Boolean isLittleEndian = true); Byte[] Write(this Byte[] data, UInt64 n, Int32 offset = 0, Boolean isLittleEndian = true); Byte[] GetBytes(this UInt16 value, Boolean isLittleEndian = true); Byte[] GetBytes(this Int16 value, Boolean isLittleEndian = true); Byte[] GetBytes(this UInt32 value, Boolean isLittleEndian = true); Byte[] GetBytes(this Int32 value, Boolean isLittleEndian = true); Byte[] GetBytes(this UInt64 value, Boolean isLittleEndian = true); Byte[] GetBytes(this Int64 value, Boolean isLittleEndian = true);
以上扩展实现了从字节数组读取整数、向字节数组写入整数、把整数转为字节数组。
其中最亮眼的莫过于isLittleEndian,这是小端字节序,简称小字节序,相对而言,还有大端字节序。
在计算机内存和网络数据流中,数据都是以字节形式存在,一个整数Int32占用4个字节,谁在前面谁在后面呢?
通俗来讲,小端字节序就是整数较小部分放在前面,例如 0x12345678,在.net内存中应该是 [0x78, 0x56, 0x34, 0x12],因为.net是小端字节序,而0x78是这个整数最小的一个字节。
那么哪些场景是大端字节序呢?
已知,Java和Tcp/IP等一部网络协议,都是大端字节序,其它编程语言基本上都是小端字节序。ARM指令集比较特殊,不同芯片可能大小端混用。
因此,在日常开发中,如无例外,一般都是默认小端字节序,唯有开发网络协议时稍微注意一下。
字节序交换
在物联网领域,特别是工业物联网中,读取传感器数据时需要做16位交换或者32位交换。
典型场景有 ABCD / BADC / CDAB / DCBA,AB互换就是swap16,AB和CD互换是swap32。
Byte[] Swap(this Byte[] data, Boolean swap16, Boolean swap32);
7位压缩编码整数
一个Int32整数,在内存中需要占用4个字节,但是绝大多数时候它很小,只有几十几百,占用的4个字节常常有两个是0x00,相当浪费。于是前辈们研究了各种整数压缩算法,例如Base 128 Varints和ZigZag等。这些算法想读懂不容易,感兴趣的同学自己去研究,我们这里只讲解最简单的一种。
整数0x12的二进制形式 0b0001_0010,最高位为0,在压缩算法中只需要1个字节,表示为 0b0001_0010;
整数0x1234的二进制形式 0b0001_0010_0011_0100,最高位为0,只需要2个字节,表示为 0b0010_0100_1011_0100;
因此,7位压缩编码整数的原理就是用7位表示数字,最高位表示下一个字节是否还有数据。从而得到,小于128的数字,只需要1个字节保存,小于等于0x3FFF=16383的整数,需要2个字节保存。
可用扩展如下:
Int32 ReadEncodedInt(this Stream stream); UInt64 ReadEncodedInt64(this Stream stream); Stream WriteEncodedInt(this Stream stream, Int64 value); Byte[] GetEncodedInt(Int64 value);
随着计算机内存和网络带宽的扩大,压缩整数的使用场景越来越少。
我的项目里面,曾经把100亿行数据放入一台Redis机器,占用360G内存,最重要的就是压缩整数!
十六进制编码
为了方便Http传输或数据库保存小量字节数据,常常采用十六进制编码(HEX编码)。
String ToHex(this Byte[] data, Int32 offset = 0, Int32 count = -1); String ToHex(this Byte[] data, String separate, Int32 groupSize = 0, Int32 maxLength = -1); Byte[] ToHex(this String data, Int32 startIndex = 0, Int32 length = -1);
同时,也支持HEX字符串转为字节数组。
BASE64编码
传输和存储中小量字节数据,用BASE64更合适
String ToBase64(this Byte[] data, Int32 offset = 0, Int32 count = -1, Boolean lineBreak = false); String ToUrlBase64(this Byte[] data, Int32 offset = 0, Int32 count = -1); Byte[] ToBase64(this String data);
但是在Http中使用Base64一定要小心,某些符号不支持拼接url,因此才有ToUrlBase64,特殊处理了不支持的字符。
HEX编码和Base64编码可参考码神工具,加密解密。
上图看到,Base64编码后,得到两个字符串,下面一个就是UrlBase64,避开了加号。
字节数据搜索
常用字符串搜索IndexOf,但是有时候要在一个字节数据流内搜索目标字符串,例如解析Http请求头中表单Post数据时需要查找分隔符。
内部提供了 Boyer Moore 算法实现,具体算法原理这里不展开,理论上最好的情况下可以做到O(n)。而传统的窗口滑动搜索,算法复杂度一般是O(m*n)。
/// <summary>Boyer Moore 字符串搜索算法,比KMP更快,常用于IDE工具的查找</summary> /// <param name="source"></param> /// <param name="pattern"></param> /// <param name="offset"></param> /// <param name="count"></param> /// <returns></returns> public static Int32 IndexOf(this Byte[] source, Byte[] pattern, Int32 offset = 0, Int32 count = -1)
IndexOf 以扩展方法形式提供,在source中搜索pattern,从offset开始搜索,最多找count个字节。
准备一个超大字节数据,尝试在其中搜索目标字符串,可以体验到不一般的性能提升:
[Fact] public void IndexOf() { var d = "------WebKitFormBoundary3ZXeqQWNjAzojVR7".GetBytes(); var buf = new Byte[8 * 1024 * 1024]; buf.Write(7 * 1024 * 1024, d); var p = buf.IndexOf(d); Assert.Equal(7 * 1024 * 1024, p); p = buf.IndexOf(d, 7 * 1024 * 1024 - 1); Assert.Equal(7 * 1024 * 1024, p); p = buf.IndexOf(d, 7 * 1024 * 1024 + 1); Assert.Equal(-1, p); } private static readonly Byte[] NewLine2 = new[] { (Byte)'\r', (Byte)'\n', (Byte)'\r', (Byte)'\n' }; [Fact] public void IndexOf2() { var str = "Content-Disposition: form-data; name=\"name\"\r\n\r\n大石头"; var buf = str.GetBytes(); var p = buf.IndexOf("\r\n\r\n".GetBytes()); Assert.Equal(43, p); p = buf.IndexOf(NewLine2); Assert.Equal(43, p); var pk = new Packet(buf); var value = pk.Slice(p + 4).ToStr(); Assert.Equal("大石头", value); }
在 .NETCore3.0/.NET5.0 里,新增了 Span.IndexOf ,还没来得及深入研究,希望它有更好的性能。