C#如何從byte[]中直接讀取Structure實(shí)例詳解
序、前言
emmmmm,首先這篇文章講的不是用BinaryFormatter來(lái)進(jìn)行結(jié)構(gòu)體的二進(jìn)制轉(zhuǎn)換,說(shuō)真的BinaryFormatter這個(gè)類(lèi)其實(shí)現(xiàn)在的作用并不是特別大了,因?yàn)锽inaryFormatter二進(jìn)制序列化出來(lái)的結(jié)果只能用于.net平臺(tái),現(xiàn)在可能就用于如存入Redis這種情況下會(huì)在使用。
去年年尾的樣子,我閱讀學(xué)習(xí)某C++開(kāi)發(fā)的程序源碼時(shí),發(fā)現(xiàn)作者用了一個(gè)很騷的操作直接將byte[]數(shù)組轉(zhuǎn)為了結(jié)構(gòu)體對(duì)象:
上面的data變量是一個(gè)指向unsigned char類(lèi)型的指針,就只要一個(gè)簡(jiǎn)單的類(lèi)型轉(zhuǎn)換就可以將一堆unsigned char轉(zhuǎn)換成想要的結(jié)構(gòu)體,這著實(shí)有點(diǎn)讓筆者有點(diǎn)羨慕。
后來(lái),筆者想用C#開(kāi)發(fā)一個(gè)流量分析程序,由于需要對(duì)IP報(bào)文進(jìn)行仔細(xì)的特征提取,所以不能直接使用第三方數(shù)據(jù)包解析庫(kù)(如:PacketDotNet)直接解析,會(huì)丟失部分特征,然而使用BinaryReader進(jìn)行報(bào)文頭解析的話(huà),整個(gè)解析代碼會(huì)寫(xiě)的喪心病狂的惡(e)心(xin),正在苦惱的時(shí)候,突然想起上面提到的那個(gè)騷操作時(shí),筆者突然冒出了一個(gè)想法,C#里也支持結(jié)構(gòu)體,那我能不能也像C++這樣直接從字節(jié)序列中讀取出結(jié)構(gòu)體呢?
注:本文所有代碼在.net Standard 2.0上測(cè)試通過(guò)。
一、先聲明,后調(diào)用~
那么在開(kāi)始前,我們先定義一下要用到的IPv4報(bào)文頭結(jié)構(gòu)體,各位同學(xué)IPv4報(bào)文頭結(jié)構(gòu)有沒(méi)有忘掉啊,如果忘了的話(huà)記得先去補(bǔ)補(bǔ)TCP網(wǎng)絡(luò)基礎(chǔ)哈~
因?yàn)镮Pv4頭是允許可變長(zhǎng)度的,所以我們的結(jié)構(gòu)體只需要解析到目的地址就夠了,后面的可變選項(xiàng)部分在報(bào)文頭前16字節(jié)解析完成之前是不知道會(huì)有多長(zhǎng)的。
IPv4頭部結(jié)構(gòu)體定義如下,由于IPv4頭部定義中,各個(gè)字段并不是都8位整長(zhǎng)的,所以有幾個(gè)字段是相互并在一起的:
public struct IPv4Header
{
/// <summary>
/// IP協(xié)議版本及頭部長(zhǎng)度
/// </summary>
private byte _verHlen;
/// <summary>
/// 差異化服務(wù)及顯式擁塞通告
/// </summary>
private byte _dscpEcn;
/// <summary>
/// 報(bào)文全長(zhǎng)
/// </summary>
private ushort _totalLength;
/// <summary>
/// 標(biāo)識(shí)符
/// </summary>
private ushort _identification;
/// <summary>
/// 標(biāo)志位及分片偏移
/// </summary>
private ushort _flagsOffset;
/// <summary>
/// 存活時(shí)間
/// </summary>
private byte _ttl;
/// <summary>
/// 協(xié)議
/// </summary>
private byte _protocol;
/// <summary>
/// 頭部檢驗(yàn)和
/// </summary>
private ushort _checksum;
/// <summary>
/// 源地址
/// </summary>
private int _srcAddr;
/// <summary>
/// 目標(biāo)地址
/// </summary>
private int _dstAddr;
}
當(dāng)然,為了方便后續(xù)的使用,還可以在此技術(shù)上設(shè)置一些可讀屬性:
public struct IPv4Header
{
/// <summary>
/// IP協(xié)議版本及頭部長(zhǎng)度
/// </summary>
private byte _verHlen;
/// <summary>
/// 差異化服務(wù)及顯式擁塞通告
/// </summary>
private byte _dscpEcn;
/// <summary>
/// 報(bào)文全長(zhǎng)
/// </summary>
private ushort _totalLength;
/// <summary>
/// 標(biāo)識(shí)符
/// </summary>
private ushort _identification;
/// <summary>
/// 標(biāo)志位及分片偏移
/// </summary>
private ushort _flagsOffset;
/// <summary>
/// 存活時(shí)間
/// </summary>
private byte _ttl;
/// <summary>
/// 協(xié)議
/// </summary>
private byte _protocol;
/// <summary>
/// 頭部檢驗(yàn)和
/// </summary>
private ushort _checksum;
/// <summary>
/// 源地址
/// </summary>
private int _srcAddr;
/// <summary>
/// 目標(biāo)地址
/// </summary>
private int _dstAddr;
/// <summary>
/// IP協(xié)議版本
/// </summary>
public int Version
{
get
{
return (this._verHlen & 0xF0) >> 4;
}
}
/// <summary>
/// 頭部長(zhǎng)度
/// </summary>
public int HeaderLength
{
get
{
return this._verHlen & 0x0F;
}
}
/// <summary>
/// 差異化服務(wù)
/// </summary>
public int DSCP
{
get
{
return (this._dscpEcn & 0xFC) >> 2;
}
}
/// <summary>
/// 顯式擁塞通告
/// </summary>
public int ECN
{
get
{
return this._dscpEcn & 0x03;
}
}
/// <summary>
/// 報(bào)文全長(zhǎng)
/// </summary>
public ushort TotalLength
{
get
{
return this._totalLength;
}
}
/// <summary>
/// 標(biāo)識(shí)符
/// </summary>
public ushort Identification
{
get
{
return this._identification;
}
}
/// <summary>
/// 保留字段
/// </summary>
public int Reserved
{
get
{
return (this._flagsOffset & 0x80) >> 7;
}
}
/// <summary>
/// 禁止分片標(biāo)志位
/// </summary>
public bool DF
{
get
{
return (this._flagsOffset & 0x40) == 1;
}
}
/// <summary>
/// 更多分片標(biāo)志位
/// </summary>
public bool MF
{
get
{
return (this._flagsOffset & 0x20) == 1;
}
}
/// <summary>
/// 分片偏移
/// </summary>
public int FragmentOffset
{
get
{
return this._flagsOffset & 0x1F;
}
}
/// <summary>
/// 存活時(shí)間
/// </summary>
public byte TTL
{
get
{
return this._ttl;
}
}
/// <summary>
/// 協(xié)議
/// </summary>
public byte Protocol
{
get
{
return this._protocol;
}
}
/// <summary>
/// 頭部檢驗(yàn)和
/// </summary>
public ushort HeaderChecksum
{
get
{
return this._checksum;
}
}
/// <summary>
/// 源地址
/// </summary>
public IPAddress SrcAddr
{
get
{
return new IPAddress(BitConverter.GetBytes(this._srcAddr));
}
}
/// <summary>
/// 目的地址
/// </summary>
public IPAddress DstAddr
{
get
{
return new IPAddress(BitConverter.GetBytes(this._dstAddr));
}
}
}
二、byte[]轉(zhuǎn)Structure第一版
首先筆者先看了一圈文檔,看看C#有沒(méi)有什么方法支持將byte[]轉(zhuǎn)為結(jié)構(gòu)體,逛了一圈發(fā)現(xiàn)一個(gè)有這么一個(gè)函數(shù):
System.Runtime.InteropServices.Marshal.PtrToStructure<T>(IntPtr)
這個(gè)方法接收兩個(gè)參數(shù),一個(gè)結(jié)構(gòu)體泛型和一個(gè)指向結(jié)構(gòu)體數(shù)據(jù)的安全指針(IntPtr),然后這個(gè)方法就能返回一個(gè)結(jié)構(gòu)體實(shí)例出來(lái)了。
那么現(xiàn)在的問(wèn)題就是該如何取得一個(gè)byte[]對(duì)象的安全指針呢?這里筆者第一反應(yīng)是利用System.Runtime.InteropServices.Marshal.AllocHGlobal方法分配一塊堆外內(nèi)存出來(lái),然后將待轉(zhuǎn)換的byte[]對(duì)象復(fù)制到這塊堆外內(nèi)存中,接著利用PtrToStructure<T>函數(shù)將byte[]對(duì)象轉(zhuǎn)換成我們想要的結(jié)構(gòu)體對(duì)象實(shí)例,最后釋放掉堆外內(nèi)存就可以了。
將上面的步驟轉(zhuǎn)換為C#代碼,就形成了第一版的BytesToStructure<T>函數(shù):
/// <summary>
/// 將 byte[] 轉(zhuǎn)為指定結(jié)構(gòu)體實(shí)例
/// </summary>
/// <typeparam name="T">目標(biāo)結(jié)構(gòu)體類(lèi)型</typeparam>
/// <param name="bytes">待轉(zhuǎn)換 byte[]</param>
/// <returns>轉(zhuǎn)換后的結(jié)構(gòu)體實(shí)例</returns>
public static T BytesToStructure<T>(byte[] bytes) where T : struct
{
int size = Marshal.SizeOf(typeof(T));
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(bytes, 0, ptr, size);
return Marshal.PtrToStructure<T>(ptr);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
之后就只要抓一下包看看效果就好了。
抓包我們用SharpPcap,順便讓它幫我們過(guò)濾一下僅捕獲IP報(bào)文。代碼如下:
public static void Main(string[] args)
{
CaptureDeviceList devices = CaptureDeviceList.Instance;
if (devices.Count <= 0)
{
Console.WriteLine("No device found on this machine");
return;
}
else
{
Console.WriteLine("available devices:");
Console.WriteLine("-----------------------------");
}
int index = 0;
foreach (ICaptureDevice item in devices)
{
Console.WriteLine($"{index++}) {item.Name}");
}
Console.Write("enter your choose: ");
index = int.Parse(Console.ReadLine());
Console.WriteLine();
ICaptureDevice device = devices[index];
device.OnPacketArrival += new PacketArrivalEventHandler((sender, e) =>
{
Packet packet = Packet.ParsePacket(e.Packet.LinkLayerType, e.Packet.Data);
if (packet.Extract(typeof(IPPacket)) is IPPacket ipPacket)
{
IPv4Header header = StructHelper.BytesToStructure<IPv4Header>(ipPacket.Bytes);
Console.WriteLine($"{header.SrcAddr} ==> {header.DstAddr}");
}
});
device.Open(DeviceMode.Promiscuous, 1000);
device.Filter = "ip";
Console.CancelKeyPress += new ConsoleCancelEventHandler((sender, e) => device.Close());
device.Capture();
}
啟動(dòng)上面的代碼,選擇需要捕獲數(shù)據(jù)包的網(wǎng)卡,就可以看到此網(wǎng)卡上所有IP報(bào)文記錄及其源地址與目標(biāo)地址了:
三、大端字節(jié)序、小端字節(jié)序……
剛剛上面我們已經(jīng)成功的將byte[]對(duì)象轉(zhuǎn)換為我們想要的結(jié)構(gòu)體了,但我們轉(zhuǎn)換出來(lái)的結(jié)構(gòu)體真的正確嗎,我們可以將我們讀取出來(lái)的結(jié)構(gòu)體和PacketDotNet包解析出來(lái)的IP報(bào)文頭數(shù)據(jù)進(jìn)行比較:
我們可以看到我們轉(zhuǎn)換出來(lái)的IPv4報(bào)文頭結(jié)構(gòu)體中的報(bào)文總長(zhǎng)字段和PacketDotNet解析出來(lái)的數(shù)據(jù)不一致,我們的轉(zhuǎn)換函數(shù)出來(lái)的包總長(zhǎng)是15872,而PacketDotNet解析出來(lái)的包總長(zhǎng)只有62。
到底誰(shuí)是對(duì)的呢,不用猜,肯定是我們的轉(zhuǎn)換函數(shù)有問(wèn)題,如果看官您不相信,可以用WireShark抓包做比較,看看WireShark會(huì)挺誰(shuí)的結(jié)果。
那么到底是哪里錯(cuò)了呢?相信不少有實(shí)戰(zhàn)經(jīng)驗(yàn)的看官已經(jīng)知道問(wèn)題的原因了:大小字節(jié)序。
我們分別將15872和62轉(zhuǎn)為二進(jìn)制格式:
| 數(shù)值 | 15872 | 62 |
| 二進(jìn)制 | 00111110 00000000 | 00000000 00111110 |
15872和62這兩個(gè)數(shù)字轉(zhuǎn)換為二進(jìn)制之后,15872的00111110在前面,00000000在后面,而62則正好相反。
一般來(lái)說(shuō)計(jì)算機(jī)硬件有兩種儲(chǔ)存數(shù)據(jù)的方式:大端字節(jié)序(big endian)和小端字節(jié)序(little endian)。
舉例來(lái)說(shuō),數(shù)值0x2211使用兩個(gè)字節(jié)儲(chǔ)存:高位字節(jié)是0x22,低位字節(jié)是0x11。
大端字節(jié)序:
高位字節(jié)在前,低位字節(jié)在后,這是人類(lèi)讀寫(xiě)數(shù)值的方法。
小端字節(jié)序:
低位字節(jié)在前,高位字節(jié)在后,即以0x1122形式儲(chǔ)存。
在網(wǎng)絡(luò)中傳輸數(shù)據(jù),一般使用的是大端字節(jié)序,然而在計(jì)算機(jī)內(nèi)部中,為了方便計(jì)算,大多都會(huì)使用小端字節(jié)序進(jìn)行儲(chǔ)存。
.net CLR默認(rèn)會(huì)使用當(dāng)前計(jì)算機(jī)系統(tǒng)使用的字節(jié)順序,而筆者測(cè)試時(shí)用的系統(tǒng)是Windows 7 x64,內(nèi)部默認(rèn)用的是小端字節(jié)序,所以在一切均為默認(rèn)的情況下,多字節(jié)字段在轉(zhuǎn)換后都會(huì)因?yàn)樽止?jié)序不正確而讀取為錯(cuò)誤值。
.net提供了一個(gè)屬性用于開(kāi)發(fā)者獲取當(dāng)前計(jì)算機(jī)系統(tǒng)使用的字節(jié)序:
System.BitConverter.IsLittleEndian
如果此屬性為true,則表示當(dāng)前計(jì)算機(jī)正在使用小端字節(jié)序,否則為大端字節(jié)序。
回到剛剛的問(wèn)題,為了防止大小端字節(jié)序?qū)D(zhuǎn)換產(chǎn)生影響,我們可以使用Attribute對(duì)結(jié)構(gòu)體中各個(gè)多字節(jié)字段進(jìn)行標(biāo)記,并在轉(zhuǎn)換前判斷字節(jié)序是否一致,如果不一致則進(jìn)行順序調(diào)整,代碼如下:
首先定義一個(gè)大小端字節(jié)序枚舉:
/// <summary>
/// 字節(jié)序枚舉
/// </summary>
public enum Endianness
{
/// <summary>
/// 大端字節(jié)序
/// </summary>
BigEndian,
/// <summary>
/// 小端字節(jié)序
/// </summary>
LittleEndian
}
然后定義大小端字節(jié)序聲明特性
/// <summary>
/// 字節(jié)序特性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class EndianAttribute : Attribute
{
/// <summary>
/// 標(biāo)記字段的字節(jié)序
/// </summary>
public Endianness Endianness { get; private set; }
/// <summary>
/// 構(gòu)造函數(shù)
/// </summary>
/// <param name="endianness">字節(jié)序</param>
public EndianAttribute(Endianness endianness)
{
this.Endianness = endianness;
}
}
我們?cè)谶@里使用AttributeUsage特性限制此EndianAttribute特性?xún)H限字段使用。
然后是轉(zhuǎn)換函數(shù):
/// <summary>
/// 調(diào)整字節(jié)順序
/// </summary>
/// <typeparam name="T">待調(diào)整字節(jié)順序的結(jié)構(gòu)體類(lèi)型</typeparam>
/// <param name="bytes">字節(jié)數(shù)組</param>
private static byte[] RespectEndianness<T>(byte[] bytes)
{
Type type = typeof(T);
var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(f => f.IsDefined(typeof(EndianAttribute), false)).Select(field => new
{
Field = field,
Attribute = (EndianAttribute)field.GetCustomAttributes(typeof(EndianAttribute), false).First(),
Offset = Marshal.OffsetOf(type, field.Name).ToInt32()
}).ToList();
foreach (var field in fields)
{
if ((field.Attribute.Endianness == Endianness.BigEndian && BitConverter.IsLittleEndian) ||
(field.Attribute.Endianness == Endianness.LittleEndian && !BitConverter.IsLittleEndian))
{
Array.Reverse(bytes, field.Offset, Marshal.SizeOf(field.Field.FieldType));
}
}
return bytes;
}
此函數(shù)會(huì)先使用反射獲取所有含有EndianAttribute特性的公開(kāi)或非公開(kāi)實(shí)例字段,然后依次求出其偏移,最后判斷字段標(biāo)注的字節(jié)序是否與當(dāng)前計(jì)算機(jī)的字節(jié)序相同,如果不同則進(jìn)行順序翻轉(zhuǎn)。另外上面的函數(shù)使用了Linq,需要引入System.Linq命名空間,且Linq函數(shù)中還使用到了匿名類(lèi)。
接下來(lái)要對(duì)轉(zhuǎn)換函數(shù)進(jìn)行修改,只需要在轉(zhuǎn)換前調(diào)用一下調(diào)序函數(shù)即可。
/// <summary>
/// 將 byte[] 轉(zhuǎn)為指定結(jié)構(gòu)體實(shí)例
/// </summary>
/// <typeparam name="T">目標(biāo)結(jié)構(gòu)體類(lèi)型</typeparam>
/// <param name="bytes">待轉(zhuǎn)換 byte[]</param>
/// <returns>轉(zhuǎn)換后的結(jié)構(gòu)體實(shí)例</returns>
public static T BytesToStructure<T>(byte[] bytes) where T : struct
{
bytes = RespectEndianness<T>(bytes);
int size = Marshal.SizeOf(typeof(T));
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(bytes, 0, ptr, size);
return Marshal.PtrToStructure<T>(ptr);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
當(dāng)然了,我們還要對(duì)結(jié)構(gòu)體中的各個(gè)多字節(jié)字段標(biāo)記上EndianAttribute特性:
public struct IPv4Header
{
/// <summary>
/// IP協(xié)議版本及頭部長(zhǎng)度
/// </summary>
private byte _verHlen;
/// <summary>
/// 差異化服務(wù)及顯式擁塞通告
/// </summary>
private byte _dscpEcn;
/// <summary>
/// 報(bào)文全長(zhǎng)
/// </summary>
[Endian(Endianness.BigEndian)]
private ushort _totalLength;
/// <summary>
/// 標(biāo)識(shí)符
/// </summary>
[Endian(Endianness.BigEndian)]
private ushort _identification;
/// <summary>
/// 標(biāo)志位及分片偏移
/// </summary>
[Endian(Endianness.BigEndian)]
private ushort _flagsOffset;
/// <summary>
/// 存活時(shí)間
/// </summary>
private byte _ttl;
/// <summary>
/// 協(xié)議
/// </summary>
private byte _protocol;
/// <summary>
/// 頭部檢驗(yàn)和
/// </summary>
[Endian(Endianness.BigEndian)]
private ushort _checksum;
/// <summary>
/// 源地址
/// </summary>
private int _srcAddr;
/// <summary>
/// 目標(biāo)地址
/// </summary>
private int _dstAddr;
}
需要說(shuō)一點(diǎn),就是最后的源地址和目標(biāo)地址,筆者上面用的是
public IPAddress(byte[] address)
這個(gè)構(gòu)造函數(shù)來(lái)構(gòu)造IPAddress類(lèi),并且是使用BitConverter.GetBytes這個(gè)方法將int類(lèi)型轉(zhuǎn)為byte[]并傳入構(gòu)造函數(shù)的,所以不用注明大端序,否則會(huì)導(dǎo)致轉(zhuǎn)換結(jié)果不正確(錯(cuò)誤結(jié)果和正確結(jié)果會(huì)正好顛倒)。
重啟程序,看看現(xiàn)在我們的轉(zhuǎn)換函數(shù)轉(zhuǎn)換出來(lái)的結(jié)果是不是和PacketDotNet轉(zhuǎn)換結(jié)果一樣了?
四、性能提升!性能提升!
在解決了大字節(jié)序小字節(jié)序的問(wèn)題之后,讓我們重新審視一下剛剛上面的轉(zhuǎn)換函數(shù),可以看到在剛才的函數(shù)中,每次要從byte[]中讀取結(jié)構(gòu)體時(shí),都要經(jīng)過(guò)“申請(qǐng)堆外內(nèi)存——復(fù)制對(duì)象——讀取結(jié)構(gòu)體——釋放堆外內(nèi)存”這四步,申請(qǐng)堆外內(nèi)存,復(fù)制對(duì)象和釋放堆外內(nèi)存這三步照理來(lái)說(shuō)是浪費(fèi)性能的,明明byte[]已經(jīng)在內(nèi)存中了,但就是為了獲取它的安全句柄而大費(fèi)周章的再去申請(qǐng)一塊內(nèi)存,畢竟申請(qǐng)和釋放內(nèi)存也算是一筆不小的開(kāi)支了。
那除了Marshal.AllocHGlobal以外還有別的什么方法能獲取到托管對(duì)象的安全句柄呢?筆者又去網(wǎng)上找了一下,您還別說(shuō),這還真的有。朋友,您聽(tīng)說(shuō)過(guò)GCHandle嗎?
System.Runtime.InteropServices.GCHandle.Alloc
此方法允許傳入任意一個(gè)object對(duì)象,它將返回一個(gè)GCHandle實(shí)例并保護(hù)傳入的對(duì)象不會(huì)被GC回收掉,當(dāng)使用完畢后,需要調(diào)用此GCHandle實(shí)例的Free方法進(jìn)行釋放。而GCHandle結(jié)構(gòu)體有一個(gè)實(shí)例方法AddrOfPinnedObject,此方法將返回此固定對(duì)象的地址及安全指針(IntPtr)。
利用GCHandle.Alloc方法,就可以避免重復(fù)的申請(qǐng)、復(fù)制和釋放內(nèi)存了,由此我們對(duì)剛剛的第一版BytesToStructure函數(shù)進(jìn)行改進(jìn),第二版BytesToStructure函數(shù)閃亮登場(chǎng):
/// <summary>
/// 將 byte[] 轉(zhuǎn)為指定結(jié)構(gòu)體實(shí)例
/// </summary>
/// <typeparam name="T">目標(biāo)結(jié)構(gòu)體類(lèi)型</typeparam>
/// <param name="bytes">待轉(zhuǎn)換 byte[]</param>
/// <returns>轉(zhuǎn)換后的結(jié)構(gòu)體實(shí)例</returns>
public static T BytesToStructureV2<T>(byte[] bytes) where T : struct
{
bytes = RespectEndianness<T>(bytes);
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
}
finally
{
handle.Free();
}
}
現(xiàn)在我們來(lái)比較兩個(gè)轉(zhuǎn)換函數(shù)的效率試試:
我們使用相同的數(shù)據(jù)包,讓兩個(gè)轉(zhuǎn)換函數(shù)重復(fù)運(yùn)行1000w次,查看兩個(gè)函數(shù)使用的時(shí)間差距:
注意:因?yàn)檎{(diào)整大小端字節(jié)序會(huì)使用到反射,會(huì)嚴(yán)重的影響到函數(shù)本身的運(yùn)行效率(運(yùn)行時(shí)間大部份都在用于反射),所以在測(cè)試時(shí),筆者會(huì)注釋掉調(diào)整字節(jié)序調(diào)整的代碼。
| BytesToStructure<T> | BytesToStructureV2<T> | |
| 1000w次轉(zhuǎn)換耗時(shí) | 5069 ms | 2914 ms. |
五、榨干潛能,使用不安全代碼!
我們?cè)趧倓偟拇a里通過(guò)避免“申請(qǐng)內(nèi)存——復(fù)制數(shù)據(jù)——釋放內(nèi)存”的步驟來(lái)提升函數(shù)的執(zhí)行效率,那經(jīng)過(guò)上面的改造,我們的轉(zhuǎn)換函數(shù)還有提升的空間嗎?
答案是有的。
C#和Java最大的不同點(diǎn)在于C#允許程序員使用不安全代碼,這里的不安全代碼并不是指一定存在漏洞會(huì)被攻擊者利用的不安全,而是使用指針的代碼。是的!C#允許使用指針!只需要在編譯時(shí)打開(kāi)/unsafe開(kāi)關(guān)。
文章一開(kāi)始的C++代碼利用指針進(jìn)行轉(zhuǎn)換,C#其實(shí)也可以:
/// <summary>
/// 將 byte[] 轉(zhuǎn)為指定結(jié)構(gòu)體實(shí)例
/// </summary>
/// <typeparam name="T">目標(biāo)結(jié)構(gòu)體類(lèi)型</typeparam>
/// <param name="bytes">待轉(zhuǎn)換 byte[]</param>
/// <returns>轉(zhuǎn)換后的結(jié)構(gòu)體實(shí)例</returns>
public static unsafe T BytesToStructureV3<T>(byte[] bytes) where T : struct
{
bytes = RespectEndianness<T>(bytes);
fixed (byte* ptr = &bytes[0])
{
return (T)Marshal.PtrToStructure((IntPtr)ptr, typeof(T));
}
}
這個(gè)第三版函數(shù)使用了兩個(gè)關(guān)鍵字unsafe和fixed,unsafe表示此代碼為不安全代碼,C#中不安全代碼必須在unsafe標(biāo)識(shí)區(qū)域內(nèi)使用,且編譯時(shí)要啟用/unsafe開(kāi)關(guān)。fixed在這里主要是為了將指針?biāo)赶虻淖兞俊搬斪 保苊釭C誤重定位變量以產(chǎn)生錯(cuò)誤。
同樣,我們注釋掉大小端字節(jié)序調(diào)整函數(shù),再次重復(fù)運(yùn)行1000w次,看看三個(gè)函數(shù)的用時(shí):
| BytesToStructure<T> | BytesToStructureV2<T> | BytesToStructureV3<T> | |
| 1000w次轉(zhuǎn)換耗時(shí) | 5069 ms | 2914 ms. | 2004 ms |
又比之前縮短了進(jìn)1s的時(shí)間。當(dāng)然了因?yàn)檫@是重復(fù)1000w次的耗時(shí),而因?yàn)槲覀冏⑨尩袅舜笮《俗止?jié)序調(diào)整函數(shù),實(shí)際情況下啟用大小端字節(jié)序調(diào)整函數(shù)的話(huà),時(shí)間會(huì)爆炸性的增長(zhǎng)。可見(jiàn)反射是一個(gè)多么浪費(fèi)性能的操作。
六、小結(jié)
emmmmm,說(shuō)一個(gè)比較尷尬的事情,其實(shí)本文討論的這種byte[]轉(zhuǎn)為結(jié)構(gòu)體的情況其實(shí)在日常開(kāi)發(fā)中很少會(huì)用到,首先是因?yàn)榻Y(jié)構(gòu)體這種數(shù)據(jù)結(jié)構(gòu)在日常開(kāi)發(fā)中就很少會(huì)用到,平時(shí)開(kāi)發(fā)的話(huà)類(lèi)才是大頭,另外如果是因?yàn)橐虲/C++開(kāi)發(fā)的Dll交互,可以利用.net中System.Runtime.InteropServices命名空間下的StructLayoutAttribute、FieldOffset等特性自定義標(biāo)記結(jié)構(gòu)體的結(jié)構(gòu),CLR會(huì)在結(jié)構(gòu)體傳入或傳出時(shí)自動(dòng)進(jìn)行托管內(nèi)存與非托管內(nèi)存之間內(nèi)存格式的轉(zhuǎn)換。
所以本文其實(shí)是為了后面的博客做服務(wù)的,那就是不安全代碼,這個(gè)看似神秘,實(shí)則鋒利無(wú)比的雙刃劍,盡請(qǐng)期待。
上一篇:Unity3D使用GL實(shí)現(xiàn)圖案解鎖功能
欄 目:C#教程
下一篇:Unity3D Shader實(shí)現(xiàn)掃描顯示效果
本文標(biāo)題:C#如何從byte[]中直接讀取Structure實(shí)例詳解
本文地址:http://www.jygsgssxh.com/a1/C_jiaocheng/4848.html
您可能感興趣的文章
- 01-10Extjs4如何處理后臺(tái)json數(shù)據(jù)中日期和時(shí)間
- 01-10asp.net中XML如何做增刪改查操作
- 01-10C#.NET中如何批量插入大量數(shù)據(jù)到數(shù)據(jù)庫(kù)中
- 01-10C#影院售票系統(tǒng)畢業(yè)設(shè)計(jì)(1)
- 01-10Silverlight將圖片轉(zhuǎn)換為byte的實(shí)現(xiàn)代碼
- 01-10解析C#中的常量及如何在C#編程中定義常量
- 01-10如何使用C#從word文檔中提取圖片
- 01-10c#檢測(cè)文本文件編碼的方法
- 01-10C#實(shí)現(xiàn)從多列的DataTable里取需要的幾列
- 01-10在C#中如何使用正式表達(dá)式獲取匹配所需數(shù)據(jù)


閱讀排行
- 1C語(yǔ)言 while語(yǔ)句的用法詳解
- 2java 實(shí)現(xiàn)簡(jiǎn)單圣誕樹(shù)的示例代碼(圣誕
- 3利用C語(yǔ)言實(shí)現(xiàn)“百馬百擔(dān)”問(wèn)題方法
- 4C語(yǔ)言中計(jì)算正弦的相關(guān)函數(shù)總結(jié)
- 5c語(yǔ)言計(jì)算三角形面積代碼
- 6什么是 WSH(腳本宿主)的詳細(xì)解釋
- 7C++ 中隨機(jī)函數(shù)random函數(shù)的使用方法
- 8正則表達(dá)式匹配各種特殊字符
- 9C語(yǔ)言十進(jìn)制轉(zhuǎn)二進(jìn)制代碼實(shí)例
- 10C語(yǔ)言查找數(shù)組里數(shù)字重復(fù)次數(shù)的方法
本欄相關(guān)
- 01-10C#通過(guò)反射獲取當(dāng)前工程中所有窗體并
- 01-10關(guān)于ASP網(wǎng)頁(yè)無(wú)法打開(kāi)的解決方案
- 01-10WinForm限制窗體不能移到屏幕外的方法
- 01-10WinForm繪制圓角的方法
- 01-10C#實(shí)現(xiàn)txt定位指定行完整實(shí)例
- 01-10WinForm實(shí)現(xiàn)仿視頻播放器左下角滾動(dòng)新
- 01-10C#停止線(xiàn)程的方法
- 01-10C#實(shí)現(xiàn)清空回收站的方法
- 01-10C#通過(guò)重寫(xiě)Panel改變邊框顏色與寬度的
- 01-10C#實(shí)現(xiàn)讀取注冊(cè)表監(jiān)控當(dāng)前操作系統(tǒng)已
隨機(jī)閱讀
- 01-10delphi制作wav文件的方法
- 01-11Mac OSX 打開(kāi)原生自帶讀寫(xiě)NTFS功能(圖文
- 04-02jquery與jsp,用jquery
- 01-10使用C語(yǔ)言求解撲克牌的順子及n個(gè)骰子
- 01-11ajax實(shí)現(xiàn)頁(yè)面的局部加載
- 08-05dedecms(織夢(mèng))副欄目數(shù)量限制代碼修改
- 01-10SublimeText編譯C開(kāi)發(fā)環(huán)境設(shè)置
- 01-10C#中split用法實(shí)例總結(jié)
- 08-05DEDE織夢(mèng)data目錄下的sessions文件夾有什
- 08-05織夢(mèng)dedecms什么時(shí)候用欄目交叉功能?


