为什么要使用二进制数据

通常我们写一个简单的网络通讯软件可能使用的最多的是字符串类型,比较简单,例如发送格式为(head)19|Msg:Heart|100,x,y,z…,在接收端会解析收到的socket数据。

这样通常是完全可行的,但是随着数据量变大,网络吞吐量就变大,可能发送的字符串就不合适了,可能会数据量变大。

举例:

假如你要发送你的年收入和你的坐标,例如你的年收入是一亿两千万(123,456,789)(幸福死了)你的坐标是1.234567,如果通过字符串传输,你的收入就是9位,你的坐标可能你发小数因为精度问题还不准确通常使用二进制发送会大大节省。一个int32是4位,float类型也是4位,这样8位就够了。

看下面的例子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

static void Main(string[] args)

{

    Console.WriteLine("Hello, World!");

    int money = 123456789;

    float x = 1.234567f;

    byte[] moneybyte = BitConverter.GetBytes(money);

    byte[] xbyte = BitConverter.GetBytes(x);

    Console.WriteLine($"moneybyte: {moneybyte.Length},{money} :xbyte: {xbyte.Length},{x}" );

    int moneyget = BitConverter.ToInt32(moneybyte);

    float xget = BitConverter.ToSingle(xbyte);

    Console.WriteLine($"moneyget: {moneyget} :xget: {xget}");

}

输出结果

1

2

3

Hello, World!

moneybyte: 4,123456789 :xbyte: 4,1.234567

moneyget: 123456789 :xget: 1.234567

我们看到对于数字32位占4个字节,这样如果是大量的数据就会很节省,甚至你可以使用int16,或者bool占用更小的字节。

对于大量密集的网络程序使用二进制数据进行发送很必要的。

初步思考如何方便的使用二进制或者封装

是不是有这样的疑问,如果要同步一个数据包含很多类型数据,如何拼接和解析呢,好像二进制没有字符串那么直观和好使用。

比如我要同步的数据是如下数据(通常我们把这种格式称作协议),需要发送结构和解析正确的匹配才能解析。

协议头|发送的大小|我的名字|18|123456789|1.234567|我的介绍|结束

对于二进制如果我们有这样的结构

1

2

3

4

5

6

7

8

9

public struct mydata

{

    public string name;

    public int age;

    public int money;

    public float x;

    public string readme;

      

}

我们可以根据结构体内的属性进行二进制发送就可以了,接收方也有这样的数据结构也进行解析就可以了,这里要注意每个属性的顺序不能是错误的。

网上有一些把结构体或者类打包成二进制的方法,这里就不过多说明了。

使用Protobuf

protobuf就是专门为实现这个而生的,从名字就可以看出来。

Protobuf 的官方 C# 库是 Google.Protobuf,可以通过 NuGet 包管理器来方便的使用。

我们这里就来简单说一下如何使用:

安装

首先vs里创建一个c#控制台程序。

然后可以通过 NuGet安装

第一个协议

我们创建一个Person.proto文件

1

2

3

4

5

6

7

syntax = "proto3";

message Person {

  string name = 1;

  int32 age = 2;

  string email = 3;

}

我们需要把这个proto转成c#可以解析的c#程序

我们可以来到Protobuf库下载执行程序,这个程序可以把proto解析成c#文件。

我们下载好之后:输入指令

1

2

3

F:\Downloads\protoc-29.3-win64\bin>protoc --csharp_out=. Person.proto

F:\Downloads\protoc-29.3-win64\bin>

具体指令可以参考库里的文档

–csharp_out输出cs文件 .是当前路径

执行成功后会有一个Person.cs我们可以放入我们的项目,这样就很容易解析协议了。

生成的cs如下:

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

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

// <auto-generated>

//     Generated by the protocol buffer compiler.  DO NOT EDIT!

//     source: test.proto

// </auto-generated>

#pragma warning disable 1591, 0612, 3021, 8981

#region Designer generated code

using pb = global::Google.Protobuf;

using pbc = global::Google.Protobuf.Collections;

using pbr = global::Google.Protobuf.Reflection;

using scg = global::System.Collections.Generic;

/// <summary>Holder for reflection information generated from test.proto</summary>

public static partial class TestReflection {

  #region Descriptor

  /// <summary>File descriptor for test.proto</summary>

  public static pbr::FileDescriptor Descriptor {

    get { return descriptor; }

  }

  private static pbr::FileDescriptor descriptor;

  static TestReflection() {

    byte[] descriptorData = global::System.Convert.FromBase64String(

        string.Concat(

          "Cgp0ZXN0LnByb3RvIjIKBlBlcnNvbhIMCgRuYW1lGAEgASgJEgsKA2FnZRgC",

          "IAEoBRINCgVlbWFpbBgDIAEoCWIGcHJvdG8z"));

    descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,

        new pbr::FileDescriptor[] { },

        new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {

          new pbr::GeneratedClrTypeInfo(typeof(global::Person), global::Person.Parser, new[]{ "Name", "Age", "Email" }, null, null, null, null)

        }));

  }

  #endregion

}

#region Messages

[global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")]

public sealed partial class Person : pb::IMessage<Person>

#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE

    , pb::IBufferMessage

#endif

{

  private static readonly pb::MessageParser<Person> _parser = new pb::MessageParser<Person>(() => new Person());

  private pb::UnknownFieldSet _unknownFields;

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public static pb::MessageParser<Person> Parser { get { return _parser; } }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public static pbr::MessageDescriptor Descriptor {

    get { return global::TestReflection.Descriptor.MessageTypes[0]; }

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  pbr::MessageDescriptor pb::IMessage.Descriptor {

    get { return Descriptor; }

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public Person() {

    OnConstruction();

  }

  partial void OnConstruction();

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public Person(Person other) : this() {

    name_ = other.name_;

    age_ = other.age_;

    email_ = other.email_;

    _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public Person Clone() {

    return new Person(this);

  }

  /// <summary>Field number for the "name" field.</summary>

  public const int NameFieldNumber = 1;

  private string name_ = "";

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public string Name {

    get { return name_; }

    set {

      name_ = pb::ProtoPreconditions.CheckNotNull(value, "value");

    }

  }

  /// <summary>Field number for the "age" field.</summary>

  public const int AgeFieldNumber = 2;

  private int age_;

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public int Age {

    get { return age_; }

    set {

      age_ = value;

    }

  }

  /// <summary>Field number for the "email" field.</summary>

  public const int EmailFieldNumber = 3;

  private string email_ = "";

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public string Email {

    get { return email_; }

    set {

      email_ = pb::ProtoPreconditions.CheckNotNull(value, "value");

    }

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public override bool Equals(object other) {

    return Equals(other as Person);

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public bool Equals(Person other) {

    if (ReferenceEquals(other, null)) {

      return false;

    }

    if (ReferenceEquals(other, this)) {

      return true;

    }

    if (Name != other.Name) return false;

    if (Age != other.Age) return false;

    if (Email != other.Email) return false;

    return Equals(_unknownFields, other._unknownFields);

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public override int GetHashCode() {

    int hash = 1;

    if (Name.Length != 0) hash ^= Name.GetHashCode();

    if (Age != 0) hash ^= Age.GetHashCode();

    if (Email.Length != 0) hash ^= Email.GetHashCode();

    if (_unknownFields != null) {

      hash ^= _unknownFields.GetHashCode();

    }

    return hash;

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public override string ToString() {

    return pb::JsonFormatter.ToDiagnosticString(this);

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public void WriteTo(pb::CodedOutputStream output) {

  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE

    output.WriteRawMessage(this);

  #else

    if (Name.Length != 0) {

      output.WriteRawTag(10);

      output.WriteString(Name);

    }

    if (Age != 0) {

      output.WriteRawTag(16);

      output.WriteInt32(Age);

    }

    if (Email.Length != 0) {

      output.WriteRawTag(26);

      output.WriteString(Email);

    }

    if (_unknownFields != null) {

      _unknownFields.WriteTo(output);

    }

  #endif

  }

  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {

    if (Name.Length != 0) {

      output.WriteRawTag(10);

      output.WriteString(Name);

    }

    if (Age != 0) {

      output.WriteRawTag(16);

      output.WriteInt32(Age);

    }

    if (Email.Length != 0) {

      output.WriteRawTag(26);

      output.WriteString(Email);

    }

    if (_unknownFields != null) {

      _unknownFields.WriteTo(ref output);

    }

  }

  #endif

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public int CalculateSize() {

    int size = 0;

    if (Name.Length != 0) {

      size += 1 + pb::CodedOutputStream.ComputeStringSize(Name);

    }

    if (Age != 0) {

      size += 1 + pb::CodedOutputStream.ComputeInt32Size(Age);

    }

    if (Email.Length != 0) {

      size += 1 + pb::CodedOutputStream.ComputeStringSize(Email);

    }

    if (_unknownFields != null) {

      size += _unknownFields.CalculateSize();

    }

    return size;

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public void MergeFrom(Person other) {

    if (other == null) {

      return;

    }

    if (other.Name.Length != 0) {

      Name = other.Name;

    }

    if (other.Age != 0) {

      Age = other.Age;

    }

    if (other.Email.Length != 0) {

      Email = other.Email;

    }

    _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);

  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  public void MergeFrom(pb::CodedInputStream input) {

  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE

    input.ReadRawMessage(this);

  #else

    uint tag;

    while ((tag = input.ReadTag()) != 0) {

    if ((tag & 7) == 4) {

      // Abort on any end group tag.

      return;

    }

    switch(tag) {

        default:

          _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);

          break;

        case 10: {

          Name = input.ReadString();

          break;

        }

        case 16: {

          Age = input.ReadInt32();

          break;

        }

        case 26: {

          Email = input.ReadString();

          break;

        }

      }

    }

  #endif

  }

  #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]

  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]

  void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {

    uint tag;

    while ((tag = input.ReadTag()) != 0) {

    if ((tag & 7) == 4) {

      // Abort on any end group tag.

      return;

    }

    switch(tag) {

        default:

          _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);

          break;

        case 10: {

          Name = input.ReadString();

          break;

        }

        case 16: {

          Age = input.ReadInt32();

          break;

        }

        case 26: {

          Email = input.ReadString();

          break;

        }

      }

    }

  }

  #endif

}

#endregion

#endregion Designer generated code

使用

我们开始使用

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

static void Main(string[] args)

{

    Console.WriteLine("Hello, World!");

    var person = new Person

    {

        Age = 18,

        Name = "",

        Email = ""

    };

    byte[] serializedData = person.ToByteArray();

    Console.WriteLine($"serialsize {serializedData.Length} :Serialized Data: "  + BitConverter.ToString(serializedData));

    // 从字节数组反序列化

    var deserializedPerson = Person.Parser.ParseFrom(serializedData);

    Console.WriteLine($"Deserialized Person: Name={deserializedPerson.Name}, Age={deserializedPerson.Age}, Email={deserializedPerson.Email}");

}

代码中我们给person赋值,并通过ToByteArray二进制转化,得到二进制数组后就可以通过网络发送了。

当接收方收到这个二进制数据就可以通过ParseFrom进行解析。

执行结果

1

2

3

Hello, World!

serialsize 2 :Serialized Data: 10-12

Deserialized Person: Name=, Age=18, Email=

我们看到二进制大小是2是因为使用了一种变长编码 (varint) 的优化方案可以看下官方的文档。通常短数据比较多,使用变长编码的方式能够节省一些。


 

更多推荐