https://boogipop.com/2024/02/02/%E3%80%90.NET%20%E5%AE%89%E5%85%A8%E3%80%91ASP.NET%20Deserialization%2001/#04-%E5%BA%8F%E5%88%97%E5%8C%96%E5%92%8C%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F

https://github.com/Y4er/dotnet-deserialization/blob/main/dotnet-serialize-101.md Y4er师傅三年前的文章了

这里简单将要 其实.Net主要就是C# 这么理解也行 .Net是参考java改过来的 其中SDK对应java的JDK

jvm对应clr,java se runtime对应 .net framework,java对应C#

(这个其中的namespace和php里的差不多 所以说学过php和java的再来学这个.Net的话 应该比较好入门)

这里用的是IDEA的Rider来写 没用VS 搭建环境就不写了

序列化

在java中,序列化和反序列化需要实现Serializable接口,在dotnet中则是使用特性的方式进行标记Serializable。

1
2
3
4
5
6
7
[Serializable]  
public class MyObject
{
public int n1;
[NonSerialized] public int n2;
public String str;
}

使用[NonSerialized]来指定不可序列化的字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace NetSerializer
{
[Serializable]
public class MyObject
{
public int n1;
[NonSerialized] public int n2;
public String str;
}

class Program
{
public static void BinaryFormatterSerialize(string file, object o)
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
FileStream fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None);
binaryFormatter.Serialize(fileStream, o);
fileStream.Close();
Console.WriteLine($"serialize object {o} to file {file}.");
}

public static object BinaryFormatterDeserialFromFile(string file)
{
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
object o = formatter.Deserialize(stream);
stream.Close();
return o;
}

static void Main(string[] args)
{
try
{
MyObject myObject = new MyObject();
myObject.n1 = 1;
myObject.n2 = 2;
myObject.str = "jack";

BinaryFormatterSerialize("1.bin", myObject);
MyObject myObject1 = (MyObject)BinaryFormatterDeserialFromFile("1.bin");

Console.WriteLine($"n1:{myObject1.n1}");
Console.WriteLine($"NonSerialized n2:{myObject1.n2}");
Console.WriteLine($"str:{myObject1.str}");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadKey();
}
}
}

image-20240221111317796

反序列化结果

这个n2因为我们在代码中标注了这个 [NonSerialized]字段 所以并没有进行反序列化 所以既然结果是0

image-20240221111554855

在.Net中 用的是using 并不是import

这里的namespace相当于java的package 如果不用namespace{xxx} 这种格式的话 namespace xxx;这种格式的也行

image-20240221111829134

这样写其实也行 都是一个意思 反序列化的结果也是一样的

image-20240221112058974

可以看到对象除了被标记不能被序列化的字段以外全部恢复到了原来的值。查看生成的bin文件,发现序列化之后的数据采用0001 0000开头

Formatter

在上述我们给出的例子中 我们使用的是BinaryFormatter类 这个类表示使用二进制的形式进行序列化 在dotnet中还有其他序列化的类 每个序列化的类都对应了一种序列化的格式

  1. BinaryFormatter 用于二进制格式
  2. SoapFormatter 用于序列化soap格式
  3. LosFormatter 用于序列化 Web 窗体页的视图状态
  4. ObjectStateFormatter 用于序列化状态对象图
  5. XmlSerializer 用于生成xml数据
  6. JsonSerializer 用于生成Json数据

这些类都有个共同点 就是都实现了名为IFormatter、IRemotingFormatter的接口 IRemotingFormatter是用来远程调用的RPC接口 他也实现了IFormatter接口 所以重点看IFormatter接口就行了

image-20240221112921545

在.Net中 这个 :后面就是实现的接口 并且在其中这个sealed的意思是不可继承的意思 简单点就是说不能当父类

他里面的internal参数就是说不能被这个类以外的类调用到 (里面的内部类是可以访问的)

image-20240221113344537

这个里面的get和set的意思是 就是说这个属性可以被getter方法获取到也可以被setter方法来赋值

通过这三个字段,我们可以控制序列化和反序列化时数据的类型、值以及其他信息。

这三个属性的含义

类 字段名 含义用途
SerializationBinder Binder 用于控制在序列化和反序列化期间使用的实际类型
StreamingContext Context 序列化流上下文 其中states字段包含了序列化的来源和目的地
ISurrogateSelector SurrogateSelector 序列化代理选择器 接管formatter的序列化或反序列化处理

BinaryFormatter序列化的生命周期

image-20210420105228965

这个图是反序列化的流程图

当formatter调用Serialize方法的时候,会有以下的生命周期

  1. 首先确定formatter是否有代理选择器,如果有则检查代理选择器要处理的对象类型是否和给定的对象类型一致,如果一致,代理选择器会调用ISerializable.GetObjectData()
  2. 如果没有代理选择器,或者代理选择器不处理该对象类型,则检查对象是否有[Serializable]特性。如果不能序列化则抛出异常。
  3. 检查该对象是否实现ISerializable接口,如果实现就调用其GetObjectData方法。
  4. 如果没实现ISerializable接口就使用默认的序列化策略,序列化所以没标记[NonSerialized]的字段。

而在序列化和反序列化的过程中还有四个回调事件

特性 调用关联的方法时 典型用法
OnDeserializingAttribute 反序列化之前 初始化可选字段的默认值。
OnDeserializedAttribute 反序列化之后 根据其他字段的内容修改可选字段值。
OnSerializingAttribute 序列化之前 准备序列化。 例如,创建可选数据结构。
OnSerializedAttribute 序列化之后 记录序列化事件。

01-代理器

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Permissions;

namespace ConsoleApplication1
{
[Serializable]
public class MyObject : ISerializable
{
public string str { get; set; }
public MyObject()
{
}
//实现了ISerializable接口的类必须包含有序列化构造函数,否则会出错。
protected MyObject(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("MyObject(SerializationInfo info, StreamingContext context)");
str = info.GetString("str");
}

[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData of MyObject.class");
info.AddValue("str", str, typeof(string));
}

[OnDeserializing]
private void TestOnDeserializing(StreamingContext sc)
{
Console.WriteLine("TestOnDeserializing");

}
[OnDeserialized]
private void TestOnDeserialized(StreamingContext sc)
{
Console.WriteLine("TestOnDeserialized");
}
[OnSerializing]
private void TestOnSerializing(StreamingContext sc)
{
Console.WriteLine("TestOnSerializing");
}
[OnSerialized]
private void TestOnSerialized(StreamingContext sc)
{
Console.WriteLine("TestOnSerialized");
}
}
class MySerializationSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
Console.WriteLine("GetObjectData of ISerializationSurrogate");
info.AddValue("str", ((MyObject)obj).str);
}

public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Console.WriteLine("SetObjectData of ISerializationSurrogate");
MyObject m = new MyObject();
m.str = (string)info.GetValue("str", typeof(string));
return m;
}
}
class Program
{
static void Main(string[] args)
{
try
{
MyObject myObject = new MyObject();
myObject.str = "hello";

using (MemoryStream memoryStream = new MemoryStream())
{
// 构建formatter
BinaryFormatter binaryFormatter = new BinaryFormatter();

// 设置序列化代理选择器
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof(MyObject), binaryFormatter.Context, new MySerializationSurrogate());
// 赋值给formatter 这里是否设置代理选择器决定了序列化的生命周期
binaryFormatter.SurrogateSelector = ss;
// 序列化
binaryFormatter.Serialize(memoryStream, myObject);
// 重置stream
memoryStream.Position = 0;
myObject = null;
// 反序列化
myObject = (MyObject)binaryFormatter.Deserialize(memoryStream);
Console.WriteLine(myObject.str); // hello
}

}
catch (Exception e)
{
Console.WriteLine(e.StackTrace);
}
Console.ReadKey();
}
}
}

其实重点就在这个代理器(MySerializationSurrogate)和这个序列化接口(ISerializable) 这个序列化的生命周期与这两个有关

这是一个使用了SurrogateSelector代理选择器的序列化例子,输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TestOnSerializing
GetObjectData of ISerializationSurrogate
TestOnSerialized
TestOnDeserializing
SetObjectData of ISerializationSurrogate
TestOnDeserialized
hello

//------------------

OnDeserializing方法(序列化开始)
代理器的GetObjectData,获取属性
OnSerialized方法(序列化结束)
OnDeserializing(反序列化开始)
代理器的SetObjectData,设置属性
OnDeserialized(反序列化结束)

02-非代理器和ISerializable模式

优先级是这个代理器 当代理器没有进行设置的时候 就会顺着去看ISerializable模式

image-20240222162839601

将这里注释掉 就是不设置代理器 其输出是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
TestOnSerializing
GetObjectData of MyObject.class
TestOnSerialized
TestOnDeserializing
MyObject(SerializationInfo info, StreamingContext context)
TestOnDeserialized
hello


//----------------------

OnSerializing(序列化开始)
由于实现了ISerializable接口,因此会调用本身的GetObjectData方法
OnSerialized(序列化结束)
OnDeserializing(反序列化开始)
类的实例化。protected MyObject(SerializationInfo info, StreamingContext context),在反序列化时由于实现了ISerializable接口,因此会调用构造方法,这个也是必须要加的,假如不加的话会报错
OnDeserialized(反序列化结束)

03-非代理器和非ISerializable模式

将ISerializable接口给除去 输出结果如下

1
2
3
4
5
6
TestOnSerializing
TestOnSerialized
TestOnDeserializing
TestOnDeserialized
hello

就是单纯的4个回调模式了

04-SerializationInfo

MyObject的构造函数

1
2
3
4
5
6
//实现了ISerializable接口的类必须包含有序列化构造函数,否则会出错。
protected MyObject(SerializationInfo info, StreamingContext context)
{
Console.WriteLine("MyObject(SerializationInfo info, StreamingContext context)");
str = info.GetString("str");
}

跟进SerializationInfo这个类中

image-20240222163800688

SerializationInfo info变量中表示序列化流的信息,对象的类型和值都存储在其中,查看类定义

可见其存储了对象类型、成员个数、程序集名称、类型名称等,还有一些AddValue的重载用于添加类实例字段变量键值对

(为什么我们要做这个构造函数? 里面给的东西其实和代理器的代码是差不多的 跟进这个我们构造的代理器)

image-20240222164049951

在我们构造的代理器其中的Get和Set方法 其实这个Get对应的就是我们继承的ISerializable接口下的Get方法 然后这个Set对应就是我们反序列化类的构造函数 (这里就是为什么用这个SerializationInfo类的原因 里面的函数方法是类似的)

命令执行-01

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Diagnostics;

namespace ConsoleApplication1;

public class Command
{
public static void Main(string[] args)
{
var start = Process.Start("cmd.exe", "/c calc");
// var process = new Process();
// process.StartInfo.FileName = "cmd.exe";
// process.StartInfo.Arguments = "/c calc";
// process.Start();
}
}

类似java的runtime 在Start函数中 有两个关键的参数 就是这个FileName和Arguments

image-20240222170519803

其运行的时候会先弹出cmd窗口 然后再执行calc 执行完后在关闭这个cmd窗口 并且这里也是没有回显的 接下来就解决这几个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Diagnostics;

namespace ConsoleApplication1;

public class Command
{
public static void Main(string[] args)
{
var process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.Arguments = "/c whoami";
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.UseShellExecute = false;
process.Start();
var processStandardOutput = process.StandardOutput.ReadToEnd();
Console.WriteLine(processStandardOutput);
}
}

image-20240222170918255

这样的话cmd窗口就不会弹出 回显也有了

其中RedirectStandardOutput和UseShellExecute需要注意一下

  • RedirectStandardOutput:
    • 当设置为 true 时,表示重定向标准输出流,即将进程的标准输出流(通常是在控制台窗口中显示的信息)连接到 Process.StandardOutput 流中,以便你的程序可以读取进程的输出。
  • UseShellExecute:
    • 当设置为 false 时,表示不使用操作系统的 shell 启动进程。相反,它允许你直接启动可执行文件,命令行等,而不需要借助 shell。(不弹出cmd窗口的关键)

命令执行-02

ObjectDataProvider 这个类也是可以进行命令执行的 其实最终也是调用到这个Process类才能执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;
using System.Diagnostics;
using System.Windows.Data;

namespace WpfApplication2;

public class Command
{
public static void Main(string[] args)
{
var objectDataProvider = new ObjectDataProvider();
objectDataProvider.MethodName = "Start";
objectDataProvider.MethodParameters.Add("cmd.exe");
objectDataProvider.MethodParameters.Add("/c calc");
objectDataProvider.ObjectInstance = new Process();
}
}

创建项目的时候要选择wpf

接下来分析一下是怎么样能命令执行的

image-20240223100109274

跟进ObjectDataProvider类ObjectInstance方法中

image-20240223100208142

跟进Refresh方法中

image-20240223100248546

跟进到这个QueryWorker方法中

image-20240223100321621

在这个方法中 就会一直循环来给我们刚开始传入的值进行赋值

image-20240223100529141

然后跟进这个方法中

image-20240223100557318

这里就获取到了Process类了