工具使用

工具使用

如何在go项目中使用go mod?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 116 次浏览 • 2020-05-22 17:09 • 来自相关话题

如何不借助任何第三方软件快速激活Windows10?

回复

工具软件zkbhj 发起了问题 • 1 人关注 • 0 个回复 • 115 次浏览 • 2020-05-20 14:43 • 来自相关话题

#2020学习打卡##C程序设计语言# 如何把程序跟命令关联起来?

回复

C语言zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 154 次浏览 • 2020-04-26 17:27 • 来自相关话题

序列化结构数据方法ProtoBuf使用(PHP和Go)

专业名词zkbhj 发表了文章 • 0 个评论 • 139 次浏览 • 2020-04-13 17:24 • 来自相关话题

一、在PHP下使用ProtoBuf
 
PHP如果要使用protobuf 需要安装 protoc+ 安装protobuf composer包
 
protoc用于根据protobuf数据结构文件(.proto)生成对应的类,用于生成protobuf文件安装 google/protobuf composer 包composer require google/protobuf
 
怎么用呢?开搞!
 
1、首先,需要定一个消息类型。创建一个关于Person的定义文件(以.proto为后缀),如示例为person.proto,文件内容如下:
syntax="proto3";
package test;
message Person{
string name=1;//姓名
int32 age=2;//年龄
bool sex=3;//性别

syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2package test:定义包名为test,生成类时,会产生一个目录为testmessage Person:消息主体内容,里面为各个字段的定义
 
2、生成对应的PHP类
 
定义好Person的格式后,该格式如果不生成我们所需要的类库,其实是无任何意义的,还google提供一个工具protoc生成我们要的类库。也就是最开始说的 protoc!
 
proto 最新版的下载地址是:最新3.11.3。可以通过 官方包Release 进行有选择的下载。
tar -zxvf protobuf-php-3.11.3.tar.gz
cd protobuf-3.11.3
./configure --prefix=/usr/local/protobuf
make
make install解压安装完后,就可以通过下面的命令,生成对应的类库了:
/usr/
local/protobuf/bin/protoc --php_out=./ person.proto生成后将在当前目录产生如下文件:
 
GPBMetadata/Person.phpTest/Person.php
 
3、类库生成完了,就可以在PHP项目中使用ProtoBuf了:

在PHP中使用ProtoBuf依赖一个protobuf的扩展,目前提供两种方式进行使用: 
php的c扩展,php的lib扩展包,
 
这两者均可在刚才下载包里可以找到。
 
另外,也可以使用composer进行安装该依赖扩展:
composer require google/protobuf
这里我主要是使用composer安装,应该它可以帮我产生autoload。安装好依赖后,我们就可以开始在php环境下使用protobuf了。

序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();运行后,产生的data.bin,只有14Byte
 
反序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();PHP常用的使用方法:

序列化:
1、serializeToString:序列化成二进制字符串
2、serializeToJsonString:序列化成JSON字符串

反序列化:
1、mergeFromString:二进制字符串反序列化
2、mergeFromJsonString:Json字符串反序列化

二、在Golang下使用ProtoBuf
 
安装protoc的方法和PHP中类型,编译安装即可。
 
安装好之后,需要安装ProtoBuf的编译器插件:protoc-gen-go:
 
进入GOPATH目录,运行:
go get -u github.com/golang/protobuf/protoc-gen-go  如果成功,会在GOPATH/bin下生成protoc-gen-go.exe文件(Windows上)。
 
1、准备好之后,就可以开始写proto文件了。假设文件目录为:
$GOPATH/src/test/protobuf/pb/user.proto代码如下:
//指定版本
//注意proto3与proto2的写法有些不同
syntax = "proto3";

//包名,通过protoc生成时go文件时
package test;

//手机类型
//枚举类型第一个字段必须为0
enum PhoneType {
HOME = 0;
WORK = 1;
}

//手机
message Phone {
PhoneType type = 1;
string number = 2;
}

//人
message Person {
//后面的数字表示标识号
int32 id = 1;
string name = 2;
//repeated表示可重复
//可以有多个手机
repeated Phone phones = 3;
}

//联系簿
message ContactBook {
repeated Person persons = 1;
}然后运行如下命令:
protoc --go_out=. *.proto
会生成一个user.pb.go的文件。
 
2、使用:
package main;

import (
"github.com/golang/protobuf/proto"
"go_dev/kongji/proto/test"
"io/ioutil"
"os"
"fmt"
)

func write() {
p1 := &test.Person{
Id: 1,
Name: "小张",
Phones: []*test.Phone{
{test.PhoneType_HOME, "111111111"},
{test.PhoneType_WORK, "222222222"},
},
};
p2 := &test.Person{
Id: 2,
Name: "小王",
Phones: []*test.Phone{
{test.PhoneType_HOME, "333333333"},
{test.PhoneType_WORK, "444444444"},
},
};

//创建地址簿
book := &test.ContactBook{};
book.Persons = append(book.Persons, p1);
book.Persons = append(book.Persons, p2);

//编码数据
data, _ := proto.Marshal(book);
//把数据写入文件
ioutil.WriteFile("./test.txt", data, os.ModePerm);
}

func read() {
//读取文件数据
data, _ := ioutil.ReadFile("./test.txt");
book := &test.ContactBook{};
//解码数据
proto.Unmarshal(data, book);
for _, v := range book.Persons {
fmt.Println(v.Id, v.Name);
for _, vv := range v.Phones {
fmt.Println(vv.Type, vv.Number);
}
}
}

func main() {
write();
read();
}
 

内容整理自下面链接:
https://www.jianshu.com/p/ce098058edf0
https://developers.google.cn/protocol-buffers/docs/gotutorial
https://developers.google.cn/protocol-buffers/docs/reference/go-generated
  查看全部
一、在PHP下使用ProtoBuf
 
PHP如果要使用protobuf 需要安装 protoc+ 安装protobuf composer包
 
  • protoc用于根据protobuf数据结构文件(.proto)生成对应的类,用于生成protobuf文件
  • 安装 google/protobuf composer 包composer require google/protobuf

 
怎么用呢?开搞!
 
1、首先,需要定一个消息类型。创建一个关于Person的定义文件(以.proto为后缀),如示例为person.proto,文件内容如下:
syntax="proto3";
package test;
message Person{
string name=1;//姓名
int32 age=2;//年龄
bool sex=3;//性别
}
 
  1. syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2
  2. package test:定义包名为test,生成类时,会产生一个目录为test
  3. message Person:消息主体内容,里面为各个字段的定义

 
2、生成对应的PHP类
 
定义好Person的格式后,该格式如果不生成我们所需要的类库,其实是无任何意义的,还google提供一个工具protoc生成我们要的类库。也就是最开始说的 protoc!
 
proto 最新版的下载地址是:最新3.11.3。可以通过 官方包Release 进行有选择的下载。
tar -zxvf protobuf-php-3.11.3.tar.gz
cd protobuf-3.11.3
./configure --prefix=/usr/local/protobuf
make
make install
解压安装完后,就可以通过下面的命令,生成对应的类库了:
/usr/
local/protobuf/bin/protoc --php_out=./ person.proto
生成后将在当前目录产生如下文件:
 
  • GPBMetadata/Person.php
  • Test/Person.php

 
3、类库生成完了,就可以在PHP项目中使用ProtoBuf了:

在PHP中使用ProtoBuf依赖一个protobuf的扩展,目前提供两种方式进行使用: 
  1. php的c扩展,
  2. php的lib扩展包,

 
这两者均可在刚才下载包里可以找到。
 
另外,也可以使用composer进行安装该依赖扩展:
composer require google/protobuf

这里我主要是使用composer安装,应该它可以帮我产生autoload。安装好依赖后,我们就可以开始在php环境下使用protobuf了。

序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();
运行后,产生的data.bin,只有14Byte
 
反序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();
PHP常用的使用方法:

序列化:
1、serializeToString:序列化成二进制字符串
2、serializeToJsonString:序列化成JSON字符串

反序列化:
1、mergeFromString:二进制字符串反序列化
2、mergeFromJsonString:Json字符串反序列化

二、在Golang下使用ProtoBuf
 
安装protoc的方法和PHP中类型,编译安装即可。
 
安装好之后,需要安装ProtoBuf的编译器插件:protoc-gen-go:
 
进入GOPATH目录,运行:
go get -u github.com/golang/protobuf/protoc-gen-go
  如果成功,会在GOPATH/bin下生成protoc-gen-go.exe文件(Windows上)。
 
1、准备好之后,就可以开始写proto文件了。假设文件目录为:
$GOPATH/src/test/protobuf/pb/user.proto
代码如下:
//指定版本
//注意proto3与proto2的写法有些不同
syntax = "proto3";

//包名,通过protoc生成时go文件时
package test;

//手机类型
//枚举类型第一个字段必须为0
enum PhoneType {
HOME = 0;
WORK = 1;
}

//手机
message Phone {
PhoneType type = 1;
string number = 2;
}

//人
message Person {
//后面的数字表示标识号
int32 id = 1;
string name = 2;
//repeated表示可重复
//可以有多个手机
repeated Phone phones = 3;
}

//联系簿
message ContactBook {
repeated Person persons = 1;
}
然后运行如下命令:
 protoc --go_out=. *.proto
会生成一个user.pb.go的文件。
 
2、使用:
package main;

import (
"github.com/golang/protobuf/proto"
"go_dev/kongji/proto/test"
"io/ioutil"
"os"
"fmt"
)

func write() {
p1 := &test.Person{
Id: 1,
Name: "小张",
Phones: []*test.Phone{
{test.PhoneType_HOME, "111111111"},
{test.PhoneType_WORK, "222222222"},
},
};
p2 := &test.Person{
Id: 2,
Name: "小王",
Phones: []*test.Phone{
{test.PhoneType_HOME, "333333333"},
{test.PhoneType_WORK, "444444444"},
},
};

//创建地址簿
book := &test.ContactBook{};
book.Persons = append(book.Persons, p1);
book.Persons = append(book.Persons, p2);

//编码数据
data, _ := proto.Marshal(book);
//把数据写入文件
ioutil.WriteFile("./test.txt", data, os.ModePerm);
}

func read() {
//读取文件数据
data, _ := ioutil.ReadFile("./test.txt");
book := &test.ContactBook{};
//解码数据
proto.Unmarshal(data, book);
for _, v := range book.Persons {
fmt.Println(v.Id, v.Name);
for _, vv := range v.Phones {
fmt.Println(vv.Type, vv.Number);
}
}
}

func main() {
write();
read();
}

 

内容整理自下面链接:
https://www.jianshu.com/p/ce098058edf0
https://developers.google.cn/protocol-buffers/docs/gotutorial
https://developers.google.cn/protocol-buffers/docs/reference/go-generated
 

序列化结构数据方法ProtoBuf源码解读

专业名词zkbhj 发表了文章 • 0 个评论 • 164 次浏览 • 2020-04-13 16:53 • 来自相关话题

在上一篇 《序列化结构数据方法ProtoBuf编码》 中,我们详细解析了 ProtoBuf 的编码原理。

有了这个知识储备,我们就可以深入 ProtoBuf 序列化、反序列化的源码,从代码的层面理解 ProtoBuf 具体是如何实现对数据的编码(序列化)和解码(反序列化)的。

我们重新复习一下, ProtoBuf 的序列化使用过程:
 
定义 .proto 文件protoc 编译器编译 .proto 文件生成一系列接口代码调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法

具体调用代码如下:
Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);
调用 SerializeToString 函数将 example1 对象序列化(编码)成字符串。我们的目的就是了解 SerializeToString 函数里到底发生了什么,是怎么一步一步得到最终的序列化结果的。
 

注意:并非编码成字符串数据,string 只是作为编码结果的容器


我们在 .proto 文件中定义的 message 在最终生成的对应语言的代码中,例如在 C++ (xxxx.pb.h、xxxx.pb.cpp) 中每一个在 .proto 文件中定义的 message 字段都会在代码中构造成一个类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类。其相关的类图如下所示:





 
而我们经常调用的序列化函数 SerializeToString 并定义在基类 MessageLite 中。
 
编码

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:






WriteVarint32ToArray 函数可在源码目录下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 编码 中我们解析了 Varint 编码原理和详细过程,WriteVarint32ToArray(以及 WriteVarint64ToArray)并是 Varint 编码的核心。

可以对照上一篇指出的 Varints 编码的几个关键点来阅读以下代码,可以看出编码实现确实优雅,代码如下:inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}在上面已添加详细注释,这里再强调几个关键点。
 
value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的结果其实就是将最后一个字节的第一个 bit(最高位) 置 1,其他位不变,即 xxx ... 1xxx xxxx。注意 target 是 uint8 类型的指针,这意味它只会截断获取最后一个字节,即 1xxx xxxx,这里的 1 意味着什么?这个 1 就是所谓的 msb 了,意味着后续还有字节。之后就是右移 7 位(去掉最后 7 位),处理下一个 7位。通过这里的代码应该可以体会到为什么 Varints 编码结果是低位排在前面了。

了解了最底层 IO 包中的编码函数,再结合上篇文章介绍的编码原理,对 ProtoBuf 的编码应该有了更深入的认识。

Varints 类型序列化实现

int32、int64、uint32、uint64

int32 类型编码函数对应为 WriteInt32ToArray,源码如下:// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}int64、uint32、uint64 类型与 int32 类型同理,只是处理位数有所不同。

uint32 和 uint64 也是采用 Varint 编码,所以底层编码实现与 int32、int64 一致。

 sint32、sint64

这两种类型编码函数对应为 WriteSInt32ToArray 和 WriteSInt64ToArray 。

在上一篇文章 深入 ProtoBuf - 编码 中我们已经介绍过 Varint 编码在负数的情况下编码效率很低,固对于 sint32、sint64 类型我们会采用 ZigZag 编码将负数映射成正数然后再进行 Varint 编码,而这种映射并非采用存储的 Map,而是使用移位实现。sint32 的 ZigZag 源码实现如下:inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}经过 ZigZagEncode32 编码之后,数字成为一个正数,之后等同于 int32 或 int64 进行完全相同的编码处理。

bool 与 enum

bool 和 enum 本质就是整型,编码处理与 int32、int64 相同。

32-bit、64-bit
fixed32/fixed64

fixed32 类型对应 WriteFixed32ToArray 函数,32-bit、64-bit类型的字段比起上述 Varint 类型则要简单的多,因为每个数字均是固定字节,源码如下:inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
int32 value, uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
return WriteSFixed32NoTagToArray(value, target);
}其中 WriteSFixed32NoTagToArray 源码如下:
inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
uint8* target) {
return io::CodedOutputStream::WriteLittleEndian32ToArray(
static_cast<uint32>(value), target);
}由此可知,对于位数固定的 sfixed32 是将其转成 uint32 类型,然后使用与 fixed32 相同的函数写入。

sfixed64 与 sfixed32 同理,不赘述。
 
Length delimited 字段序列化

因为其编码结构为 Tag - Length - Value,所以其字段完整的序列化会稍微多出一些过程,其中有一些需要我们进一步整理。现在以一个 string 类型字段的序列化为例,来看看其序列化的完整过程,画出其程序时序图(上文出现过)如下:






可对照上述时序图来阅读源码,其序列化实现的几个关键函数为:
 
ByteSizeLong:计算对象序列化所需要的空间大小,在内存中开辟相应大小的空间WriteTagToArray:将 Tag 值写入到之前开辟的内存中WriteStringWithSizeToArray:将 Length + Value 值写入到之前开辟的内存中

其序列化代码的重点过程在上图的右下角,先是调用 WriteTagToArray 函数将 Tag 值写入到内存,返回指向下一个字节的指针以便继续写入。调用 WriteStringWithSizeToArray 函数,这个函数主要又执行了两个函数,先是执行 WriteVarint32ToArray 函数(注意 WriteTagToArray 内部调用的也是这个函数,因为 Tag 和 Length 都采用 Varints 编码),此函数的作用是将 Length 写入。执行的第二个函数为 WriteStringToArray,此函数的作用是将 Value(一个 UTF-8 string 值) 写入到内存,其中底层调用了 memcpy() 函数。

综上,对于 Varint 类型的字段自然采用 Varint 编码。

而对于 Length delimited 类型的字段,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 编码,Value 若为 String 等类型,则直接进行 memcpy。

另外对于 embedded message 或 packed repeated ,则套用上述规则。底层编码实现实际并是遍历字段下所有内嵌字段,然后递归调用编码函数即可。

作者:404_89_117_101
链接:https://www.jianshu.com/p/62f0238beec8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 查看全部
在上一篇 《序列化结构数据方法ProtoBuf编码》 中,我们详细解析了 ProtoBuf 的编码原理。

有了这个知识储备,我们就可以深入 ProtoBuf 序列化、反序列化的源码,从代码的层面理解 ProtoBuf 具体是如何实现对数据的编码(序列化)和解码(反序列化)的。

我们重新复习一下, ProtoBuf 的序列化使用过程:
 
  • 定义 .proto 文件
  • protoc 编译器编译 .proto 文件生成一系列接口代码
  • 调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法


具体调用代码如下:
Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);

调用 SerializeToString 函数将 example1 对象序列化(编码)成字符串。我们的目的就是了解 SerializeToString 函数里到底发生了什么,是怎么一步一步得到最终的序列化结果的。
 


注意:并非编码成字符串数据,string 只是作为编码结果的容器



我们在 .proto 文件中定义的 message 在最终生成的对应语言的代码中,例如在 C++ (xxxx.pb.h、xxxx.pb.cpp) 中每一个在 .proto 文件中定义的 message 字段都会在代码中构造成一个类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类。其相关的类图如下所示:

QQ截图20200413164703.jpg

 
而我们经常调用的序列化函数 SerializeToString 并定义在基类 MessageLite 中。
 
编码

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:

6009978-d3f8eaef64fe78e9.png


WriteVarint32ToArray 函数可在源码目录下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 编码 中我们解析了 Varint 编码原理和详细过程,WriteVarint32ToArray(以及 WriteVarint64ToArray)并是 Varint 编码的核心。

可以对照上一篇指出的 Varints 编码的几个关键点来阅读以下代码,可以看出编码实现确实优雅,代码如下:
inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}
在上面已添加详细注释,这里再强调几个关键点。
 
  • value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的结果其实就是将最后一个字节的第一个 bit(最高位) 置 1,其他位不变,即 xxx ... 1xxx xxxx。注意 target 是 uint8 类型的指针,这意味它只会截断获取最后一个字节,即 1xxx xxxx,这里的 1 意味着什么?这个 1 就是所谓的 msb 了,意味着后续还有字节。之后就是右移 7 位(去掉最后 7 位),处理下一个 7位。
  • 通过这里的代码应该可以体会到为什么 Varints 编码结果是低位排在前面了。


了解了最底层 IO 包中的编码函数,再结合上篇文章介绍的编码原理,对 ProtoBuf 的编码应该有了更深入的认识。

Varints 类型序列化实现

int32、int64、uint32、uint64

int32 类型编码函数对应为 WriteInt32ToArray,源码如下:
// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}
int64、uint32、uint64 类型与 int32 类型同理,只是处理位数有所不同。


uint32 和 uint64 也是采用 Varint 编码,所以底层编码实现与 int32、int64 一致。


 sint32、sint64

这两种类型编码函数对应为 WriteSInt32ToArray 和 WriteSInt64ToArray 。

在上一篇文章 深入 ProtoBuf - 编码 中我们已经介绍过 Varint 编码在负数的情况下编码效率很低,固对于 sint32、sint64 类型我们会采用 ZigZag 编码将负数映射成正数然后再进行 Varint 编码,而这种映射并非采用存储的 Map,而是使用移位实现。sint32 的 ZigZag 源码实现如下:
inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}
经过 ZigZagEncode32 编码之后,数字成为一个正数,之后等同于 int32 或 int64 进行完全相同的编码处理。

bool 与 enum

bool 和 enum 本质就是整型,编码处理与 int32、int64 相同。

32-bit、64-bit
fixed32/fixed64

fixed32 类型对应 WriteFixed32ToArray 函数,32-bit、64-bit类型的字段比起上述 Varint 类型则要简单的多,因为每个数字均是固定字节,源码如下:
inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
int32 value, uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
return WriteSFixed32NoTagToArray(value, target);
}
其中 WriteSFixed32NoTagToArray 源码如下:
inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
uint8* target) {
return io::CodedOutputStream::WriteLittleEndian32ToArray(
static_cast<uint32>(value), target);
}
由此可知,对于位数固定的 sfixed32 是将其转成 uint32 类型,然后使用与 fixed32 相同的函数写入。

sfixed64 与 sfixed32 同理,不赘述。
 
Length delimited 字段序列化

因为其编码结构为 Tag - Length - Value,所以其字段完整的序列化会稍微多出一些过程,其中有一些需要我们进一步整理。现在以一个 string 类型字段的序列化为例,来看看其序列化的完整过程,画出其程序时序图(上文出现过)如下:

6009978-d3f8eaef64fe78e9.png


可对照上述时序图来阅读源码,其序列化实现的几个关键函数为:
 
  • ByteSizeLong:计算对象序列化所需要的空间大小,在内存中开辟相应大小的空间
  • WriteTagToArray:将 Tag 值写入到之前开辟的内存中
  • WriteStringWithSizeToArray:将 Length + Value 值写入到之前开辟的内存中


其序列化代码的重点过程在上图的右下角,先是调用 WriteTagToArray 函数将 Tag 值写入到内存,返回指向下一个字节的指针以便继续写入。调用 WriteStringWithSizeToArray 函数,这个函数主要又执行了两个函数,先是执行 WriteVarint32ToArray 函数(注意 WriteTagToArray 内部调用的也是这个函数,因为 Tag 和 Length 都采用 Varints 编码),此函数的作用是将 Length 写入。执行的第二个函数为 WriteStringToArray,此函数的作用是将 Value(一个 UTF-8 string 值) 写入到内存,其中底层调用了 memcpy() 函数。

综上,对于 Varint 类型的字段自然采用 Varint 编码。

而对于 Length delimited 类型的字段,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 编码,Value 若为 String 等类型,则直接进行 memcpy。

另外对于 embedded message 或 packed repeated ,则套用上述规则。底层编码实现实际并是遍历字段下所有内嵌字段,然后递归调用编码函数即可。

作者:404_89_117_101
链接:https://www.jianshu.com/p/62f0238beec8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

序列化结构数据方法ProtoBuf编码

专业名词zkbhj 发表了文章 • 0 个评论 • 131 次浏览 • 2020-04-13 16:26 • 来自相关话题

上一篇《序列化结构数据方法ProtoBuf初探》是对ProtoBuf的简单介绍和特点对比。接下来这篇主要总结的是ProtoBuf的编码方式。
 
编码结构

TLV 格式是我们比较熟悉的编码格式。
 

所谓的 TLV 即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 并是数据本身。


ProtoBuf 编码采用类似的结构,但是实际上又有较大区别,其编码结构可见下图:






我们来一步步解析上图所表达的编码结构。

首先,每一个 message 进行编码,其结果由一个个字段组成,每个字段可划分为 Tag - [Length] - Value,如下图所示:





 

特别注意这里的 [Length] 是可选的,含义是针对不同类型的数据编码结构可能会变成 Tag - Value 的形式,如果变成这样的形式,没有了 Length 我们该如何确定 Value 的边界?答案就是 Varint 编码,在后面将详细介绍。


继续深入 Tag ,Tag 由 field_number 和 wire_type 两个部分组成: 
field_number: message 定义字段时指定的字段编号wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。

整个 Tag 采用 Varints 编码方案进行编码,Varints 编码会在后面详细介绍。

Tag 结构如下图所示:






3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种,如下图所示:






第一列即是对应的类型编号,第二列为面向最终编码的编码类型,第三列是面向开发者的 message 字段的类型。
 

注意其中的 Start group 和 End group 两种类型已被遗弃。

 
 

另外要特别注意一点,虽然 wire_type 代表编码类型,但是 Varint 这个编码类型里针对 sint32、sint64 又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。


重新来看完整的编码结构图,现在我们可以理解一个 message 编码将由一个个的 field 组成,每个 field 根据类型将有如下两种格式:
 
Tag - Length - Value:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构,Tag - Value:编码类型表中 Varint、64-bit、32-bit 使用这种结构。


其中 Tag 由字段编号 field_number 和 编码类型 wire_type 组成, Tag 整体采用 Varints 编码。

现在来模拟一下,我们接收到了一串序列化的二进制数据,我们先读一个 Varints 编码块,进行 Varints 解码,读取最后 3 bit 得到 wire_type(由此可知是后面的 Value 采用的哪种编码),随后获取到 field_number (由此可知是哪一个字段)。依据 wire_type 来正确读取后面的 Value。接着继续读取下一个字段 field...

Varints 编码

上一节中多次提到 Varints 编码,现在我们来正式介绍这种编码方案。

总结的讲,Varints 编码的规则主要为以下三点: 
在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节存储数字对应的二进制补码补码的低位排在前面

先来看一个最为简单的例子:
int32 val = 1; // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
原码:0000 ... 0000 0001 // 1 的原码表示
补码:0000 ... 0000 0001 // 1 的补码表示
Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码,其中第一个字节的 msb = 0
编码过程:
数字 1 对应补码 0000 ... 0000 0001(规则 2),从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 ... 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit,那么第一个 7 位组的 msb = 0。最终得到0 | 000 0001(0x01) 解码过程:

我们再做一遍解码过程,加深理解。

编码结果为 0#000 0001(0x01)。首先,每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。

在上面的例子中,数字 1 的 Varints 编码中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。

注意:这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。


再看一个需要两个字节的数字 666 的编码:nt32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码:000 ... 101 0011010 // 666 的源码
补码:000 ... 101 0011010 // 666 的补码
Varints 编码:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 编码编码过程:
666 的补码为 000 ... 101 0011010,从后依次向前取 7 位组并反转排序,则得到:
0011010 | 0000101加上 msb,则1 0011010 | 0 0000101 (0x9a 0x05)
解码过程:
编码结果为 1#0011010 0#000 0101 (9a 05),与第一个例子类似,但是这里的第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止。读到两个字节后先去掉两个 msb,剩下:0011010 000 0101将这两个 7-bit 组反转得到补码:
000 0101 0011010然后还原其原码为 666。

注意:这里编码数字 6,Varints 只使用了 2 个字节。而正常情况下 int32 将使用 4 个字节存储数字 666。

 
仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。

这里为什么强调牺牲?因为每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来表达 Value。那就意味这 4 个字节能表达的最大数字为 2的28次 ,而不再是  2的32次  了。
这意味着什么?意味着当数字大于  2的28次  时,采用 Varints 编码将导致分配 5 个字节,而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高反而是下降。
但这并不影响 Varints 在实际应用时的高效,因为事实证明,在大多数情况下,小于 2的28次 的数字比大于 2的28次 的数字出现的更为频繁。


到目前为止,好像一切都很完美。但是当前的 Varints 编码却存在着明显缺陷。我们的例子好像只给出了正数,我们来看一下负数的 Varints 编码情况。int32 val = -1
原码:1000 ... 0001 // 注意这里是 8 个字节
补码:1111 ... 1111 // 注意这里是 8 个字节
再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。
上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,这 9 组的 msb 均为 1(因为还有最后一组)
最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码
Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)
注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。

为什么是十个字节? int32 不应该是 4 个字节吗?这里是 ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),而将 int32 扩展成 int64 的八个字节。
为什么之前讲正数的时候没有这种扩展?。请仔细品味 Varints 编码,正数的前提下 int32 和 int64 天然兼容!


所以目前的情况是我们定义了一个 int32 类型的变量,如果将变量值设置为负数,那么直接采用 Varints 编码的话,其编码结果将总是占用十个字节,这显然不是我们希望得到的结果。如何解决?

ZigZag 编码

在上一节中我们提到了 Varints 编码对负数编码效率低的问题。

为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。

ZigZag 的原理和概念比我们想象的简单易懂,一句话就可概括介绍 ZigZag 编码:
 

ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码


如下图所示:






对于 ZigZag 编码的思维不难理解,既然负数的 Varints 编码效率很低,那么就将负数映射到正数,然后对映射后的正数进行 Varints 编码。解码时,解出正数之后再按映射关系映射回原来的负数。

例如我们设置 int32 val = -2。映射得到 3,那么对数字 3 进行 Varints 编码,将结果存储或发送出去。接收方接到数据后进行 Varints 解码,得到数字 3,再将 3 映射回 -2。
 

这里的“映射”是以移位实现的,并非存储映射表。

 Varint 类型

介绍了 Varints 编码和 ZigZag 编码之后,我们就可以继续深入分析每个类型的编码。

在第一节中我们提到了 wire_type 目前已定义 6 种,其中两种已被遗弃(Start group 和 End group),只剩下四种类型: Varint、64-bit、Length-delimited、32-bit。

接下来我们就来一个个详细分析,彻底搞明白 ProtoBuf 针对每种类型的编码策略。

注意,我们在之前已经强调过,与其它三种类型不同,Varint 类型里不止一种编码策略。 除了 int32、int64 等类型的 Varints 编码,还有 sint32、sint64 类型的 ZigZag 编码。

int32、int64、uint32、uint64、bool、enum

当我们使用 int32、int64、uint32、uint64、bool、enum 声明字段类型时,其字段值将使用之前介绍的 Varints 编码。
 

其中 bool 的本质为 0 和 1,enum 本质为整数常量。


在结合本文开头介绍的编码结构: Tag - [Length] - Value,这里的 Value 采用 Varints 编码,因此不需要 Length,则编码结构为 Tag - Value,其中 Tag 和 Value 均采用 Vartins 编码。

int32、int64、uint32、uint64

来看一个最简单的 int32 的小例子:
 
syntax = "proto3";

// message 定义
message Example1 {
int32 int32Val = 1;
}

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(1);
// 编码结果
tag-(Varints)0#0001 000 + value-(Varints)0#000 0001 = 0x08 0x01

// 设置字段值 为 666
Example1 example1;
example1.set_int32val(666);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#0011010 0#000 0101 = 0x08 0x9a 0x05

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(-1);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#1111111 ... 0#000 0001 = 0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01int64、uint32、uint64 与 int32 同理。
 
bool、enum

bool 的例子:
syntax = "proto3";

// message 定义
message Example1 {
bool boolVal = 1;
}

// 设置字段值 为 true
Example1 example1;
example1.set_boolval(true);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 false
Example1 example1;
example1.set_boolval(false);

// 编码结果

 

这里有个有意思的现象,当 boolVal = false 时,其编码结果为空,为什么?
这里是 ProtoBuf 为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。也就是说将 false 编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是 false)。如此,可进一步节省空间提高效率。


enum 的例子:syntax = "proto3";

// message 定义
message Example1 {
enum COLOR {
YELLOW = 0;
RED = 1;
BLACK = 2;
WHITE = 3;
BLUE = 4;
}
// 枚举常量必须在 32 位整型值的范围
// 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数
COLOR colorVal = 1;
}

// 设置字段值 为 Example1_COLOR_BLUE
Example1 example1;
example1.set_colorval(Example1_COLOR_BLUE);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0100 = 08 04sint32、sint64

sint32、sint64 将采用 ZigZag 编码。编码结构依然为 Tag - Value,只不过在编码和解码的过程中多出一个映射的过程,映射后依然采用 Varints 编码。
来看 sint32 的例子:syntax = "proto3";

// message 定义
message Example1 {
sint32 sint32Val = 1;
}

// 设置字段值 为 -1
Example1 example1;
example1.set_colorval(-1);

// 编码结果,1 映射回 -1
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 -2
Example1 example1;
example1.set_colorval(-2);

// 编码结果,3 映射回 -2
编码结果:tag-(Varints)00001 000 + value-(Varints)0#000 0011 = 08 03
sint64 与 sint32 同理。

int、uint 和 sint: 之所以同时出现了这三种类型,是因为历史和代码迭代的结果。ProtoBuf 最初只有 int 类型,由于 int 类型不适合负数(负数编码效率低),所以提供了 sint。因为 sint 的一部分正数其实是表达的负数,所以其正数范围有所减小,所以在一些全是正数场景下需要提供 uint 类型。

64-bit 和 32-bit 类型

64-bit 和 32-bit 比较简单,与 Varints 一样其编码结构为 Tag-Value,不同的是不管数字大小,64-bit 存储 8 字节,32-bit 存储 4 字节。读取时同理,64-bit 直接读取 8 字节,32-bit 直接读取 4 字节。

为什么需要 64-bit 和 32-bit?之前已经分析过了 Varints 编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints 就不太合适了,此时使用 64-bit 和 32-bit 更为合适。具体的,如果数值比 256 大的话,64-bit 这个类型比 uint64 高效,如果数值比 228 大的话,32-bit 这个类型比 uint32 高效。

fixed64、sfixed64、double

来看例子:// message 定义
syntax = "proto3";

message Example1 {
fixed64 fixed64Val = 1;
sfixed64 sfixed64Val = 2;
double doubleVal = 3;
}

// 设置字段值 为 -2
example1.set_fixed64val(1)
example1.set_sfixed64val(-1)
example1.set_doubleval(1.2)

// 编码结果,总是 8 个字节
09 # 01 00 00 00 00 00 00 00
11 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)
19 # 33 33 33 33 33 33 F3 3Ffixed32、sfixed32、float

与 64-bit 同理。

Length-delimited 类型
string、bytes、EmbeddedMessage、repeated

终于遇到了体现编码结构图中 [Length] 意义的类型了。Length-delimited 类型的编码结构为 Tag - Length - Value

这种编码方式很好理解,来看例子:syntax = "proto3";

// message 定义
message Example1 {
string stringVal = 1;
bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}

//设置相应值
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");

Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();

embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);

example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");

//编码结果
0A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64
12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F
1A 10 08 01 12 0C 65 6D 62 65 64 64 65 64 49 6E 66 6F
22 02 02 03[ proto3 默认 packed = true](编码结果打包处理,见下一小节的介绍)
2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32(repeated string 为啥不进行默认 packed ?)读者可对照上面介绍过的编码来理解这段相对复杂的编码结果。(为降低难度,已按字段分行,即第一个字段的编码结果对应第一行,第二个字段对应第二行...)

补充 packed 编码

在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。
 

packed 目前只能用于 primitive 类型。


packed = true 主要使让 ProtoBuf 为我们把 repeated primitive 的编码结果打包,从而进一步压缩空间,进一步提高效率、速度。这里打包的含义其实就是:原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value...,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value...

上一节例子中 repeatedInt32Val 字段的编码结果为:22 | 02 02 03
22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。

原文出处:
作者:404_89_117_101
链接:https://www.jianshu.com/p/73c9ed3a4877
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 查看全部
上一篇《序列化结构数据方法ProtoBuf初探》是对ProtoBuf的简单介绍和特点对比。接下来这篇主要总结的是ProtoBuf的编码方式。
 
编码结构

TLV 格式是我们比较熟悉的编码格式。
 


所谓的 TLV 即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 并是数据本身。



ProtoBuf 编码采用类似的结构,但是实际上又有较大区别,其编码结构可见下图:

QQ截图20200413160739.jpg


我们来一步步解析上图所表达的编码结构。

首先,每一个 message 进行编码,其结果由一个个字段组成,每个字段可划分为 Tag - [Length] - Value,如下图所示:

QQ截图20200413160948.jpg

 


特别注意这里的 [Length] 是可选的,含义是针对不同类型的数据编码结构可能会变成 Tag - Value 的形式,如果变成这样的形式,没有了 Length 我们该如何确定 Value 的边界?答案就是 Varint 编码,在后面将详细介绍。



继续深入 Tag ,Tag 由 field_number 和 wire_type 两个部分组成: 
  • field_number: message 定义字段时指定的字段编号
  • wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。


整个 Tag 采用 Varints 编码方案进行编码,Varints 编码会在后面详细介绍。

Tag 结构如下图所示:

QQ截图20200413161046.jpg


3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种,如下图所示:

QQ截图20200413161115.jpg


第一列即是对应的类型编号,第二列为面向最终编码的编码类型,第三列是面向开发者的 message 字段的类型。
 


注意其中的 Start group 和 End group 两种类型已被遗弃。


 
 


另外要特别注意一点,虽然 wire_type 代表编码类型,但是 Varint 这个编码类型里针对 sint32、sint64 又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。



重新来看完整的编码结构图,现在我们可以理解一个 message 编码将由一个个的 field 组成,每个 field 根据类型将有如下两种格式:
 
  • Tag - Length - Value:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构,
  • Tag - Value:编码类型表中 Varint、64-bit、32-bit 使用这种结构。



其中 Tag 由字段编号 field_number 和 编码类型 wire_type 组成, Tag 整体采用 Varints 编码。

现在来模拟一下,我们接收到了一串序列化的二进制数据,我们先读一个 Varints 编码块,进行 Varints 解码,读取最后 3 bit 得到 wire_type(由此可知是后面的 Value 采用的哪种编码),随后获取到 field_number (由此可知是哪一个字段)。依据 wire_type 来正确读取后面的 Value。接着继续读取下一个字段 field...

Varints 编码

上一节中多次提到 Varints 编码,现在我们来正式介绍这种编码方案。

总结的讲,Varints 编码的规则主要为以下三点: 
  1. 在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
  2. 存储数字对应的二进制补码
  3. 补码的低位排在前面


先来看一个最为简单的例子:
int32 val =  1;  // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
原码:0000 ... 0000 0001 // 1 的原码表示
补码:0000 ... 0000 0001 // 1 的补码表示
Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码,其中第一个字节的 msb = 0

编码过程:
数字 1 对应补码 0000 ... 0000 0001(规则 2),从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 ... 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit,那么第一个 7 位组的 msb = 0。最终得到
0 | 000 0001(0x01) 
解码过程:

我们再做一遍解码过程,加深理解。

编码结果为 0#000 0001(0x01)。首先,每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。

在上面的例子中,数字 1 的 Varints 编码中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。


注意:这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。



再看一个需要两个字节的数字 666 的编码:
nt32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码:000 ... 101 0011010 // 666 的源码
补码:000 ... 101 0011010 // 666 的补码
Varints 编码:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 编码
编码过程:
666 的补码为 000 ... 101 0011010,从后依次向前取 7 位组并反转排序,则得到:
0011010 | 0000101
加上 msb,则
1 0011010 | 0 0000101 (0x9a 0x05)

解码过程:
编码结果为 1#0011010 0#000 0101 (9a 05),与第一个例子类似,但是这里的第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止。读到两个字节后先去掉两个 msb,剩下:
0011010  000 0101
将这两个 7-bit 组反转得到补码:
000 0101 0011010
然后还原其原码为 666。


注意:这里编码数字 6,Varints 只使用了 2 个字节。而正常情况下 int32 将使用 4 个字节存储数字 666。


 
仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率


这里为什么强调牺牲?因为每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来表达 Value。那就意味这 4 个字节能表达的最大数字为 2的28次 ,而不再是  2的32次  了。
这意味着什么?意味着当数字大于  2的28次  时,采用 Varints 编码将导致分配 5 个字节,而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高反而是下降。
但这并不影响 Varints 在实际应用时的高效,因为事实证明,在大多数情况下,小于 2的28次 的数字比大于 2的28次 的数字出现的更为频繁。



到目前为止,好像一切都很完美。但是当前的 Varints 编码却存在着明显缺陷。我们的例子好像只给出了正数,我们来看一下负数的 Varints 编码情况。
int32 val = -1
原码:1000 ... 0001 // 注意这里是 8 个字节
补码:1111 ... 1111 // 注意这里是 8 个字节
再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。
上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,这 9 组的 msb 均为 1(因为还有最后一组)
最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码
Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)

注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。


为什么是十个字节? int32 不应该是 4 个字节吗?这里是 ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),而将 int32 扩展成 int64 的八个字节。
为什么之前讲正数的时候没有这种扩展?。请仔细品味 Varints 编码,正数的前提下 int32 和 int64 天然兼容!



所以目前的情况是我们定义了一个 int32 类型的变量,如果将变量值设置为负数,那么直接采用 Varints 编码的话,其编码结果将总是占用十个字节,这显然不是我们希望得到的结果。如何解决?

ZigZag 编码

在上一节中我们提到了 Varints 编码对负数编码效率低的问题。

为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。

ZigZag 的原理和概念比我们想象的简单易懂,一句话就可概括介绍 ZigZag 编码:
 


ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码



如下图所示:

QQ截图20200413161656.jpg


对于 ZigZag 编码的思维不难理解,既然负数的 Varints 编码效率很低,那么就将负数映射到正数,然后对映射后的正数进行 Varints 编码。解码时,解出正数之后再按映射关系映射回原来的负数。

例如我们设置 int32 val = -2。映射得到 3,那么对数字 3 进行 Varints 编码,将结果存储或发送出去。接收方接到数据后进行 Varints 解码,得到数字 3,再将 3 映射回 -2。
 


这里的“映射”是以移位实现的,并非存储映射表。


 Varint 类型

介绍了 Varints 编码和 ZigZag 编码之后,我们就可以继续深入分析每个类型的编码。

在第一节中我们提到了 wire_type 目前已定义 6 种,其中两种已被遗弃(Start group 和 End group),只剩下四种类型: Varint、64-bit、Length-delimited、32-bit

接下来我们就来一个个详细分析,彻底搞明白 ProtoBuf 针对每种类型的编码策略。

注意,我们在之前已经强调过,与其它三种类型不同,Varint 类型里不止一种编码策略。 除了 int32、int64 等类型的 Varints 编码,还有 sint32、sint64 类型的 ZigZag 编码。

int32、int64、uint32、uint64、bool、enum

当我们使用 int32、int64、uint32、uint64、bool、enum 声明字段类型时,其字段值将使用之前介绍的 Varints 编码。
 


其中 bool 的本质为 0 和 1,enum 本质为整数常量。



在结合本文开头介绍的编码结构: Tag - [Length] - Value,这里的 Value 采用 Varints 编码,因此不需要 Length,则编码结构为 Tag - Value,其中 Tag 和 Value 均采用 Vartins 编码。

int32、int64、uint32、uint64

来看一个最简单的 int32 的小例子:
 
syntax = "proto3";

// message 定义
message Example1 {
int32 int32Val = 1;
}

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(1);
// 编码结果
tag-(Varints)0#0001 000 + value-(Varints)0#000 0001 = 0x08 0x01

// 设置字段值 为 666
Example1 example1;
example1.set_int32val(666);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#0011010 0#000 0101 = 0x08 0x9a 0x05

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(-1);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#1111111 ... 0#000 0001 = 0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01
int64、uint32、uint64 与 int32 同理。
 
bool、enum

bool 的例子:
syntax = "proto3";

// message 定义
message Example1 {
bool boolVal = 1;
}

// 设置字段值 为 true
Example1 example1;
example1.set_boolval(true);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 false
Example1 example1;
example1.set_boolval(false);

// 编码结果

 


这里有个有意思的现象,当 boolVal = false 时,其编码结果为空,为什么?
这里是 ProtoBuf 为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。也就是说将 false 编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是 false)。如此,可进一步节省空间提高效率。



enum 的例子:
syntax = "proto3";

// message 定义
message Example1 {
enum COLOR {
YELLOW = 0;
RED = 1;
BLACK = 2;
WHITE = 3;
BLUE = 4;
}
// 枚举常量必须在 32 位整型值的范围
// 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数
COLOR colorVal = 1;
}

// 设置字段值 为 Example1_COLOR_BLUE
Example1 example1;
example1.set_colorval(Example1_COLOR_BLUE);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0100 = 08 04
sint32、sint64

sint32、sint64 将采用 ZigZag 编码。编码结构依然为 Tag - Value,只不过在编码和解码的过程中多出一个映射的过程,映射后依然采用 Varints 编码。
来看 sint32 的例子:
syntax = "proto3";

// message 定义
message Example1 {
sint32 sint32Val = 1;
}

// 设置字段值 为 -1
Example1 example1;
example1.set_colorval(-1);

// 编码结果,1 映射回 -1
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 -2
Example1 example1;
example1.set_colorval(-2);

// 编码结果,3 映射回 -2
编码结果:tag-(Varints)00001 000 + value-(Varints)0#000 0011 = 08 03
sint64 与 sint32 同理。


int、uint 和 sint: 之所以同时出现了这三种类型,是因为历史和代码迭代的结果。ProtoBuf 最初只有 int 类型,由于 int 类型不适合负数(负数编码效率低),所以提供了 sint。因为 sint 的一部分正数其实是表达的负数,所以其正数范围有所减小,所以在一些全是正数场景下需要提供 uint 类型。


64-bit 和 32-bit 类型

64-bit 和 32-bit 比较简单,与 Varints 一样其编码结构为 Tag-Value,不同的是不管数字大小,64-bit 存储 8 字节,32-bit 存储 4 字节。读取时同理,64-bit 直接读取 8 字节,32-bit 直接读取 4 字节。

为什么需要 64-bit 和 32-bit?之前已经分析过了 Varints 编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints 就不太合适了,此时使用 64-bit 和 32-bit 更为合适。具体的,如果数值比 256 大的话,64-bit 这个类型比 uint64 高效,如果数值比 228 大的话,32-bit 这个类型比 uint32 高效。

fixed64、sfixed64、double

来看例子:
// message 定义
syntax = "proto3";

message Example1 {
fixed64 fixed64Val = 1;
sfixed64 sfixed64Val = 2;
double doubleVal = 3;
}

// 设置字段值 为 -2
example1.set_fixed64val(1)
example1.set_sfixed64val(-1)
example1.set_doubleval(1.2)

// 编码结果,总是 8 个字节
09 # 01 00 00 00 00 00 00 00
11 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)
19 # 33 33 33 33 33 33 F3 3F
fixed32、sfixed32、float

与 64-bit 同理。

Length-delimited 类型
string、bytes、EmbeddedMessage、repeated

终于遇到了体现编码结构图中 [Length] 意义的类型了。Length-delimited 类型的编码结构为 Tag - Length - Value

这种编码方式很好理解,来看例子:
syntax = "proto3";

// message 定义
message Example1 {
string stringVal = 1;
bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}

//设置相应值
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");

Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();

embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);

example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");

//编码结果
0A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64
12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F
1A 10 08 01 12 0C 65 6D 62 65 64 64 65 64 49 6E 66 6F
22 02 02 03[ proto3 默认 packed = true](编码结果打包处理,见下一小节的介绍)
2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32(repeated string 为啥不进行默认 packed ?)
读者可对照上面介绍过的编码来理解这段相对复杂的编码结果。(为降低难度,已按字段分行,即第一个字段的编码结果对应第一行,第二个字段对应第二行...)

补充 packed 编码

在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。
 


packed 目前只能用于 primitive 类型。



packed = true 主要使让 ProtoBuf 为我们把 repeated primitive 的编码结果打包,从而进一步压缩空间,进一步提高效率、速度。这里打包的含义其实就是:原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value...,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value...

上一节例子中 repeatedInt32Val 字段的编码结果为:
22 | 02 02 03

22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。

原文出处:
作者:404_89_117_101
链接:https://www.jianshu.com/p/73c9ed3a4877
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

#2020学习打卡##C程序设计语言# 怎么解决gcc编译后windows shell输出中文乱码问题?

回复

C语言zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 202 次浏览 • 2020-04-07 22:29 • 来自相关话题

#2020学习打卡##C程序设计语言# C语言IDE的选择推荐(Mac篇)

C语言zkbhj 发表了文章 • 0 个评论 • 189 次浏览 • 2020-04-03 11:09 • 来自相关话题

上一篇文章介绍了Windows环境下的C语言IDE选择,这篇文章介绍下Mac环境下,哪些IDE是相对比较优秀的!
 
Mac电脑作为类Unix类操作系统的代表,C语言也是天生具备的。很多Mac电脑即便不用于软件开发,也会因为各种免费开源软件的使用,在依赖包中自动安装了gcc的支持,从而具备了C语言的开发环境。 
在Mac电脑上进行C语言开发有两种方式,一是使用图形界面(GUI)进行开发,这种情况使用苹果自主开发的Xcode开发工具。二是使用纯命令行的开发工具gcc或者clang配合vim编辑器。前者适合大规模项目的开发,后者简洁高效,相对来说适合开发小规模的程序或者应急的修修补补。
 
(1)Xcode
 
Xcode 是苹果公司开发的编程软件,是开发人员建立OS X 和 iOS 应用程序的最快捷的方式。Xcode 具有统一的用户界面设计,编码、测试、调试都在一个简单的窗口内完成。在windows上类似这种能编c语言的还有微软出的visual studio。





 
安装Xcode唯一合法的方法是在Mac电脑打开App Store程序,在右上角搜索框中输入"Xcode",从搜索到的结果中一般前1、2位就是Xcode,然后点选“获取”或者“安装”。Xcode容量比较大,一般安装包都在4.5G-6G之间,依据网络的情况,需要等待不短的时间。安装完成后,第一次运行Xcode会提示安装命令行工具,按照提示就会自动安装clang命令行开发工具。命令行开发包一般是100多M,下载安装都会比较快。
 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。





 
下载地址:https://code.visualstudio.com/Download
 
(3)命令行下进行程序开发
 
这种方式适用于学习阶段,大型项目还是更倾向于使用IDE。
 
Mac的命令行C语言开发工具主要有两种,clang及gcc,前者是苹果官方推荐的,Xcode也使用Clang进行编译。后者则是GNU开源社区推荐的,并且被大多数linux支持。两者在语法的兼容性上几乎没有差别。而Clang在于错误信息、编译速度等方面有很多优势。
 
通常的命令行操作为:
vi 1.c
gcc 1.c //生成a.out可执行文件
gcc -o my.out 1.c//可以指定生成的可执行文件名
./a.out
./my.out

  查看全部
上一篇文章介绍了Windows环境下的C语言IDE选择,这篇文章介绍下Mac环境下,哪些IDE是相对比较优秀的!
 
Mac电脑作为类Unix类操作系统的代表,C语言也是天生具备的。很多Mac电脑即便不用于软件开发,也会因为各种免费开源软件的使用,在依赖包中自动安装了gcc的支持,从而具备了C语言的开发环境。 
在Mac电脑上进行C语言开发有两种方式,一是使用图形界面(GUI)进行开发,这种情况使用苹果自主开发的Xcode开发工具。二是使用纯命令行的开发工具gcc或者clang配合vim编辑器。前者适合大规模项目的开发,后者简洁高效,相对来说适合开发小规模的程序或者应急的修修补补。
 
(1)Xcode
 
Xcode 是苹果公司开发的编程软件,是开发人员建立OS X 和 iOS 应用程序的最快捷的方式。Xcode 具有统一的用户界面设计,编码、测试、调试都在一个简单的窗口内完成。在windows上类似这种能编c语言的还有微软出的visual studio。

QQ截图20200403104902.jpg

 
安装Xcode唯一合法的方法是在Mac电脑打开App Store程序,在右上角搜索框中输入"Xcode",从搜索到的结果中一般前1、2位就是Xcode,然后点选“获取”或者“安装”。Xcode容量比较大,一般安装包都在4.5G-6G之间,依据网络的情况,需要等待不短的时间。安装完成后,第一次运行Xcode会提示安装命令行工具,按照提示就会自动安装clang命令行开发工具。命令行开发包一般是100多M,下载安装都会比较快。
 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。

QQ截图20200403105149.jpg

 
下载地址:https://code.visualstudio.com/Download
 
(3)命令行下进行程序开发
 
这种方式适用于学习阶段,大型项目还是更倾向于使用IDE。
 
Mac的命令行C语言开发工具主要有两种,clang及gcc,前者是苹果官方推荐的,Xcode也使用Clang进行编译。后者则是GNU开源社区推荐的,并且被大多数linux支持。两者在语法的兼容性上几乎没有差别。而Clang在于错误信息、编译速度等方面有很多优势。
 
通常的命令行操作为:
vi 1.c
gcc 1.c //生成a.out可执行文件
gcc -o my.out 1.c//可以指定生成的可执行文件名
./a.out
./my.out

 

#2020学习打卡##C程序设计语言# C语言IDE的选择推荐(Windows篇)

C语言zkbhj 发表了文章 • 0 个评论 • 159 次浏览 • 2020-04-03 10:41 • 来自相关话题

确定好了学习目标,选择好了学习教程,制定好了学习计划,那么接下来,你需要做的就是搭建一套学习C语言的环境。我们无外乎基本上大多使用的操作系统是Windows和MACOS,所以在这里整理了一些IDE和C语言编译环境搭建的方法和经验供大家参考。这篇为Windows篇,下一篇文章分享MAC上有哪些优良实践。(文末为大家附上了各个IDE的下载地址)
 
IDE的选择
 
IDE,又称集成开发环境,顾名思义,他是一个编辑器 + 编译器的集合。所以对于接触一门新语言的“门外汉”来讲,最方便快捷的方式,就是选择一个IDE来完成这项工作。那么我们可以选择哪个IDE作为学习C语言以及以后开发C语言程序的最佳工具呢?Windows 下的C语言 IDE 众多,多如牛毛,初学者往往不知道该如何选择。我们可以综合自己的实际情况,根据这些IDE的特点来进行对自己最合适的选择。 
(1)Visual Studio
 Windows 下首先推荐大家使用微软开发的 Visual Studio(简称 VS),它是 Windows 下的标准 IDE,实际开发中大家也都在使用。为了适应最新的 Windows 操作系统,微软每隔一段时间(一般是一两年)就会对 VS 进行升级。VS 的不同版本以发布年份命名,例如 VS2010 是微软于 2010 年发布的,VS2019 是微软于 2019 年发布的。





 
当然,这款IDE也有MAC版本,且它是windows官方推出的IDE环境,功能方面自不用说,非常之强大。但是有一个缺点就是看起来相对“臃肿”,可能有很多初学者用不上的工具也会在其中提供,安装包略大,约2~3GB大小,安装时间预计半个小时左右。

优点:功能强大,微软官方维护的IDE,更新及时,操作简单方便;
缺点:略臃肿,安装包大,安装时间长,占用系统资源较多;
选择:如果你的机器性能还可以的话,建议选择该IDE。

 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。
 
相对于 Visual Studio,这款IDE更轻便,支持的语言也更丰富。

优点:跨平台、官方支持、多语言支持、界面美观、操作方便、容易上手
缺点:暂未发现
建议:首选推荐安装

 
(3)Dev C++
 
如果比较讨厌“VS”的复杂性,那么Dev C++是一个不错的选择。Dev C++ 是一款免费开源的 C/C++ IDE,内嵌 GCC 编译器(Linux GCC 编译器的 Windows 移植版),是 NOI、NOIP 等比赛的指定工具。Dev C++ 的优点是体积小(只有几十兆)、安装卸载方便、学习成本低,缺点是调试功能弱。

NOI 是National Olympiad in Informatics的缩写,译为“全国青少年信息学奥林匹克竞赛”;NOIP 是National Olympiad in informatics in Provinces的缩写,译为“全国青少年信息学奥林匹克联赛”。NOI、NOIP 都是奥林匹克竞赛的一种,参加者多为高中生,获奖者将被保送到名牌大学或者得到高考加分资格。


优点:轻巧、方便,对机器性能要求不高,安装卸载方便,学习成本低
缺点:调试功能相对较弱
选择:如果机器性能较弱,建议选择这个IDE

 
(4)Visual C++ 6.0
 
哈哈,之所以把这个IDE放在这里强调下,纯属想要怀念一下,因为大学期间学C语言就是用的这款IDE呀呀呀呀!!!不知道你还记不记得下面这些界面~










 
唉呀妈呀,仿佛还记得炎炎夏日在大学机房用那种方方正正的大脑袋电脑敲代码的日子……
Visual C++ 6.0(简称VC 6.0)是微软开发的一款经典的 IDE,很多高校都以 VC 6.0 为教学工具来讲解C和C++。但VC 6.0是1998年的产品,很古老了,在 Win7、Win8、Win10 下会有各种各样的兼容性问题,甚至根本不能运行,所以不推荐使用。

优点:可以用来回忆
缺点:太多不知从何说起
建议:强烈不建议,如果你想安装,我也拦不住

 
(5)其它 IDE
 
除了上面提到的三款 IDE,Windows 平台下还有很多其他的 IDE,它们各有特点,例如:
Code::Blocks 是一款开源、跨平台、免费的 C/C++ IDE,它和 Dev C++ 非常类似,小巧灵活,易于安装和卸载,不过它的界面要比 Dev C++ 复杂一些,不如 Dev C++ 来得清爽。Turbo C 是一款古老的、DOS 年代的C语言开发工具,程序员只能使用键盘来操作 Turbo C,不能使用鼠标,所以非常不方便。但是 Turbo C 集成了一套图形库,可以在控制台程序中画图,看起来非常炫酷,所以至今仍然有人在使用。C-Free 是一款国产的 Windows 下的C/C++ IDE,最新版本是 5.0,整个软件才 14M,非常轻巧,安装也简单,界面也比 Dev C++ 漂亮。C-Free 的缺点也是调试功能弱。可惜的是,C-Free 已经多年不更新了,组件都老了,只能在 XP、Win7 下运行,在 Win8、Win10 下可能会存在兼容性问题。
 
所以,综合对比之下,如果电脑性能不错,就是用VS,否则,选择Dev C++。
 
赶紧去下载体验吧~~~
 
纯命令行下通过指令操作
 
没有IDE,不代表就不能写代码了!当然你还有“更程序员”的方式,通过命令行执行进行操作。下面介绍windows10下如何使用cmd命令进行GCC编译的几个步骤:
 
首先了解下什么是GCC?

GCC是以GPL许可证所发行的自由软件,也是GNU计划的关键部分。GCC的初衷是为GNU操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,甚至在微软的Windows上也可以使用GCC。GCC支持多种计算机体系结构芯片,如x86、ARM、MIPS等,并已被移植到其他多种硬件平台。
GCC原名为GNU C语言编译器(GNU C Compiler),只能处理C语言。但其很快扩展,变得可处理C++,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。

 1、安装gcc编译器,需要到MinGW 的主页 www.mingw.org,进入 MinGW 下载页面,下载最新版本的 MinGW64 安装程序。或者到:https://sourceforge.net/projects/mingw-w64/files/   ,下载 Download mingw-get-setup.exe (86.5 kB)。不过这个国外网站下载速度超慢,建议进入国内网站下载离线包:https://www.jb51.net/softs/696088.html 

2、运行 Download mingw-get-setup.exe ,点击"运行",continue等,注意记住安装的目录,我的是 F:\MinGw,下面修改环境变量时还会用到。






3、安装好后,需要设置编译器的路径环境变量

选择计算机—属性---高级系统设置---环境变量,在系统变量中找到 Path 变量,在后面加入 min-gw的安装目录,我的是 F:\MinGw\bin






 
 
4、在开始菜单中,点击"运行",输入 cmd,打开命令行:输入 mingw-get,如果弹出 MinGw installation manager 窗口,说明安装正常。此时,关闭 MinGw installation manager 窗口,否则接下来的步骤会报错;
 
5、在cmd中输入命令 mingw-get install gcc,等待一会,gcc 就安装成功了。
 
以上是常规的方式,但是不翻墙可能难以进行下去,所以直接下载离线包,然后把解压后的路径设置到环境变量中即可。





 
这个时候,打开cmd,输入gcc -v 即可查看是否安装成功。





 
然后你就可以在命令行里进行编译和执行c程序啦~
#include <stdio.h>

void main()
{
printf("hello, world! My name is zhengkai!\n");
} gcc 1.c
a.exe




 
 
附件:
VS最新版下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/
VSC最新版下载地址:https://code.visualstudio.com/Download 
Dev C++下载地址:https://bloodshed-dev-c.en.softonic.com/ 查看全部
确定好了学习目标,选择好了学习教程,制定好了学习计划,那么接下来,你需要做的就是搭建一套学习C语言的环境。我们无外乎基本上大多使用的操作系统是Windows和MACOS,所以在这里整理了一些IDE和C语言编译环境搭建的方法和经验供大家参考。这篇为Windows篇,下一篇文章分享MAC上有哪些优良实践。(文末为大家附上了各个IDE的下载地址)
 
IDE的选择
 
IDE,又称集成开发环境,顾名思义,他是一个编辑器 + 编译器的集合。所以对于接触一门新语言的“门外汉”来讲,最方便快捷的方式,就是选择一个IDE来完成这项工作。那么我们可以选择哪个IDE作为学习C语言以及以后开发C语言程序的最佳工具呢?Windows 下的C语言 IDE 众多,多如牛毛,初学者往往不知道该如何选择。我们可以综合自己的实际情况,根据这些IDE的特点来进行对自己最合适的选择。 
(1)Visual Studio
 Windows 下首先推荐大家使用微软开发的 Visual Studio(简称 VS),它是 Windows 下的标准 IDE,实际开发中大家也都在使用。为了适应最新的 Windows 操作系统,微软每隔一段时间(一般是一两年)就会对 VS 进行升级。VS 的不同版本以发布年份命名,例如 VS2010 是微软于 2010 年发布的,VS2019 是微软于 2019 年发布的。

QQ截图20200403102513.jpg

 
当然,这款IDE也有MAC版本,且它是windows官方推出的IDE环境,功能方面自不用说,非常之强大。但是有一个缺点就是看起来相对“臃肿”,可能有很多初学者用不上的工具也会在其中提供,安装包略大,约2~3GB大小,安装时间预计半个小时左右。


优点:功能强大,微软官方维护的IDE,更新及时,操作简单方便;
缺点:略臃肿,安装包大,安装时间长,占用系统资源较多;
选择:如果你的机器性能还可以的话,建议选择该IDE。


 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。
 
相对于 Visual Studio,这款IDE更轻便,支持的语言也更丰富。


优点:跨平台、官方支持、多语言支持、界面美观、操作方便、容易上手
缺点:暂未发现
建议:首选推荐安装


 
(3)Dev C++
 
如果比较讨厌“VS”的复杂性,那么Dev C++是一个不错的选择。Dev C++ 是一款免费开源的 C/C++ IDE,内嵌 GCC 编译器(Linux GCC 编译器的 Windows 移植版),是 NOI、NOIP 等比赛的指定工具。Dev C++ 的优点是体积小(只有几十兆)、安装卸载方便、学习成本低,缺点是调试功能弱。


NOI 是National Olympiad in Informatics的缩写,译为“全国青少年信息学奥林匹克竞赛”;NOIP 是National Olympiad in informatics in Provinces的缩写,译为“全国青少年信息学奥林匹克联赛”。NOI、NOIP 都是奥林匹克竞赛的一种,参加者多为高中生,获奖者将被保送到名牌大学或者得到高考加分资格。



优点:轻巧、方便,对机器性能要求不高,安装卸载方便,学习成本低
缺点:调试功能相对较弱
选择:如果机器性能较弱,建议选择这个IDE


 
(4)Visual C++ 6.0
 
哈哈,之所以把这个IDE放在这里强调下,纯属想要怀念一下,因为大学期间学C语言就是用的这款IDE呀呀呀呀!!!不知道你还记不记得下面这些界面~

QQ截图20200403103514.jpg


timg.gif

 
唉呀妈呀,仿佛还记得炎炎夏日在大学机房用那种方方正正的大脑袋电脑敲代码的日子……
Visual C++ 6.0(简称VC 6.0)是微软开发的一款经典的 IDE,很多高校都以 VC 6.0 为教学工具来讲解C和C++。但VC 6.0是1998年的产品,很古老了,在 Win7、Win8、Win10 下会有各种各样的兼容性问题,甚至根本不能运行,所以不推荐使用。


优点:可以用来回忆
缺点:太多不知从何说起
建议:强烈不建议,如果你想安装,我也拦不住


 
(5)其它 IDE
 
除了上面提到的三款 IDE,Windows 平台下还有很多其他的 IDE,它们各有特点,例如:
  1. Code::Blocks 是一款开源、跨平台、免费的 C/C++ IDE,它和 Dev C++ 非常类似,小巧灵活,易于安装和卸载,不过它的界面要比 Dev C++ 复杂一些,不如 Dev C++ 来得清爽。
  2. Turbo C 是一款古老的、DOS 年代的C语言开发工具,程序员只能使用键盘来操作 Turbo C,不能使用鼠标,所以非常不方便。但是 Turbo C 集成了一套图形库,可以在控制台程序中画图,看起来非常炫酷,所以至今仍然有人在使用。
  3. C-Free 是一款国产的 Windows 下的C/C++ IDE,最新版本是 5.0,整个软件才 14M,非常轻巧,安装也简单,界面也比 Dev C++ 漂亮。C-Free 的缺点也是调试功能弱。可惜的是,C-Free 已经多年不更新了,组件都老了,只能在 XP、Win7 下运行,在 Win8、Win10 下可能会存在兼容性问题。

 
所以,综合对比之下,如果电脑性能不错,就是用VS,否则,选择Dev C++。
 
赶紧去下载体验吧~~~
 
纯命令行下通过指令操作
 
没有IDE,不代表就不能写代码了!当然你还有“更程序员”的方式,通过命令行执行进行操作。下面介绍windows10下如何使用cmd命令进行GCC编译的几个步骤:
 
首先了解下什么是GCC?


GCC是以GPL许可证所发行的自由软件,也是GNU计划的关键部分。GCC的初衷是为GNU操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,甚至在微软的Windows上也可以使用GCC。GCC支持多种计算机体系结构芯片,如x86、ARM、MIPS等,并已被移植到其他多种硬件平台。
GCC原名为GNU C语言编译器(GNU C Compiler),只能处理C语言。但其很快扩展,变得可处理C++,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。


 1、安装gcc编译器,需要到MinGW 的主页 www.mingw.org,进入 MinGW 下载页面,下载最新版本的 MinGW64 安装程序。或者到:https://sourceforge.net/projects/mingw-w64/files/   ,下载 Download mingw-get-setup.exe (86.5 kB)。不过这个国外网站下载速度超慢,建议进入国内网站下载离线包:https://www.jb51.net/softs/696088.html 

2、运行 Download mingw-get-setup.exe ,点击"运行",continue等,注意记住安装的目录,我的是 F:\MinGw,下面修改环境变量时还会用到。

QQ截图20200403113153.jpg


3、安装好后,需要设置编译器的路径环境变量


选择计算机—属性---高级系统设置---环境变量,在系统变量中找到 Path 变量,在后面加入 min-gw的安装目录,我的是 F:\MinGw\bin



QQ截图20200403121319.jpg

 
 
4、在开始菜单中,点击"运行",输入 cmd,打开命令行:输入 mingw-get,如果弹出 MinGw installation manager 窗口,说明安装正常。此时,关闭 MinGw installation manager 窗口,否则接下来的步骤会报错;
 
5、在cmd中输入命令 mingw-get install gcc,等待一会,gcc 就安装成功了。
 
以上是常规的方式,但是不翻墙可能难以进行下去,所以直接下载离线包,然后把解压后的路径设置到环境变量中即可。

QQ截图20200403121018.jpg

 
这个时候,打开cmd,输入gcc -v 即可查看是否安装成功。

QQ截图20200403121453.jpg

 
然后你就可以在命令行里进行编译和执行c程序啦~
#include <stdio.h>

void main()
{
printf("hello, world! My name is zhengkai!\n");
}
 
gcc 1.c
a.exe

QQ截图20200403122249.jpg

 
 
附件:
VS最新版下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/
VSC最新版下载地址:https://code.visualstudio.com/Download 
Dev C++下载地址:https://bloodshed-dev-c.en.softonic.com/

supervisor的常用操作命令有哪些?

回复

工具软件zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 192 次浏览 • 2020-03-16 17:10 • 来自相关话题

vim常用命令总结

工具软件zkbhj 发表了文章 • 0 个评论 • 823 次浏览 • 2017-02-16 15:36 • 来自相关话题

 





 vim 选择文本,删除,复制,粘贴

文本的选择,对于编辑器来说,是很基本的东西,也经常被用到,总结如下:

v 从光标当前位置开始,光标所经过的地方会被选中,再按一下v结束。

V 从光标当前行开始,光标经过的行都会被选中,再按一下V结束。

Ctrl + v 从光标当前位置开始,选中光标起点和终点所构成的矩形区域,再按一下Ctrl + v结束。

ggVG 选中全部的文本, 其中gg为跳到行首,V选中整行,G末尾

选中后就可以用编辑命令对其进行编辑,如
d 删除

y 复制 (默认是复制到"寄存器)

p 粘贴 (默认从"寄存器取出内容粘贴)

"+y 复制到系统剪贴板(也就是vim的+寄存器)

"+p 从系统剪贴板粘贴

vim命令总结

1.删除字符

要删除一个字符,只需要将光标移到该字符上按下"x"。

2.删除一行

删除一整行内容使用"dd"命令。删除后下面的行会移上来填补空缺。

3.删除换行符

在Vim中你可以把两行合并为一行,也就是说两行之间的换行符被删除了:命令是"J"。

4.撤销

如果你误删了过多的内容。显然你可以再输入一遍,但是命令"u" 更简便,它可以撤消上一次的操作。

5.重做

如果你撤消了多次,你还可以用CTRL-R(重做)来反转撤消的动作。换句话说,它是对撤消的撤消。撤消命令还有另一种形式,"U"命令,它一次撤消对一行的全部操作。第二次使用该命令则会撤消前一个"U"的操作。用"u"和CTRL-R你可以找回任何一个操作状态。

6.追加

"i"命令可以在当前光标之前插入文本。
"a"命令可以在当前光标之后插入文本。
"o"命令可以在当前行的下面另起一行,并使当前模式转为Insert模式。
"O"命令(注意是大写的字母O)将在当前行的上面另起一行。

7.使用命令计数

假设你要向上移动9行。这可以用"kkkkkkkkk"或"9k"来完成。事实上,很多命令都可以接受一个数字作为重复执行同一命令的次数。比如刚才的例子,要在行尾追加三个感叹号,当时用的命令是"a!!!"。另一个办法是用"3a!"命令。3说明该命令将被重复执行3次。同样,删除3个字符可以用"3x"。指定的数字要紧挨在它所要修饰的命令前面。

8.退出

要退出Vim,用命令"ZZ"。该命令保存当前文件并退出Vim。

9.放弃编辑

丢弃所有的修改并退出,用命令":q!"。用":e!"命令放弃所有修改并重新载入该文件的原始内容。

10.以Word为单位的移动

使用"w"命令可以将光标向前移动一个word的首字符上;比如"3w"将光标向前移动3个words。"b"命令则将光标向后移动到前一个word的首字符上。
"e"命令会将光标移动到下一个word的最后一个字符。命令"ge",它将光标移动到前一个word的最后一个字符上。、

11.移动到行首或行尾

"$"命令将光标移动到当前行行尾。如果你的键盘上有一个键,它的作用也一样。"^"命令将光标移动到当前行的第一个非空白字符上。"0"命令则总是把光标移动到当前行的第一个字符上。键也是如此。"$"命令还可接受一个计数,如"1$"会将光标移动到当前行行尾,"2$"则会移动到下一行的行尾,如此类推。"0"命令却不能接受类似这样的计数,命令"^"前加上一个计数也没有任何效果。

12.移动到指定字符上

命令"fx"在当前行上查找下一个字符x(向右方向),可以带一个命令计数"F"命令向左方向搜索。"tx"命令形同"fx"命令,只不过它不是把光标停留在被搜索字符上,而是在它之前的一个字符上。提示:"t"意为"To"。该命令的反方向版是"Tx"。这4个命令都可以用";"来重复。以","也是重复同样的命令,但是方向与原命令的方向相反。

13.以匹配一个括号为目的移动

用命令"%"跳转到与当前光标下的括号相匹配的那一个括号上去。如果当前光标在"("上,它就向前跳转到与它匹配的")"上,如果当前在")"上,它就向后自动跳转到匹配的"("上去.

14.移动到指定行

用"G"命令指定一个命令计数,这个命令就会把光标定位到由命令计数指定的行上。比如"33G"就会把光标置于第33行上。没有指定命令计数作为参数的话, "G"会把光标定位到最后一行上。"gg"命令是跳转到第一行的快捷的方法。
另一个移动到某行的方法是在命令"%"之前指定一个命令计数比如"50%"将会把光标定位在文件的中间. "90%"跳到接近文件尾的地方。
命令"H","M","L",分别将光标跳转到第一行,中间行,结尾行部分。

15.告诉你当前的位置

使用CTRL-G命令。"set number"在每行的前面显示一个行号。相反关闭行号用命令":set nonumber"。":set ruler"在Vim窗口的右下角显示当前光标位置。

16.滚屏

CTRL-U显示文本的窗口向上滚动了半屏。CTRL-D命令将窗口向下移动半屏。一次滚动一行可以使用CTRL-E(向上滚动)和CTRL-Y(向下滚动)。要向前滚动一整屏使用命令CTRL-F。另外CTRL-B是它的反向版。"zz"命令会把当前行置为屏幕正中央,"zt"命令会把当前行置于屏幕顶端,"zb"则把当前行置于屏幕底端.

17.简单搜索

"/string"命令可用于搜索一个字符串。要查找上次查找的字符串的下一个位置,使用"n"命令。如果你知道你要找的确切位置是目标字符串的第几次出现,还可以在"n"之前放置一个命令计数。"3n"会去查找目标字符串的第3次出现。
"?"命令与"/"的工作相同,只是搜索方向相反."N"命令会重复前一次查找,但是与最初用"/"或"?"指定的搜索方向相反。
如果查找内容忽略大小写,则用命令"set ignorecase", 返回精确匹配用命令"set noignorecase" 。

18.在文本中查找下一个word

把光标定位于这个word上然后按下""键。Vim将会取当前光标所在的word并将它作用目标字符串进行搜索。"#"命令是""的反向版。还可以在这两个命令前加一个命令计数:"3*"查找当前光标下的word的第三次出现。

19.查找整个word

如果你用"/the"来查找Vim也会匹配到"there"。要查找作为独立单词的"the"使用如下命令:"/the>"。">"是一个特殊的记法,它只匹配一个word的结束处。近似地,"\<"匹配到一个word的开始处。这样查找作为一个word的"the"就可以用:"/"。

20.高亮显示搜索结果

开启这一功能用":set hlsearch",关闭这一功能:":set nohlsearch"。如果只是想去掉当前的高亮显示,可以使用下面的命令:":nohlsearch"(可以简写为noh)。

21.匹配一行的开头与结尾

^ 字符匹配一行的开头。$字符匹配一行的末尾。
所以"/was$"只匹配位于一行末尾的单词was,所以"/^was"只匹配位于一行开始的单词was。

22.匹配任何的单字符

.这个字符可以匹配到任何字符。比如"c.m"可以匹配任何前一个字符是c,后一个字符是m的情况,不管中间的字符是什么。

23.匹配特殊字符

放一个反斜杠在特殊字符前面。如果你查找"ter。",用命令"/ter\。"

24.使用标记

当你用"G"命令从一个地方跳转到另一个地方时,Vim会记得你起跳的位置。这个位置在Vim中是一个标记。使用命令" "可以使你跳回到刚才的出发点。命令可以在两点之间来回跳转。CTRL-O命令是跳转到你更早些时间停置光标的位置(提示:O意为older). CTRL-I则是跳回到后来停置光标的更新的位置(提示:I在键盘上位于O前面)。
注:使用CTRL-I 与按下键一样。

25.具名标记

命令"ma"将当前光标下的位置名之为标记"a"。从a到z一共可以使用26个自定义的标记。要跳转到一个你定义过的标记,使用命令" marks "marks就是定义的标记的名字。命令" 'a "使你跳转到a所在行的行首,"a "会精确定位a所在的位置。命令:":marks"用来查看标记的列表。
命令delm!删除所有标记。

26.操作符命令和位移

"dw"命令可以删除一个word,"d4w"命令是删除4个word,依此类推。类似有"d2e"、"d$"。此类命令有一个固定的模式:操作符命令+位移命令。首先键入一个操作符命令。比如"d"是一个删除操作符。接下来是一个位移命。比如"w"。这样任何移动光标命令所及之处,都是命令的作用范围。

27.改变文本

操作符命令是"c",改变命令。它的行为与"d"命令类似,不过在命令执行后会进入Insert模式。比如"cw"改变一个word。或者,更准确地说,它删除一个word并让你置身于Insert模式。
"cc"命令可以改变整行。不过仍保持原来的缩进。
"c$"改变当前光标到行尾的内容。
快捷命令:x 代表dl(删除当前光标下的字符)
X 代表dh(删除当前光标左边的字符)
D 代表d$(删除到行尾的内容)
C 代表c$(修改到行尾的内容)
s 代表cl(修改一个字符)
S 代表cc(修改一整行)
命令"3dw"和"d3w"都是删除3个word。第一个命令"3dw"可以看作是删除一个word的操作执行3次;第二个命令"d3w"是一次删除3个word。这是其中不明显的差异。事实上你可以在两处都放上命令记数,比如,"3d2w"是删除两个word,重复执行3次,总共是6个word。

28.替换单个字符

"r"命令不是一个操作符命令。它等待你键入下一个字符用以替换当前光标下的那个字符。"r"命令前辍以一个命令记数是将多个字符都替换为即将输入的那个字符。要把一个字符替换为一个换行符使用"r"。它会删除一个字符并插入一个换行符。在此处使用命令记数只会删除指定个数的字符:"4r"将把4个字符替换为一个换行符。

29.重复改动

"."命令会重复上一次做出的改动。"."命令会重复你做出的所有修改,除了"u"命令CTRL-R和以冒号开头的命令。"."需要在Normal模式下执行,它重复的是命令,而不是被改动的内容,

30.Visual模式

按"v"可以进入Visual模式。移动光标以覆盖你想操纵的文本范围。同时被选中的文本会以高亮显示。最后键入操作符命令。

31.移动文本

以"d"或"x"这样的命令删除文本时,被删除的内容还是被保存了起来。你还可以用p命令把它取回来。"P"命令是把被去回的内容放在光标之前,"p"则是放在光标之后。对于以"dd"删除的整行内容,"P"会把它置于当前行的上一行。"p"则是至于当前行的后一行。也可以对命令"p"和"P"命令使用命令记数。它的效果是同样的内容被取回指定的次数。这样一来"dd"之后的"3p"就可以把被删除行的3 份副本放到当前位置。
命令"xp"将光标所在的字符与后一个字符交换。

32.复制文本(VIM编辑器内复制)

"y"操作符命令会把文本复制到一个寄存器3中。然后可以用"p"命令把它取回。因为"y"是一个操作符命令,所以你可以用"yw"来复制一个word. 同样可以使用命令记数。如下例中用"y2w"命令复制两个word,"yy"命令复制一整行,"Y"也是复制整行的内容,复制当前光标至行尾的命令是"y$"。

33.文本对象

"diw" 删除当前光标所在的word(不包括空白字符) "daw" 删除当前光标所在的word(包括空白字符)

34.快捷命令

x 删除当前光标下的字符("dl"的快捷命令)
X 删除当前光标之前的字符("dh"的快捷命令)
D 删除自当前光标至行尾的内容("d$"的快捷命令)
dw 删除自当前光标至下一个word的开头
db 删除自当前光标至前一个word的开始
diw 删除当前光标所在的word(不包括空白字符)
daw 删除当前光标所在的word(包括空白字符)
dG 删除当前行至文件尾的内容
dgg 删除当前行至文件头的内容
如果你用"c"命令代替"d"这些命令就都变成更改命令。使用"y"就是yank命令,如此类推。

35.编辑另一个文件

用命令":edit foo.txt",也可简写为":e foo.txt"。

36.文件列表

可以在启动Vim时就指定要编辑多个文件,用命令"vim one.c two.c three.c"。Vim将在启动后只显示第一个文件,完成该文件的编辑后,可以用令:":next"或":n"要保存工作成果并继续下一个文件的编辑,命令:":wnext"或":wn"可以合并这一过程。

37.显示当前正在编辑的文件

用命令":args"。

38.移动到另一个文件

用命令":previous" ":prev"回到上一个文件,合并保存步骤则是":wprevious" ":wprev"。要移到最后一个文件":last",到第一个":first".不过没有":wlast"或者":wfirst"这样的命令。可以在":next"和":previous"命令前面使用一个命令计数。

39.编辑另一个文件列表

不用重新启动Vim,就可以重新定义一个文件列表。命令":args five.c six.c seven.h"定义了要编辑的三个文件。

39.自动存盘

命令":set autowrite","set aw"。自动把内容写回文件: 如果文件被修改过,在每个:next、:rewind、:last、:first、:previous、:stop、:suspend、:tag、:!、:make、CTRL-] 和 CTRL-^命令时进行。
命令":set autowriteall","set awa"。和 'autowrite' 类似,但也适用于":edit"、":enew"、":quit"、":qall"、":exit"、":xit"、":recover" 和关闭 Vim 窗口。置位本选项也意味着 Vim 的行为就像打开 'autowrite' 一样。

40.切换到另一文件

要在两个文件间快速切换,使用CTRL-^。

41.文件标记

以大写字母命名的标记。它们是全局标记,它们可以用在任何文件中。比如,正在编辑"fab1.Java",用命令"50%mF"在文件的中间设置一个名为F的标记。然后在"fab2.java"文件中,用命令"GnB"在最后一行设置名为B的标记。在可以用"F"命令跳转到文件"fab1.java"的半中间。或者编辑另一个文件,"'B"命令会再把你带回文件"fab2.java"的最后一行。
要知道某个标记所代表的位置是什么,可以将该标记的名字作为"marks"命令的参数":marks M"或者连续跟上几个参数":marks MJK"
可以用CTRL-O和CTRL-I可以跳转到较早的位置和靠后的某位置。

42.查看文件

仅是查看文件,不向文件写入内容,可以用只读形式编辑文件。用命令:
vim -R file。如果是想强制性地避免对文件进行修改,可以用命令:
vim -M file。

43.更改文件名

将现有文件存成新的文件,用命令":sav(eas) move.c"。如果想改变当前正在编辑的文件名,但不想保存该文件,就可以用命令:":f(ile) move.c"。

44.分割一个窗口

打开一个新窗口最简单的办法就是使用命令:":split"。CTRL-W 命令可以切换当前活动窗口。

45.关闭窗口

用命令:"close".可以关闭当前窗口。实际上,任何退出文件编辑的命令":quit"和"ZZ"都会关闭窗口,但是用":close"可以阻止你关闭最后一个Vim,以免以意外地整个关闭了Vim。

46.关闭除当前窗口外的所有其他窗口

用命令:":only",关闭除当前窗口外的所有其它窗口。如果这些窗口中有被修改过的,你会得到一个错误信息,同时那个窗口会被留下来。

47.为另一个文件分隔出一个窗口

命令":split two.c"可以打开第二个窗口同时在新打开的窗口中开始编辑作为
参数的文件。如果要打开一个新窗口并开始编辑一个空的缓冲区,使用命令:":new"。

48.垂直分割

用命令":vsplit或::vsplit two.c"。同样有一个对应的":vnew"命令,用于垂直分隔窗口并在其中打开一个新的空缓冲区。

49.切换窗口

CTRL-W h 到左边的窗口
CTRL-W j 到下面的窗口
CTRL-W k 到上面的窗口
CTRL-W l 到右边的窗口
CTRL-W t 到顶部窗口
CTRL-W b 到底部窗口

50.针对所有窗口操作的命令

":qall"放弃所有操作并退出,":wall"保存所有,":wqall"保存所有并退出。

51.为每一个文件打开一个窗口

使用"-o"选项可以让Vim为每一个文件打开一个窗口:
"vim -o one.txt two.txt three.txt"。

52.使用vimdiff查看不同

"vimdiff main.c~ main.c",另一种进入diff模式的办法可以在Vim运行中操作。编辑文件"main.c",然后打开另一个分隔窗口显示其不同:
":edit main.c"
":vertical diffpatch main.c.diff"。

53.页签

命令":tabe(dit) thatfile"在一个窗口中打开"thatfile",该窗口占据着整个的Vim显示区域。命令":tab split/new"结果是新建了一个拥有一个窗口的页签。以用"gt"命令在不同的页签间切换。

这是我总结的一些基本用法,可能对初用者会有帮助,独乐乐不如众乐乐,是吧!

说明:以下黑色为vi和vim均有的一般功能,而红色为Vim(Vi Improved)所特有功能。Vim一般的Unix和Linux下均有安装。

三种状态

Command: 任何输入都会作为编辑命令,而不会出现在屏幕上,任何输入都引起立即反映
Insert: 任何输入的数据都置于编辑寄存器,按ESC,可跳回command方式
Escape: 以“:”或者“/”为前导的指令,出现在屏幕的最下一行,任何输入都被当成特别指令。

离开vi

:q! 离开vi,并放弃刚在缓冲区内编辑的内容。
:wq 将缓冲区内的资料写入磁盘中,并离开vi。
:x 同wq。
(注意—— :X 是文件加密,一定要与:x存盘退出相区别)

进入输入模式

a (append) 由游标之后加入资料。
A 由该行之末加入资料。
i (insert) 由游标之前加入资料。
I 由该行之首加入资料。
o (open) 新增一行於该行之下供输入资料之用。
O 新增一行於该行之上供输入资料之用。

删除与修改

x 删除游标所在该字元。
X 删除游标所在之前一字元。
r 用接於此指令之后的字元取代(replace)游标所在字元。如:ra将游标所在字元以 a 取代之。
R 进入取代状态,直到《ESC》为止。
s 删除游标所在之字元,并进入输入模式直到《ESC》。
S 删除游标所在之该行资料,并进入输入模式直到《ESC》。

光标的移动

0 移至该行之首
$ 移至该行之末。
e 移动到下个字的最後一个字母
w 移动到下个字的第一个字母。
b 移动到上个字的第一个字母。
^ 移至该行的第一个字元处。
H 移至视窗的第一行。
M 移至视窗的中间那行。
L 移至视窗的最后一行。
G 移至该文件的最后一行。
+ 移至下一列的第一个字元处。
- 移至上一列的第一个字元处。
:n 移至该文件的第 n 列。
n+ 移至游标所在位置之后的第 n 列。
n- 移至游标所在位置之前的第 n 列。
Ctrl+g 显示该行之行号、文件名称、文件中最末行之行号、游标所在行号占总行号之百分比。

视窗的移动

Ctrl+f 视窗往下卷一页。
Ctrl+b 视窗往上卷一页。
Ctrl+d 视窗往下卷半页。
Ctrl+u 视窗往上卷半页。
Ctrl+e 视窗往下卷一行。
Ctrl+y 视窗往上卷一行。

剪切、复制、删除

Operator + Scope = command
Operator
d 剪切
y 复制。
p 粘帖,与 d 和 y 配和使用。可将最后d或y的资料放置於游标所在位置之行列下。
c 修改,类似delete与insert的组和。删除一个字组、句子等之资料,并插入新建资料。
Scope
e 由游标所在位置至该字串的最后一个字元。
w 由游标所在位置至下一个字串的第一个字元。
b 由游标所在位置至前一个字串的第一个字元。
$ 由游标所在位置至该行的最后一个字元。
0 由游标所在位置至该行的第一个字元。

整行动作

dd 删除整行。
D 以行为单位,删除游标后之所有字元。
cc 修改整行的内容。
yy 使游标所在该行复制到记忆体缓冲区。

取消前一动作(Undo)

u 恢复最后一个指令之前的结果。
U 恢复游标该行之所有改变。
u 可以多次撤消指令,一次撤消一个操作,直至本次操作开始为止。
Ctrl+r 可以恢复撤消前内容,按多次可恢复多次。

查找与替换

/字串 往游标之后寻找该字串。
?字串 往游标之前寻找该字串。
n 往下继续寻找下一个相同的字串。
N 往上继续寻找下一个相同的字串。
% 查找“(”,“)”,“{”,“}”的配对符。
s 搜寻某行列范围。
g 搜寻整个编辑缓冲区的资料。
:1,$s/old/new/g 将文件中所有的『old』改成『new』。
:10,20s/^/ / 将第10行至第20行资料的最前面插入5个空白。
(vim)
/字符串 后边输入查询内容可保存至缓冲区中,可用↑↓进行以往内容选择。
另外:将光标移动在选定单词下方按*,则可以选中此单词作为查询字符,可以避免输入一长串字符的麻烦。

大小写替换

首先用按v开启选择功能,然后用↑↓←→键来选定所要替换的字符,若是小写变大写,则按U;反之按u;
如果是选择单词,则可以在按v后,按w,最后按U/u,这样就可以将字符随意的改变大小写了,而不用删除后重新敲入。

资料的连接

句子的连接。将游标所在之下一行连接至游标该行的后面。
环境的设定
:set all 可设置的环境变量列表
:set 环境变量的当前值
:set nu 设定资料的行号。
:set nonu 取消行号设定。
:set ai 自动内缩。
:set noai 取消自动内缩。
:set ruler 会在屏幕右下角显示当前光标所处位置,并随光移动而改变,占用屏幕空间较小,使用也比较方便,推荐使用。
:set hlsearch 在使用查找功能时,会高亮显示所有匹配的内容。
:set nohlsearch 关闭此功能。
:set incsearch 使Vim在输入字符串的过程中,光标就可定位显示匹配点。
:set nowrapscan 关闭查找自动回环功能,即查找到文件结尾处,结束查找;默认状态是自动回环

ex指令
读写资料

:10,20w test 将第10行至第20行的资料写入test文件。
:10,20w>>test 将第10行至第20行的资料加在test文件之后。
:r test 将test文件的资料读入编辑缓冲区的最后。
:e [filename] 编辑新的文件。
:e! [filename] 放弃当前修改的文件,编辑新的文件。
:sh 进入shell环境,使用exit退出,回到编辑器中。

:!cmd 运行命令cmd后,返回到编辑器中。

删除、复制及搬移

:10,20d 删除第10行至第20行的资料。
:10d 删除第10行的资料。
:%d 删除整个编辑缓冲区。
:10,20co30 将第10行至第20行的资料复制至第30行之后。
:10,20mo30 将第10行至第20行的资料搬移至第30行之后。 查看全部

555482-20161202001047365-2061100822.jpg

 

555482-20161202000652615-1023421454.png

 vim 选择文本,删除,复制,粘贴

文本的选择,对于编辑器来说,是很基本的东西,也经常被用到,总结如下:

v 从光标当前位置开始,光标所经过的地方会被选中,再按一下v结束。

V 从光标当前行开始,光标经过的行都会被选中,再按一下V结束。

Ctrl + v 从光标当前位置开始,选中光标起点和终点所构成的矩形区域,再按一下Ctrl + v结束。

ggVG 选中全部的文本, 其中gg为跳到行首,V选中整行,G末尾

选中后就可以用编辑命令对其进行编辑,如
d 删除

y 复制 (默认是复制到"寄存器)

p 粘贴 (默认从"寄存器取出内容粘贴)

"+y 复制到系统剪贴板(也就是vim的+寄存器)

"+p 从系统剪贴板粘贴

vim命令总结

1.删除字符

要删除一个字符,只需要将光标移到该字符上按下"x"。

2.删除一行

删除一整行内容使用"dd"命令。删除后下面的行会移上来填补空缺。

3.删除换行符

在Vim中你可以把两行合并为一行,也就是说两行之间的换行符被删除了:命令是"J"。

4.撤销

如果你误删了过多的内容。显然你可以再输入一遍,但是命令"u" 更简便,它可以撤消上一次的操作。

5.重做

如果你撤消了多次,你还可以用CTRL-R(重做)来反转撤消的动作。换句话说,它是对撤消的撤消。撤消命令还有另一种形式,"U"命令,它一次撤消对一行的全部操作。第二次使用该命令则会撤消前一个"U"的操作。用"u"和CTRL-R你可以找回任何一个操作状态。

6.追加

"i"命令可以在当前光标之前插入文本。
"a"命令可以在当前光标之后插入文本。
"o"命令可以在当前行的下面另起一行,并使当前模式转为Insert模式。
"O"命令(注意是大写的字母O)将在当前行的上面另起一行。

7.使用命令计数

假设你要向上移动9行。这可以用"kkkkkkkkk"或"9k"来完成。事实上,很多命令都可以接受一个数字作为重复执行同一命令的次数。比如刚才的例子,要在行尾追加三个感叹号,当时用的命令是"a!!!"。另一个办法是用"3a!"命令。3说明该命令将被重复执行3次。同样,删除3个字符可以用"3x"。指定的数字要紧挨在它所要修饰的命令前面。

8.退出

要退出Vim,用命令"ZZ"。该命令保存当前文件并退出Vim。

9.放弃编辑

丢弃所有的修改并退出,用命令":q!"。用":e!"命令放弃所有修改并重新载入该文件的原始内容。

10.以Word为单位的移动

使用"w"命令可以将光标向前移动一个word的首字符上;比如"3w"将光标向前移动3个words。"b"命令则将光标向后移动到前一个word的首字符上。
"e"命令会将光标移动到下一个word的最后一个字符。命令"ge",它将光标移动到前一个word的最后一个字符上。、

11.移动到行首或行尾

"$"命令将光标移动到当前行行尾。如果你的键盘上有一个键,它的作用也一样。"^"命令将光标移动到当前行的第一个非空白字符上。"0"命令则总是把光标移动到当前行的第一个字符上。键也是如此。"$"命令还可接受一个计数,如"1$"会将光标移动到当前行行尾,"2$"则会移动到下一行的行尾,如此类推。"0"命令却不能接受类似这样的计数,命令"^"前加上一个计数也没有任何效果。

12.移动到指定字符上

命令"fx"在当前行上查找下一个字符x(向右方向),可以带一个命令计数"F"命令向左方向搜索。"tx"命令形同"fx"命令,只不过它不是把光标停留在被搜索字符上,而是在它之前的一个字符上。提示:"t"意为"To"。该命令的反方向版是"Tx"。这4个命令都可以用";"来重复。以","也是重复同样的命令,但是方向与原命令的方向相反。

13.以匹配一个括号为目的移动

用命令"%"跳转到与当前光标下的括号相匹配的那一个括号上去。如果当前光标在"("上,它就向前跳转到与它匹配的")"上,如果当前在")"上,它就向后自动跳转到匹配的"("上去.

14.移动到指定行

用"G"命令指定一个命令计数,这个命令就会把光标定位到由命令计数指定的行上。比如"33G"就会把光标置于第33行上。没有指定命令计数作为参数的话, "G"会把光标定位到最后一行上。"gg"命令是跳转到第一行的快捷的方法。
另一个移动到某行的方法是在命令"%"之前指定一个命令计数比如"50%"将会把光标定位在文件的中间. "90%"跳到接近文件尾的地方。
命令"H","M","L",分别将光标跳转到第一行,中间行,结尾行部分。

15.告诉你当前的位置

使用CTRL-G命令。"set number"在每行的前面显示一个行号。相反关闭行号用命令":set nonumber"。":set ruler"在Vim窗口的右下角显示当前光标位置。

16.滚屏

CTRL-U显示文本的窗口向上滚动了半屏。CTRL-D命令将窗口向下移动半屏。一次滚动一行可以使用CTRL-E(向上滚动)和CTRL-Y(向下滚动)。要向前滚动一整屏使用命令CTRL-F。另外CTRL-B是它的反向版。"zz"命令会把当前行置为屏幕正中央,"zt"命令会把当前行置于屏幕顶端,"zb"则把当前行置于屏幕底端.

17.简单搜索

"/string"命令可用于搜索一个字符串。要查找上次查找的字符串的下一个位置,使用"n"命令。如果你知道你要找的确切位置是目标字符串的第几次出现,还可以在"n"之前放置一个命令计数。"3n"会去查找目标字符串的第3次出现。
"?"命令与"/"的工作相同,只是搜索方向相反."N"命令会重复前一次查找,但是与最初用"/"或"?"指定的搜索方向相反。
如果查找内容忽略大小写,则用命令"set ignorecase", 返回精确匹配用命令"set noignorecase" 。

18.在文本中查找下一个word

把光标定位于这个word上然后按下""键。Vim将会取当前光标所在的word并将它作用目标字符串进行搜索。"#"命令是""的反向版。还可以在这两个命令前加一个命令计数:"3*"查找当前光标下的word的第三次出现。

19.查找整个word

如果你用"/the"来查找Vim也会匹配到"there"。要查找作为独立单词的"the"使用如下命令:"/the>"。">"是一个特殊的记法,它只匹配一个word的结束处。近似地,"\<"匹配到一个word的开始处。这样查找作为一个word的"the"就可以用:"/"。

20.高亮显示搜索结果

开启这一功能用":set hlsearch",关闭这一功能:":set nohlsearch"。如果只是想去掉当前的高亮显示,可以使用下面的命令:":nohlsearch"(可以简写为noh)。

21.匹配一行的开头与结尾

^ 字符匹配一行的开头。$字符匹配一行的末尾。
所以"/was$"只匹配位于一行末尾的单词was,所以"/^was"只匹配位于一行开始的单词was。

22.匹配任何的单字符

.这个字符可以匹配到任何字符。比如"c.m"可以匹配任何前一个字符是c,后一个字符是m的情况,不管中间的字符是什么。

23.匹配特殊字符

放一个反斜杠在特殊字符前面。如果你查找"ter。",用命令"/ter\。"

24.使用标记

当你用"G"命令从一个地方跳转到另一个地方时,Vim会记得你起跳的位置。这个位置在Vim中是一个标记。使用命令" "可以使你跳回到刚才的出发点。命令可以在两点之间来回跳转。CTRL-O命令是跳转到你更早些时间停置光标的位置(提示:O意为older). CTRL-I则是跳回到后来停置光标的更新的位置(提示:I在键盘上位于O前面)。
注:使用CTRL-I 与按下键一样。

25.具名标记

命令"ma"将当前光标下的位置名之为标记"a"。从a到z一共可以使用26个自定义的标记。要跳转到一个你定义过的标记,使用命令" marks "marks就是定义的标记的名字。命令" 'a "使你跳转到a所在行的行首,"a "会精确定位a所在的位置。命令:":marks"用来查看标记的列表。
命令delm!删除所有标记。

26.操作符命令和位移

"dw"命令可以删除一个word,"d4w"命令是删除4个word,依此类推。类似有"d2e"、"d$"。此类命令有一个固定的模式:操作符命令+位移命令。首先键入一个操作符命令。比如"d"是一个删除操作符。接下来是一个位移命。比如"w"。这样任何移动光标命令所及之处,都是命令的作用范围。

27.改变文本

操作符命令是"c",改变命令。它的行为与"d"命令类似,不过在命令执行后会进入Insert模式。比如"cw"改变一个word。或者,更准确地说,它删除一个word并让你置身于Insert模式。
"cc"命令可以改变整行。不过仍保持原来的缩进。
"c$"改变当前光标到行尾的内容。
快捷命令:x 代表dl(删除当前光标下的字符)
X 代表dh(删除当前光标左边的字符)
D 代表d$(删除到行尾的内容)
C 代表c$(修改到行尾的内容)
s 代表cl(修改一个字符)
S 代表cc(修改一整行)
命令"3dw"和"d3w"都是删除3个word。第一个命令"3dw"可以看作是删除一个word的操作执行3次;第二个命令"d3w"是一次删除3个word。这是其中不明显的差异。事实上你可以在两处都放上命令记数,比如,"3d2w"是删除两个word,重复执行3次,总共是6个word。

28.替换单个字符

"r"命令不是一个操作符命令。它等待你键入下一个字符用以替换当前光标下的那个字符。"r"命令前辍以一个命令记数是将多个字符都替换为即将输入的那个字符。要把一个字符替换为一个换行符使用"r"。它会删除一个字符并插入一个换行符。在此处使用命令记数只会删除指定个数的字符:"4r"将把4个字符替换为一个换行符。

29.重复改动

"."命令会重复上一次做出的改动。"."命令会重复你做出的所有修改,除了"u"命令CTRL-R和以冒号开头的命令。"."需要在Normal模式下执行,它重复的是命令,而不是被改动的内容,

30.Visual模式

按"v"可以进入Visual模式。移动光标以覆盖你想操纵的文本范围。同时被选中的文本会以高亮显示。最后键入操作符命令。

31.移动文本

以"d"或"x"这样的命令删除文本时,被删除的内容还是被保存了起来。你还可以用p命令把它取回来。"P"命令是把被去回的内容放在光标之前,"p"则是放在光标之后。对于以"dd"删除的整行内容,"P"会把它置于当前行的上一行。"p"则是至于当前行的后一行。也可以对命令"p"和"P"命令使用命令记数。它的效果是同样的内容被取回指定的次数。这样一来"dd"之后的"3p"就可以把被删除行的3 份副本放到当前位置。
命令"xp"将光标所在的字符与后一个字符交换。

32.复制文本(VIM编辑器内复制)

"y"操作符命令会把文本复制到一个寄存器3中。然后可以用"p"命令把它取回。因为"y"是一个操作符命令,所以你可以用"yw"来复制一个word. 同样可以使用命令记数。如下例中用"y2w"命令复制两个word,"yy"命令复制一整行,"Y"也是复制整行的内容,复制当前光标至行尾的命令是"y$"。

33.文本对象

"diw" 删除当前光标所在的word(不包括空白字符) "daw" 删除当前光标所在的word(包括空白字符)

34.快捷命令

x 删除当前光标下的字符("dl"的快捷命令)
X 删除当前光标之前的字符("dh"的快捷命令)
D 删除自当前光标至行尾的内容("d$"的快捷命令)
dw 删除自当前光标至下一个word的开头
db 删除自当前光标至前一个word的开始
diw 删除当前光标所在的word(不包括空白字符)
daw 删除当前光标所在的word(包括空白字符)
dG 删除当前行至文件尾的内容
dgg 删除当前行至文件头的内容
如果你用"c"命令代替"d"这些命令就都变成更改命令。使用"y"就是yank命令,如此类推。

35.编辑另一个文件

用命令":edit foo.txt",也可简写为":e foo.txt"。

36.文件列表

可以在启动Vim时就指定要编辑多个文件,用命令"vim one.c two.c three.c"。Vim将在启动后只显示第一个文件,完成该文件的编辑后,可以用令:":next"或":n"要保存工作成果并继续下一个文件的编辑,命令:":wnext"或":wn"可以合并这一过程。

37.显示当前正在编辑的文件

用命令":args"。

38.移动到另一个文件

用命令":previous" ":prev"回到上一个文件,合并保存步骤则是":wprevious" ":wprev"。要移到最后一个文件":last",到第一个":first".不过没有":wlast"或者":wfirst"这样的命令。可以在":next"和":previous"命令前面使用一个命令计数。

39.编辑另一个文件列表

不用重新启动Vim,就可以重新定义一个文件列表。命令":args five.c six.c seven.h"定义了要编辑的三个文件。

39.自动存盘

命令":set autowrite","set aw"。自动把内容写回文件: 如果文件被修改过,在每个:next、:rewind、:last、:first、:previous、:stop、:suspend、:tag、:!、:make、CTRL-] 和 CTRL-^命令时进行。
命令":set autowriteall","set awa"。和 'autowrite' 类似,但也适用于":edit"、":enew"、":quit"、":qall"、":exit"、":xit"、":recover" 和关闭 Vim 窗口。置位本选项也意味着 Vim 的行为就像打开 'autowrite' 一样。

40.切换到另一文件

要在两个文件间快速切换,使用CTRL-^。

41.文件标记

以大写字母命名的标记。它们是全局标记,它们可以用在任何文件中。比如,正在编辑"fab1.Java",用命令"50%mF"在文件的中间设置一个名为F的标记。然后在"fab2.java"文件中,用命令"GnB"在最后一行设置名为B的标记。在可以用"F"命令跳转到文件"fab1.java"的半中间。或者编辑另一个文件,"'B"命令会再把你带回文件"fab2.java"的最后一行。
要知道某个标记所代表的位置是什么,可以将该标记的名字作为"marks"命令的参数":marks M"或者连续跟上几个参数":marks MJK"
可以用CTRL-O和CTRL-I可以跳转到较早的位置和靠后的某位置。

42.查看文件

仅是查看文件,不向文件写入内容,可以用只读形式编辑文件。用命令:
vim -R file。如果是想强制性地避免对文件进行修改,可以用命令:
vim -M file。

43.更改文件名

将现有文件存成新的文件,用命令":sav(eas) move.c"。如果想改变当前正在编辑的文件名,但不想保存该文件,就可以用命令:":f(ile) move.c"。

44.分割一个窗口

打开一个新窗口最简单的办法就是使用命令:":split"。CTRL-W 命令可以切换当前活动窗口。

45.关闭窗口

用命令:"close".可以关闭当前窗口。实际上,任何退出文件编辑的命令":quit"和"ZZ"都会关闭窗口,但是用":close"可以阻止你关闭最后一个Vim,以免以意外地整个关闭了Vim。

46.关闭除当前窗口外的所有其他窗口

用命令:":only",关闭除当前窗口外的所有其它窗口。如果这些窗口中有被修改过的,你会得到一个错误信息,同时那个窗口会被留下来。

47.为另一个文件分隔出一个窗口

命令":split two.c"可以打开第二个窗口同时在新打开的窗口中开始编辑作为
参数的文件。如果要打开一个新窗口并开始编辑一个空的缓冲区,使用命令:":new"。

48.垂直分割

用命令":vsplit或::vsplit two.c"。同样有一个对应的":vnew"命令,用于垂直分隔窗口并在其中打开一个新的空缓冲区。

49.切换窗口

CTRL-W h 到左边的窗口
CTRL-W j 到下面的窗口
CTRL-W k 到上面的窗口
CTRL-W l 到右边的窗口
CTRL-W t 到顶部窗口
CTRL-W b 到底部窗口

50.针对所有窗口操作的命令

":qall"放弃所有操作并退出,":wall"保存所有,":wqall"保存所有并退出。

51.为每一个文件打开一个窗口

使用"-o"选项可以让Vim为每一个文件打开一个窗口:
"vim -o one.txt two.txt three.txt"。

52.使用vimdiff查看不同

"vimdiff main.c~ main.c",另一种进入diff模式的办法可以在Vim运行中操作。编辑文件"main.c",然后打开另一个分隔窗口显示其不同:
":edit main.c"
":vertical diffpatch main.c.diff"。

53.页签

命令":tabe(dit) thatfile"在一个窗口中打开"thatfile",该窗口占据着整个的Vim显示区域。命令":tab split/new"结果是新建了一个拥有一个窗口的页签。以用"gt"命令在不同的页签间切换。

这是我总结的一些基本用法,可能对初用者会有帮助,独乐乐不如众乐乐,是吧!

说明:以下黑色为vi和vim均有的一般功能,而红色为Vim(Vi Improved)所特有功能。Vim一般的Unix和Linux下均有安装。

三种状态

Command: 任何输入都会作为编辑命令,而不会出现在屏幕上,任何输入都引起立即反映
Insert: 任何输入的数据都置于编辑寄存器,按ESC,可跳回command方式
Escape: 以“:”或者“/”为前导的指令,出现在屏幕的最下一行,任何输入都被当成特别指令。

离开vi

:q! 离开vi,并放弃刚在缓冲区内编辑的内容。
:wq 将缓冲区内的资料写入磁盘中,并离开vi。
:x 同wq。
(注意—— :X 是文件加密,一定要与:x存盘退出相区别)

进入输入模式

a (append) 由游标之后加入资料。
A 由该行之末加入资料。
i (insert) 由游标之前加入资料。
I 由该行之首加入资料。
o (open) 新增一行於该行之下供输入资料之用。
O 新增一行於该行之上供输入资料之用。

删除与修改

x 删除游标所在该字元。
X 删除游标所在之前一字元。
r 用接於此指令之后的字元取代(replace)游标所在字元。如:ra将游标所在字元以 a 取代之。
R 进入取代状态,直到《ESC》为止。
s 删除游标所在之字元,并进入输入模式直到《ESC》。
S 删除游标所在之该行资料,并进入输入模式直到《ESC》。

光标的移动

0 移至该行之首
$ 移至该行之末。
e 移动到下个字的最後一个字母
w 移动到下个字的第一个字母。
b 移动到上个字的第一个字母。
^ 移至该行的第一个字元处。
H 移至视窗的第一行。
M 移至视窗的中间那行。
L 移至视窗的最后一行。
G 移至该文件的最后一行。
+ 移至下一列的第一个字元处。
- 移至上一列的第一个字元处。
:n 移至该文件的第 n 列。
n+ 移至游标所在位置之后的第 n 列。
n- 移至游标所在位置之前的第 n 列。
Ctrl+g 显示该行之行号、文件名称、文件中最末行之行号、游标所在行号占总行号之百分比。

视窗的移动

Ctrl+f 视窗往下卷一页。
Ctrl+b 视窗往上卷一页。
Ctrl+d 视窗往下卷半页。
Ctrl+u 视窗往上卷半页。
Ctrl+e 视窗往下卷一行。
Ctrl+y 视窗往上卷一行。

剪切、复制、删除

Operator + Scope = command
Operator
d 剪切
y 复制。
p 粘帖,与 d 和 y 配和使用。可将最后d或y的资料放置於游标所在位置之行列下。
c 修改,类似delete与insert的组和。删除一个字组、句子等之资料,并插入新建资料。
Scope
e 由游标所在位置至该字串的最后一个字元。
w 由游标所在位置至下一个字串的第一个字元。
b 由游标所在位置至前一个字串的第一个字元。
$ 由游标所在位置至该行的最后一个字元。
0 由游标所在位置至该行的第一个字元。

整行动作

dd 删除整行。
D 以行为单位,删除游标后之所有字元。
cc 修改整行的内容。
yy 使游标所在该行复制到记忆体缓冲区。

取消前一动作(Undo)

u 恢复最后一个指令之前的结果。
U 恢复游标该行之所有改变。
u 可以多次撤消指令,一次撤消一个操作,直至本次操作开始为止。
Ctrl+r 可以恢复撤消前内容,按多次可恢复多次。

查找与替换

/字串 往游标之后寻找该字串。
?字串 往游标之前寻找该字串。
n 往下继续寻找下一个相同的字串。
N 往上继续寻找下一个相同的字串。
% 查找“(”,“)”,“{”,“}”的配对符。
s 搜寻某行列范围。
g 搜寻整个编辑缓冲区的资料。
:1,$s/old/new/g 将文件中所有的『old』改成『new』。
:10,20s/^/ / 将第10行至第20行资料的最前面插入5个空白。
(vim)
/字符串 后边输入查询内容可保存至缓冲区中,可用↑↓进行以往内容选择。
另外:将光标移动在选定单词下方按*,则可以选中此单词作为查询字符,可以避免输入一长串字符的麻烦。

大小写替换

首先用按v开启选择功能,然后用↑↓←→键来选定所要替换的字符,若是小写变大写,则按U;反之按u;
如果是选择单词,则可以在按v后,按w,最后按U/u,这样就可以将字符随意的改变大小写了,而不用删除后重新敲入。

资料的连接

句子的连接。将游标所在之下一行连接至游标该行的后面。
环境的设定
:set all 可设置的环境变量列表
:set 环境变量的当前值
:set nu 设定资料的行号。
:set nonu 取消行号设定。
:set ai 自动内缩。
:set noai 取消自动内缩。
:set ruler 会在屏幕右下角显示当前光标所处位置,并随光移动而改变,占用屏幕空间较小,使用也比较方便,推荐使用。
:set hlsearch 在使用查找功能时,会高亮显示所有匹配的内容。
:set nohlsearch 关闭此功能。
:set incsearch 使Vim在输入字符串的过程中,光标就可定位显示匹配点。
:set nowrapscan 关闭查找自动回环功能,即查找到文件结尾处,结束查找;默认状态是自动回环

ex指令
读写资料

:10,20w test 将第10行至第20行的资料写入test文件。
:10,20w>>test 将第10行至第20行的资料加在test文件之后。
:r test 将test文件的资料读入编辑缓冲区的最后。
:e [filename] 编辑新的文件。
:e! [filename] 放弃当前修改的文件,编辑新的文件。
:sh 进入shell环境,使用exit退出,回到编辑器中。

:!cmd 运行命令cmd后,返回到编辑器中。

删除、复制及搬移

:10,20d 删除第10行至第20行的资料。
:10d 删除第10行的资料。
:%d 删除整个编辑缓冲区。
:10,20co30 将第10行至第20行的资料复制至第30行之后。
:10,20mo30 将第10行至第20行的资料搬移至第30行之后。

如何在go项目中使用go mod?

回复

GoLangzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 116 次浏览 • 2020-05-22 17:09 • 来自相关话题

如何不借助任何第三方软件快速激活Windows10?

回复

工具软件zkbhj 发起了问题 • 1 人关注 • 0 个回复 • 115 次浏览 • 2020-05-20 14:43 • 来自相关话题

#2020学习打卡##C程序设计语言# 如何把程序跟命令关联起来?

回复

C语言zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 154 次浏览 • 2020-04-26 17:27 • 来自相关话题

#2020学习打卡##C程序设计语言# 怎么解决gcc编译后windows shell输出中文乱码问题?

回复

C语言zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 202 次浏览 • 2020-04-07 22:29 • 来自相关话题

supervisor的常用操作命令有哪些?

回复

工具软件zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 192 次浏览 • 2020-03-16 17:10 • 来自相关话题

Linux上启动Kibana总提示端口被占用怎么处理?

回复

ElasticSearchzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 374 次浏览 • 2020-02-17 16:28 • 来自相关话题

Linux如何定时删除某个目录下的所有文件,如日志文件?

回复

服务器zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 447 次浏览 • 2019-12-02 11:39 • 来自相关话题

Linux如何查找文件内指定内容并复制到新文件中?

回复

服务器zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 583 次浏览 • 2019-11-08 14:58 • 来自相关话题

Yii2 如何获取Header参数?

回复

Yii框架zkbhj 回复了问题 • 1 人关注 • 1 个回复 • 1004 次浏览 • 2019-07-30 15:05 • 来自相关话题

怎么在Linux上用命令行执行PHP脚本?

回复

PHPzkbhj 回复了问题 • 1 人关注 • 1 个回复 • 622 次浏览 • 2019-07-09 11:48 • 来自相关话题

序列化结构数据方法ProtoBuf使用(PHP和Go)

专业名词zkbhj 发表了文章 • 0 个评论 • 139 次浏览 • 2020-04-13 17:24 • 来自相关话题

一、在PHP下使用ProtoBuf
 
PHP如果要使用protobuf 需要安装 protoc+ 安装protobuf composer包
 
protoc用于根据protobuf数据结构文件(.proto)生成对应的类,用于生成protobuf文件安装 google/protobuf composer 包composer require google/protobuf
 
怎么用呢?开搞!
 
1、首先,需要定一个消息类型。创建一个关于Person的定义文件(以.proto为后缀),如示例为person.proto,文件内容如下:
syntax="proto3";
package test;
message Person{
string name=1;//姓名
int32 age=2;//年龄
bool sex=3;//性别

syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2package test:定义包名为test,生成类时,会产生一个目录为testmessage Person:消息主体内容,里面为各个字段的定义
 
2、生成对应的PHP类
 
定义好Person的格式后,该格式如果不生成我们所需要的类库,其实是无任何意义的,还google提供一个工具protoc生成我们要的类库。也就是最开始说的 protoc!
 
proto 最新版的下载地址是:最新3.11.3。可以通过 官方包Release 进行有选择的下载。
tar -zxvf protobuf-php-3.11.3.tar.gz
cd protobuf-3.11.3
./configure --prefix=/usr/local/protobuf
make
make install解压安装完后,就可以通过下面的命令,生成对应的类库了:
/usr/
local/protobuf/bin/protoc --php_out=./ person.proto生成后将在当前目录产生如下文件:
 
GPBMetadata/Person.phpTest/Person.php
 
3、类库生成完了,就可以在PHP项目中使用ProtoBuf了:

在PHP中使用ProtoBuf依赖一个protobuf的扩展,目前提供两种方式进行使用: 
php的c扩展,php的lib扩展包,
 
这两者均可在刚才下载包里可以找到。
 
另外,也可以使用composer进行安装该依赖扩展:
composer require google/protobuf
这里我主要是使用composer安装,应该它可以帮我产生autoload。安装好依赖后,我们就可以开始在php环境下使用protobuf了。

序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();运行后,产生的data.bin,只有14Byte
 
反序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();PHP常用的使用方法:

序列化:
1、serializeToString:序列化成二进制字符串
2、serializeToJsonString:序列化成JSON字符串

反序列化:
1、mergeFromString:二进制字符串反序列化
2、mergeFromJsonString:Json字符串反序列化

二、在Golang下使用ProtoBuf
 
安装protoc的方法和PHP中类型,编译安装即可。
 
安装好之后,需要安装ProtoBuf的编译器插件:protoc-gen-go:
 
进入GOPATH目录,运行:
go get -u github.com/golang/protobuf/protoc-gen-go  如果成功,会在GOPATH/bin下生成protoc-gen-go.exe文件(Windows上)。
 
1、准备好之后,就可以开始写proto文件了。假设文件目录为:
$GOPATH/src/test/protobuf/pb/user.proto代码如下:
//指定版本
//注意proto3与proto2的写法有些不同
syntax = "proto3";

//包名,通过protoc生成时go文件时
package test;

//手机类型
//枚举类型第一个字段必须为0
enum PhoneType {
HOME = 0;
WORK = 1;
}

//手机
message Phone {
PhoneType type = 1;
string number = 2;
}

//人
message Person {
//后面的数字表示标识号
int32 id = 1;
string name = 2;
//repeated表示可重复
//可以有多个手机
repeated Phone phones = 3;
}

//联系簿
message ContactBook {
repeated Person persons = 1;
}然后运行如下命令:
protoc --go_out=. *.proto
会生成一个user.pb.go的文件。
 
2、使用:
package main;

import (
"github.com/golang/protobuf/proto"
"go_dev/kongji/proto/test"
"io/ioutil"
"os"
"fmt"
)

func write() {
p1 := &test.Person{
Id: 1,
Name: "小张",
Phones: []*test.Phone{
{test.PhoneType_HOME, "111111111"},
{test.PhoneType_WORK, "222222222"},
},
};
p2 := &test.Person{
Id: 2,
Name: "小王",
Phones: []*test.Phone{
{test.PhoneType_HOME, "333333333"},
{test.PhoneType_WORK, "444444444"},
},
};

//创建地址簿
book := &test.ContactBook{};
book.Persons = append(book.Persons, p1);
book.Persons = append(book.Persons, p2);

//编码数据
data, _ := proto.Marshal(book);
//把数据写入文件
ioutil.WriteFile("./test.txt", data, os.ModePerm);
}

func read() {
//读取文件数据
data, _ := ioutil.ReadFile("./test.txt");
book := &test.ContactBook{};
//解码数据
proto.Unmarshal(data, book);
for _, v := range book.Persons {
fmt.Println(v.Id, v.Name);
for _, vv := range v.Phones {
fmt.Println(vv.Type, vv.Number);
}
}
}

func main() {
write();
read();
}
 

内容整理自下面链接:
https://www.jianshu.com/p/ce098058edf0
https://developers.google.cn/protocol-buffers/docs/gotutorial
https://developers.google.cn/protocol-buffers/docs/reference/go-generated
  查看全部
一、在PHP下使用ProtoBuf
 
PHP如果要使用protobuf 需要安装 protoc+ 安装protobuf composer包
 
  • protoc用于根据protobuf数据结构文件(.proto)生成对应的类,用于生成protobuf文件
  • 安装 google/protobuf composer 包composer require google/protobuf

 
怎么用呢?开搞!
 
1、首先,需要定一个消息类型。创建一个关于Person的定义文件(以.proto为后缀),如示例为person.proto,文件内容如下:
syntax="proto3";
package test;
message Person{
string name=1;//姓名
int32 age=2;//年龄
bool sex=3;//性别
}
 
  1. syntax="proto3":表明使用的是proto3格式,如果不指定则为proto2
  2. package test:定义包名为test,生成类时,会产生一个目录为test
  3. message Person:消息主体内容,里面为各个字段的定义

 
2、生成对应的PHP类
 
定义好Person的格式后,该格式如果不生成我们所需要的类库,其实是无任何意义的,还google提供一个工具protoc生成我们要的类库。也就是最开始说的 protoc!
 
proto 最新版的下载地址是:最新3.11.3。可以通过 官方包Release 进行有选择的下载。
tar -zxvf protobuf-php-3.11.3.tar.gz
cd protobuf-3.11.3
./configure --prefix=/usr/local/protobuf
make
make install
解压安装完后,就可以通过下面的命令,生成对应的类库了:
/usr/
local/protobuf/bin/protoc --php_out=./ person.proto
生成后将在当前目录产生如下文件:
 
  • GPBMetadata/Person.php
  • Test/Person.php

 
3、类库生成完了,就可以在PHP项目中使用ProtoBuf了:

在PHP中使用ProtoBuf依赖一个protobuf的扩展,目前提供两种方式进行使用: 
  1. php的c扩展,
  2. php的lib扩展包,

 
这两者均可在刚才下载包里可以找到。
 
另外,也可以使用composer进行安装该依赖扩展:
composer require google/protobuf

这里我主要是使用composer安装,应该它可以帮我产生autoload。安装好依赖后,我们就可以开始在php环境下使用protobuf了。

序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();
运行后,产生的data.bin,只有14Byte
 
反序列化
<?php
include 'vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person->mergeFromString($bindata);
echo $person->getName();
PHP常用的使用方法:

序列化:
1、serializeToString:序列化成二进制字符串
2、serializeToJsonString:序列化成JSON字符串

反序列化:
1、mergeFromString:二进制字符串反序列化
2、mergeFromJsonString:Json字符串反序列化

二、在Golang下使用ProtoBuf
 
安装protoc的方法和PHP中类型,编译安装即可。
 
安装好之后,需要安装ProtoBuf的编译器插件:protoc-gen-go:
 
进入GOPATH目录,运行:
go get -u github.com/golang/protobuf/protoc-gen-go
  如果成功,会在GOPATH/bin下生成protoc-gen-go.exe文件(Windows上)。
 
1、准备好之后,就可以开始写proto文件了。假设文件目录为:
$GOPATH/src/test/protobuf/pb/user.proto
代码如下:
//指定版本
//注意proto3与proto2的写法有些不同
syntax = "proto3";

//包名,通过protoc生成时go文件时
package test;

//手机类型
//枚举类型第一个字段必须为0
enum PhoneType {
HOME = 0;
WORK = 1;
}

//手机
message Phone {
PhoneType type = 1;
string number = 2;
}

//人
message Person {
//后面的数字表示标识号
int32 id = 1;
string name = 2;
//repeated表示可重复
//可以有多个手机
repeated Phone phones = 3;
}

//联系簿
message ContactBook {
repeated Person persons = 1;
}
然后运行如下命令:
 protoc --go_out=. *.proto
会生成一个user.pb.go的文件。
 
2、使用:
package main;

import (
"github.com/golang/protobuf/proto"
"go_dev/kongji/proto/test"
"io/ioutil"
"os"
"fmt"
)

func write() {
p1 := &test.Person{
Id: 1,
Name: "小张",
Phones: []*test.Phone{
{test.PhoneType_HOME, "111111111"},
{test.PhoneType_WORK, "222222222"},
},
};
p2 := &test.Person{
Id: 2,
Name: "小王",
Phones: []*test.Phone{
{test.PhoneType_HOME, "333333333"},
{test.PhoneType_WORK, "444444444"},
},
};

//创建地址簿
book := &test.ContactBook{};
book.Persons = append(book.Persons, p1);
book.Persons = append(book.Persons, p2);

//编码数据
data, _ := proto.Marshal(book);
//把数据写入文件
ioutil.WriteFile("./test.txt", data, os.ModePerm);
}

func read() {
//读取文件数据
data, _ := ioutil.ReadFile("./test.txt");
book := &test.ContactBook{};
//解码数据
proto.Unmarshal(data, book);
for _, v := range book.Persons {
fmt.Println(v.Id, v.Name);
for _, vv := range v.Phones {
fmt.Println(vv.Type, vv.Number);
}
}
}

func main() {
write();
read();
}

 

内容整理自下面链接:
https://www.jianshu.com/p/ce098058edf0
https://developers.google.cn/protocol-buffers/docs/gotutorial
https://developers.google.cn/protocol-buffers/docs/reference/go-generated
 

序列化结构数据方法ProtoBuf源码解读

专业名词zkbhj 发表了文章 • 0 个评论 • 164 次浏览 • 2020-04-13 16:53 • 来自相关话题

在上一篇 《序列化结构数据方法ProtoBuf编码》 中,我们详细解析了 ProtoBuf 的编码原理。

有了这个知识储备,我们就可以深入 ProtoBuf 序列化、反序列化的源码,从代码的层面理解 ProtoBuf 具体是如何实现对数据的编码(序列化)和解码(反序列化)的。

我们重新复习一下, ProtoBuf 的序列化使用过程:
 
定义 .proto 文件protoc 编译器编译 .proto 文件生成一系列接口代码调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法

具体调用代码如下:
Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);
调用 SerializeToString 函数将 example1 对象序列化(编码)成字符串。我们的目的就是了解 SerializeToString 函数里到底发生了什么,是怎么一步一步得到最终的序列化结果的。
 

注意:并非编码成字符串数据,string 只是作为编码结果的容器


我们在 .proto 文件中定义的 message 在最终生成的对应语言的代码中,例如在 C++ (xxxx.pb.h、xxxx.pb.cpp) 中每一个在 .proto 文件中定义的 message 字段都会在代码中构造成一个类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类。其相关的类图如下所示:





 
而我们经常调用的序列化函数 SerializeToString 并定义在基类 MessageLite 中。
 
编码

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:






WriteVarint32ToArray 函数可在源码目录下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 编码 中我们解析了 Varint 编码原理和详细过程,WriteVarint32ToArray(以及 WriteVarint64ToArray)并是 Varint 编码的核心。

可以对照上一篇指出的 Varints 编码的几个关键点来阅读以下代码,可以看出编码实现确实优雅,代码如下:inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}在上面已添加详细注释,这里再强调几个关键点。
 
value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的结果其实就是将最后一个字节的第一个 bit(最高位) 置 1,其他位不变,即 xxx ... 1xxx xxxx。注意 target 是 uint8 类型的指针,这意味它只会截断获取最后一个字节,即 1xxx xxxx,这里的 1 意味着什么?这个 1 就是所谓的 msb 了,意味着后续还有字节。之后就是右移 7 位(去掉最后 7 位),处理下一个 7位。通过这里的代码应该可以体会到为什么 Varints 编码结果是低位排在前面了。

了解了最底层 IO 包中的编码函数,再结合上篇文章介绍的编码原理,对 ProtoBuf 的编码应该有了更深入的认识。

Varints 类型序列化实现

int32、int64、uint32、uint64

int32 类型编码函数对应为 WriteInt32ToArray,源码如下:// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}int64、uint32、uint64 类型与 int32 类型同理,只是处理位数有所不同。

uint32 和 uint64 也是采用 Varint 编码,所以底层编码实现与 int32、int64 一致。

 sint32、sint64

这两种类型编码函数对应为 WriteSInt32ToArray 和 WriteSInt64ToArray 。

在上一篇文章 深入 ProtoBuf - 编码 中我们已经介绍过 Varint 编码在负数的情况下编码效率很低,固对于 sint32、sint64 类型我们会采用 ZigZag 编码将负数映射成正数然后再进行 Varint 编码,而这种映射并非采用存储的 Map,而是使用移位实现。sint32 的 ZigZag 源码实现如下:inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}经过 ZigZagEncode32 编码之后,数字成为一个正数,之后等同于 int32 或 int64 进行完全相同的编码处理。

bool 与 enum

bool 和 enum 本质就是整型,编码处理与 int32、int64 相同。

32-bit、64-bit
fixed32/fixed64

fixed32 类型对应 WriteFixed32ToArray 函数,32-bit、64-bit类型的字段比起上述 Varint 类型则要简单的多,因为每个数字均是固定字节,源码如下:inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
int32 value, uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
return WriteSFixed32NoTagToArray(value, target);
}其中 WriteSFixed32NoTagToArray 源码如下:
inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
uint8* target) {
return io::CodedOutputStream::WriteLittleEndian32ToArray(
static_cast<uint32>(value), target);
}由此可知,对于位数固定的 sfixed32 是将其转成 uint32 类型,然后使用与 fixed32 相同的函数写入。

sfixed64 与 sfixed32 同理,不赘述。
 
Length delimited 字段序列化

因为其编码结构为 Tag - Length - Value,所以其字段完整的序列化会稍微多出一些过程,其中有一些需要我们进一步整理。现在以一个 string 类型字段的序列化为例,来看看其序列化的完整过程,画出其程序时序图(上文出现过)如下:






可对照上述时序图来阅读源码,其序列化实现的几个关键函数为:
 
ByteSizeLong:计算对象序列化所需要的空间大小,在内存中开辟相应大小的空间WriteTagToArray:将 Tag 值写入到之前开辟的内存中WriteStringWithSizeToArray:将 Length + Value 值写入到之前开辟的内存中

其序列化代码的重点过程在上图的右下角,先是调用 WriteTagToArray 函数将 Tag 值写入到内存,返回指向下一个字节的指针以便继续写入。调用 WriteStringWithSizeToArray 函数,这个函数主要又执行了两个函数,先是执行 WriteVarint32ToArray 函数(注意 WriteTagToArray 内部调用的也是这个函数,因为 Tag 和 Length 都采用 Varints 编码),此函数的作用是将 Length 写入。执行的第二个函数为 WriteStringToArray,此函数的作用是将 Value(一个 UTF-8 string 值) 写入到内存,其中底层调用了 memcpy() 函数。

综上,对于 Varint 类型的字段自然采用 Varint 编码。

而对于 Length delimited 类型的字段,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 编码,Value 若为 String 等类型,则直接进行 memcpy。

另外对于 embedded message 或 packed repeated ,则套用上述规则。底层编码实现实际并是遍历字段下所有内嵌字段,然后递归调用编码函数即可。

作者:404_89_117_101
链接:https://www.jianshu.com/p/62f0238beec8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 查看全部
在上一篇 《序列化结构数据方法ProtoBuf编码》 中,我们详细解析了 ProtoBuf 的编码原理。

有了这个知识储备,我们就可以深入 ProtoBuf 序列化、反序列化的源码,从代码的层面理解 ProtoBuf 具体是如何实现对数据的编码(序列化)和解码(反序列化)的。

我们重新复习一下, ProtoBuf 的序列化使用过程:
 
  • 定义 .proto 文件
  • protoc 编译器编译 .proto 文件生成一系列接口代码
  • 调用生成的接口实现对 .proto 定义的字段的读取以及 message 对象的序列化、反序列化方法


具体调用代码如下:
Example1 example1;
example1.set_int32val(val);
example1.set_stringval("hello,world");
example1.SerializeToString(&output);

调用 SerializeToString 函数将 example1 对象序列化(编码)成字符串。我们的目的就是了解 SerializeToString 函数里到底发生了什么,是怎么一步一步得到最终的序列化结果的。
 


注意:并非编码成字符串数据,string 只是作为编码结果的容器



我们在 .proto 文件中定义的 message 在最终生成的对应语言的代码中,例如在 C++ (xxxx.pb.h、xxxx.pb.cpp) 中每一个在 .proto 文件中定义的 message 字段都会在代码中构造成一个类,且这些 message 消息类继承于 ::google::protobuf::Message,而 ::google::protobuf::Message 继承于一个更为轻量的 MessageLite 类。其相关的类图如下所示:

QQ截图20200413164703.jpg

 
而我们经常调用的序列化函数 SerializeToString 并定义在基类 MessageLite 中。
 
编码

当某个 Message 调用 SerializeToString 时,经过一层层调用最终会调用底层的关键编码函数 WriteVarint32ToArray 或 WriteVarint64ToArray,整个过程如下图所示:

6009978-d3f8eaef64fe78e9.png


WriteVarint32ToArray 函数可在源码目录下的 google.protobuf.io 包下的 coded_stream.h 中找到。在上一篇 深入 ProtoBuf - 编码 中我们解析了 Varint 编码原理和详细过程,WriteVarint32ToArray(以及 WriteVarint64ToArray)并是 Varint 编码的核心。

可以对照上一篇指出的 Varints 编码的几个关键点来阅读以下代码,可以看出编码实现确实优雅,代码如下:
inline uint8* CodedOutputStream::WriteVarint32ToArray(uint32 value, uint8* target) {
// 0x80 -> 1000 0000
// 大于 1000 0000 意味这进行 Varints 编码时至少需要两个字节
// 如果 value < 0x80,则只需要一个字节,编码结果和原值一样,则没有循环直接返回
// 如果至少需要两个字节
while (value >= 0x80) {
// 如果还有后续字节,则 value | 0x80 将 value 的最后字节的最高 bit 位设置为 1,并取后七位
*target = static_cast<uint8>(value | 0x80);
// 处理完七位,后移,继续处理下一个七位
value >>= 7;
// 指针加一,(数组后移一位)
++target;
}
// 跳出循环,则表示已无后续字节,但还有最后一个字节

// 把最后一个字节放入数组
*target = static_cast<uint8>(value);
// 结束地址指向数组最后一个元素的末尾
return target + 1;
}

// Varint64 同理
inline uint8* CodedOutputStream::WriteVarint64ToArray(uint64 value,
uint8* target) {
while (value >= 0x80) {
*target = static_cast<uint8>(value | 0x80);
value >>= 7;
++target;
}
*target = static_cast<uint8>(value);
return target + 1;
}
在上面已添加详细注释,这里再强调几个关键点。
 
  • value | 0x80:xxx ... xxxx xxxx | 000 ... 1000 0000 的结果其实就是将最后一个字节的第一个 bit(最高位) 置 1,其他位不变,即 xxx ... 1xxx xxxx。注意 target 是 uint8 类型的指针,这意味它只会截断获取最后一个字节,即 1xxx xxxx,这里的 1 意味着什么?这个 1 就是所谓的 msb 了,意味着后续还有字节。之后就是右移 7 位(去掉最后 7 位),处理下一个 7位。
  • 通过这里的代码应该可以体会到为什么 Varints 编码结果是低位排在前面了。


了解了最底层 IO 包中的编码函数,再结合上篇文章介绍的编码原理,对 ProtoBuf 的编码应该有了更深入的认识。

Varints 类型序列化实现

int32、int64、uint32、uint64

int32 类型编码函数对应为 WriteInt32ToArray,源码如下:
// WriteTagToArray 函数将 Tag 部分写入
// WriteInt32NoTagToArray 函数将 Value 部分写入
// WriteTagToArray 和 WriteInt32NoTagToArray 底层
// 均调用 coded_stream.h 中的 WriteVarint32ToArray
//因为 ProtoBuf 中的 Tag 均采用 Varint 编码
// int32 的 Value 部分也采用 Varint 编码
inline uint8* WireFormatLite::WriteInt32ToArray(int field_number, int32 value,
uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_VARINT, target);
return WriteInt32NoTagToArray(value, target);
}
int64、uint32、uint64 类型与 int32 类型同理,只是处理位数有所不同。


uint32 和 uint64 也是采用 Varint 编码,所以底层编码实现与 int32、int64 一致。


 sint32、sint64

这两种类型编码函数对应为 WriteSInt32ToArray 和 WriteSInt64ToArray 。

在上一篇文章 深入 ProtoBuf - 编码 中我们已经介绍过 Varint 编码在负数的情况下编码效率很低,固对于 sint32、sint64 类型我们会采用 ZigZag 编码将负数映射成正数然后再进行 Varint 编码,而这种映射并非采用存储的 Map,而是使用移位实现。sint32 的 ZigZag 源码实现如下:
inline uint32 WireFormatLite::ZigZagEncode32(int32 n) {
// 右移为算数右移
// 左移时需要先将 n 转成 uint32 类型,防止溢出
// 当 n 为正数时 result = 2 * n
// 当 n 为负数时 result = - (2 * n + 1)
return (static_cast<uint32>(n) << 1) ^ static_cast<uint32>(n >> 31);
}
经过 ZigZagEncode32 编码之后,数字成为一个正数,之后等同于 int32 或 int64 进行完全相同的编码处理。

bool 与 enum

bool 和 enum 本质就是整型,编码处理与 int32、int64 相同。

32-bit、64-bit
fixed32/fixed64

fixed32 类型对应 WriteFixed32ToArray 函数,32-bit、64-bit类型的字段比起上述 Varint 类型则要简单的多,因为每个数字均是固定字节,源码如下:
inline uint8* WireFormatLite::WriteSFixed32ToArray(int field_number,
int32 value, uint8* target) {
target = WriteTagToArray(field_number, WIRETYPE_FIXED32, target);
return WriteSFixed32NoTagToArray(value, target);
}
其中 WriteSFixed32NoTagToArray 源码如下:
inline uint8* WireFormatLite::WriteSFixed32NoTagToArray(int32 value,
uint8* target) {
return io::CodedOutputStream::WriteLittleEndian32ToArray(
static_cast<uint32>(value), target);
}
由此可知,对于位数固定的 sfixed32 是将其转成 uint32 类型,然后使用与 fixed32 相同的函数写入。

sfixed64 与 sfixed32 同理,不赘述。
 
Length delimited 字段序列化

因为其编码结构为 Tag - Length - Value,所以其字段完整的序列化会稍微多出一些过程,其中有一些需要我们进一步整理。现在以一个 string 类型字段的序列化为例,来看看其序列化的完整过程,画出其程序时序图(上文出现过)如下:

6009978-d3f8eaef64fe78e9.png


可对照上述时序图来阅读源码,其序列化实现的几个关键函数为:
 
  • ByteSizeLong:计算对象序列化所需要的空间大小,在内存中开辟相应大小的空间
  • WriteTagToArray:将 Tag 值写入到之前开辟的内存中
  • WriteStringWithSizeToArray:将 Length + Value 值写入到之前开辟的内存中


其序列化代码的重点过程在上图的右下角,先是调用 WriteTagToArray 函数将 Tag 值写入到内存,返回指向下一个字节的指针以便继续写入。调用 WriteStringWithSizeToArray 函数,这个函数主要又执行了两个函数,先是执行 WriteVarint32ToArray 函数(注意 WriteTagToArray 内部调用的也是这个函数,因为 Tag 和 Length 都采用 Varints 编码),此函数的作用是将 Length 写入。执行的第二个函数为 WriteStringToArray,此函数的作用是将 Value(一个 UTF-8 string 值) 写入到内存,其中底层调用了 memcpy() 函数。

综上,对于 Varint 类型的字段自然采用 Varint 编码。

而对于 Length delimited 类型的字段,Tag-Length-Value 中的 Tag 和 Length 依然采用 Varint 编码,Value 若为 String 等类型,则直接进行 memcpy。

另外对于 embedded message 或 packed repeated ,则套用上述规则。底层编码实现实际并是遍历字段下所有内嵌字段,然后递归调用编码函数即可。

作者:404_89_117_101
链接:https://www.jianshu.com/p/62f0238beec8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

序列化结构数据方法ProtoBuf编码

专业名词zkbhj 发表了文章 • 0 个评论 • 131 次浏览 • 2020-04-13 16:26 • 来自相关话题

上一篇《序列化结构数据方法ProtoBuf初探》是对ProtoBuf的简单介绍和特点对比。接下来这篇主要总结的是ProtoBuf的编码方式。
 
编码结构

TLV 格式是我们比较熟悉的编码格式。
 

所谓的 TLV 即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 并是数据本身。


ProtoBuf 编码采用类似的结构,但是实际上又有较大区别,其编码结构可见下图:






我们来一步步解析上图所表达的编码结构。

首先,每一个 message 进行编码,其结果由一个个字段组成,每个字段可划分为 Tag - [Length] - Value,如下图所示:





 

特别注意这里的 [Length] 是可选的,含义是针对不同类型的数据编码结构可能会变成 Tag - Value 的形式,如果变成这样的形式,没有了 Length 我们该如何确定 Value 的边界?答案就是 Varint 编码,在后面将详细介绍。


继续深入 Tag ,Tag 由 field_number 和 wire_type 两个部分组成: 
field_number: message 定义字段时指定的字段编号wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。

整个 Tag 采用 Varints 编码方案进行编码,Varints 编码会在后面详细介绍。

Tag 结构如下图所示:






3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种,如下图所示:






第一列即是对应的类型编号,第二列为面向最终编码的编码类型,第三列是面向开发者的 message 字段的类型。
 

注意其中的 Start group 和 End group 两种类型已被遗弃。

 
 

另外要特别注意一点,虽然 wire_type 代表编码类型,但是 Varint 这个编码类型里针对 sint32、sint64 又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。


重新来看完整的编码结构图,现在我们可以理解一个 message 编码将由一个个的 field 组成,每个 field 根据类型将有如下两种格式:
 
Tag - Length - Value:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构,Tag - Value:编码类型表中 Varint、64-bit、32-bit 使用这种结构。


其中 Tag 由字段编号 field_number 和 编码类型 wire_type 组成, Tag 整体采用 Varints 编码。

现在来模拟一下,我们接收到了一串序列化的二进制数据,我们先读一个 Varints 编码块,进行 Varints 解码,读取最后 3 bit 得到 wire_type(由此可知是后面的 Value 采用的哪种编码),随后获取到 field_number (由此可知是哪一个字段)。依据 wire_type 来正确读取后面的 Value。接着继续读取下一个字段 field...

Varints 编码

上一节中多次提到 Varints 编码,现在我们来正式介绍这种编码方案。

总结的讲,Varints 编码的规则主要为以下三点: 
在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节存储数字对应的二进制补码补码的低位排在前面

先来看一个最为简单的例子:
int32 val = 1; // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
原码:0000 ... 0000 0001 // 1 的原码表示
补码:0000 ... 0000 0001 // 1 的补码表示
Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码,其中第一个字节的 msb = 0
编码过程:
数字 1 对应补码 0000 ... 0000 0001(规则 2),从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 ... 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit,那么第一个 7 位组的 msb = 0。最终得到0 | 000 0001(0x01) 解码过程:

我们再做一遍解码过程,加深理解。

编码结果为 0#000 0001(0x01)。首先,每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。

在上面的例子中,数字 1 的 Varints 编码中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。

注意:这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。


再看一个需要两个字节的数字 666 的编码:nt32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码:000 ... 101 0011010 // 666 的源码
补码:000 ... 101 0011010 // 666 的补码
Varints 编码:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 编码编码过程:
666 的补码为 000 ... 101 0011010,从后依次向前取 7 位组并反转排序,则得到:
0011010 | 0000101加上 msb,则1 0011010 | 0 0000101 (0x9a 0x05)
解码过程:
编码结果为 1#0011010 0#000 0101 (9a 05),与第一个例子类似,但是这里的第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止。读到两个字节后先去掉两个 msb,剩下:0011010 000 0101将这两个 7-bit 组反转得到补码:
000 0101 0011010然后还原其原码为 666。

注意:这里编码数字 6,Varints 只使用了 2 个字节。而正常情况下 int32 将使用 4 个字节存储数字 666。

 
仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率。

这里为什么强调牺牲?因为每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来表达 Value。那就意味这 4 个字节能表达的最大数字为 2的28次 ,而不再是  2的32次  了。
这意味着什么?意味着当数字大于  2的28次  时,采用 Varints 编码将导致分配 5 个字节,而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高反而是下降。
但这并不影响 Varints 在实际应用时的高效,因为事实证明,在大多数情况下,小于 2的28次 的数字比大于 2的28次 的数字出现的更为频繁。


到目前为止,好像一切都很完美。但是当前的 Varints 编码却存在着明显缺陷。我们的例子好像只给出了正数,我们来看一下负数的 Varints 编码情况。int32 val = -1
原码:1000 ... 0001 // 注意这里是 8 个字节
补码:1111 ... 1111 // 注意这里是 8 个字节
再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。
上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,这 9 组的 msb 均为 1(因为还有最后一组)
最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码
Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)
注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。

为什么是十个字节? int32 不应该是 4 个字节吗?这里是 ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),而将 int32 扩展成 int64 的八个字节。
为什么之前讲正数的时候没有这种扩展?。请仔细品味 Varints 编码,正数的前提下 int32 和 int64 天然兼容!


所以目前的情况是我们定义了一个 int32 类型的变量,如果将变量值设置为负数,那么直接采用 Varints 编码的话,其编码结果将总是占用十个字节,这显然不是我们希望得到的结果。如何解决?

ZigZag 编码

在上一节中我们提到了 Varints 编码对负数编码效率低的问题。

为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。

ZigZag 的原理和概念比我们想象的简单易懂,一句话就可概括介绍 ZigZag 编码:
 

ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码


如下图所示:






对于 ZigZag 编码的思维不难理解,既然负数的 Varints 编码效率很低,那么就将负数映射到正数,然后对映射后的正数进行 Varints 编码。解码时,解出正数之后再按映射关系映射回原来的负数。

例如我们设置 int32 val = -2。映射得到 3,那么对数字 3 进行 Varints 编码,将结果存储或发送出去。接收方接到数据后进行 Varints 解码,得到数字 3,再将 3 映射回 -2。
 

这里的“映射”是以移位实现的,并非存储映射表。

 Varint 类型

介绍了 Varints 编码和 ZigZag 编码之后,我们就可以继续深入分析每个类型的编码。

在第一节中我们提到了 wire_type 目前已定义 6 种,其中两种已被遗弃(Start group 和 End group),只剩下四种类型: Varint、64-bit、Length-delimited、32-bit。

接下来我们就来一个个详细分析,彻底搞明白 ProtoBuf 针对每种类型的编码策略。

注意,我们在之前已经强调过,与其它三种类型不同,Varint 类型里不止一种编码策略。 除了 int32、int64 等类型的 Varints 编码,还有 sint32、sint64 类型的 ZigZag 编码。

int32、int64、uint32、uint64、bool、enum

当我们使用 int32、int64、uint32、uint64、bool、enum 声明字段类型时,其字段值将使用之前介绍的 Varints 编码。
 

其中 bool 的本质为 0 和 1,enum 本质为整数常量。


在结合本文开头介绍的编码结构: Tag - [Length] - Value,这里的 Value 采用 Varints 编码,因此不需要 Length,则编码结构为 Tag - Value,其中 Tag 和 Value 均采用 Vartins 编码。

int32、int64、uint32、uint64

来看一个最简单的 int32 的小例子:
 
syntax = "proto3";

// message 定义
message Example1 {
int32 int32Val = 1;
}

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(1);
// 编码结果
tag-(Varints)0#0001 000 + value-(Varints)0#000 0001 = 0x08 0x01

// 设置字段值 为 666
Example1 example1;
example1.set_int32val(666);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#0011010 0#000 0101 = 0x08 0x9a 0x05

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(-1);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#1111111 ... 0#000 0001 = 0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01int64、uint32、uint64 与 int32 同理。
 
bool、enum

bool 的例子:
syntax = "proto3";

// message 定义
message Example1 {
bool boolVal = 1;
}

// 设置字段值 为 true
Example1 example1;
example1.set_boolval(true);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 false
Example1 example1;
example1.set_boolval(false);

// 编码结果

 

这里有个有意思的现象,当 boolVal = false 时,其编码结果为空,为什么?
这里是 ProtoBuf 为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。也就是说将 false 编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是 false)。如此,可进一步节省空间提高效率。


enum 的例子:syntax = "proto3";

// message 定义
message Example1 {
enum COLOR {
YELLOW = 0;
RED = 1;
BLACK = 2;
WHITE = 3;
BLUE = 4;
}
// 枚举常量必须在 32 位整型值的范围
// 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数
COLOR colorVal = 1;
}

// 设置字段值 为 Example1_COLOR_BLUE
Example1 example1;
example1.set_colorval(Example1_COLOR_BLUE);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0100 = 08 04sint32、sint64

sint32、sint64 将采用 ZigZag 编码。编码结构依然为 Tag - Value,只不过在编码和解码的过程中多出一个映射的过程,映射后依然采用 Varints 编码。
来看 sint32 的例子:syntax = "proto3";

// message 定义
message Example1 {
sint32 sint32Val = 1;
}

// 设置字段值 为 -1
Example1 example1;
example1.set_colorval(-1);

// 编码结果,1 映射回 -1
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 -2
Example1 example1;
example1.set_colorval(-2);

// 编码结果,3 映射回 -2
编码结果:tag-(Varints)00001 000 + value-(Varints)0#000 0011 = 08 03
sint64 与 sint32 同理。

int、uint 和 sint: 之所以同时出现了这三种类型,是因为历史和代码迭代的结果。ProtoBuf 最初只有 int 类型,由于 int 类型不适合负数(负数编码效率低),所以提供了 sint。因为 sint 的一部分正数其实是表达的负数,所以其正数范围有所减小,所以在一些全是正数场景下需要提供 uint 类型。

64-bit 和 32-bit 类型

64-bit 和 32-bit 比较简单,与 Varints 一样其编码结构为 Tag-Value,不同的是不管数字大小,64-bit 存储 8 字节,32-bit 存储 4 字节。读取时同理,64-bit 直接读取 8 字节,32-bit 直接读取 4 字节。

为什么需要 64-bit 和 32-bit?之前已经分析过了 Varints 编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints 就不太合适了,此时使用 64-bit 和 32-bit 更为合适。具体的,如果数值比 256 大的话,64-bit 这个类型比 uint64 高效,如果数值比 228 大的话,32-bit 这个类型比 uint32 高效。

fixed64、sfixed64、double

来看例子:// message 定义
syntax = "proto3";

message Example1 {
fixed64 fixed64Val = 1;
sfixed64 sfixed64Val = 2;
double doubleVal = 3;
}

// 设置字段值 为 -2
example1.set_fixed64val(1)
example1.set_sfixed64val(-1)
example1.set_doubleval(1.2)

// 编码结果,总是 8 个字节
09 # 01 00 00 00 00 00 00 00
11 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)
19 # 33 33 33 33 33 33 F3 3Ffixed32、sfixed32、float

与 64-bit 同理。

Length-delimited 类型
string、bytes、EmbeddedMessage、repeated

终于遇到了体现编码结构图中 [Length] 意义的类型了。Length-delimited 类型的编码结构为 Tag - Length - Value

这种编码方式很好理解,来看例子:syntax = "proto3";

// message 定义
message Example1 {
string stringVal = 1;
bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}

//设置相应值
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");

Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();

embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);

example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");

//编码结果
0A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64
12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F
1A 10 08 01 12 0C 65 6D 62 65 64 64 65 64 49 6E 66 6F
22 02 02 03[ proto3 默认 packed = true](编码结果打包处理,见下一小节的介绍)
2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32(repeated string 为啥不进行默认 packed ?)读者可对照上面介绍过的编码来理解这段相对复杂的编码结果。(为降低难度,已按字段分行,即第一个字段的编码结果对应第一行,第二个字段对应第二行...)

补充 packed 编码

在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。
 

packed 目前只能用于 primitive 类型。


packed = true 主要使让 ProtoBuf 为我们把 repeated primitive 的编码结果打包,从而进一步压缩空间,进一步提高效率、速度。这里打包的含义其实就是:原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value...,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value...

上一节例子中 repeatedInt32Val 字段的编码结果为:22 | 02 02 03
22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。

原文出处:
作者:404_89_117_101
链接:https://www.jianshu.com/p/73c9ed3a4877
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 查看全部
上一篇《序列化结构数据方法ProtoBuf初探》是对ProtoBuf的简单介绍和特点对比。接下来这篇主要总结的是ProtoBuf的编码方式。
 
编码结构

TLV 格式是我们比较熟悉的编码格式。
 


所谓的 TLV 即 Tag - Length - Value。Tag 作为该字段的唯一标识,Length 代表 Value 数据域的长度,最后的 Value 并是数据本身。



ProtoBuf 编码采用类似的结构,但是实际上又有较大区别,其编码结构可见下图:

QQ截图20200413160739.jpg


我们来一步步解析上图所表达的编码结构。

首先,每一个 message 进行编码,其结果由一个个字段组成,每个字段可划分为 Tag - [Length] - Value,如下图所示:

QQ截图20200413160948.jpg

 


特别注意这里的 [Length] 是可选的,含义是针对不同类型的数据编码结构可能会变成 Tag - Value 的形式,如果变成这样的形式,没有了 Length 我们该如何确定 Value 的边界?答案就是 Varint 编码,在后面将详细介绍。



继续深入 Tag ,Tag 由 field_number 和 wire_type 两个部分组成: 
  • field_number: message 定义字段时指定的字段编号
  • wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。


整个 Tag 采用 Varints 编码方案进行编码,Varints 编码会在后面详细介绍。

Tag 结构如下图所示:

QQ截图20200413161046.jpg


3 bit 的 wire_type 最多可以表达 8 种编码类型,目前 ProtoBuf 已经定义了 6 种,如下图所示:

QQ截图20200413161115.jpg


第一列即是对应的类型编号,第二列为面向最终编码的编码类型,第三列是面向开发者的 message 字段的类型。
 


注意其中的 Start group 和 End group 两种类型已被遗弃。


 
 


另外要特别注意一点,虽然 wire_type 代表编码类型,但是 Varint 这个编码类型里针对 sint32、sint64 又会有一些特别编码(ZigTag 编码)处理,相当于 Varint 这个编码类型里又存在两种不同编码。



重新来看完整的编码结构图,现在我们可以理解一个 message 编码将由一个个的 field 组成,每个 field 根据类型将有如下两种格式:
 
  • Tag - Length - Value:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构,
  • Tag - Value:编码类型表中 Varint、64-bit、32-bit 使用这种结构。



其中 Tag 由字段编号 field_number 和 编码类型 wire_type 组成, Tag 整体采用 Varints 编码。

现在来模拟一下,我们接收到了一串序列化的二进制数据,我们先读一个 Varints 编码块,进行 Varints 解码,读取最后 3 bit 得到 wire_type(由此可知是后面的 Value 采用的哪种编码),随后获取到 field_number (由此可知是哪一个字段)。依据 wire_type 来正确读取后面的 Value。接着继续读取下一个字段 field...

Varints 编码

上一节中多次提到 Varints 编码,现在我们来正式介绍这种编码方案。

总结的讲,Varints 编码的规则主要为以下三点: 
  1. 在每个字节开头的 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
  2. 存储数字对应的二进制补码
  3. 补码的低位排在前面


先来看一个最为简单的例子:
int32 val =  1;  // 设置一个 int32 的字段的值 val = 1; 这时编码的结果如下
原码:0000 ... 0000 0001 // 1 的原码表示
补码:0000 ... 0000 0001 // 1 的补码表示
Varints 编码:0#000 0001(0x01) // 1 的 Varints 编码,其中第一个字节的 msb = 0

编码过程:
数字 1 对应补码 0000 ... 0000 0001(规则 2),从末端开始取每 7 位一组并且反转排序(规则 3),因为 0000 ... 0000 0001 除了第一个取出的 7 位组(即原数列的后 7 位),剩下的均为 0。所以只需取第一个 7 位组,无需再取下一个 7 bit,那么第一个 7 位组的 msb = 0。最终得到
0 | 000 0001(0x01) 
解码过程:

我们再做一遍解码过程,加深理解。

编码结果为 0#000 0001(0x01)。首先,每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。

在上面的例子中,数字 1 的 Varints 编码中 msb = 0,所以只需要读完第一个字节无需再读。去掉 msb 之后,剩下的 000 0001 就是补码的逆序,但是这里只有一个字节,所以无需反转,直接解释补码 000 0001,还原即为数字 1。


注意:这里编码数字 1,Varints 只使用了 1 个字节。而正常情况下 int32 将使用 4 个字节存储数字 1。



再看一个需要两个字节的数字 666 的编码:
nt32 val = 666; // 设置一个 int32 的字段的值 val = 666; 这时编码的结果如下
原码:000 ... 101 0011010 // 666 的源码
补码:000 ... 101 0011010 // 666 的补码
Varints 编码:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 编码
编码过程:
666 的补码为 000 ... 101 0011010,从后依次向前取 7 位组并反转排序,则得到:
0011010 | 0000101
加上 msb,则
1 0011010 | 0 0000101 (0x9a 0x05)

解码过程:
编码结果为 1#0011010 0#000 0101 (9a 05),与第一个例子类似,但是这里的第一个字节 msb = 1,所以需要再读一个字节,第二个字节的 msb = 0,则读取两个字节后停止。读到两个字节后先去掉两个 msb,剩下:
0011010  000 0101
将这两个 7-bit 组反转得到补码:
000 0101 0011010
然后还原其原码为 666。


注意:这里编码数字 6,Varints 只使用了 2 个字节。而正常情况下 int32 将使用 4 个字节存储数字 666。


 
仔细品味上述的 Varints 编码,我们可以发现 Varints 的本质实际上是每个字节都牺牲一个 bit 位(msb),来表示是否已经结束(是否还需要读取下一个字节),msb 实际上就起到了 Length 的作用,正因为有了 msb(Length),所以我们可以摆脱原来那种无论数字大小都必须分配四个字节的窘境。通过 Varints 我们可以让小的数字用更少的字节表示。从而提高了空间利用和效率


这里为什么强调牺牲?因为每个字节都拿出一个 bit 做 msb,而原先这个 bit 是可直接用来表示 Value 的,现在每个字节都少了一个 bit 位即只有 7 位能真正用来表达 Value。那就意味这 4 个字节能表达的最大数字为 2的28次 ,而不再是  2的32次  了。
这意味着什么?意味着当数字大于  2的28次  时,采用 Varints 编码将导致分配 5 个字节,而原先明明只需要 4 个字节,此时 Varints 编码的效率不仅不是提高反而是下降。
但这并不影响 Varints 在实际应用时的高效,因为事实证明,在大多数情况下,小于 2的28次 的数字比大于 2的28次 的数字出现的更为频繁。



到目前为止,好像一切都很完美。但是当前的 Varints 编码却存在着明显缺陷。我们的例子好像只给出了正数,我们来看一下负数的 Varints 编码情况。
int32 val = -1
原码:1000 ... 0001 // 注意这里是 8 个字节
补码:1111 ... 1111 // 注意这里是 8 个字节
再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。
上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,这 9 组的 msb 均为 1(因为还有最后一组)
最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码
Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)

注意,因为负数必须在最高位(符号位)置 1,这一点意味着无论如何,负数都必须占用所有字节,所以它的补码总是占满 8 个字节。你没法像正数那样去掉多余的高位(都是 0)。再加上 msb,最终 Varints 编码的结果将固定在 10 个字节。


为什么是十个字节? int32 不应该是 4 个字节吗?这里是 ProtoBuf 基于兼容性的考虑(比如开发者将 int64 的字段改成 int32 后应当不影响旧程序),而将 int32 扩展成 int64 的八个字节。
为什么之前讲正数的时候没有这种扩展?。请仔细品味 Varints 编码,正数的前提下 int32 和 int64 天然兼容!



所以目前的情况是我们定义了一个 int32 类型的变量,如果将变量值设置为负数,那么直接采用 Varints 编码的话,其编码结果将总是占用十个字节,这显然不是我们希望得到的结果。如何解决?

ZigZag 编码

在上一节中我们提到了 Varints 编码对负数编码效率低的问题。

为解决这个问题,ProtoBuf 为我们提供了 sint32、sint64 两种类型,当你在使用这两种类型定义字段时,ProtoBuf 将使用 ZigZag 编码,而 ZigZag 编码将解决负数编码效率低的问题。

ZigZag 的原理和概念比我们想象的简单易懂,一句话就可概括介绍 ZigZag 编码:
 


ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码



如下图所示:

QQ截图20200413161656.jpg


对于 ZigZag 编码的思维不难理解,既然负数的 Varints 编码效率很低,那么就将负数映射到正数,然后对映射后的正数进行 Varints 编码。解码时,解出正数之后再按映射关系映射回原来的负数。

例如我们设置 int32 val = -2。映射得到 3,那么对数字 3 进行 Varints 编码,将结果存储或发送出去。接收方接到数据后进行 Varints 解码,得到数字 3,再将 3 映射回 -2。
 


这里的“映射”是以移位实现的,并非存储映射表。


 Varint 类型

介绍了 Varints 编码和 ZigZag 编码之后,我们就可以继续深入分析每个类型的编码。

在第一节中我们提到了 wire_type 目前已定义 6 种,其中两种已被遗弃(Start group 和 End group),只剩下四种类型: Varint、64-bit、Length-delimited、32-bit

接下来我们就来一个个详细分析,彻底搞明白 ProtoBuf 针对每种类型的编码策略。

注意,我们在之前已经强调过,与其它三种类型不同,Varint 类型里不止一种编码策略。 除了 int32、int64 等类型的 Varints 编码,还有 sint32、sint64 类型的 ZigZag 编码。

int32、int64、uint32、uint64、bool、enum

当我们使用 int32、int64、uint32、uint64、bool、enum 声明字段类型时,其字段值将使用之前介绍的 Varints 编码。
 


其中 bool 的本质为 0 和 1,enum 本质为整数常量。



在结合本文开头介绍的编码结构: Tag - [Length] - Value,这里的 Value 采用 Varints 编码,因此不需要 Length,则编码结构为 Tag - Value,其中 Tag 和 Value 均采用 Vartins 编码。

int32、int64、uint32、uint64

来看一个最简单的 int32 的小例子:
 
syntax = "proto3";

// message 定义
message Example1 {
int32 int32Val = 1;
}

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(1);
// 编码结果
tag-(Varints)0#0001 000 + value-(Varints)0#000 0001 = 0x08 0x01

// 设置字段值 为 666
Example1 example1;
example1.set_int32val(666);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#0011010 0#000 0101 = 0x08 0x9a 0x05

// 设置字段值 为 1
Example1 example1;
example1.set_int32val(-1);
// 编码结果
tag-(Varints)00001 000 + value-(Varints)1#1111111 ... 0#000 0001 = 0x08 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0x01
int64、uint32、uint64 与 int32 同理。
 
bool、enum

bool 的例子:
syntax = "proto3";

// message 定义
message Example1 {
bool boolVal = 1;
}

// 设置字段值 为 true
Example1 example1;
example1.set_boolval(true);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 false
Example1 example1;
example1.set_boolval(false);

// 编码结果

 


这里有个有意思的现象,当 boolVal = false 时,其编码结果为空,为什么?
这里是 ProtoBuf 为了提高效率做的又一个小技巧:规定一个默认值机制,当读出来的字段为空的时候就设置字段的值为默认值。而 bool 类型的默认值为 false。也就是说将 false 编码然后传递(消耗一个字节),不如直接不输出任何编码结果(空),终端解析时发现该字段为空,它会按照规定设置其值为默认值(也就是 false)。如此,可进一步节省空间提高效率。



enum 的例子:
syntax = "proto3";

// message 定义
message Example1 {
enum COLOR {
YELLOW = 0;
RED = 1;
BLACK = 2;
WHITE = 3;
BLUE = 4;
}
// 枚举常量必须在 32 位整型值的范围
// 使用 Varints 编码,对负数不够高效,因此不推荐在枚举中使用负数
COLOR colorVal = 1;
}

// 设置字段值 为 Example1_COLOR_BLUE
Example1 example1;
example1.set_colorval(Example1_COLOR_BLUE);

// 编码结果
tag-(Varints)00001 000 + value-(Varints)0#000 0100 = 08 04
sint32、sint64

sint32、sint64 将采用 ZigZag 编码。编码结构依然为 Tag - Value,只不过在编码和解码的过程中多出一个映射的过程,映射后依然采用 Varints 编码。
来看 sint32 的例子:
syntax = "proto3";

// message 定义
message Example1 {
sint32 sint32Val = 1;
}

// 设置字段值 为 -1
Example1 example1;
example1.set_colorval(-1);

// 编码结果,1 映射回 -1
tag-(Varints)00001 000 + value-(Varints)0#000 0001 = 08 01

// 设置字段值 为 -2
Example1 example1;
example1.set_colorval(-2);

// 编码结果,3 映射回 -2
编码结果:tag-(Varints)00001 000 + value-(Varints)0#000 0011 = 08 03
sint64 与 sint32 同理。


int、uint 和 sint: 之所以同时出现了这三种类型,是因为历史和代码迭代的结果。ProtoBuf 最初只有 int 类型,由于 int 类型不适合负数(负数编码效率低),所以提供了 sint。因为 sint 的一部分正数其实是表达的负数,所以其正数范围有所减小,所以在一些全是正数场景下需要提供 uint 类型。


64-bit 和 32-bit 类型

64-bit 和 32-bit 比较简单,与 Varints 一样其编码结构为 Tag-Value,不同的是不管数字大小,64-bit 存储 8 字节,32-bit 存储 4 字节。读取时同理,64-bit 直接读取 8 字节,32-bit 直接读取 4 字节。

为什么需要 64-bit 和 32-bit?之前已经分析过了 Varints 编码在一定范围内是有高效的,超过某一个数字占用字节反而更多,效率更低。如果现在有场景是存在大量的大数字,那么使用 Varints 就不太合适了,此时使用 64-bit 和 32-bit 更为合适。具体的,如果数值比 256 大的话,64-bit 这个类型比 uint64 高效,如果数值比 228 大的话,32-bit 这个类型比 uint32 高效。

fixed64、sfixed64、double

来看例子:
// message 定义
syntax = "proto3";

message Example1 {
fixed64 fixed64Val = 1;
sfixed64 sfixed64Val = 2;
double doubleVal = 3;
}

// 设置字段值 为 -2
example1.set_fixed64val(1)
example1.set_sfixed64val(-1)
example1.set_doubleval(1.2)

// 编码结果,总是 8 个字节
09 # 01 00 00 00 00 00 00 00
11 # FF FF FF FF FF FF FF FF (没有 ZigZag 编码)
19 # 33 33 33 33 33 33 F3 3F
fixed32、sfixed32、float

与 64-bit 同理。

Length-delimited 类型
string、bytes、EmbeddedMessage、repeated

终于遇到了体现编码结构图中 [Length] 意义的类型了。Length-delimited 类型的编码结构为 Tag - Length - Value

这种编码方式很好理解,来看例子:
syntax = "proto3";

// message 定义
message Example1 {
string stringVal = 1;
bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}

//设置相应值
Example1 example1;
example1.set_stringval("hello,world");
example1.set_bytesval("are you ok?");

Example1_EmbeddedMessage *embeddedExample2 = new Example1_EmbeddedMessage();

embeddedExample2->set_int32val(1);
embeddedExample2->set_stringval("embeddedInfo");
example1.set_allocated_embeddedexample1(embeddedExample2);

example1.add_repeatedint32val(2);
example1.add_repeatedint32val(3);
example1.add_repeatedstringval("repeated1");
example1.add_repeatedstringval("repeated2");

//编码结果
0A 0B 68 65 6C 6C 6F 2C 77 6F 72 6C 64
12 0B 61 72 65 20 79 6F 75 20 6F 6B 3F
1A 10 08 01 12 0C 65 6D 62 65 64 64 65 64 49 6E 66 6F
22 02 02 03[ proto3 默认 packed = true](编码结果打包处理,见下一小节的介绍)
2A 09 72 65 70 65 61 74 65 64 31 2A 09 72 65 70 65 61 74 65 64 32(repeated string 为啥不进行默认 packed ?)
读者可对照上面介绍过的编码来理解这段相对复杂的编码结果。(为降低难度,已按字段分行,即第一个字段的编码结果对应第一行,第二个字段对应第二行...)

补充 packed 编码

在 proto2 中为我们提供了可选的设置 [packed = true],而这一可选项在 proto3 中已成默认设置。
 


packed 目前只能用于 primitive 类型。



packed = true 主要使让 ProtoBuf 为我们把 repeated primitive 的编码结果打包,从而进一步压缩空间,进一步提高效率、速度。这里打包的含义其实就是:原先的 repeated 字段的编码结构为 Tag-Length-Value-Tag-Length-Value-Tag-Length-Value...,因为这些 Tag 都是相同的(同一字段),因此可以将这些字段的 Value 打包,即将编码结构变为 Tag-Length-Value-Value-Value...

上一节例子中 repeatedInt32Val 字段的编码结果为:
22 | 02 02 03

22 即 00100010 -> wire_type = 2(Length-delimited), field_number = 4(repeatedInt32Val 字段),02 字节长度为 2,则读取两个字节,之后按照 Varints 解码出数字 2 和 3。

原文出处:
作者:404_89_117_101
链接:https://www.jianshu.com/p/73c9ed3a4877
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

#2020学习打卡##C程序设计语言# C语言IDE的选择推荐(Mac篇)

C语言zkbhj 发表了文章 • 0 个评论 • 189 次浏览 • 2020-04-03 11:09 • 来自相关话题

上一篇文章介绍了Windows环境下的C语言IDE选择,这篇文章介绍下Mac环境下,哪些IDE是相对比较优秀的!
 
Mac电脑作为类Unix类操作系统的代表,C语言也是天生具备的。很多Mac电脑即便不用于软件开发,也会因为各种免费开源软件的使用,在依赖包中自动安装了gcc的支持,从而具备了C语言的开发环境。 
在Mac电脑上进行C语言开发有两种方式,一是使用图形界面(GUI)进行开发,这种情况使用苹果自主开发的Xcode开发工具。二是使用纯命令行的开发工具gcc或者clang配合vim编辑器。前者适合大规模项目的开发,后者简洁高效,相对来说适合开发小规模的程序或者应急的修修补补。
 
(1)Xcode
 
Xcode 是苹果公司开发的编程软件,是开发人员建立OS X 和 iOS 应用程序的最快捷的方式。Xcode 具有统一的用户界面设计,编码、测试、调试都在一个简单的窗口内完成。在windows上类似这种能编c语言的还有微软出的visual studio。





 
安装Xcode唯一合法的方法是在Mac电脑打开App Store程序,在右上角搜索框中输入"Xcode",从搜索到的结果中一般前1、2位就是Xcode,然后点选“获取”或者“安装”。Xcode容量比较大,一般安装包都在4.5G-6G之间,依据网络的情况,需要等待不短的时间。安装完成后,第一次运行Xcode会提示安装命令行工具,按照提示就会自动安装clang命令行开发工具。命令行开发包一般是100多M,下载安装都会比较快。
 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。





 
下载地址:https://code.visualstudio.com/Download
 
(3)命令行下进行程序开发
 
这种方式适用于学习阶段,大型项目还是更倾向于使用IDE。
 
Mac的命令行C语言开发工具主要有两种,clang及gcc,前者是苹果官方推荐的,Xcode也使用Clang进行编译。后者则是GNU开源社区推荐的,并且被大多数linux支持。两者在语法的兼容性上几乎没有差别。而Clang在于错误信息、编译速度等方面有很多优势。
 
通常的命令行操作为:
vi 1.c
gcc 1.c //生成a.out可执行文件
gcc -o my.out 1.c//可以指定生成的可执行文件名
./a.out
./my.out

  查看全部
上一篇文章介绍了Windows环境下的C语言IDE选择,这篇文章介绍下Mac环境下,哪些IDE是相对比较优秀的!
 
Mac电脑作为类Unix类操作系统的代表,C语言也是天生具备的。很多Mac电脑即便不用于软件开发,也会因为各种免费开源软件的使用,在依赖包中自动安装了gcc的支持,从而具备了C语言的开发环境。 
在Mac电脑上进行C语言开发有两种方式,一是使用图形界面(GUI)进行开发,这种情况使用苹果自主开发的Xcode开发工具。二是使用纯命令行的开发工具gcc或者clang配合vim编辑器。前者适合大规模项目的开发,后者简洁高效,相对来说适合开发小规模的程序或者应急的修修补补。
 
(1)Xcode
 
Xcode 是苹果公司开发的编程软件,是开发人员建立OS X 和 iOS 应用程序的最快捷的方式。Xcode 具有统一的用户界面设计,编码、测试、调试都在一个简单的窗口内完成。在windows上类似这种能编c语言的还有微软出的visual studio。

QQ截图20200403104902.jpg

 
安装Xcode唯一合法的方法是在Mac电脑打开App Store程序,在右上角搜索框中输入"Xcode",从搜索到的结果中一般前1、2位就是Xcode,然后点选“获取”或者“安装”。Xcode容量比较大,一般安装包都在4.5G-6G之间,依据网络的情况,需要等待不短的时间。安装完成后,第一次运行Xcode会提示安装命令行工具,按照提示就会自动安装clang命令行开发工具。命令行开发包一般是100多M,下载安装都会比较快。
 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。

QQ截图20200403105149.jpg

 
下载地址:https://code.visualstudio.com/Download
 
(3)命令行下进行程序开发
 
这种方式适用于学习阶段,大型项目还是更倾向于使用IDE。
 
Mac的命令行C语言开发工具主要有两种,clang及gcc,前者是苹果官方推荐的,Xcode也使用Clang进行编译。后者则是GNU开源社区推荐的,并且被大多数linux支持。两者在语法的兼容性上几乎没有差别。而Clang在于错误信息、编译速度等方面有很多优势。
 
通常的命令行操作为:
vi 1.c
gcc 1.c //生成a.out可执行文件
gcc -o my.out 1.c//可以指定生成的可执行文件名
./a.out
./my.out

 

#2020学习打卡##C程序设计语言# C语言IDE的选择推荐(Windows篇)

C语言zkbhj 发表了文章 • 0 个评论 • 159 次浏览 • 2020-04-03 10:41 • 来自相关话题

确定好了学习目标,选择好了学习教程,制定好了学习计划,那么接下来,你需要做的就是搭建一套学习C语言的环境。我们无外乎基本上大多使用的操作系统是Windows和MACOS,所以在这里整理了一些IDE和C语言编译环境搭建的方法和经验供大家参考。这篇为Windows篇,下一篇文章分享MAC上有哪些优良实践。(文末为大家附上了各个IDE的下载地址)
 
IDE的选择
 
IDE,又称集成开发环境,顾名思义,他是一个编辑器 + 编译器的集合。所以对于接触一门新语言的“门外汉”来讲,最方便快捷的方式,就是选择一个IDE来完成这项工作。那么我们可以选择哪个IDE作为学习C语言以及以后开发C语言程序的最佳工具呢?Windows 下的C语言 IDE 众多,多如牛毛,初学者往往不知道该如何选择。我们可以综合自己的实际情况,根据这些IDE的特点来进行对自己最合适的选择。 
(1)Visual Studio
 Windows 下首先推荐大家使用微软开发的 Visual Studio(简称 VS),它是 Windows 下的标准 IDE,实际开发中大家也都在使用。为了适应最新的 Windows 操作系统,微软每隔一段时间(一般是一两年)就会对 VS 进行升级。VS 的不同版本以发布年份命名,例如 VS2010 是微软于 2010 年发布的,VS2019 是微软于 2019 年发布的。





 
当然,这款IDE也有MAC版本,且它是windows官方推出的IDE环境,功能方面自不用说,非常之强大。但是有一个缺点就是看起来相对“臃肿”,可能有很多初学者用不上的工具也会在其中提供,安装包略大,约2~3GB大小,安装时间预计半个小时左右。

优点:功能强大,微软官方维护的IDE,更新及时,操作简单方便;
缺点:略臃肿,安装包大,安装时间长,占用系统资源较多;
选择:如果你的机器性能还可以的话,建议选择该IDE。

 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。
 
相对于 Visual Studio,这款IDE更轻便,支持的语言也更丰富。

优点:跨平台、官方支持、多语言支持、界面美观、操作方便、容易上手
缺点:暂未发现
建议:首选推荐安装

 
(3)Dev C++
 
如果比较讨厌“VS”的复杂性,那么Dev C++是一个不错的选择。Dev C++ 是一款免费开源的 C/C++ IDE,内嵌 GCC 编译器(Linux GCC 编译器的 Windows 移植版),是 NOI、NOIP 等比赛的指定工具。Dev C++ 的优点是体积小(只有几十兆)、安装卸载方便、学习成本低,缺点是调试功能弱。

NOI 是National Olympiad in Informatics的缩写,译为“全国青少年信息学奥林匹克竞赛”;NOIP 是National Olympiad in informatics in Provinces的缩写,译为“全国青少年信息学奥林匹克联赛”。NOI、NOIP 都是奥林匹克竞赛的一种,参加者多为高中生,获奖者将被保送到名牌大学或者得到高考加分资格。


优点:轻巧、方便,对机器性能要求不高,安装卸载方便,学习成本低
缺点:调试功能相对较弱
选择:如果机器性能较弱,建议选择这个IDE

 
(4)Visual C++ 6.0
 
哈哈,之所以把这个IDE放在这里强调下,纯属想要怀念一下,因为大学期间学C语言就是用的这款IDE呀呀呀呀!!!不知道你还记不记得下面这些界面~










 
唉呀妈呀,仿佛还记得炎炎夏日在大学机房用那种方方正正的大脑袋电脑敲代码的日子……
Visual C++ 6.0(简称VC 6.0)是微软开发的一款经典的 IDE,很多高校都以 VC 6.0 为教学工具来讲解C和C++。但VC 6.0是1998年的产品,很古老了,在 Win7、Win8、Win10 下会有各种各样的兼容性问题,甚至根本不能运行,所以不推荐使用。

优点:可以用来回忆
缺点:太多不知从何说起
建议:强烈不建议,如果你想安装,我也拦不住

 
(5)其它 IDE
 
除了上面提到的三款 IDE,Windows 平台下还有很多其他的 IDE,它们各有特点,例如:
Code::Blocks 是一款开源、跨平台、免费的 C/C++ IDE,它和 Dev C++ 非常类似,小巧灵活,易于安装和卸载,不过它的界面要比 Dev C++ 复杂一些,不如 Dev C++ 来得清爽。Turbo C 是一款古老的、DOS 年代的C语言开发工具,程序员只能使用键盘来操作 Turbo C,不能使用鼠标,所以非常不方便。但是 Turbo C 集成了一套图形库,可以在控制台程序中画图,看起来非常炫酷,所以至今仍然有人在使用。C-Free 是一款国产的 Windows 下的C/C++ IDE,最新版本是 5.0,整个软件才 14M,非常轻巧,安装也简单,界面也比 Dev C++ 漂亮。C-Free 的缺点也是调试功能弱。可惜的是,C-Free 已经多年不更新了,组件都老了,只能在 XP、Win7 下运行,在 Win8、Win10 下可能会存在兼容性问题。
 
所以,综合对比之下,如果电脑性能不错,就是用VS,否则,选择Dev C++。
 
赶紧去下载体验吧~~~
 
纯命令行下通过指令操作
 
没有IDE,不代表就不能写代码了!当然你还有“更程序员”的方式,通过命令行执行进行操作。下面介绍windows10下如何使用cmd命令进行GCC编译的几个步骤:
 
首先了解下什么是GCC?

GCC是以GPL许可证所发行的自由软件,也是GNU计划的关键部分。GCC的初衷是为GNU操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,甚至在微软的Windows上也可以使用GCC。GCC支持多种计算机体系结构芯片,如x86、ARM、MIPS等,并已被移植到其他多种硬件平台。
GCC原名为GNU C语言编译器(GNU C Compiler),只能处理C语言。但其很快扩展,变得可处理C++,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。

 1、安装gcc编译器,需要到MinGW 的主页 www.mingw.org,进入 MinGW 下载页面,下载最新版本的 MinGW64 安装程序。或者到:https://sourceforge.net/projects/mingw-w64/files/   ,下载 Download mingw-get-setup.exe (86.5 kB)。不过这个国外网站下载速度超慢,建议进入国内网站下载离线包:https://www.jb51.net/softs/696088.html 

2、运行 Download mingw-get-setup.exe ,点击"运行",continue等,注意记住安装的目录,我的是 F:\MinGw,下面修改环境变量时还会用到。






3、安装好后,需要设置编译器的路径环境变量

选择计算机—属性---高级系统设置---环境变量,在系统变量中找到 Path 变量,在后面加入 min-gw的安装目录,我的是 F:\MinGw\bin






 
 
4、在开始菜单中,点击"运行",输入 cmd,打开命令行:输入 mingw-get,如果弹出 MinGw installation manager 窗口,说明安装正常。此时,关闭 MinGw installation manager 窗口,否则接下来的步骤会报错;
 
5、在cmd中输入命令 mingw-get install gcc,等待一会,gcc 就安装成功了。
 
以上是常规的方式,但是不翻墙可能难以进行下去,所以直接下载离线包,然后把解压后的路径设置到环境变量中即可。





 
这个时候,打开cmd,输入gcc -v 即可查看是否安装成功。





 
然后你就可以在命令行里进行编译和执行c程序啦~
#include <stdio.h>

void main()
{
printf("hello, world! My name is zhengkai!\n");
} gcc 1.c
a.exe




 
 
附件:
VS最新版下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/
VSC最新版下载地址:https://code.visualstudio.com/Download 
Dev C++下载地址:https://bloodshed-dev-c.en.softonic.com/ 查看全部
确定好了学习目标,选择好了学习教程,制定好了学习计划,那么接下来,你需要做的就是搭建一套学习C语言的环境。我们无外乎基本上大多使用的操作系统是Windows和MACOS,所以在这里整理了一些IDE和C语言编译环境搭建的方法和经验供大家参考。这篇为Windows篇,下一篇文章分享MAC上有哪些优良实践。(文末为大家附上了各个IDE的下载地址)
 
IDE的选择
 
IDE,又称集成开发环境,顾名思义,他是一个编辑器 + 编译器的集合。所以对于接触一门新语言的“门外汉”来讲,最方便快捷的方式,就是选择一个IDE来完成这项工作。那么我们可以选择哪个IDE作为学习C语言以及以后开发C语言程序的最佳工具呢?Windows 下的C语言 IDE 众多,多如牛毛,初学者往往不知道该如何选择。我们可以综合自己的实际情况,根据这些IDE的特点来进行对自己最合适的选择。 
(1)Visual Studio
 Windows 下首先推荐大家使用微软开发的 Visual Studio(简称 VS),它是 Windows 下的标准 IDE,实际开发中大家也都在使用。为了适应最新的 Windows 操作系统,微软每隔一段时间(一般是一两年)就会对 VS 进行升级。VS 的不同版本以发布年份命名,例如 VS2010 是微软于 2010 年发布的,VS2019 是微软于 2019 年发布的。

QQ截图20200403102513.jpg

 
当然,这款IDE也有MAC版本,且它是windows官方推出的IDE环境,功能方面自不用说,非常之强大。但是有一个缺点就是看起来相对“臃肿”,可能有很多初学者用不上的工具也会在其中提供,安装包略大,约2~3GB大小,安装时间预计半个小时左右。


优点:功能强大,微软官方维护的IDE,更新及时,操作简单方便;
缺点:略臃肿,安装包大,安装时间长,占用系统资源较多;
选择:如果你的机器性能还可以的话,建议选择该IDE。


 
(2)Visual Studio Code
 
Microsoft在2015年4月30日Build 开发者大会上正式宣布了 Visual Studio Code 项目:一个运行于 Mac OS X、Windows和 Linux 之上的,针对于编写现代 Web 和云应用的跨平台源代码编辑器。该编辑器支持多种语言和文件格式的编写,截止2019年9月,已经支持了如下37种语言或文件:F#、HandleBars、Markdown、Python、Java、PHP、Haxe、Ruby、Sass、Rust、PowerShell、Groovy、R、Makefile、HTML、JSON、TypeScript、Batch、Visual Basic、Swift、Less、SQL、XML、Lua、Go、C++、Ini、Razor、Clojure、C#、Objective-C、CSS、JavaScript、Perl、Coffee Script、Dockerfile。
 
相对于 Visual Studio,这款IDE更轻便,支持的语言也更丰富。


优点:跨平台、官方支持、多语言支持、界面美观、操作方便、容易上手
缺点:暂未发现
建议:首选推荐安装


 
(3)Dev C++
 
如果比较讨厌“VS”的复杂性,那么Dev C++是一个不错的选择。Dev C++ 是一款免费开源的 C/C++ IDE,内嵌 GCC 编译器(Linux GCC 编译器的 Windows 移植版),是 NOI、NOIP 等比赛的指定工具。Dev C++ 的优点是体积小(只有几十兆)、安装卸载方便、学习成本低,缺点是调试功能弱。


NOI 是National Olympiad in Informatics的缩写,译为“全国青少年信息学奥林匹克竞赛”;NOIP 是National Olympiad in informatics in Provinces的缩写,译为“全国青少年信息学奥林匹克联赛”。NOI、NOIP 都是奥林匹克竞赛的一种,参加者多为高中生,获奖者将被保送到名牌大学或者得到高考加分资格。



优点:轻巧、方便,对机器性能要求不高,安装卸载方便,学习成本低
缺点:调试功能相对较弱
选择:如果机器性能较弱,建议选择这个IDE


 
(4)Visual C++ 6.0
 
哈哈,之所以把这个IDE放在这里强调下,纯属想要怀念一下,因为大学期间学C语言就是用的这款IDE呀呀呀呀!!!不知道你还记不记得下面这些界面~

QQ截图20200403103514.jpg


timg.gif

 
唉呀妈呀,仿佛还记得炎炎夏日在大学机房用那种方方正正的大脑袋电脑敲代码的日子……
Visual C++ 6.0(简称VC 6.0)是微软开发的一款经典的 IDE,很多高校都以 VC 6.0 为教学工具来讲解C和C++。但VC 6.0是1998年的产品,很古老了,在 Win7、Win8、Win10 下会有各种各样的兼容性问题,甚至根本不能运行,所以不推荐使用。


优点:可以用来回忆
缺点:太多不知从何说起
建议:强烈不建议,如果你想安装,我也拦不住


 
(5)其它 IDE
 
除了上面提到的三款 IDE,Windows 平台下还有很多其他的 IDE,它们各有特点,例如:
  1. Code::Blocks 是一款开源、跨平台、免费的 C/C++ IDE,它和 Dev C++ 非常类似,小巧灵活,易于安装和卸载,不过它的界面要比 Dev C++ 复杂一些,不如 Dev C++ 来得清爽。
  2. Turbo C 是一款古老的、DOS 年代的C语言开发工具,程序员只能使用键盘来操作 Turbo C,不能使用鼠标,所以非常不方便。但是 Turbo C 集成了一套图形库,可以在控制台程序中画图,看起来非常炫酷,所以至今仍然有人在使用。
  3. C-Free 是一款国产的 Windows 下的C/C++ IDE,最新版本是 5.0,整个软件才 14M,非常轻巧,安装也简单,界面也比 Dev C++ 漂亮。C-Free 的缺点也是调试功能弱。可惜的是,C-Free 已经多年不更新了,组件都老了,只能在 XP、Win7 下运行,在 Win8、Win10 下可能会存在兼容性问题。

 
所以,综合对比之下,如果电脑性能不错,就是用VS,否则,选择Dev C++。
 
赶紧去下载体验吧~~~
 
纯命令行下通过指令操作
 
没有IDE,不代表就不能写代码了!当然你还有“更程序员”的方式,通过命令行执行进行操作。下面介绍windows10下如何使用cmd命令进行GCC编译的几个步骤:
 
首先了解下什么是GCC?


GCC是以GPL许可证所发行的自由软件,也是GNU计划的关键部分。GCC的初衷是为GNU操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,甚至在微软的Windows上也可以使用GCC。GCC支持多种计算机体系结构芯片,如x86、ARM、MIPS等,并已被移植到其他多种硬件平台。
GCC原名为GNU C语言编译器(GNU C Compiler),只能处理C语言。但其很快扩展,变得可处理C++,后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective -C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。


 1、安装gcc编译器,需要到MinGW 的主页 www.mingw.org,进入 MinGW 下载页面,下载最新版本的 MinGW64 安装程序。或者到:https://sourceforge.net/projects/mingw-w64/files/   ,下载 Download mingw-get-setup.exe (86.5 kB)。不过这个国外网站下载速度超慢,建议进入国内网站下载离线包:https://www.jb51.net/softs/696088.html 

2、运行 Download mingw-get-setup.exe ,点击"运行",continue等,注意记住安装的目录,我的是 F:\MinGw,下面修改环境变量时还会用到。

QQ截图20200403113153.jpg


3、安装好后,需要设置编译器的路径环境变量


选择计算机—属性---高级系统设置---环境变量,在系统变量中找到 Path 变量,在后面加入 min-gw的安装目录,我的是 F:\MinGw\bin



QQ截图20200403121319.jpg

 
 
4、在开始菜单中,点击"运行",输入 cmd,打开命令行:输入 mingw-get,如果弹出 MinGw installation manager 窗口,说明安装正常。此时,关闭 MinGw installation manager 窗口,否则接下来的步骤会报错;
 
5、在cmd中输入命令 mingw-get install gcc,等待一会,gcc 就安装成功了。
 
以上是常规的方式,但是不翻墙可能难以进行下去,所以直接下载离线包,然后把解压后的路径设置到环境变量中即可。

QQ截图20200403121018.jpg

 
这个时候,打开cmd,输入gcc -v 即可查看是否安装成功。

QQ截图20200403121453.jpg

 
然后你就可以在命令行里进行编译和执行c程序啦~
#include <stdio.h>

void main()
{
printf("hello, world! My name is zhengkai!\n");
}
 
gcc 1.c
a.exe

QQ截图20200403122249.jpg

 
 
附件:
VS最新版下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/
VSC最新版下载地址:https://code.visualstudio.com/Download 
Dev C++下载地址:https://bloodshed-dev-c.en.softonic.com/

数据收集引擎:Logstash

工具软件zkbhj 发表了文章 • 0 个评论 • 265 次浏览 • 2019-12-11 21:17 • 来自相关话题

Logstash介绍

Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash可以动态地将来自不同数据源的数据统一起来,并将数据标准化到你所选择的目的地。





 
集中、转换和存储你的数据
 Logstash是一个开源的服务器端数据处理管道,可以同时从多个数据源获取数据,并对其进行转换,然后将其发送到你最喜欢的“存储”。(当然,我们最喜欢的是Elasticsearch)

输入:采集各种样式、大小和来源的数据

数据往往以各种各样的形式,或分散或集中地存在于很多系统中。Logstash 支持各种输入选择 ,可以在同一时间从众多常用来源捕捉事件。能够以连续的流式传输方式,轻松地从您的日志、指标、Web 应用、数据存储以及各种 AWS 服务采集数据。





 
过滤器:实时解析和转换数据

数据从源传输到存储库的过程中,Logstash 过滤器能够解析各个事件,识别已命名的字段以构建结构,并将它们转换成通用格式,以便更轻松、更快速地分析和实现商业价值。

Logstash 能够动态地转换和解析数据,不受格式或复杂度的影响: 
利用 Grok 从非结构化数据中派生出结构从 IP 地址破译出地理坐标将 PII 数据匿名化,完全排除敏感字段整体处理不受数据源、格式或架构的影响





 
输出:选择你的存储,导出你的数据

尽管 Elasticsearch 是我们的首选输出方向,能够为我们的搜索和分析带来无限可能,但它并非唯一选择。

Logstash 提供众多输出选择,您可以将数据发送到您要指定的地方,并且能够灵活地解锁众多下游用例。





安装Logstash





 
首先,让我们通过最基本的Logstash管道来测试一下刚才安装的Logstash

Logstash管道有两个必需的元素,输入和输出,以及一个可选元素过滤器。输入插件从数据源那里消费数据,过滤器插件根据你的期望修改数据,输出插件将数据写入目的地。





 
接下来,允许Logstash最基本的管道,例如:
bin/logstash -e 'input { stdin {} } output { stdout {} }'(画外音:选项 -e 的意思是允许你从命令行指定配置)

启动以后,下面我们在命令行下输入"hello world"






用Logstash解析日志

 在上一小节中,你已经创建了一个基本的Logstash管道来测试你的Logstash设置。在现实世界中,一个Logstash管理会稍微复杂一些:它通常有一个或多个input, filter 和 output 插件。

在这一小节中,你将创建一个Logstash管道,并且使用Filebeat将Apache Web日志作为input,解析这些日志,然后将解析的数据写到一个Elasticsearch集群中。你将在配置文件中定义管道,而不是在命令行中定义管道配置。

在开始之前,请先下载示例数据。

配置Filebeat来发送日志行到Logstash

在你创建Logstash管道之前,你需要先配置Filebeat来发送日志行到Logstash。Filebeat客户端是一个轻量级的、资源友好的工具,它从服务器上的文件中收集日志,并将这些日志转发到你的Logstash实例以进行处理。Filebeat设计就是为了可靠性和低延迟。Filebeat在主机上占用的资源很少,而且Beats input插件将对Logstash实例的资源需求降到最低。

(画外音:注意,在一个典型的用例中,Filebeat和Logstash实例是分开的,它们分别运行在不同的机器上。在本教程中,Logstash和Filebeat在同一台机器上运行。)

关于Filebeat请参考《开始使用Filebeat》
 
更多:https://www.cnblogs.com/cjsblog/p/9459781.html 查看全部
Logstash介绍

Logstash是一个开源数据收集引擎,具有实时管道功能。Logstash可以动态地将来自不同数据源的数据统一起来,并将数据标准化到你所选择的目的地。

874963-20180811151005760-1802603955.png

 
集中、转换和存储你的数据
 Logstash是一个开源的服务器端数据处理管道,可以同时从多个数据源获取数据,并对其进行转换,然后将其发送到你最喜欢的“存储”。(当然,我们最喜欢的是Elasticsearch)

输入:采集各种样式、大小和来源的数据

数据往往以各种各样的形式,或分散或集中地存在于很多系统中。Logstash 支持各种输入选择 ,可以在同一时间从众多常用来源捕捉事件。能够以连续的流式传输方式,轻松地从您的日志、指标、Web 应用、数据存储以及各种 AWS 服务采集数据。

874963-20180811145656085-1071322041.png

 
过滤器:实时解析和转换数据

数据从源传输到存储库的过程中,Logstash 过滤器能够解析各个事件,识别已命名的字段以构建结构,并将它们转换成通用格式,以便更轻松、更快速地分析和实现商业价值。

Logstash 能够动态地转换和解析数据,不受格式或复杂度的影响: 
  • 利用 Grok 从非结构化数据中派生出结构
  • 从 IP 地址破译出地理坐标
  • 将 PII 数据匿名化,完全排除敏感字段
  • 整体处理不受数据源、格式或架构的影响


874963-20180811145809984-850775448.png

 
输出:选择你的存储,导出你的数据

尽管 Elasticsearch 是我们的首选输出方向,能够为我们的搜索和分析带来无限可能,但它并非唯一选择。

Logstash 提供众多输出选择,您可以将数据发送到您要指定的地方,并且能够灵活地解锁众多下游用例。
874963-20180811150315582-1149748851.png


安装Logstash

QQ截图20191211211319.jpg

 
首先,让我们通过最基本的Logstash管道来测试一下刚才安装的Logstash

Logstash管道有两个必需的元素,输入和输出,以及一个可选元素过滤器。输入插件从数据源那里消费数据,过滤器插件根据你的期望修改数据,输出插件将数据写入目的地。

874963-20180811152809345-286170144.png

 
接下来,允许Logstash最基本的管道,例如:
bin/logstash -e 'input { stdin {} } output { stdout {} }'
(画外音:选项 -e 的意思是允许你从命令行指定配置)

启动以后,下面我们在命令行下输入"hello world"

874963-20180811153425324-1597854249.png


用Logstash解析日志

 在上一小节中,你已经创建了一个基本的Logstash管道来测试你的Logstash设置。在现实世界中,一个Logstash管理会稍微复杂一些:它通常有一个或多个input, filter 和 output 插件。

在这一小节中,你将创建一个Logstash管道,并且使用Filebeat将Apache Web日志作为input,解析这些日志,然后将解析的数据写到一个Elasticsearch集群中。你将在配置文件中定义管道,而不是在命令行中定义管道配置。

在开始之前,请先下载示例数据。

配置Filebeat来发送日志行到Logstash

在你创建Logstash管道之前,你需要先配置Filebeat来发送日志行到Logstash。Filebeat客户端是一个轻量级的、资源友好的工具,它从服务器上的文件中收集日志,并将这些日志转发到你的Logstash实例以进行处理。Filebeat设计就是为了可靠性和低延迟。Filebeat在主机上占用的资源很少,而且Beats input插件将对Logstash实例的资源需求降到最低。

(画外音:注意,在一个典型的用例中,Filebeat和Logstash实例是分开的,它们分别运行在不同的机器上。在本教程中,Logstash和Filebeat在同一台机器上运行。)

关于Filebeat请参考《开始使用Filebeat》
 
更多:https://www.cnblogs.com/cjsblog/p/9459781.html

Elasticsearch查询总结合集(二)

ElasticSearchzkbhj 发表了文章 • 0 个评论 • 263 次浏览 • 2019-11-27 14:37 • 来自相关话题

多字段搜索(Multifield Search)

查询很少是只拥有一个match查询子句的查询。我们经常需要对一个或者多个字段使用相同或者不同的查询字符串进行搜索,这意味着我们需要将多个查询子句和它们得到的相关度分值以一种有意义的方式进行合并。

也许我们正在寻找一本名为战争与和平的书,它的作者是Leo Tolstoy。也许我们正在使用"最少应该匹配(Minimum Should Match)"来搜索ES中的文档。另外我们也可能会寻找拥有名为John而姓为Smith的用户。

在本章中我们会讨论一些构建多字段搜索的工具,以及如何根据你的实际情况来决定使用哪种方案。

多个查询字符串(Multiple Query Strings)

处理字段查询最简单的方法是将搜索词条对应到特定的字段上。如果我们知道战争与和平是标题,而Leo Tolstoy是作者,那么我们可以简单地将每个条件当做一个match子句,然后通过bool查询将它们合并:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }}
]
}
}
}bool查询采用了一种"匹配越多越好(More-matches-is-better)"的方法,因此每个match子句的分值会被累加来得到文档最终的_score。匹配两个子句的文档相比那些只匹配一个子句的文档的分值会高一些。

当然,你并不是只能使用match子句:bool查询可以包含任何其他类型的查询,包括其它的bool查询。我们可以添加一个子句来指定我们希望的译者:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}我们为什么将译者的查询子句放在一个单独的bool查询中?所有的4个match查询都是should子句,那么为何不将译者的查询子句和标题及作者的查询子句放在同一层次上呢?

答案在于分值是如何计算的。bool查询会运行每个match查询,将它们的分值相加,然后乘以匹配的查询子句的数量,最后除以所有查询子句的数量。相同层次的每个子句都拥有相同的权重。在上述查询中,bool查询中包含的译者查询子句只占了总分值的三分之一。如果我们将译者查询子句放到和标题及作者相同的层次上,就会减少标题和作者子句的权重,让它们各自只占四分之一。

设置子句优先级

上述查询中每个子句占有三分之一的权重也许并不是我们需要的。相比译者字段,我们可能对标题和作者字段更有兴趣。我们对查询进行调整来让标题和作者相对更重要。

在所有可用措施中,我们可以采用的最简单的方法是boost参数。为了增加title和author字段的权重,我们可以给它们一个大于1的boost值:GET /_search
{
"query": {
"bool": {
"should": [
{ "match": {
"title": {
"query": "War and Peace",
"boost": 2
}}},
{ "match": {
"author": {
"query": "Leo Tolstoy",
"boost": 2
}}},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}以上的title和k字段的boost值为2。 嵌套的bool查询自居的默认boost值为k。

通过试错(Trial and Error)的方式可以确定"最佳"的boost值:设置一个boost值,执行测试查询,重复这个过程。一个合理boost值的范围在1和10之间,也可能是15。比它更高的值的影响不会起到很大的作用,因为分值会被规范化(Normalized)。

单一查询字符串(Single Query String)

bool查询是多字段查询的中流砥柱。在很多场合下它都能很好地工作,特别是当你能够将不同的查询字符串映射到不同的字段时。

问题在于,现在的用户期望能够在一个地方输入所有的搜索词条,然后应用能够知道如何为他们得到正确的结果。所以当我们把含有多个字段的搜索表单称为高级搜索(Advanced Search)时,是有一些讽刺意味的。高级搜索虽然对用户而言会显得更"高级",但是实际上它的实现方式更简单。

对于多词,多字段查询并没有一种万能的方法。要得到最佳的结果,你需要了解你的数据以及如何使用恰当的工具。

了解你的数据

当用户的唯一输入就是一个查询字符串时,你会经常碰到以下三种情况:

最佳字段(Best fields)

当搜索代表某些概念的单词时,例如"brown fox",几个单词合在一起表达出来的意思比单独的单词更多。类似title和body的字段,尽管它们是相关联的,但是也是互相竞争着的。文档在相同的字段中应该有尽可能多的单词(译注:搜索的目标单词),文档的分数应该来自拥有最佳匹配的字段。

多数字段(Most fields)

一个用来调优相关度的常用技术是将相同的数据索引到多个字段中,每个字段拥有自己的分析链(Analysis Chain)。

主要字段会含有单词的词干部分,同义词和消除了变音符号的单词。它用来尽可能多地匹配文档。

相同的文本可以被索引到其它的字段中来提供更加精确的匹配。一个字段或许会包含未被提取词干的单词,另一个字段是包含了变音符号的单词,第三个字段则使用shingle来提供关于单词邻近度(Word Proximity)的信息。

以上这些额外的字段扮演者signal的角色,用来增加每个匹配的文档的相关度分值。越多的字段被匹配则意味着文档的相关度越高。

跨字段(Cross fields)

对于一些实体,标识信息会在多个字段中出现,每个字段中只含有一部分信息:
 
Person:first_name 和 last_nameBook:title,author 和 descriptionAddress:street,city,country 和 postcode

此时,我们希望在任意字段中找到尽可能多的单词。我们需要在多个字段中进行查询,就好像这些字段是一个字段那样。

以上这些都是多词,多字段查询,但是每种都需要使用不同的策略。我们会在本章剩下的部分解释每种策略。 查看全部
多字段搜索(Multifield Search)

查询很少是只拥有一个match查询子句的查询。我们经常需要对一个或者多个字段使用相同或者不同的查询字符串进行搜索,这意味着我们需要将多个查询子句和它们得到的相关度分值以一种有意义的方式进行合并。

也许我们正在寻找一本名为战争与和平的书,它的作者是Leo Tolstoy。也许我们正在使用"最少应该匹配(Minimum Should Match)"来搜索ES中的文档。另外我们也可能会寻找拥有名为John而姓为Smith的用户。

在本章中我们会讨论一些构建多字段搜索的工具,以及如何根据你的实际情况来决定使用哪种方案。

多个查询字符串(Multiple Query Strings)

处理字段查询最简单的方法是将搜索词条对应到特定的字段上。如果我们知道战争与和平是标题,而Leo Tolstoy是作者,那么我们可以简单地将每个条件当做一个match子句,然后通过bool查询将它们合并:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }}
]
}
}
}
bool查询采用了一种"匹配越多越好(More-matches-is-better)"的方法,因此每个match子句的分值会被累加来得到文档最终的_score。匹配两个子句的文档相比那些只匹配一个子句的文档的分值会高一些。

当然,你并不是只能使用match子句:bool查询可以包含任何其他类型的查询,包括其它的bool查询。我们可以添加一个子句来指定我们希望的译者:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
我们为什么将译者的查询子句放在一个单独的bool查询中?所有的4个match查询都是should子句,那么为何不将译者的查询子句和标题及作者的查询子句放在同一层次上呢?

答案在于分值是如何计算的。bool查询会运行每个match查询,将它们的分值相加,然后乘以匹配的查询子句的数量,最后除以所有查询子句的数量。相同层次的每个子句都拥有相同的权重。在上述查询中,bool查询中包含的译者查询子句只占了总分值的三分之一。如果我们将译者查询子句放到和标题及作者相同的层次上,就会减少标题和作者子句的权重,让它们各自只占四分之一。

设置子句优先级

上述查询中每个子句占有三分之一的权重也许并不是我们需要的。相比译者字段,我们可能对标题和作者字段更有兴趣。我们对查询进行调整来让标题和作者相对更重要。

在所有可用措施中,我们可以采用的最简单的方法是boost参数。为了增加title和author字段的权重,我们可以给它们一个大于1的boost值:
GET /_search
{
"query": {
"bool": {
"should": [
{ "match": {
"title": {
"query": "War and Peace",
"boost": 2
}}},
{ "match": {
"author": {
"query": "Leo Tolstoy",
"boost": 2
}}},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
以上的title和k字段的boost值为2。 嵌套的bool查询自居的默认boost值为k。

通过试错(Trial and Error)的方式可以确定"最佳"的boost值:设置一个boost值,执行测试查询,重复这个过程。一个合理boost值的范围在1和10之间,也可能是15。比它更高的值的影响不会起到很大的作用,因为分值会被规范化(Normalized)。

单一查询字符串(Single Query String)

bool查询是多字段查询的中流砥柱。在很多场合下它都能很好地工作,特别是当你能够将不同的查询字符串映射到不同的字段时。

问题在于,现在的用户期望能够在一个地方输入所有的搜索词条,然后应用能够知道如何为他们得到正确的结果。所以当我们把含有多个字段的搜索表单称为高级搜索(Advanced Search)时,是有一些讽刺意味的。高级搜索虽然对用户而言会显得更"高级",但是实际上它的实现方式更简单。

对于多词,多字段查询并没有一种万能的方法。要得到最佳的结果,你需要了解你的数据以及如何使用恰当的工具。

了解你的数据

当用户的唯一输入就是一个查询字符串时,你会经常碰到以下三种情况:

最佳字段(Best fields)

当搜索代表某些概念的单词时,例如"brown fox",几个单词合在一起表达出来的意思比单独的单词更多。类似title和body的字段,尽管它们是相关联的,但是也是互相竞争着的。文档在相同的字段中应该有尽可能多的单词(译注:搜索的目标单词),文档的分数应该来自拥有最佳匹配的字段。

多数字段(Most fields)

一个用来调优相关度的常用技术是将相同的数据索引到多个字段中,每个字段拥有自己的分析链(Analysis Chain)。

主要字段会含有单词的词干部分,同义词和消除了变音符号的单词。它用来尽可能多地匹配文档。

相同的文本可以被索引到其它的字段中来提供更加精确的匹配。一个字段或许会包含未被提取词干的单词,另一个字段是包含了变音符号的单词,第三个字段则使用shingle来提供关于单词邻近度(Word Proximity)的信息。

以上这些额外的字段扮演者signal的角色,用来增加每个匹配的文档的相关度分值。越多的字段被匹配则意味着文档的相关度越高。

跨字段(Cross fields)

对于一些实体,标识信息会在多个字段中出现,每个字段中只含有一部分信息:
 
  • Person:first_name 和 last_name
  • Book:title,author 和 description
  • Address:street,city,country 和 postcode


此时,我们希望在任意字段中找到尽可能多的单词。我们需要在多个字段中进行查询,就好像这些字段是一个字段那样。

以上这些都是多词,多字段查询,但是每种都需要使用不同的策略。我们会在本章剩下的部分解释每种策略。

Elasticsearch查询总结合集

ElasticSearchzkbhj 发表了文章 • 0 个评论 • 280 次浏览 • 2019-11-26 16:38 • 来自相关话题

一、term、terms查询(精确查询)
 
term query会去倒排索引中寻找确切的term,它并不知道分词器的存在,这种查询适合keyword、numeric、date等明确值的

term:查询某个字段里含有某个关键词的文档GET /customer/doc/_search/
{
"query": {
"term": {
"title": "blog"
}
}
}terms:查询某个字段里含有多个关键词的文档
GET /customer/doc/_search/
{
"query": {
"terms": {
"title": [ "blog","first"]
}
}
}
二、match查询(模糊查询)

match query 知道分词器的存在,会对field进行分词操作,然后再查询。GET /customer/doc/_search/
{
"query": {
"match": {
"title": "my ss" #它和term区别可以理解为term是精确查询,这边match模糊查询;match会对my ss分词为两个单词,然后term对认为这是一个单词
}
}
}match_all:查询所有文档
GET /customer/doc/_search/
{
"query": {
"match_all": {}
}
}multi_match:可以指定多个字段
GET /customer/doc/_search/
{
"query": {
"multi_match": {
"query" : "blog",
"fields": ["name","title"] #只要里面一个字段包含值 blog 既可以
}
}
}
 三、match_phrase:短语匹配查询

ES引擎首先分析查询字符串,从分析后的文本中构建短语查询,这意味着必须匹配短语中的所有分词,并且保证各个分词的相对位置不变

_source:当我们希望返回结果只是一部分字段时,可以加上_source
GET /customer/doc/_search/
{
"_source":["title"], #只返回title字段
"query": {
"match_all": {}
}
}
 四、排序

使用sort实现排序(类似sql):desc 降序,asc升序

fuzzy实现模糊查询

value:查询的关键字

boost:查询的权值,默认值是1.0

min_similarity:设置匹配的最小相似度,默认值0.5,对于字符串,取值0-1(包括0和1);对于数值,取值可能大于1;对于日期取值为1d,1m等,1d等于1天

prefix_length:指明区分词项的共同前缀长度,默认是0GET /customer/doc/_search/
{

"query": {
"fuzzy": {
"name": {
"value": "blg"
}
}
}
}

#返回:
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.19178805,
"hits": [
{
"_index": "customer",
"_type": "doc",
"_id": "1",
"_score": 0.19178805,
"_source": {
"name": "blog",
"tags": [
"testing"
],
"title": "i am a doc"
}
}
]
}
} 查看全部
一、term、terms查询(精确查询)
 
term query会去倒排索引中寻找确切的term,它并不知道分词器的存在,这种查询适合keyword、numeric、date等明确值的

term:查询某个字段里含有某个关键词的文档
GET /customer/doc/_search/
{
"query": {
"term": {
"title": "blog"
}
}
}
terms:查询某个字段里含有多个关键词的文档
GET /customer/doc/_search/
{
"query": {
"terms": {
"title": [ "blog","first"]
}
}
}

二、match查询(模糊查询)

match query 知道分词器的存在,会对field进行分词操作,然后再查询。
GET /customer/doc/_search/
{
"query": {
"match": {
"title": "my ss" #它和term区别可以理解为term是精确查询,这边match模糊查询;match会对my ss分词为两个单词,然后term对认为这是一个单词
}
}
}
match_all:查询所有文档
GET /customer/doc/_search/
{
"query": {
"match_all": {}
}
}
multi_match:可以指定多个字段
GET /customer/doc/_search/
{
"query": {
"multi_match": {
"query" : "blog",
"fields": ["name","title"] #只要里面一个字段包含值 blog 既可以
}
}
}

 三、match_phrase:短语匹配查询

ES引擎首先分析查询字符串,从分析后的文本中构建短语查询,这意味着必须匹配短语中的所有分词,并且保证各个分词的相对位置不变

_source:当我们希望返回结果只是一部分字段时,可以加上_source
GET /customer/doc/_search/
{
"_source":["title"], #只返回title字段
"query": {
"match_all": {}
}
}

 四、排序

使用sort实现排序(类似sql):desc 降序,asc升序

fuzzy实现模糊查询

value:查询的关键字

boost:查询的权值,默认值是1.0

min_similarity:设置匹配的最小相似度,默认值0.5,对于字符串,取值0-1(包括0和1);对于数值,取值可能大于1;对于日期取值为1d,1m等,1d等于1天

prefix_length:指明区分词项的共同前缀长度,默认是0
GET /customer/doc/_search/
{

"query": {
"fuzzy": {
"name": {
"value": "blg"
}
}
}
}

#返回:
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.19178805,
"hits": [
{
"_index": "customer",
"_type": "doc",
"_id": "1",
"_score": 0.19178805,
"_source": {
"name": "blog",
"tags": [
"testing"
],
"title": "i am a doc"
}
}
]
}
}

MySQL 8.0 正式版发布:比 MySQL 5.7 快 2 倍

数据库zkbhj 发表了文章 • 0 个评论 • 771 次浏览 • 2018-05-25 10:13 • 来自相关话题

MySQL 8.0 正式版 8.0.11 已发布,官方表示 MySQL 8 要比 MySQL 5.7 快 2 倍,还带来了大量的改进和更快的性能!

注意:从 MySQL 5.7 升级到 MySQL 8.0 仅支持通过使用 in-place 方式进行升级,并且不支持从 MySQL 8.0 降级到 MySQL 5.7(或从某个 MySQL 8.0 版本降级到任意一个更早的 MySQL 8.0 版本)。唯一受支持的替代方案是在升级之前对数据进行备份。

 
下面简要介绍 MySQL 8 中值得关注的新特性和改进。

1. 性能:MySQL 8.0 的速度要比 MySQL 5.7 快 2 倍。MySQL 8.0 在以下方面带来了更好的性能:读/写工作负载、IO 密集型工作负载、以及高竞争("hot spot"热点竞争问题)工作负载。

2. NoSQL:MySQL 从 5.7 版本开始提供 NoSQL 存储功能,目前在 8.0 版本中这部分功能也得到了更大的改进。该项功能消除了对独立的 NoSQL 文档数据库的需求,而 MySQL 文档存储也为 schema-less 模式的 JSON 文档提供了多文档事务支持和完整的 ACID 合规性。

3. 窗口函数(Window Functions):从 MySQL 8.0 开始,新增了一个叫窗口函数的概念,它可以用来实现若干新的查询方式。窗口函数与 SUM()、COUNT() 这种集合函数类似,但它不会将多行查询结果合并为一行,而是将结果放回多行当中。即窗口函数不需要 GROUP BY。

4. 隐藏索引:在 MySQL 8.0 中,索引可以被“隐藏”和“显示”。当对索引进行隐藏时,它不会被查询优化器所使用。我们可以使用这个特性用于性能调试,例如我们先隐藏一个索引,然后观察其对数据库的影响。如果数据库性能有所下降,说明这个索引是有用的,然后将其“恢复显示”即可;如果数据库性能看不出变化,说明这个索引是多余的,可以考虑删掉。

5. 降序索引:MySQL 8.0 为索引提供按降序方式进行排序的支持,在这种索引中的值也会按降序的方式进行排序。

6. 通用表表达式(Common Table Expressions CTE):在复杂的查询中使用嵌入式表时,使用 CTE 使得查询语句更清晰。

7. UTF-8 编码:从 MySQL 8 开始,使用 utf8mb4 作为 MySQL 的默认字符集。

8. JSON:MySQL 8 大幅改进了对 JSON 的支持,添加了基于路径查询参数从 JSON 字段中抽取数据的 JSON_EXTRACT() 函数,以及用于将数据分别组合到 JSON 数组和对象中的 JSON_ARRAYAGG() 和 JSON_OBJECTAGG() 聚合函数。

9. 可靠性:InnoDB 现在支持表 DDL 的原子性,也就是 InnoDB 表上的 DDL 也可以实现事务完整性,要么失败回滚,要么成功提交,不至于出现 DDL 时部分成功的问题,此外还支持 crash-safe 特性,元数据存储在单个事务数据字典中。

10. 高可用性(High Availability):InnoDB 集群为您的数据库提供集成的原生 HA 解决方案。

11. 安全性:对 OpenSSL 的改进、新的默认身份验证、SQL 角色、密码强度、授权。

    MySQL 8.0.11更改(2018-04-19,可用性)

仅使用就地升级方法支持从MySQL 5.7升级到MySQL 8.0。

不支持从MySQL 8.0降级到MySQL 5.7(或从MySQL 8.0发布到以前的MySQL 8.0发行版)。唯一受支持的替代方法是在升级之前恢复备份 。

 

详细更新说明:https://dev.mysql.com/doc/reln ... .html  
官方发布说明:https://blogs.oracle.com/mysql ... ql-80  
MySQL 8 正式版的新增功能:https://mysqlserverteam.com/wh ... able/

下载地址

Windows (x86, 64-bit), ZIP Archive(mysql-8.0.11-winx64.zip) 183.3M
https://dev.mysql.com/downloads/file/?id=476233

Windows (x86, 64-bit), ZIP Archive(mysql-8.0.11-winx64-debug-test.zip) 230.5M
Debug Binaries & Test Suite
https://dev.mysql.com/downloads/file/?id=476234

其他版本下载地址 >>> https://dev.mysql.com/downloads/mysql/8.0.html

简介:

MySQL是一个开放源码的小型关联式数据库管理系统,开发者为瑞典MySQL AB公司。目前MySQL被广泛地应用在Internet上的中小型网站中。由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多中小型网站为了降低网站总体拥有成本而选择了MySQL作为网站数据库。

MySQL的特性

使用C和C++编写,并使用了多种编译器进行测试,保证源代码的可移植性。

支持AIX、BSDi、FreeBSD、HP-UX、Linux、Mac OS、Novell Netware、NetBSD、OpenBSD、OS/2 Wrap、Solaris、SunOS、Windows等多种操作系统。

为多种编程语言提供了API。这些编程语言包括C、C++、C#、Delphi、Eiffel、Java、Perl、PHP、Python、Ruby和Tcl等。

支持多线程,充分利用CPU资源,支持多用户。

优化的SQL查询算法,有效地提高查询速度。

既能够作为一个单独的应用程序应用在客户端服务器网络环境中,也能够作为一个库而嵌入到其他的软件中。

提供多语言支持,常见的编码如中文的GB 2312、BIG5,日文的Shift_JIS等都可以用作数据表名和数据列名。

提供TCP/IP、ODBC和JDBC等多种数据库连接途径。

提供用于管理、检查、优化数据库操作的管理工具。

可以处理拥有上千万条记录的大型数据库。

中文 MySQL 文档:http://tool.oschina.net/apidoc ... .1-zh

英文 MySQL 文档:http://tool.oschina.net/apidoc ... .5-en 查看全部
MySQL 8.0 正式版 8.0.11 已发布,官方表示 MySQL 8 要比 MySQL 5.7 快 2 倍,还带来了大量的改进和更快的性能!


注意:从 MySQL 5.7 升级到 MySQL 8.0 仅支持通过使用 in-place 方式进行升级,并且不支持从 MySQL 8.0 降级到 MySQL 5.7(或从某个 MySQL 8.0 版本降级到任意一个更早的 MySQL 8.0 版本)。唯一受支持的替代方案是在升级之前对数据进行备份。


 
下面简要介绍 MySQL 8 中值得关注的新特性和改进。

1. 性能:MySQL 8.0 的速度要比 MySQL 5.7 快 2 倍。MySQL 8.0 在以下方面带来了更好的性能:读/写工作负载、IO 密集型工作负载、以及高竞争("hot spot"热点竞争问题)工作负载。

2. NoSQL:MySQL 从 5.7 版本开始提供 NoSQL 存储功能,目前在 8.0 版本中这部分功能也得到了更大的改进。该项功能消除了对独立的 NoSQL 文档数据库的需求,而 MySQL 文档存储也为 schema-less 模式的 JSON 文档提供了多文档事务支持和完整的 ACID 合规性。

3. 窗口函数(Window Functions):从 MySQL 8.0 开始,新增了一个叫窗口函数的概念,它可以用来实现若干新的查询方式。窗口函数与 SUM()、COUNT() 这种集合函数类似,但它不会将多行查询结果合并为一行,而是将结果放回多行当中。即窗口函数不需要 GROUP BY。

4. 隐藏索引:在 MySQL 8.0 中,索引可以被“隐藏”和“显示”。当对索引进行隐藏时,它不会被查询优化器所使用。我们可以使用这个特性用于性能调试,例如我们先隐藏一个索引,然后观察其对数据库的影响。如果数据库性能有所下降,说明这个索引是有用的,然后将其“恢复显示”即可;如果数据库性能看不出变化,说明这个索引是多余的,可以考虑删掉。

5. 降序索引:MySQL 8.0 为索引提供按降序方式进行排序的支持,在这种索引中的值也会按降序的方式进行排序。

6. 通用表表达式(Common Table Expressions CTE):在复杂的查询中使用嵌入式表时,使用 CTE 使得查询语句更清晰。

7. UTF-8 编码:从 MySQL 8 开始,使用 utf8mb4 作为 MySQL 的默认字符集。

8. JSON:MySQL 8 大幅改进了对 JSON 的支持,添加了基于路径查询参数从 JSON 字段中抽取数据的 JSON_EXTRACT() 函数,以及用于将数据分别组合到 JSON 数组和对象中的 JSON_ARRAYAGG() 和 JSON_OBJECTAGG() 聚合函数。

9. 可靠性:InnoDB 现在支持表 DDL 的原子性,也就是 InnoDB 表上的 DDL 也可以实现事务完整性,要么失败回滚,要么成功提交,不至于出现 DDL 时部分成功的问题,此外还支持 crash-safe 特性,元数据存储在单个事务数据字典中。

10. 高可用性(High Availability):InnoDB 集群为您的数据库提供集成的原生 HA 解决方案。

11. 安全性:对 OpenSSL 的改进、新的默认身份验证、SQL 角色、密码强度、授权。

    MySQL 8.0.11更改(2018-04-19,可用性)

仅使用就地升级方法支持从MySQL 5.7升级到MySQL 8.0。

不支持从MySQL 8.0降级到MySQL 5.7(或从MySQL 8.0发布到以前的MySQL 8.0发行版)。唯一受支持的替代方法是在升级之前恢复备份 。

 

详细更新说明:https://dev.mysql.com/doc/reln ... .html  
官方发布说明:https://blogs.oracle.com/mysql ... ql-80  
MySQL 8 正式版的新增功能:https://mysqlserverteam.com/wh ... able/

下载地址

Windows (x86, 64-bit), ZIP Archive(mysql-8.0.11-winx64.zip) 183.3M
https://dev.mysql.com/downloads/file/?id=476233

Windows (x86, 64-bit), ZIP Archive(mysql-8.0.11-winx64-debug-test.zip) 230.5M
Debug Binaries & Test Suite
https://dev.mysql.com/downloads/file/?id=476234

其他版本下载地址 >>> https://dev.mysql.com/downloads/mysql/8.0.html

简介:

MySQL是一个开放源码的小型关联式数据库管理系统,开发者为瑞典MySQL AB公司。目前MySQL被广泛地应用在Internet上的中小型网站中。由于其体积小、速度快、总体拥有成本低,尤其是开放源码这一特点,许多中小型网站为了降低网站总体拥有成本而选择了MySQL作为网站数据库。

MySQL的特性

使用C和C++编写,并使用了多种编译器进行测试,保证源代码的可移植性。

支持AIX、BSDi、FreeBSD、HP-UX、Linux、Mac OS、Novell Netware、NetBSD、OpenBSD、OS/2 Wrap、Solaris、SunOS、Windows等多种操作系统。

为多种编程语言提供了API。这些编程语言包括C、C++、C#、Delphi、Eiffel、Java、Perl、PHP、Python、Ruby和Tcl等。

支持多线程,充分利用CPU资源,支持多用户。

优化的SQL查询算法,有效地提高查询速度。

既能够作为一个单独的应用程序应用在客户端服务器网络环境中,也能够作为一个库而嵌入到其他的软件中。

提供多语言支持,常见的编码如中文的GB 2312、BIG5,日文的Shift_JIS等都可以用作数据表名和数据列名。

提供TCP/IP、ODBC和JDBC等多种数据库连接途径。

提供用于管理、检查、优化数据库操作的管理工具。

可以处理拥有上千万条记录的大型数据库。

中文 MySQL 文档:http://tool.oschina.net/apidoc ... .1-zh

英文 MySQL 文档:http://tool.oschina.net/apidoc ... .5-en

linux运维命令之ps、netstat、nmap和pmap

服务器zkbhj 发表了文章 • 0 个评论 • 1010 次浏览 • 2018-05-17 18:30 • 来自相关话题

1.先看ps,linux下面可不是p图的哦!

ps -ef 查询正在运行的进程信息,一般在该操作之后用grep和AWK提取需要的信息。

如:ps -ef|grep ssh 查看ssh进程

ps -ef|awk '{print $2}' 打印所有进程信息对应的端口号(PID)

ps -aux        列出目前所有的正在内存当中的程序

ps -ajxf        列出类似程序树的程序显示

ps -l             将目前属于您自己这次登入的 PID 与相关信息列示出来

ps -A            列出进程信息

ps -u root    列出用户root下进程信息

当然,ps只是一次获取信息,用top命令就可以显示进程信息,并实时更新

2.再看netstat,这个命令用于显示各种网络相关信息,如网络连接、路由表、接口状态 (Interface Statistics)、masquerade 连接和多播成员 (Multicast Memberships) 等等。

netstat -a    列出所有端口 (包括监听和未监听的)

netstat -at   列出所有的TCP端口

netstat -au  列出所有的UDP端口

netstat -an  列出目前已经连接的和正在监听的端口号


netstat -ap  在上面命令的基础上列出连接的PID(进程号),用这个PID,可以使用KILL 来杀死连接

netstat -l     列出所有有监听的服务状态(还有另外一种说法,列出所有处于监听状态的 Sockets)

netstat -lx   列出所有监听 UNIX 端口

netstat -s   列出所有端口的统计信息

netstat -p   列出所有端口的端口号(PID)和进程名

netstat -ie  列出所有网络接口列表的详细信息,是ifconfig的替代命令

netstat -r    列出核心路由信息表

netstat -apn|grep PID/NAME  列出对应端口号和名称对应的信息

牛刀小试:

netstat -nat |awk '{print $6}'|sort|uniq -c     查看TCP所有状态




3.pmap命令用于报告进程的内存映射关系,是一个常用的Linux运维命令,在性能检测方面很有用。

pmap -x   显示拓展格式

pmap -d  显示设备格式

pmap -q  不显示头尾行




4.pmap命令几乎是黑客最喜欢的命令,现在来看下这个刺激而心跳的命令吧!

nmap -sP 192.168.0.*  寻找所有在线主机并打印其内网ip地址和MAC地址

nmap -sP 192.168.0.1/24  探测192.168.1.0网段有多少机器是开机的,并探测其MAC地址及MAC芯片信息


注意:上面两个命令等同

nmap -sP(Ping Scan)命令仅执行ping扫描(主机发现),然后打印出响应ping扫描的可用主机。不做进一步测试(如端口扫描或者OS探测)。

 并且 -sP选项能和任意发现探针类型(即-P*选项,-PN除外)组合使用。


nmap -sA  ip_addr        扫描指定ip地址对应的主机

nmap -Pn  ip_addr        扫描访问可能阻塞时,该命令比上一条更好用

nmap -PN (No ping)    跳过主机发现阶段 查看全部
1.先看ps,linux下面可不是p图的哦!

ps -ef 查询正在运行的进程信息,一般在该操作之后用grep和AWK提取需要的信息。

如:ps -ef|grep ssh 查看ssh进程

ps -ef|awk '{print $2}' 打印所有进程信息对应的端口号(PID)

ps -aux        列出目前所有的正在内存当中的程序

ps -ajxf        列出类似程序树的程序显示

ps -l             将目前属于您自己这次登入的 PID 与相关信息列示出来

ps -A            列出进程信息

ps -u root    列出用户root下进程信息

当然,ps只是一次获取信息,用top命令就可以显示进程信息,并实时更新

2.再看netstat,这个命令用于显示各种网络相关信息,如网络连接、路由表、接口状态 (Interface Statistics)、masquerade 连接和多播成员 (Multicast Memberships) 等等。

netstat -a    列出所有端口 (包括监听和未监听的)

netstat -at   列出所有的TCP端口

netstat -au  列出所有的UDP端口

netstat -an  列出目前已经连接的和正在监听的端口号


netstat -ap  在上面命令的基础上列出连接的PID(进程号),用这个PID,可以使用KILL 来杀死连接

netstat -l     列出所有有监听的服务状态(还有另外一种说法,列出所有处于监听状态的 Sockets)

netstat -lx   列出所有监听 UNIX 端口

netstat -s   列出所有端口的统计信息

netstat -p   列出所有端口的端口号(PID)和进程名

netstat -ie  列出所有网络接口列表的详细信息,是ifconfig的替代命令

netstat -r    列出核心路由信息表

netstat -apn|grep PID/NAME  列出对应端口号和名称对应的信息

牛刀小试:

netstat -nat |awk '{print $6}'|sort|uniq -c     查看TCP所有状态




3.pmap命令用于报告进程的内存映射关系,是一个常用的Linux运维命令,在性能检测方面很有用。

pmap -x   显示拓展格式

pmap -d  显示设备格式

pmap -q  不显示头尾行




4.pmap命令几乎是黑客最喜欢的命令,现在来看下这个刺激而心跳的命令吧!

nmap -sP 192.168.0.*  寻找所有在线主机并打印其内网ip地址和MAC地址

nmap -sP 192.168.0.1/24  探测192.168.1.0网段有多少机器是开机的,并探测其MAC地址及MAC芯片信息


注意:上面两个命令等同

nmap -sP(Ping Scan)命令仅执行ping扫描(主机发现),然后打印出响应ping扫描的可用主机。不做进一步测试(如端口扫描或者OS探测)。

 并且 -sP选项能和任意发现探针类型(即-P*选项,-PN除外)组合使用。


nmap -sA  ip_addr        扫描指定ip地址对应的主机

nmap -Pn  ip_addr        扫描访问可能阻塞时,该命令比上一条更好用

nmap -PN (No ping)    跳过主机发现阶段
    工欲善其事必先利其器!记录了一些常用工具的使用方法和经验技巧。