当前位置:网站首页>Go微服务(二)——Protobuf详细入门
Go微服务(二)——Protobuf详细入门
2022-07-04 17:30:00 【用脑袋装水】
篇幅可能较长,可以先收藏,方便后续观看。
Protobuf入门
1. 为什么选择Protobuf
Protobuf是Protocol Buffers的简称,它是Google公司开发的⼀种数据描述语⾔,并于2008年对外开 源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语⾔,通过附带⼯具⽣成代码并实现将结 构化数据序列化的功能。但是我们更关注的是Protobuf作为接⼝规范的描述语⾔,可以作为设计安全的 跨语⾔PRC接⼝的基础⼯具。
什么是Protobuf?
- Protobuf是Protocol Buffers的简称。
- Google 2008年开源的一种数据描述语言。
- 接口规范的描述语言
- 附带工具生成代码 并实现 将结构化数据 序列化的功能。
- 设计安全的跨语言RPC接口的基础工具
Protobuf的优势:
- 编解码效率
- 高压缩比
- 多语言支持
2. Go 语言Protobuf开发环境搭建
Protobuf 编译器 :
Protobuf的编译器叫做:protoc(protobuf compiler)
Golang安装使用Protobuf:
1. 下载Protobuf的编译器
下载protobuf的编译器,点击前往:
解压,解压位置看你自己,解压后将
/bin/protoc.exe
添加到环境变量里测试:
cmd 输入
protoc --version
2. 安装go protocol buffers的插件 protoc-gen-go
Protobuf核⼼的⼯具集是C++语⾔开发的,在官⽅的protoc编译器中并不⽀持Go语⾔。要想基于 .proto⽂件⽣成相应的Go代码,需要安装相应的插件。
cmd输入:
go install google.golang.org/protobuf/cmd/[email protected]
这个插件会自动下载到你的go path的bin目录下。(编译器会自动到这里找这个插件)
3. 实战测试:
创建 myProto.proto
文件
syntax = "proto3";
package main;
option go_package = "./";
message String {
string name = 1;
int64 age = 2;
}
注意:如果在上面
protoc --version
成功,而这里提示无法识别protoc命令的话,重启一下goland就好了。
3. Protobuf基本用法
首先看下下面这个proto文件,我们后面的proto基本用法都是基于这个proto进行讲解
syntax = "proto3";
package pkgName;
option go_package = "./";
message mmData {
optional int32 num = 1;
optional int32 def_num = 2 [default=10];
required string str = 3;
repeated string rep_str = 4;
}
0. 使用protoc 生成go文件
(可看完·3. Protobuf的基本用法·,在来看这部分)
---my_project
|---06-protocol_buffers
|---pbrpc
|---service
|---service.proto
pbrpc/service/service.proto
:
syntax = "proto3";
package hello;
// go module = MicroServiceStudy01
option go_package = "MicroServiceStudy01/06-protocol_buffers/pbrpc/service";
message Request{
string value = 1;
}
使用protoc:
cd 06-protocol_buffers/pbrpc/service
protoc -I . --go_out=. hello.proto
如果这样执行的话,他的结果是在你go_out目录(这里是当前目录)存放,并且按照你定义的go_package的名称,在你go_out目录下创建一个目录结构:
如果你不想让他帮你生成一个go_package的目录结构,那么就需要指定一个前缀:
protoc -I . --go_out=. --go_opt=module="MicroServiceStudy01/06-protocol_buffers/pbrpc/service" hello.proto
这样就没有再根据go_pacakage生成目录结构,而是直接存放在了go_out目录:
我不理解,如果目的是存放在当前目录,为什么不把
go_package="./"
,如果想存放在当前目录下的子目录,就go_package=“./subpkg “
,上面这种做法,我无法理解,暂时就当做学了个参数用法吧,有大佬明白的可以留言。
--go_out=./
:proto-gen-go插件编译产物的存放目录,这里是存放到当前目录,注意生成 的.pb.go
文件的最终位置是你的--go_out=?
位置+go_package=?
位置,后者是在--go_out
位置之后,进一步指定生成的.pb.go
文件的存放路径。-I ../
:--proto_path=PATH
的缩写表示引入文件的目录路径,这里有坑。(这里如果看不懂,看到下面的import就明白了)
-I
参数简单来说,就是如果多个proto文件之间有互相依赖,生成某个proto文件时,需要import其他几个proto文件,这时候就要用-I
来指定搜索目录。如果没有指定-I
参数,则在当前目录进行搜索。(这里的例子命令便是)每个
-I
参数都引入一个目录,proto文件中引入了几个外部proto文件理论来说就需要多少个-I
(同一目录的可以一次性引入),再加上待编译的proto也需要引入,所以上面这里就用了两个-I
来引入目录文件。--go_opt=moudle=....
:protoc—gen-go插件的opt参数,采用go moudle模式.hello.proto
:proto文件路径。
1. syntax
表明使用proto3语法;如果你没有指定这个,编译器会使用proto2语法;这个指定语法行必须是文件的非空非注释的第一个行
2. 包(Package)
proto文件使用关键字package指定当前包名,类似于模块,定义proto包名,可以为.proto文件新增一个可选的package声明符作为生成语言的namespace,用来防止不同的消息类型有命名冲突.
3. 选项(Options)
在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
在消息定义之前,可以通过option来进行配置,常用的option:
option go_package = "path;name";
- path 表示生成的go文件的存放地址,会自动生成目录的。
- name 表示生成的go文件所属的包名
4. 消息类型(message)
Protobuf中定义一个消息类型是通过关键字message字段指定的,这个关键字可以理解为Go语言的stuct关键字,用protobuf编译器将proto编译成Go代码之后,每个message都会生成一个名字与之对应的stuct结构体。
如上面的,就会生成一个名字为mmData
的结构体。
变量(字段)的定义格式为:
[修饰符(可选)][数据类型][变量名(字段名)] = [唯一标识符] ;
其中唯一标识符是用来标识字段的,同一个message中字段的标识符不能相同。
1. 字段规则(字段修饰符)
message
中的字段规则有三种。
required
: 字段属性为必填字段。若不设置,则会导致编解码异常,导致消息被丢弃。optional
: 字段属性为可选字段。发送方可以选择性根据需要进行设置;对于optional属性的字段,可以通过default关键字为字段设置默认值,即当发送方没有对该字段进行设置的时候,将使用默认值。
如果没有对字段设置默认值,就会根据特定的类型给字段赋予特定的默认值。
对于bool类型,默认值为false;对于string类型,默认值为空字符串;对于数值类型,默认值为0;对于枚举类型,默认值是枚举类型中的第一个值。
repeated
: 字段属性为可重复字段,该字段可以包含[0,n]个元素,字段中的元素顺序被保留。类似于go的切片。
注意:
- 在proto3版本中,字段规则上移除了required,并把optional字段改名为singular。所有没有指定字段规则的字段默认为optional,对于为什么删除了require规则,参考:为什么 proto3 移除了 required 和 optional?
- 在proto2版本中,默认配置下,一个optional没有被设置或者被显示的设置为默认值,在序列化二进制格式的时候,这个字段将会被去掉,导致反序列化之后,无法区分当初没有设置还是设置了默认值,即使使用hasXXX()方法,对于设置的默认值的字段,也是返回false。解决方法:区分 Protobuf 中缺失值和默认值
2. 标识号(唯一标识符)
在消息体的定义中,每个字段都必须要有一个唯一的标识号。
这些标识号是用来在消息的二进制格式中识别各个字段的,一旦使用就不能再改变,否则会导致原有消息编解码出现异常。
标识号是[0,2^29 - 1]范围内的一个整数,其中**[19000,19999)之间的标识号在protobuf协议的实现中被预留了**,所以特写注意不要使用这个范围内的标识号,若使用进行编译的时候也会告警:
Field numbers 19000 through 19999 are reserved for the protocol buffer library implementation.
注意:
[1,15]内的标识号在编码的时候占用一个字节,[16,2047]之内的标识符占用两个字节,所以尽量为频繁使用的字段分配[1,15]内的标识号,另外预留出来一部分给未来可能频繁使用的字段。
3. 数据类型
3.1 基本数据类型
关于字段的默认值:
string类型的变量,默认值是空字符串
bytes类型的变量,默认值是空byte数组
bool类型的变量,默认值是false
数字类型的变量,默认值是0
枚举类型的变量,默认值是第一个枚举值,而且这个第一个枚举值的数字值必须是0
3.2 枚举类型
字段类型除了上述基本的字段类型之外,也可以是枚举类型。
syntax = "proto3";
package main;
option go_package = "./";
// 定义枚举类型
enum DayName {
Sun = 0;
Mon = 1;
Tues = 2;
Wed = 3;
Thur = 4;
Fri = 5;
Sat = 6;
}
message workDay {
// 消息类型使用枚举类型
optional DayName day = 1;
}
protoc --go_out=./ hello.proto
生成的go文件里对应为const:
// 定义枚举类型
type DayName int32
const (
DayName_Sun DayName = 0
DayName_Mon DayName = 1
DayName_Tues DayName = 2
DayName_Wed DayName = 3
DayName_Thur DayName = 4
DayName_Fri DayName = 5
DayName_Sat DayName = 6
)
....
type WorkDay struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// 消息类型使用枚举类型
Day *DayName `protobuf:"varint,1,opt,name=day,proto3,enum=main.DayName,oneof" json:"day,omitempty"`
}
枚举常量的值必须在32位整数范围内,因为enum值是使用可编码方式存储的,对负数存储不够高效,因此不推荐在enum中使用负数。
枚举类型可以定义在message内,也可以定义在message外,若定义在message内,其他message要使用则需要通过messageType.enumType来进行引用。
默认情况下,枚举类型中的字段值不可重复,但是通过对enum添加option allow_alias = true;
来达到对同一个枚举值起一个别名的目的,若不添加allow_alise并且有重复的枚举值编译的时候会报错。
syntax = "proto3";
package pkgName;
option go_package = "./";
// 定义枚举类型
enum DayName {
// 若不添加该option,会报错:
// "pkgName.Test" uses the same enum value as "pkgName.Sat".
// If this is intended, set 'option allow_alias = true;' to the enum definition.
option allow_alias = true;
Sun = 0;
Mon = 1;
Tues = 2;
Wed = 3;
Thur = 4;
Fri = 5;
Sat = 6;
Test = 6; // Test与Sat字段值重名
}
3.3 map数据类型
除了上述类型之外,message还支持map<Type,Type>类型。
syntax = "proto3";
package pkgName;
option go_package = "./";
message TData {
map<int32, string> data = 1;
}
在生成的go文件对应map类型:
type TData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Data map[int32]string `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
注意:
- protobuf中的map实质上是无序的
- proto中map类型不能用optional/required/repeated任何类型修饰。
3.4 message类型
protobuf允许将其他消息类型用作字段类型。
如下面userData中存在一个workDay类型的数据:
syntax = "proto3";
package pkgName;
option go_package = "./";
message workDay {
int day = 1;
}
message userData {
workDay userDays = 1;
}
3.5 嵌套消息类型
message可以无限嵌套
syntax = "proto3";
package pkgName;
option go_package = "./";
message OuterData1 {
// 嵌套消息定义
message TData {
int32 a = 1;
}
// 引用嵌套消息
TData data1 = 1;
OuterData2.TData data2 = 2;
}
message OuterData2 {
// 嵌套消息定义
message TData {
int32 a = 1;
}
}
在生成的hello.pb.go
中对应:
type OuterData1 struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// 引用嵌套消息
Data1 *OuterData1_TData `protobuf:"bytes,1,opt,name=data1,proto3" json:"data1,omitempty"`
Data2 *OuterData2_TData `protobuf:"bytes,2,opt,name=data2,proto3" json:"data2,omitempty"`
}
....
type OuterData2 struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
3.6 Any字段(没看懂)
syntax = "proto3";
import "google/protobuf/any.proto";
package pkgName;
option go_package = "./";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 21;
}
3.7 oneof 字段
如果你的 message 包含许多可选字段,并且最多只能同时设置其中一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。
Oneof 共享内存中的所有字段,并且最多只能同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。你可以使用特殊的 case() 或 WhichOneof() 方法检查 oneof 字段中当前是哪个值(如果有)被设置,具体方法取决于你选择的语言。
使用案例:
要在 .proto 中定义 oneof,请使用 oneof 关键字,后跟你的 oneof 名称,在本例中为 test_oneof:
syntax = "proto3";
import "google/protobuf/any.proto";
package pkgName;
option go_package = "./";
message SampleMessage {
oneof test_oneof {
string name = 1;
string nike_name = 2;
}
}
然后,将 oneof 字段添加到test_oneof的定义中。
你可以在test_oneof添加任何类型的字段,但不能使用 required,optional 或 repeated 关键字。如果需要向 oneof 添加重复字段,可以使用包含重复字段的 message。
在生成的代码中,oneof 字段与常规 optional 方法具有相同的 getter 和 setter。你还可以使用特殊方法检查 oneof 中的值(如果有)。
在生成的hello.pb.go
中为:
type SampleMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to TestOneof:
// *SampleMessage_Name
// *SampleMessage_NikeName
TestOneof isSampleMessage_TestOneof `protobuf_oneof:"test_oneof"`
}
....
type SampleMessage_Name struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3,oneof"`
}
type SampleMessage_NikeName struct {
NikeName string `protobuf:"bytes,2,opt,name=nike_name,json=nikeName,proto3,oneof"`
}
....
5. 定义服务(service)
如果要将 message 类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将以你选择的语言生成服务接口和stub(桩)。
因此,例如,如果要定义一个 RPC 服务,其中包含一个根据 SearchRequest 返回 SearchResponse 的方法,可以在 .proto
文件中定义它,如下所示:
syntax = "proto3";
package pkgName;
option go_package = "./";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
string result = 1;
}
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
与 ProtoBuf 直接搭配使用的 RPC 系统是 gRPC :一个 Google 开发的平台无关语言无关的开源 RPC 系统。gRPC 和 ProtoBuf 能够非常完美的配合,你可以使用专门的 ProtoBuf 编译插件直接从.proto 文件生成相关 RPC 代码。
6. import导入其他proto文件
import
我们可以通过import导入其他proto文件,并使用该proto文件中的定义的消息类型。
---my_project
|---protocol
|---aaa
| |---aaa.proto
|---bbb
|---bbb.proto
aaa/aaa.proto
syntax = "proto3";
package aaa;
option go_package = "./";
message Something {
string msg = 1;
}
bbb/bbb.proto
syntax = "proto3";
package bbb;
option go_package = "./";
import "aaa/aaa.proto";
message Something2 {
aaa.Something something = 1;
}
虽然会报红但是不用管,生成 pb.go 的时候,假设当前在 my_project/protocol/bbb 目录下,则执行:
protoc -I ../ -I ./ --go_out=./ bbb.proto
# -I ../ : 在上一层目录中寻找引入的proto文件
# -I ./ : 在本层文件中找待编译的proto文件(顺序无所谓)
protoc有一个参数
-I
,表示引入文件的目录路径,这里有坑。
-I
参数简单来说,就是如果多个proto文件之间有互相依赖,生成某个proto文件时,需要import其他几个proto文件,这时候就要用-I
来指定搜索目录。如果没有指定-I
参数,则在当前目录进行搜索。每个
-I
参数都引入一个目录,proto文件中引入了几个外部proto文件理论来说就需要多少个-I
(同一目录的可以一次性引入),再加上待编译的proto也需要引入,所以上面这里就用了两个-I
来引入目录文件。
这样 protoc 可以在 -I path + import path => “./…/aaa/aaa.proto”
路径下找到 aaa.proto 这个文件。
# 当然也可以 import “aaa.proto”,-I=./…/aaa,同样可以执行成功。
protoc -I ../aaa -I ./ --go_out=./ bbb.proto
import public
默认情况下,proto只允许引用直接import的文件中定义的数据类型。
如b.proto中导入了a.proto,c.proto中导入了b.proto;默认情况下,c.proto中只能引用b.proto中定义的数据类型,而引用不到a.proto中的数据类型。若c.proto要使用a.proto中定义的数据类型,则b.proto引用a.proto的时候要使用import public。
---my_project
|---protocol
|---aaa
| |---aaa.proto
|---bbb
|---bbb.proto
|---ccc
|---ccc.proto
aaa/aaa.proto
syntax = "proto3";
package aaa;
option go_package = "./";
message Something {
string msg = 1;
}
bbb/bbb.proto
syntax = "proto3";
package bbb;
option go_package = "./";
// import "aaa/aaa.proto"; 不加会报错
import public "aaa/aaa.proto";
message Something2 {
aaa.Something something = 1;
}
ccc/ccc.proto
syntax = "proto3";
package ccc;
option go_package = "./";
import "bbb/bbb.proto";
message Something3 {
aaa.Something something = 1;
}
执行:
protoc -I ../ -I ../ -I ./ --go_out=./ ccc.proto
这种用法在迁移proto文件到新的位置的时候十分有用,如Message类要从old.proto迁移到new.proto文件中,这个时候如果要在不修改对old.proto的文件的情况下,直接将Message移动到new.proto中,然后在old.proto中import public new.proto即可。
7. 更新Message消息类型原则
为了达到前后消息类型兼容的目的,扩展Message消息类型的时候需要注意一下几点:
- 不要更改任何已有的字段的数值标识。
- 所添加的字段属性必须是optional 或者repeated类型,如果扩展required类型,会导致旧的消息解析异常
- 非required字段可以移除。要保证它们的标示在新的消息类型中不再使用
- 一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。
- int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
- sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
- string和bytes是兼容的——只要bytes是有效的UTF-8编码。
- 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
- fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
4. 基于Protobuf的RPC(可跳过这部分)
对于没有⽤过Protobuf的读者,建议先从官⽹了解下基本⽤法。这⾥我们尝试将Protobuf和RPC结合在 ⼀起使⽤,通过Protobuf来最终保证RPC的接⼝规范和安全。Protobuf中最基本的数据单元是 message,是类似Go语⾔中结构体的存在。在message中可以嵌套message或其它的基础数据类型的 成员。
定义RPC数据结构:
07-pbrpc/service/service.proto
syntax = "proto3";
package hello;
// go module = MicroServiceStudy01
option go_package = "MicroServiceStudy01/07-pbrpc/service";
message Request{
string value = 1;
}
message Response{
string value = 1;
}
生成go语言结构:
$ cd 07-pbrpc
$ protoc -I ./service --go_out=./service --go_opt=module="MicroServiceStudy01/07-pbrpc/service" service/service.prot
o
定义RPC接口:
基于 生成的数据结构,定义接口:
07-pbrpc/service/interface.go
package service
const HelloServiceName = "HelloService"
type HelloService interface {
// Hello
// 这里的 Request 和 Response 是基于protobuf生成的service.pb.go里的结构
Hello(request *Request, response *Response) error
}
这个接口时为了约束参数,详见2.更安全的RPC接口
向之前没有联合protobuf使用的时候,我们这里的接口方法的参数类型是我们自己写的结构体类型,而使用了protobuf之后,这里的参数类型就需要引用我们通过protobuf生成的.pb.go
文件里的结构体类型。
我们定义的接口要放在一个 独立的文件里类似于当前的service
包,他就相当于一个契约包,用来 约束服务端server(提供RPC服务)和我们的客户端client(调用RPC服务)。
定义服务端:
07-pbrpc/server/server.go
type HelloService struct{
}
func (hs *HelloService) Hello(req *service.Request, resp *service.Response) error {
resp.Value = "hello:" + req.Value
return nil
}
// 通过接口约束 Server 端
var _ service.HelloService = (*HelloService)(nil)
func main() {
rpc.RegisterName(service.HelloServiceName, new(HelloService))
listen, err := net.Listen("tcp", ":1234")
if err != nil {
log.Fatal("Listen TCP err:", err)
}
for {
conn, err := listen.Accept()
if err != nil {
log.Fatal("Accept err:", err)
}
// 这里使用的还是json,先忽略 往下看
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
定义客户端:
07-pbrpc/client/client.go
type HelloServiceClient struct {
*rpc.Client
}
func (hsc HelloServiceClient) Hello(req *service.Request, resp *service.Response) error {
return hsc.Client.Call(service.HelloServiceName+".Hello", req, resp)
}
// 通过接口约束 Client 端
var _ service.HelloService = (*HelloServiceClient)(nil)
func DialHelloService(network, address string) (*HelloServiceClient, error) {
conn, err := net.Dial(network, address)
if err != nil {
log.Fatal("net.Dail err: ", err)
}
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
return &HelloServiceClient{
client}, nil
}
func main() {
client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
log.Fatal("Dial err: ", err)
}
resp := &service.Response{
}
err = client.Hello(&service.Request{
Value: "world"}, resp)
if err != nil {
log.Fatal(err)
}
fmt.Println(resp)
}
此时我们只是Hello方法的参数使用的是protobuf生成的service.pb.go中的结构体,但是其他逻辑依然没有改变,使用的还是json-rpc
,所以这里会发现,我们这次虽然定义了相关的protobuf,但是我们和protobuf还没有半毛钱关系,只是用到了他为我们生成的结构体;
那么我们如何将json编码换成protobuf编码呢?
将07-pbrpc/server/server.go
里的go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
修改成go rpc.ServeCodec(server.NewServerCodec(conn))
即可。
这是我们4.基于Protobuf的RPC
的重点,官方的net/rpc包里是没有protoc的插件
我看的视频的发布者仿照net/rpc/jsonrpc
自己写了个关于Proto Codec 编解码的包,但是视频中没有放出来,而这里的NewServerCodec
就用到了那个包里的方法,大家不用深究,逻辑就是这么个逻辑,重在理解。
边栏推荐
- 使用SSH
- Behind the ultra clear image quality of NBA Live Broadcast: an in-depth interpretation of Alibaba cloud video cloud "narrowband HD 2.0" technology
- Scala basic tutorial -- 19 -- actor
- [mathematical modeling of graduate students in Jiangxi Province in 2022] analysis and code implementation of haze removal by nucleation of water vapor supersaturation
- 如何使用 wget 和 curl 下载文件
- 【uniapp】uniapp开发app在线预览pdf文件
- 一种将Tree-LSTM的强化学习用于连接顺序选择的方法
- [go language question brushing chapter] go conclusion chapter | introduction to functions, structures, interfaces, and errors
- File processing examples of fopen, FREAD, fwrite, fseek
- My colleagues quietly told me that flying Book notification can still play like this
猜你喜欢
删除二叉搜索树中的节点附图详解
Wireshark packet capturing TLS protocol bar displays version inconsistency
MXNet对GoogLeNet的实现(并行连结网络)
Li Kou brush question diary /day7/6.30
1、 Introduction to C language
[发布] 一个测试 WebService 和数据库连接的工具 - DBTest v1.0
提升复杂场景三维重建精度 | 基于PaddleSeg分割无人机遥感影像
力扣刷题日记/day7/2022.6.29
My colleagues quietly told me that flying Book notification can still play like this
基于NCF的多模块协同实例
随机推荐
ByteDance dev better technology salon was successfully held, and we joined hands with Huatai to share our experience in improving the efficiency of web research and development
Wanghongru research group of Institute of genomics, Chinese Academy of Agricultural Sciences is cordially invited to join
完善的js事件委托
IBM WebSphere MQ检索邮件
【2022年江西省研究生数学建模】水汽过饱和的核化除霾 思路分析及代码实现
Scala basic tutorial -- 12 -- Reading and writing data
Send and receive IBM WebSphere MQ messages
. Net ORM framework hisql practice - Chapter 2 - using hisql to realize menu management (add, delete, modify and check)
Scala基础教程--12--读写数据
【OpenCV入门到精通之九】OpenCV之视频截取、图片与视频互转
Halcon模板匹配
6.26CF模拟赛E:价格最大化题解
力扣刷題日記/day6/6.28
Scala basic tutorial -- 13 -- advanced function
信息学奥赛一本通 1336:【例3-1】找树根和孩子
.NET ORM框架HiSql实战-第二章-使用Hisql实现菜单管理(增删改查)
神经网络物联网应用技术学什么
6.26CF模拟赛B:数组缩减题解
Wireshark packet capturing TLS protocol bar displays version inconsistency
TorchDrug教程