2023年3月

前言

在计算机运行过程中,由于种种原因致使数据在存储过程中可能出现差错。为了能及时发现错误并及时纠正错误,通常使用一些编码方式。

奇偶校验

奇偶校验是一种添加一个
奇偶位
用来指示之前的数据中包含有
奇数
还是
偶数

1
的检验方式。

对于一个二进制数:
\(b_nb_{n-1}...b_2b_1\)
,添加一个校验位
s
,采取

校验,即校验位使新数据中的1的个数为偶数。

新数据:
\(b_nb_{n-1}...b_2b_1s\)


\(b_n \oplus b_{n-1} \oplus ... \oplus b_2 \oplus b_1 \oplus s = 0\)
,则
\(s = b_n \oplus b_{n-1} \oplus ... \oplus b_2 \oplus b_1\)

在传输过程中,如果只有
1位

奇数个位
)发生了改变(包括
校验位
),那么将可以检测出来错误。但是如果有
偶数个位
发生了改变,那么校验位将是正确的,因此不能检测错误。而且,即使出现了错误,也不知道是哪一位出现了错误,数据必须整体丢弃并且重新传输。

汉明码

简介

汉明码的实质上是
多重
奇偶校验,通过巧妙的
分组
,实现了校验并纠正一位错误的能力。

编码纠错理论

​ 任何一种编码是否具有检测能力和纠错能力,都与编码的最小距离有关。所谓编码最小距离,是指在一种编码系统中,任意两组合法代码之间的最少二进制位数的差异。

根据纠错理论得:

$ L -1 = D + C \ 且 \ (D \ge C)$

即编码最小距离
L
越大,则其检测错误的位数D越大,纠正错误的位数
C
也越大,且纠错能力恒小于或等于检错能力。

所以,如果我们在信息编码中增加若干位检测位,增大
L
,显然便能提高检错和纠错能力。
汉明码
就是根据这一理论提出的具有一位纠错能力的编码。

关于编码最小距离

例1.1

假设我们的一种
编码系统
是所有的3位二进制数,即
\(设集合 S =\left\{ 000,001,010,011,100,101,110,666666 \right\}\)

那么对于任何一个数据,比如我们传输
\(010\)
这个数据,它发生错误会改变为:

  1. 第一位
    0
    变为
    1

    \(110\)
  2. 第二位
    1
    变为
    0

    \(000\)
  3. 第三位
    0
    变为
    1

    \(011\)

显然,这三种数据都在集合当中,我们检验不出错误。

此时就是,
\(L = 1\)
, 那么
\(0 = D + C\)

例1.2

\(设集合 S =\left\{ 001,010,100 \right\}\)

可以看出,我们的编码最小距离为2,
\(L = 2\)
,那么
\(1 = D+C\)
,那么
\(D = 1, C = 0\)
。即,可以检一位错,不能纠错。

比如,我们传输数据
\(010\)

  1. 第一位
    0
    变为
    1

    \(110\)
  2. 第二位
    1
    变为
    0

    \(000\)
  3. 第三位
    0
    变为
    1

    \(011\)

但是,
\(110,000,011\)
均不在集合S中,所以我们可以判断这是出错了,那怎么纠错呢?

对于出错数据
\(110\)
,它的可能正确数据:

  1. 第一位
    0
    变为
    1

    \(010\)
  2. 第二位
    0
    变为
    1

    \(100\)
  3. 第三位
    1
    变为
    0

    \(666666\)
    (不存在S中)

所以是会有两种情况,故,无法纠错。

例1.3

\(设集合 S =\left\{ 000,666666 \right\}\)

则,
\(L = 3, D = 1 \;且 \;C= 1 \;或\; D = 2\;且\; C = 0\)

核心公式

设欲检测的二进制代码为
n
位,为使其具有纠错能力,需增添k位检测位,组成
n + k
位的代码。为了能准确对错误定位以及指出代码没错,新增添的检测位数
k
应满足:

\(2^k \ge n + k + 1\)

变换一下:

$2^k - 1\ge n + k $

k
位检测位可以提供
\(2^k\)
种状态,
减去一种正确
的状态,即,
所有错误的状态应该包括分别每一位出错的情况
,这里每一位出错包括检测位,所以就是
\(n + k\)

这样是不是更好理解。

由此求出不同代码长度 n,所需检测位的位数 k:

n K(最小)
1 2
2~4 3
5~11 4
12~26 5
27~57 6
58~120 7

分组

k的位数确定后,便可由它们所承担的检测任务设定它们在传送代码中的位置及它们的取值。


\(n + k\)
位代码自左至右依次编为第
\(1,2,3,···,n + k\)
,而将
\(k\)
位检测位记作
\(C_i(i = 1,2,4,8,···)\)
,分别安插在
\(n + k\)
位代码编号的第
\(1,2,4,8,···,2^{k-1}位上\)

  • \(C_1\)
    :检测的
    \(g_1\)
    小组包含1,3,5,7,9,11,··· 位。
  • \(C_2\)
    :检测的
    \(g_2\)
    小组包含2,3,6,7,10,11,14,15,··· 位。
  • \(C_4\)
    :检测的
    \(g_3\)
    小组包含4,5,6,7,12,13,14,15,··· 位。
  • ··········

这种小组的划分有以下特点:

  1. 每个小组
    \(g_i\)
    有一位且仅有一位为它所独占,这一位是其他小组所没有的,即
    \(g_i\)
    小组独占第
    \(2^{i-1}\)
    位(
    \(i = 1,2,3,···\)
    )。
  2. 每两个小组
    \(g_i\)

    \(g_j\)
    共同占有一位是其他小组没有的,即每两个小组
    \(g_i\)

    \(g_j\)
    共同占有第
    \(2^{i - 1} + 2^{j - 1}\)
    位(
    \(i,j = 1,2,···\)
    )。
  3. 每3个小组
    \(g_i,g_j和g_l共同占有第\)
    \(2^{i -1} + 2^{j - 1} + 2^{l - 1}\)
    位,是其他小组所没有的。
  4. 依次类推。

特点似乎有点不明所以,总结起来就是:

  • \(g_1小组\)
    : 位数为
    \(xxx1\)
    的二进制数表示。
  • \(g_2小组\)
    : 位数为
    \(xx1x\)
    的二进制数表示。
  • \(g_3小组\)
    : 位数为
    \(x1xx\)
    的二进制数表示。
  • ······

稍后,我们会明白这样设计的缘由。


现在我们需要传递一个4位二进制数,记为
\(b_4b_3b_2b_1\)

那么根据前面的核心公式
\(2^k \ge n + k + 1\)
,可以求出最小的 k为3。

二进制序号 1 2 3 4 5 6 7
名称 \(C_1\) \(C_2\) \(b_4\) \(C_4\) \(b_3\) \(b_2\) \(b_1\)

则,根据配偶原则:

\(C_1 \oplus b_4 \oplus b_3 \oplus b_1 = 0\)

\(C_2 \oplus b_4 \oplus b_2 \oplus b_1 = 0\)

\(C_4 \oplus b_3 \oplus b_2 \oplus b_1 = 0\)

例2


\(b_4b_3b_2b_1 = 1101\)
,则:

\(C_1 = b_4 \oplus b_3 \oplus b_1 = 1 \oplus 1 \oplus 1= 1\)

\(C_2 = b_4 \oplus b_2 \oplus b_1 = 1 \oplus 0 \oplus 1 =0\)

\(C_4 = b_3 \oplus b_2 \oplus b_1 = 1 \oplus 0 \oplus 1 = 0\)

故 0101的汉明码应为
\(C_1C_2b_4C_4b_3b_2b_1\)
,即1010101。

纠错过程

汉明码的纠错过程实际上就是对配偶原则(或者奇)的检验,根据新数据的状态,便可直接指出错误的位置。直接看例子:

例3

已知,传送的正确汉明码是
\(1010101\)
(配偶原则),传送后接受到的汉明码为
\(1010666666\)

二进制序号 1 2 3 4 5 6 7
发送的汉明码 1 0 1 0 1 0 1
接收的汉明码 1 0 1 0 1 1 1

检测:

  • \(P_4 = 4 \oplus 5 \oplus 6 \oplus 7 = 0 \oplus 1 \oplus 1 \oplus 1 = 1\)
  • \(P_2 = 2 \oplus 3 \oplus 6 \oplus 7 = 0 \oplus 1 \oplus 1 \oplus 1 = 1\)
  • \(P_1 = 1 \oplus 3 \oplus 5 \oplus 7 = 1 \oplus 1 \oplus 1 \oplus 1 = 0\)

可见,
\(P_4 和 P_2\)
都不为 0,显然出错了。

那如何找出错误呢?

这里是假设只出现一位错误,因为此时的
\(L = 3\)
,只能检一位错,纠一位错。

  • 因为
    \(P_1 = 0\)
    ,所以可以认为
    \(1,3,5,7\)
    都没有错。
  • \(P_4 = 1,P_2 = 1\)
    ,说明
    \(4,5,6,7\)
    中有一位错,
    \(2,3,6,7\)
    中有一位错。
  • 那现在错误的范围就缩小到了,
    \(4,6\)
    中有一位是错的,
    \(2,6\)
    中有一位错的。
  • 显然,第6位是错的。

此时,更加
巧合
的是:二进制数
\(P_4P_2P_1 = 110\)
恰好是
\(6\)

换句话说,
检测出的信息
所表示的

就是出错的位置。

韦恩图

我们用韦恩图来表示一下刚刚找错误位的过程:

刚刚找
6
的过程不就是:
\(\neg P_1 \cap P_2 \cap P_4\)
,这样十分巧妙地利用集合就找出了错误的位。

这样可以理解之前设计的原因了吧。

参考文献

维基百科

唐朔飞. 计算机组成原理[M]. 第3版. 高等教育出版社, 2020.

1、创建表

SqlSugar支持了3种模式的建表(无实体建表、实体建表,实体特性建表),非常的灵活

可以多个数据库 MYSQL MSSQL ORACLE SQLITE PGSQL 等用同一语法创建数据库,不需要考虑数据库的兼容性

中间标准:

string  大文本

5.1.3.44-preview06  推荐

[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]

string 设置长度的字符串

[SugarColumn(Length=10)]

public string FieldName{ get; set; }

int 整数

public int FieldName{ get; set; }

short 整数小

public short FieldName{ get; set; }

long 大数字

public long FieldName{ get; set; }

bool  真假

public bool FieldName{ get; set; }

decimal  默认

public decimal  FieldName{ get; set; }

decimal  自定义

//18,2  18,4 18,6 这几种兼容性好

[SugarColumn(Length=18,DecimalDigits=2)]

public decimal  FieldName{ get; set; }

DateTime 时间

public DateTime FieldName{ get; set; }

枚举 (数据库存int)

public 枚举 FieldName{ get; set; }

byte[] 二进制

public byte[]  FileInfo{get;set;}

建议:升级到 SqlSugarCore 5.1.3.46-preview09 及以上

对多库支持了比较好

SqlServer特殊配置:和他库不同一般选用Nvarchar,可以使用这个配置让他和其他数据库区分(其他库是varchar)

DbType = SqlSugar.DbType.SqlServer,


ConnectionString ="字符串",


IsAutoCloseConnection =
true
,


MoreSettings=
new
ConnMoreSettings() {



SqlServerCodeFirstNvarchar=
true
,



}

1.1通过特性建表

我们可以通过创建实体来进行建表

public class CodeFirstTable1
{
        [SugarColumn(IsIdentity = true, IsPrimaryKey = true)]
        public int Id { get; set; } 
        public string Name { get; set; }
        //ColumnDataType 自定格式的情况 length不要设置 (想要多库兼容看4.2和9)
        [SugarColumn(ColumnDataType = "Nvarchar(255)")]
        public string Text { get; set; }
        [SugarColumn(IsNullable = true)]//可以为NULL
        public DateTime CreateTime { get; set; }
}
 
/***创建单个表***/
db.CodeFirst.SetStringDefaultLength(200).InitTables(typeof(CodeFirstTable1));//这样一个表就能成功创建了
/***手动建多个表***/
db.CodeFirst.SetStringDefaultLength(200)
.InitTables(typeof(CodeFirstTable1),typeof(CodeFirstTable2)); 

建表特性如下

名称 描述
IsIdentity 是否创建自增标识
IsPrimaryKey 是否创建主键标识
ColumnName 创建数据库字段的名称(默认取实体类属性名称)
ColumnDataType

创建数据库字段的类型

用法1: “varchar(20)” 不需要设置长度

用法2:      不设置该参数   系统会根据C#类型自动生成相应的数据库类型

用法3:       多库兼容可以用 :看标题9

IsIgnore ORM不处理该列
ColumnDescription 备注 表注释 (新版本支持XML文件)
Length 长度 设成10会生成   xxx类型(10), 没括号的不设置
IsNullable 是否可以为null默为false
DecimalDigits 精度 如 decimal(18,2) length=18,DecimalDigits=2
OracleSequenceName 设置Oracle序列,设置后该列等同于自增列
OldColumnName 修改列名用,这样不会新增或者删除列
IndexGroupNameList 已弃用 ,新用法看文档4.3
UniqueGroupNameList 已弃用, 新用法看文档4.3

注意:有2个属性用处不同

DefaultValue

IsOnlyIgnoreInsert

DefaultValue=默认值 用来建表设置字段默认值

IsOnlyIgnoreInsert=true  插入数据时取默认值

很多情况需要2个一起使用

如果只建表不插入数据用1个

如果建表并且插入数据用2个

2.2无特性建表

如果我们的实体不需要加特性,那么我们可以通过特性方式建表

SugarClient db = new SqlSugarClient(new ConnectionConfig()
{
    DbType = DbType.SqlServer,
    ConnectionString = Config.ConnectionString3,
    InitKeyType = InitKeyType.Attribute,
    IsAutoCloseConnection = true,
    ConfigureExternalServices = new ConfigureExternalServices()
    {
        EntityService = (s, p) =>
        {
            //如果是Order实体进行相关配置
            p.IfTable<Order>()
            .UpdateProperty(it => it.id, it =>
            {
                it.IsIdentity = true;
                it.IsPrimarykey = true;
            })
            .UpdateProperty(it => it.Name, it => {
                it.Length = 100;
                it.IsNullable = true;
  
            })
            .OneToOne(it => it.Item, nameof(Order.ItemId));
              
            //如果Custom实体进行相关配置
             p.IfTable<Custom>()
             .UpdateProperty(it => it.id, it =>
             {
                it.IsIdentity = true;
                it.IsPrimarykey = true;
             })
              .UpdateProperty(it => it.Text, it => {
                it.DataType= StaticConfig.CodeFirst_BigString;//支持多库的MaxString用法
              })
              
                          
            //可以结合全局逻辑一起使用,下面的和上面的有冲突的话,下面会覆盖上面的
               
              
            //统一设置 nullable等于isnullable=true
            //低版本C#看标题2.2 
             if(p.IsPrimaryKey==false&&new NullabilityInfoContext()
                         .Create(c).WriteState is NullabilityState.Nullable)
             {
                           p.IsNullable = true;
             }
              
               
               
        }
    }
});
//性能说明:
//EntityService 相同实体只会执行一次性不需太操作

1.3 无实体建表

功能与实体建类一模一样,如果使用SqlSugar中间标准可以支持多个数据库一套代码建表

 var type = db.DynamicBuilder().CreateClass("UnitEntityA",
                new SugarTable()
                {
                    TableDescription = "表备注",
                    //DisabledUpdateAll=true 可以禁止更新只创建
                }
            )
           .CreateProperty("Id", typeof(int), new SugarColumn() { IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "列备注" }) 
           .CreateProperty("Name", typeof(string), new SugarColumn() {Length=200, ColumnDescription = "列备注" })
           .BuilderType();
 
            db.CodeFirst.InitTables(type);

3、数据库维护

SqlSugar有一套数据库维护API,并且能够很好的支持多种数据库,例如备份数据库等常用功能

//例1 获取所有表
var tables = db.DbMaintenance.GetTableInfoList(false);//true 走缓存 false不走缓存
foreach (var table in tables)
{
     Console.WriteLine(table.Description);//输出表信息
      
     //获取列信息
     //var columns=db.DbMaintenance.GetColumnInfosByTableName("表名",false);
}
 
//例2
db.DbMaintenance.IsAnyTable("tablename",false)//验证表名是否缓存不走缓存

所以API

GetDataBaseList 获取所有数据库名称 List
GetViewInfoList 查询所有视图 List
GetTableInfoList 获取所有表,查询所有的表 (GetTableInfoList(是否缓存)) List
GetColumnInfosByTableName

获取列根据表名,获取字段,字段信

息GetColumnInfosByTableName(表名,是否缓存)

List
GetIsIdentities 获取自增列 List
GetPrimaries 获取主键 List
IsAnyTable 表是否存在,判断表存不存在 ( IsAny(表名,是否缓存)) bool
IsAnyColumn 列是否存在 bool
IsPrimaryKey 主键是否存在 bool
IsIdentity 自增是否存在 bool
IsAnyConstraint 约束是否存在 bool
DropTable 删除表 bool
TruncateTable 清空表 bool
CreateTable

看标题 1.1,1.2,1.3

bool
AddColumn 添加列 bool
UpdateColumn 更新列 bool
AddPrimaryKey 添加主键 bool
DropConstraint 删除约束 bool
BackupDataBase 备份库 bool
DropColumn 删除列 bool
RenameColumn 重命名列 bool
AddDefaultValue 添加默认值 bool
AddTableRemark 添加表描述,表注释 bool
AddColumnRemark 添加列描述,表注释 bool
DeleteColumnRemark 删除列描述,表注释 bool
RenameTable 重命名表 bool
CreateIndex 创建索引,唯一约束(唯一索引) bool
IsAnyIndex 索引是否存在 bool
GetIndexList 获取所有索引
GetProcList 获取所有存储过程

2、跨库支持

可以自动识别在哪个库

实体

 [TenantAttribute("1")]//对应ConfigId
  public class C1Table
  {
     public string Id { get; set; }
  }
    
  [TenantAttribute("2")]
  public class C2Table
  {
      public string Id { get; set; }
  }

查询

//通过ConfigId进行数据库区分
var db = new SqlSugarClient(new List<ConnectionConfig>()
{
 //这儿声名所有上下文都生效
 new ConnectionConfig(){ConfigId="0",DbType=DbType.SqlServer,ConnectionString=..,IsAutoCloseConnection=true},
 new ConnectionConfig(){ConfigId="1",DbType=DbType.MySql,ConnectionString=..,IsAutoCloseConnection=true  }
});
//自动跨库联表查询
var query5 = db.QueryableWithAttr<Order>()//如果MySql和SqlServer自动支持同服务器的跨库联表查询
         .LeftJoin<Custom>   ((o, cus ) => o.CustomId == cus.Id)//多个条件用&&
         .LeftJoin<OrderDetail> ((o, cus, oritem) => o.Id == oritem.OrderId)
         .Where(o => o.Id == 1)  
         .Select((o, cus , oritem) => new ViewOrder { Id = o.Id, CustomName = cus.Name })
         .ToList();  
//手动跨库联表查询 ,这种方式结合dblink可以跨服务器
var query5 = db.Queryable().As("xxx.表名")
         .LeftJoin<Custom>   ((o, cus ) => o.CustomId == cus.Id ,"yyyy.表名") 
         .LeftJoin<OrderDetail> ((o, cus, oritem) => o.Id == oritem.OrderId ,"zzzz.名表")
         .Where(o => o.Id == 1)  
         .Select((o, cus , oritem) => new ViewOrder { Id = o.Id, CustomName = cus.Name })
         .ToList(); 

插入

db.InsertableWithAttr(list).Execommand()

更新

db.UpdateableWithAttr(list).Execommand()

删除

db.UpdateableWithAttr(list).Execommand() 

只要实体配置了数据库,就不要考虑换库了,直接使用,并且支持事务

3、过滤器

SqlSugar支持了全新的过滤器,可以是接口,集成该接口的类都生效,支持多表查询

db.QueryFilter
.AddTableFilter<IDeletedFilter>(it => it.IsDeleted==false)//IDeletedFilter是自定义接口,继承这个接口的实体有效
.AddTableFilterIF<ITenantFilter>(isAdmint==false,it=>it.OrgId==用户OrgId);//ITenantFilter自定义接口
 
//用例1:单条语句清空,只影响当前语句
db.Queryable<Order>().ClearFilter().ToList();//所有过滤器都无效
db.Queryable<Order>().ClearFilter<IDeletedFilter>().ToList();//只有IDeletedFilter过滤器无效
db.Queryable<Order>().ClearFilter<IDeletedFilter,ITenantFilter>().ToList();//IDeletedFilter+ITenantFilter无效
   
//用例2:当前上下文清空 ,不会影响其他请求,只是当前请求清空
db.QueryFilter.Clear();
db.QueryFilter.Clear<IDeletedFilter>(); 
 
//用例3:清空并还原 ,不会影响其他请求,只是当前请求清空
db.QueryFilter.ClearAndBackup();//有多个重载 ClearAndBackup<T,T2>();
db.Queryable<Order>().ToList();
db.QueryFilter.Restore();//还原过滤器 (适合下面代码还需要过滤器情况)

4、子查询升级

1. ToList() 可以直接查询一个对象

2. First()  可以直接查单个对象

3.ToList(it=>it.Id) 可以查List<int>一个字段集合

4..SelectStringJoin(z => z.Name, ",") 子查询将一列用逗号拼接成字符串

 var list=db.Queryable<Order>()
 .Select(it => new
         {
           CustomId = it.CustomId,
           OrderId = it.Id,
           OrderName = it.Name,
           CustomList = SqlFunc.Subqueryable<Custom>().Where(c => c.Id == it.CustomId).ToList()
         })
        .ToList();

5、自定义类型支持

5.1 自定义类型转换器

下面只是讲解怎么定义转换器,ORM自带的功能就包含下面功能,只是用来讲解

public class DictionaryConvert : ISugarDataConverter
{
public SugarParameter ParameterConverter<T>(object value, int i)
{
//该功能ORM自带的IsJson就能实现这里只是用这个用例来给大家学习
var name = "@myp" + i;
var str = new SerializeService().SerializeObject(value);
return new SugarParameter(name, str);
}

public T QueryConverter<T>(IDataRecord dr, int i)
{
//打断点调试
//该功能ORM自带的IsJson就能实现这里只是用这个用例来给大家学习
var str = dr.GetValue(i) + "";
return new SerializeService().DeserializeObject<T>(str);
}
}
//使用自定义转换器
[SugarColumn(ColumnDataType="varchar(2000)",SqlParameterDbType=typeof(DictionaryConvert))]
public Dictionary<string, object> DcValue { get; set; }//5.1.3.53-preview08 

现有类型支持

5.1 json类型

https://www.donet5.com/Home/Doc?typeId=1232

5.2 枚举类型

int存储:直接用就行了

public DbType DcValue { get; set; }

string存储:高版本如下写法

[SugarColumn(ColumnDataType="varchar(20)",SqlParameterDbType=typeof(EnumToStringConvert))]
public DbType DcValue { get; set; }

3.数据库独有类型支持
看左边菜单 【数据库特性】 该菜单下面有 SqlServer菜单或者MySql菜单等 , 针对不同数据库都有专门的介绍

总结

SqlSugar在2021年到2022年大量的开源应用使用了SqlSugar,带动了SqlSugar的快速发展,我这边要感谢大家

Admin.NET通用管理平台

ZrAdminNetCore 后台

管理Yi框架(Ruoyi Vue)

SimpleAdmin (new)

vboot-netmagic.net (Vue2.0)

网关采集系统(Blazor)

RuYiAdmin

CoreShop商城

Blog.Core

YuebonCore

企业级框架Furion

WebFirst

腾讯APIJSON.NET

WaterCloud微服务

ViperFamilyBucket应用框架通用后台

SmartSqlWMS仓库管理系统a

pevolo-apiFytSoaCms

开源项目地址:
https://www.donet5.com/Home/Doc?typeId=1215

喜欢SqlSugar的可以在下面源码点个赞,点个关注

SqlSugar源码:
https://github.com/DotNetNext/SqlSugar/

大家好,最近我们重新开发了园子的博客备份功能,今天发布第一个预览版,欢迎大家试用。

点击博客后台侧边栏的
博客备份
进入新版博客备份:

点击
创建备份
按钮创建博客备份任务(目前每天只能创建一次备份),待备份任务完成后就可以下载备份文件(文件是包含自己博客所有博文的 SQLite 数据库文件压缩包)。

园子的 vscode 插件(
博客园Cnblogs客户端
)也集成了博客备份功能,不仅可以查看通过博客后台下载的备份文件,而且可以直接在 vscode 插件中操作博客备份的创建、下载、查看。

点击
博客备份
右侧的加号按钮创建备份任务

待备份任务完成后,点击备份任务右侧的下载按钮将备份文件下载到本地

待下载完成后,就可以在 vscode 中本地查看备份文件中的博文内容


目前的这个新版预览版还很简单,但这一微步是一个重要的起步——我们要努力让用户更方便地掌控自己的数据。

protobuf

protobuf概述

protobuf简介

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化 。它很适合做
数据存储

RPC 数据交换格式
。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。

  1. protobuf是类似与json一样的数据描述语言(数据格式)

  2. protobuf非常适合于RPC数据交换格式

注意:
protobuf
本身并不是和
gRPC
绑定的。它也可以被用于非RPC场景,如存储等

protobuf的优劣势

1)优势:

  1. 序列化后体积相比Json和XML很小,适合网络传输

  2. 序列化反序列化速度很快,快于Json的处理速度

  3. 消息格式升级和兼容性还不错

  4. 支持跨平台多语言

2)劣势:

  1. 应用不够广(相比xml和json)

  2. 二进制格式导致可读性差

  3. 缺乏自描述

protoc安装(windows)

protoc就是protobuf的编译器,它把proto文件编译成不同的语言

下载安装protoc编译器(protoc)

下载protobuf:
https://github.com/protocolbuffers/protobuf/releases/download/v3.20.1/protoc-3.20.1-win64.zip

解压后,将目录中的 bin 目录的路径添加到系统环境变量,然后打开cmd输入
protoc
查看输出信息,此时则安装成功

安装protocbuf的go插件(protoc-gen-go)

由于protobuf并没直接支持go语言需要我们手动安装相关插件

protocol buffer编译器需要一个插件来根据提供的proto文件生成 Go 代码,Go1.16+要使用下面的命令安装插件:

 go install google.golang.org/protobuf/cmd/protoc-gen-go@latest  // 目前最新版是v1.3.0

安装grpc(grpc)

 go get -u -v google.golang.org/grpc@latest    // 目前最新版是v1.53.0

安装grpc的go插件(protoc-gen-go-grpc)

说明:在
google.golang.org/protobuf
中,
protoc-gen-go
纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能,所以如果要生成grpc相关的代码需要安装grpc-go相关的插件:
protoc-gen-go-grpc

 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是v1.3.0

protobuf语法

protobuf语法

  • 类型:类型不仅可以是标量类型(
    int

    string
    等),也可以是复合类型(
    enum
    等),也可以是其他
    message

  • 字段名:字段名比较推荐的是使用下划线/分隔名称

  • 字段编号:一个message内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名

  • 字段规则:消息字段可以是以下字段之一


    • singular
      :格式正确的消息可以有零个或一个字段(但不能超过一个)。使用 proto3 语法时,如果未为给定字段指定其他字段规则,则这是默认字段规则

    • optional
      :与
      singular
      相同,不过可以检查该值是否明确设置

    • repeated
      :在格式正确的消息中,此字段类型可以重复零次或多次。系统会保留重复值的顺序

    • map
      :这是一个成对的键值对字段

  • 保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,编译器就会报错

简单语法

proto文件基本语法

 syntax = "proto3"; // 指定版本信息,不指定会报错
 package pb;// 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
    string name = 1;   // 名字
    int32  age = 2 ;   // 年龄
 }
 
 enum test{
 int32 age = 0;
 }

protobuf消息的定义(或者称为描述)通常都写在一个以 .proto 结尾的文件中:

  1. 第一行指定正在使用
    proto3
    语法:如果不这样做,协议缓冲区编译器将假定正在使用proto2(这也必须是文件的第一个非空的非注释行)

  2. 第二行package指明当前是pb包(生成go文件之后和Go的包名保持一致)

  3. message关键字定义一个Person消息体,类似于go语言中的结构体,是包含一系列类型数据的集合。


    • 许多标准的简单数据类型都可以作为字段类型,包括
      bool

      int32

      float

      double
      ,和
      string

    • 也可以使用其他message类型作为字段类型。

在message中有一个字符串类型的value成员,该成员编码时用1代替名字。在json中是通过成员的名字来绑定对应的数据,但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,能够快速传输,缺点是不利于阅读。

message常见的数据类型与go中类型对比

.proto类型 Go类型 介绍
double float64 64位浮点数
float float32 32位浮点数
int32 int32 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。
int64 int64 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。
uint32 uint32 使用可变长度编码。
uint64 uint64 使用可变长度编码。
sint32 int32 使用可变长度编码。符号整型值。这些比常规int32s编码负数更有效。
sint64 int64 使用可变长度编码。符号整型值。这些比常规int64s编码负数更有效。
fixed32 uint32 总是四字节。如果值通常大于228,则比uint 32更有效
fixed64 uint64 总是八字节。如果值通常大于256,则比uint64更有效
sfixed32 int32 总是四字节。
sfixed64 int64 总是八字节。
bool bool 布尔类型
string string 字符串必须始终包含UTF - 8编码或7位ASCII文本
bytes []byte 可以包含任意字节序列

protobuff语法进阶

message嵌套

messsage除了能放简单数据类型外,还能存放另外的message类型:

 syntax = "proto3"; // 指定版本信息,不指定会报错
 package pb;// 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
    string name = 1;  // 名字
    int32  age = 2 ;  // 年龄
    // 定义一个message
    message PhoneNumber {
   string number = 1;
   int64 type = 2;
 }
 PhoneNumber phone = 3;
 }

message成员编号,可以不从1开始,但是不能重复,不能使用19000 - 19999

repeated关键字

repeadted关键字类似与go中的切片,编译之后对应的也是go的切片,用法如下:

 syntax = "proto3"; // 指定版本信息,不指定会报错
 package pb;// 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
    string name = 1;   // 名字
    int32  age = 2 ;   // 年龄
    // 定义一个message
    message PhoneNumber {
   string number = 1;
   int64 type = 2;
 }
 repeated PhoneNumber phone = 3;
 }

默认值

解析数据时,如果编码的消息不包含特定的单数元素,则解析对象对象中的相应字段将设置为该字段的默认值

不同类型的默认值不同,具体如下:

  • 对于字符串,默认值为空字符串

  • 对于字节,默认值为空字节

  • 对于bools,默认值为false

  • 对于数字类型,默认值为零

  • 对于枚举,默认值是第
    一个定义的枚举值,该值必须为0

  • repeated字段默认值是空列表

  • message字段的默认值为空对象

enum关键字

在定义消息类型时,可能会希望其中一个字段有一个预定义的值列表

比如说,电话号码字段有个类型,这个类型可以是,home,work,mobile

我们可以通过enum在消息定义中添加每个可能值的常量来非常简单的执行此操作。示例如下:

 syntax = "proto3"; // 指定版本信息,不指定会报错
 package pb;// 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
    string name = 1;   // 名字
    int32  age = 2 ;   // 年龄
    // 定义一个message
    message PhoneNumber {
   string number = 1;
   PhoneType type = 2;
 }
 
 repeated PhoneNumber phone = 3;
 }
 
 // enum为关键字,作用为定义一种枚举类型
 enum PhoneType {
 MOBILE = 0;
    HOME = 1;
    WORK = 2;
 }

如上,enum的第一个常量映射为0,每个枚举定义
必须
包含一个映射到零的常量作为其第一个元素。这是因为:

  • 必须有一个零值,以便我们可以使用0作为数字默认值。

  • 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。

enum还可以为不同的枚举常量指定相同的值来定义别名。如果想要使用这个功能必须将
allow_alias
选项设置为true,负责编译器将报错。示例如下:

 syntax = "proto3"; // 指定版本信息,不指定会报错
 package pb;// 后期生成go文件的包名
 // message为关键字,作用为定义一种消息类型
 message Person{
    string name = 1;   // 名字
    int32  age = 2 ;   // 年龄
    // 定义一个message
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
   }
    repeated PhoneNumber phone = 3;
 }
 
 // enum为关键字,作用为定义一种枚举类型
 enum PhoneType {
 // 如果不设置将报错
    option allow_alias = true;
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
    Personal = 2;
 }

oneof关键字

如果有一个包含许多字段的消息,并且最多只能同时设置其中的一个字段,则可以使用oneof功能,示例如下:

 message Person{
    string name = 1; // 名字
    int32  age = 2 ; // 年龄
    //定义一个message
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
   }
 
    repeated PhoneNumber phone = 3;
    oneof data{
        string school = 5;
        int32 score = 6;
   }
 }

定义RPC服务

如果需要将message与RPC一起使用,则可以在
.proto
文件中定义RPC服务接口,protobuf编译器将根据你选择的语言生成RPC接口代码。示例如下:

 //定义RPC服务
 service HelloService {
    rpc Hello (Person)returns (Person);
 }

注意:默认protobuf编译期间,不编译服务,如果要想让其编译,需要使用gRPC

protobuf编译

编译器调用

protobuf 编译是通过编译器 protoc 进行的,通过这个编译器,我们可以把 .proto 文件生成
go
,Java,Python,C++, Ruby或者C# 代码

可以使用以下命令来通过 .proto 文件生成go代码(以及grpc代码)

 // 将当前目录中的所有 .proto文件进行编译生成go代码
 protoc --go_out=./ --go_opt=paths=source_relative *.proto

protobuf 编译器会把 .proto 文件编译成 .pd.go 文件

--go_out 参数

作用:指定go代码生成的基本路径

  1. protocol buffer编译器会将生成的Go代码输出到命令行参数
    go_out
    指定的位置

  2. go_out
    标志的参数是你希望编译器编写 Go 输出的目录

  3. 编译器会为每个
    .proto
    文件输入创建一个源文件

  4. 输出文件的名称是通过将
    .proto
    扩展名替换为
    .pb.go
    而创建的

--go_opt 参数

protoc-gen-go
提供了
--go_opt
参数来为其指定参数,可以设置多个:

  1. paths=import
    :生成的文件会按
    go_package
    路径来生成,当然是在
    --go_out
    目录


    • 例如,
      go_out/$go_package/pb_filename.pb.go

    • 如果未指定路径标志,这就是默认输出模式

  2. paths=source_relative
    :输出文件与输入文件放在相同的目录中


    • 例如,一个
      protos/buzz.proto
      输入文件会产生一个位于
      protos/buzz.pb.go
      的输出文件。

  3. module=$PREFIX
    :输出文件放在以 Go 包的导入路径命名的目录中,但是从输出文件名中删除了指定的目录前缀。


    • 例如,输入文件
      pros/buzz.proto
      ,其导入路径为
      example.com/project/protos/fizz
      并指定
      example.com/project

      module
      前缀,结果会产生一个名为
      pros/fizz/buzz.pb.go
      的输出文件。

    • 在module路径之外生成任何 Go 包都会导致错误,此模式对于将生成的文件直接输出到 Go 模块非常有用。

--proto_path 参数

--proto_path=IMPORT_PATH

  • IMPORT_PATH是 .proto 文件所在的路径,如果忽略则默认当前目录。

  • 如果有多个目录则可以多次调用--proto_path,它们将会顺序的被访问并执行导入。

使用示例:

 protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
 // 编译器将从 `src` 目录中读取输入文件 `foo.proto` 和 `bar/baz.proto`,并将输出文件 `foo.pb.go` 和 `bar/baz.pb.go` 写入 `out` 目录。如果需要,编译器会自动创建嵌套的输出子目录,但不会创建输出目录本身

使用grpc的go插件

安装proto-gen-go-grpc


google.golang.org/protobuf
中,
protoc-gen-go
纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能。生成gRPC相关代码需要安装grpc-go相关的插件
protoc-gen-go-grpc

 // 安装protoc-gen-go-grpc
 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest  // 目前最新版是v1.3.0

生成grpc的go代码:

 // 主要是--go_grpc_out参数会生成go代码
 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative  *.proto

--go-grpc_out 参数

作用:指定grpc go代码生成的基本路径

命令会产生的go文件:

  1. protoc-gen-go
    :包含所有类型的序列化和反序列化的go代码

  2. protoc-gen-go-grpc
    :包含service中的用来给client调用的接口定义以及service中的用来给服务端实现的接口定义

--go-grpc_opt 参数


protoc-gen-go
类似,
protoc-gen-go-grpc
提供
--go-grpc_opt
来指定参数,并可以设置多个

github.com/golang/protobuf

google.golang.org/protobuf

github.com/golang/protobuf

  1. github.com/golang/protobuf
    现在已经废弃

  2. 它可以同时生成pb和gRPC相关代码的

用法:

 // 它在--go_out加了plugin关键字,paths参数有两个选项,分别是 import 和 source_relative
 --go_out=plugins=grpc,paths=import:.  *.proto

google.golang.org/protobuf


  1. github.com/golang/protobuf
    的升级版本,
    v1.4.0
    之后
    github.com/golang/protobuf
    仅是
    google.golang.org/protobuf
    的包装

  2. 它纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能
    ,生成gRPC相关代码需要安装grpc-go相关的插件
    protoc-gen-go-grpc

用法:

 // 它额外添加了参数--go-grpc_out以调用protoc-gen-go-grpc插件生成grpc代码
 protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative  *.proto