go-micro教程 — 第二章 go-micro v3 使用Gin、Etcd
go-micro教程 — 第二章 go-micro v3 使用Gin、Etcd
- 前言
- 一、启动Etcd集群
- 二、创建项目并安装相关依赖
- 2.1 创建项目
- 2.2 初始化项目
- 2.3 安装 proto
- 2.4 安装 protoc-gen-go
- 2.5 安装 protoc-gen-micro
- 2.6 安装micro v3 构建工具
- 2.7 安装gin
- 三、开发项目
- 3.1 创建`web`模块
- 3.2 进入`web`文件夹
- 3.3 创建services下的子模块
- 3.4 删除go.mod文件
- 3.5 根据proto生成pb文件
- 3.5.1 修改 `test.proto`
- 3.5.2 生成pb文件
- 3.6 修改handler/test.go文件
- 3.7 修改services/test/main.go文件
- ※3.8 通过web模块直接调用services下的test服务
- 3.8.1 创建`web/handler/testHandler.go`文件
- 3.8.2 修改web/main.go文件
- 四、使用consul作为注册中心注册服务
- 4.1 安装配置consul
- 4.2 项目加入consul包
- 4.3 修改services/test/main.go
- 4.4 修改web/handler/testHandler.go
- 4.5 验证
- 4.6 关停 Consul
- 五、使用etcd作为注册中心
- 5.1 项目加入etcd包
- 5.2 修改services/test/main.go
- 5.3 修改web/handler/testHandler.go
- 5.4 验证
- 5.5 关停 Etcd
- 5.6 查看服务注册情况
- ※六、go-micro v3服务注册发现源码探究
前言
注意
:本文使用的Go版本为 go1.17.6
使用 1.18.x
版本或其他版本在操作时总是碰到各种问题,比如依赖下载异常、版本不一致等问题。当然也可能是我电脑的问题。
参考文档:go 微服务之go-micro v3+gin
本文代码:https://gitee.com/XiMuQi/go-micro-demo,对三、四、五步的操作均打了标签。
一、启动Etcd集群
在使用Etcd作为注册中心前需要先有Etcd节点或者Etcd集群,Etcd集群的安装配置及启动,详见:Etcd教程 — 第四章 Etcd集群安全配置。
二、创建项目并安装相关依赖
注意
:2.3到 2.6 步骤执行完同时会在 ${GOPATH}\bin
下生成exe文件。
2.1 创建项目
本文创建的项目名称为 go-micro-demo
2.2 初始化项目
go mod init go-micro-demo
2.3 安装 proto
见:Go — 相关依赖对应的exe 1、protobuf
2.4 安装 protoc-gen-go
go get github.com/golang/protobuf/protoc-gen-go
2.5 安装 protoc-gen-micro
注意
:是安装 asim
下的而不是micro
下的,因为micro
下的始终下载不了,这个也是go micro 3.0 框架。
go get github.com/asim/go-micro/cmd/protoc-gen-micro/v3
2.6 安装micro v3 构建工具
- 需要用到Micro 3.0 的micro工具,主要是用于快速构建micro项目,但是不使用这个的配置,用下面2的
go install github.com/micro/micro/v3@latest
- 下载go micro 3.0 库,下面库没有上面micro构建工具
go get github.com/asim/go-micro/v3
2.7 安装gin
go get -u github.com/gin-gonic/gin
三、开发项目
3.1 创建web
模块
在 go-micro-demo
下创建web
文件夹
3.2 进入web
文件夹
创建main.go
文件
package mainimport ("fmt""github.com/gin-gonic/gin""net/http"
)const addr = ":9000"func Index(c *gin.Context) {c.JSON(http.StatusOK, map[string]interface{}{"message": "Gin访问成功",})
}func main() {r := gin.Default()r.Handle("GET", "/", Index)if err := r.Run(addr); err != nil {fmt.Println("err")}
}
然后执行 go run .
或者在Goland中执行main函数,启动web服务。
启动成功后,在浏览器访问 http://127.0.0.1:9000, 得到如下响应:
// 20220704141435
// http://127.0.0.1:9000/{"message": "Gin访问成功"
}
3.3 创建services下的子模块
在 go-micro-demo\services
下执行micro new test
命令,创建后端测试
服务模块。
H:\Goland\go-micro-demo\services>micro new test
Creating service test.
├── micro.mu
├── main.go
├── generate.go
├── handler
│ └── test.go
├── proto
│ └── test.proto
├── Dockerfile
├── Makefile
├── README.md
├── .gitignore
└── go.moddownload protoc zip packages (protoc-$VERSION-$PLATFORM.zip) and install:visit https://github.com/protocolbuffers/protobuf/releasescompile the proto file test.proto:cd test
make init
go mod vendor
make proto
3.4 删除go.mod文件
删除services\test
模块下的go.mod
文件,统一使用go-micro-demo
下的go.mod
。
3.5 根据proto生成pb文件
3.5.1 修改 test.proto
主要是修改go_package
指定生成pb文件的路经,这里是将 ./改为 ../
即可。
syntax = "proto3";package test;//option go_package = "./proto;test";
//将 ./改为 ../
option go_package = "../proto;test";service Test {rpc Call(Request) returns (Response) {}rpc Stream(StreamingRequest) returns (stream StreamingResponse) {}rpc PingPong(stream Ping) returns (stream Pong) {}
}message Message {string say = 1;
}message Request {string name = 1;
}message Response {string msg = 1;
}message StreamingRequest {int64 count = 1;
}message StreamingResponse {int64 count = 1;
}message Ping {int64 stroke = 1;
}message Pong {int64 stroke = 1;
}
3.5.2 生成pb文件
进入到 go-micro-demo\services\test\proto
。执行生成命令:
protoc --proto_path=. --micro_out=. --go_out=. *.proto
执行完后可以在go-micro-demo\services\test\proto
看到生成的test.pb.go
和test.pb.micro.go
文件。
3.6 修改handler/test.go文件
修改 micro-demo\services\test\handler
下的 test.go
主要是修改引入的 pb文件位置。
import ("context"log "github.com/micro/micro/v3/service/logger"test "go-micro-demo/services/test/proto"//test "test/proto"
)
3.7 修改services/test/main.go文件
- 修改引入的pb文件位置。
- 将原来
micro
下的包替换成asim
下的。
package mainimport (//修改 1"go-micro-demo/services/test/handler" //"test/handler"pb "go-micro-demo/services/test/proto" //pb "test/proto"//修改 2 替换micro为asimservice "github.com/asim/go-micro/v3" //"github.com/micro/micro/v3/service""github.com/asim/go-micro/v3/logger" //"github.com/micro/micro/v3/service/logger"
)func main() {//修改 3srv := service.NewService( // service.Newservice.Name("test"),service.Version("latest"),)// Register handler//修改 4_ = pb.RegisterTestHandler(srv.Server(), new(handler.Test))// Run serviceif err := srv.Run(); err != nil {logger.Fatal(err)}
}
然后执行 go run .
或者在Goland中执行main函数,启动名为test
的服务。
如果启动时报:
handler\test.go:6:2: no required module provides package github.com/micro/micro/v3/service/logger; to add it:go get github.com/micro/micro/v3/service/logger
执行go mod tidy
后重启。
启动成功后,控制台显示内容:
API server listening at: 127.0.0.1:58596
2022-07-04 14:50:44 file=v3@v3.7.1/service.go:206 level=info Starting [service] test
2022-07-04 14:50:44 file=server/rpc_server.go:820 level=info Transport [http] Listening on [::]:58603
2022-07-04 14:50:44 file=server/rpc_server.go:840 level=info Broker [http] Connected to 127.0.0.1:58604
2022-07-04 14:50:45 file=server/rpc_server.go:654 level=info Registry [mdns] Registering node: test-69072bf3-9123-4177-88cb-9d898a8219e5
※3.8 通过web模块直接调用services下的test服务
3.8.1 创建web/handler/testHandler.go
文件
package handlerimport ("github.com/asim/go-micro/v3""github.com/gin-gonic/gin"testpb "go-micro-demo/services/test/proto""net/http"
)func Index(c *gin.Context) {c.JSON(http.StatusOK, map[string]interface{}{"message": "index",})
}func ServiceOne(c *gin.Context) {service := micro.NewService()service.Init()// 创建微服务客户端client := testpb.NewTestService("test", service.Client())// 调用服务rsp, err := client.Call(c, &testpb.Request{Name: c.Query("key"),})if err != nil {c.JSON(200, gin.H{"code": 500, "msg": err.Error()})return}c.JSON(200, gin.H{"code": 200, "msg": rsp.Msg})
}
3.8.2 修改web/main.go文件
package mainimport ("fmt""github.com/gin-gonic/gin""go-micro-demo/web/handler""net/http"
)const addr = ":9000"func Index(c *gin.Context) {c.JSON(http.StatusOK, map[string]interface{}{"message": "Gin访问成功",})
}func main() {r := gin.Default()r.Handle("GET", "/", Index)r.Handle("GET", "/test-req", handler.ServiceOne)if err := r.Run(addr); err != nil {fmt.Println("err")}
}
然后执行 go run .
或者在Goland中执行main函数,重新启动web服务。
启动结果:
[GIN-debug] GET / --> main.Index (3 handlers)
[GIN-debug] GET /test-req --> go-micro-demo/web/handler.ServiceOne (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :9000
浏览器访问http://127.0.0.1:9000/test-req?key=哈哈
,得到如下响应:
// 20220704150031
// http://127.0.0.1:9000/test-req?key=%E5%93%88%E5%93%88{"code": 200,"msg": "Hello 哈哈"
}
四、使用consul作为注册中心注册服务
4.1 安装配置consul
安装配置 consul,详见 Consul安装。
启动consul后,使用浏览器访问http://192.168.1.222:8500
,如果能访问成功则说明consul正常。
4.2 项目加入consul包
go get -u github.com/asim/go-micro/plugins/registry/consul/v3
4.3 修改services/test/main.go
- 加入 consul包。
- 配置 consul 的地址及注册的名称。
- 修改 注册服务的方式为consul方式。
主要修改或加入的地方 consul 1
、consul 2
、consul 3
package mainimport (//修改 1"go-micro-demo/services/test/handler" //"test/handler"pb "go-micro-demo/services/test/proto" //pb "test/proto"//修改 2 替换micro为asimservice "github.com/asim/go-micro/v3" //"github.com/micro/micro/v3/service""github.com/asim/go-micro/v3/logger" //"github.com/micro/micro/v3/service/logger"//consul 1"github.com/asim/go-micro/plugins/registry/consul/v3""github.com/asim/go-micro/v3/registry"
)//consul 2
const (ServerName = "consul-test"ConsulAddr = "192.168.1.222:8500"
)func main() {// consul 3consulReg := consul.NewRegistry(registry.Addrs(ConsulAddr),)srv := service.NewService(service.Name(ServerName), // 服务名字service.Registry(consulReg),// 注册中心)// Register handler//修改 4_ = pb.RegisterTestHandler(srv.Server(), new(handler.Test))// Run serviceif err := srv.Run(); err != nil {logger.Fatal(err)}
}
如果引入的包报红,执行go mod tidy
即可。
执行go run .,重新启动services/test模块的名为consul-test的服务。然后访问Consul 的UI界面:http://192.168.1.222:8500,可以看到服务名称为consul-test
的节点已经注册到了consul注册中心
上。
4.4 修改web/handler/testHandler.go
主要修改或加入的地方 consul 1
、consul 2
、consul 3
package handlerimport ("github.com/asim/go-micro/v3""github.com/gin-gonic/gin"testpb "go-micro-demo/services/test/proto""net/http"//consul 1"github.com/asim/go-micro/plugins/registry/consul/v3""github.com/asim/go-micro/v3/registry"
)func Index(c *gin.Context) {c.JSON(http.StatusOK, map[string]interface{}{"message": "index",})
}func ServiceOne(c *gin.Context) {//consul 2consulReg := consul.NewRegistry(registry.Addrs("192.168.1.222:8500"),)service := micro.NewService(micro.Registry(consulReg), //设置注册中心)service.Init()//consul 3 创建微服务客户端【将服务名称从test改为为consul-test】client := testpb.NewTestService("consul-test", service.Client())// 调用服务rsp, err := client.Call(c, &testpb.Request{Name: c.Query("key"),})if err != nil {c.JSON(200, gin.H{"code": 500, "msg": err.Error()})return}c.JSON(200, gin.H{"code": 200, "msg": rsp.Msg})
}
执行go run .,重启web。
4.5 验证
浏览器访问http://127.0.0.1:9000/test-req?key=哈哈
,得到如下响应:
// 20220704212337
// http://127.0.0.1:9000/test-req?key=%E5%93%88%E5%93%88{"code": 200,"msg": "Hello 哈哈"
}
4.6 关停 Consul
如果关停Consul服务,再重新访问http://127.0.0.1:9000/test-req?key=哈哈
,会得到如下响应:
// 20220704212448
// http://127.0.0.1:9000/test-req?key=%E5%93%88%E5%93%88{"code": 500,"msg": "{\"id\":\"go.micro.client\",\"code\":500,\"detail\":\"error selecting consul-test node: Get \\\"http: //192.168.1.222: 8500/v1/health/service/consul-test?stale=\\\": dial tcp 192.168.1.222:8500: connectex: No connection could be made because the target machine actively refused it.\",\"status\":\"Internal Server Error\"}"
}
客户端和服务端的控制台同样显示如下信息,等到Consul恢复正常即会停止报错:
2022-07-04T21:26:03.837+0800 [ERROR] watch: Watch errored: type=services error="Get "http://192.168.1.222:8500/v1/catalog/services": dial tcp 192.168.1.222:8500: connectex: No conn
ection could be made because the target machine actively refused it." retry=1m20s
2022-07-04T21:26:54.163+0800 [ERROR] watch: Watch errored: type=service error="Get "http://192.168.1.222:8500/v1/health/service/consul-test": dial tcp 192.168.1.222:8500: connectex
: No connection could be made because the target machine actively refused it." retry=2m5s
2022-07-04T21:27:25.895+0800 [ERROR] watch: Watch errored: type=services error="Get "http://192.168.1.222:8500/v1/catalog/services": dial tcp 192.168.1.222:8500: connectex: No conn
ection could be made because the target machine actively refused it." retry=2m5s
五、使用etcd作为注册中心
5.1 项目加入etcd包
go get -u "github.com/asim/go-micro/plugins/registry/etcd/v3"
5.2 修改services/test/main.go
将consul
替换成etcd
。
- 加入
etcd
包。 - 配置
etcd
的地址及注册的名称。 - 修改注册服务的方式为
etcd
方式。
主要修改或加入的地方 etcd 1
、etcd 2
、etcd 3
package mainimport (//修改 1"go-micro-demo/services/test/handler" //"test/handler"pb "go-micro-demo/services/test/proto" //pb "test/proto"//修改 2 替换micro为asimservice "github.com/asim/go-micro/v3" //"github.com/micro/micro/v3/service""github.com/asim/go-micro/v3/logger" //"github.com/micro/micro/v3/service/logger"//etcd 1"github.com/asim/go-micro/plugins/registry/etcd/v3""github.com/asim/go-micro/v3/registry"
)//etcd 2
const (ServerName = "etcd-test"EtcdAddr = "192.168.1.221:2379"
)
func main() {//etcd 3etcdReg := etcd.NewRegistry(registry.Addrs(EtcdAddr),)srv := service.NewService(service.Name(ServerName), // 服务名字service.Registry(etcdReg), // 注册中心)// Register handler//修改 4_ = pb.RegisterTestHandler(srv.Server(), new(handler.Test))// Run serviceif err := srv.Run(); err != nil {logger.Fatal(err)}
}
5.3 修改web/handler/testHandler.go
package handlerimport ("github.com/asim/go-micro/plugins/registry/consul/v3""github.com/asim/go-micro/v3""github.com/gin-gonic/gin"testpb "go-micro-demo/services/test/proto""net/http"//etcd 1"github.com/asim/go-micro/plugins/registry/etcd/v3""github.com/asim/go-micro/v3/registry"
)func Index(c *gin.Context) {c.JSON(http.StatusOK, map[string]interface{}{"message": "index",})
}func ServiceOne(c *gin.Context) {// etcd 2etcdReg := etcd.NewRegistry(registry.Addrs("192.168.1.221:2379"),)service := micro.NewService(micro.Registry(etcdReg), //设置注册中心)service.Init()//etcd 3 创建微服务客户端【将服务名称从consul-test改为etcd-test】client := testpb.NewTestService("etcd-test", service.Client())// 调用服务rsp, err := client.Call(c, &testpb.Request{Name: c.Query("key"),})if err != nil {c.JSON(200, gin.H{"code": 500, "msg": err.Error()})return}c.JSON(200, gin.H{"code": 200, "msg": rsp.Msg})
}
执行go run .,重启web。
5.4 验证
浏览器端发送 http://127.0.0.1:9000/test-req?key=哈哈
,返回结果:
// 20220704215208
// http://127.0.0.1:9000/test-req?key=%E5%93%88%E5%93%88{"code": 200,"msg": "Hello 哈哈"
}
5.5 关停 Etcd
如果关停Etcd服务,再重新访问 http://127.0.0.1:9000/test-req?key=哈哈
,会得到如下响应:
// 20220704215457
// http://127.0.0.1:9000/test-req?key=%E5%93%88%E5%93%88{"code": 500,"msg": "{\"id\":\"go.micro.client\",\"code\":500,\"detail\":\"error selecting etcd-test node: context deadline exceeded\",\"status\":\"Internal Server Error\"}"
}
客户端的控制台同样显示如下信息:
{"level":"warn","ts":"2022-07-04T21:54:57.129+0800","logger":"etcd-client","caller":"v3@v3.5.0/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"etcd-endp
oints://0xc000024c40/#initially=[192.168.1.221:2379]","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: last connection error: connection error:desc = \"transport: Error while dialing dial tcp 192.168.1.221:2379: connectex: No connection could be made because the target machine actively refused it.\""}
注意
:go-micro需要等到Etcd恢复正常后重启相关服务才能正常使用。
5.6 查看服务注册情况
Etcd虽然没有和Consul一样的UI界面查看服务注册情况。但是可以通过查看指定前缀的所有键值对来查看go-micro的注册信息:
etcdctl get --prefix /micro
#或
etcdctl get --prefix /micro/registry
go-micro 注册时的节点ID信息:
/micro/registry/etcd-test/etcd-test-3bbd2f13-d24e-41dd-9b4e-e8f5b601a549
注意:后面的etcd-test-3bbd2f13-d24e-41dd-9b4e-e8f5b601a549
是和services/test/main.go
运行后在控制台显示的节点ID完全一致。
API server listening at: 127.0.0.1:58099
2022-07-04 23:11:48 file=v3@v3.7.1/service.go:206 level=info Starting [service] etcd-test
2022-07-04 23:11:48 file=server/rpc_server.go:820 level=info Transport [http] Listening on [::]:58104
2022-07-04 23:11:48 file=server/rpc_server.go:840 level=info Broker [http] Connected to 127.0.0.1:58105
2022-07-04 23:11:48 file=server/rpc_server.go:654 level=info Registry [etcd] Registering node: etcd-test-3bbd2f13-d24e-41dd-9b4e-e8
f5b601a549
其余注册信息:
/micro/registry/etcd-test/etcd-test-3bbd2f13-d24e-41dd-9b4e-e8f5b601a549
{"name":"etcd-test","version":"latest","metadata":null,"endpoints":[{"name":"Test.Call","request":{"name":"Request","ty pe":"Request","values":[{"name":"MessageState","type":"MessageState","values":[{"name":"NoUnkeyedLiterals","type":"NoUnkeyedLiterals","values":null},{"name":"DoNotCompare","type":"DoNotCompare","values":null},{"name":"DoNotCopy","type":"DoNotCopy","values":null},{"name":"MessageInfo","type":"MessageInfo","values":null}]},{"name":"int32","type":"int32","va lues":null},{"name":"unknownFields","type":"[]uint8","values":null},{"name":"name","type":"string","values":null}]},"re sponse":{"name":"Response","type":"Response","values":[{"name":"MessageState","type":"MessageState","values":[{"name":"NoUnkeyedLiterals","type":"NoUnkeyedLiterals","values":null},{"name":"DoNotCompare","type":"DoNotCompare","values":null},{"name":"DoNotCopy","type":"DoNotCopy","values":null},{"name":"MessageInfo","type":"MessageInfo","values":null}]},{"n ame":"int32","type":"int32","values":null},{"name":"unknownFields","type":"[]uint8","values":null},{"name":"msg","type":"string","values":null}]},"metadata":{}},{"name":"Test.PingPong","request":{"name":"Context","type":"Context","values":null},"response":{"name":"Stream","type":"Stream","values":null},"metadata":{"stream":"true"}},{"name":"Test.Stream"," request":{"name":"Context","type":"Context","values":null},"response":{"name":"Stream","type":"Stream","values":null}," metadata":{"stream":"true"}}],"nodes":[{"id":"etcd-test-3bbd2f13-d24e-41dd-9b4e-e8f5b601a549","address":"192.168.91.1:58104","metadata":{"broker":"http","protocol":"mucp","registry":"etcd","server":"mucp","transport":"http"}}]
}
※六、go-micro v3服务注册发现源码探究
参见:Go语言微服务实战之再探服务发现
go-micro教程 — 第二章 go-micro v3 使用Gin、Etcd相关推荐
- Etcd教程 — 第二章 Etcd集群静态发现
Etcd教程 - 第二章 Etcd集群静态发现 一.Etcd集群安装方式 二.Etcd集群静态发现 2.1 静态启动的方式 ※2.2 单机搭建Etcd集群 2.2.1 安装 goreman工具 2.2 ...
- javascript进阶教程第二章对象案例实战
javascript进阶教程第二章对象案例实战 一.学习任务 通过几个案例练习回顾学过的知识 通过案例练习补充几个之前没有见到或者虽然讲过单是讲的不仔细的知识点. 二.具体实例 温馨提示 面向对象的知 ...
- 乐行学院Redis5学习教程 第二章 redis5远程访问及工具Redis Desktop Manager使用
乐行学院Redis5学习教程 第二章 redis5远程访问工具Redis Desktop Manager 检查服务器端口和防火墙 Redis Desktop Manager介绍 Redis Deskt ...
- html第二章排列页面内容题目,HTML教程 第二章 页面布局及文字设计.doc
HTML教程 第二章 页面布局与文字设计 标题 一般文章都有标题.副标题.章和节等结构,HTML中也提供了相应的标题标签,其中n为标题的等HTML总共提供六个等级的标题,n越小,标题字号就越大,以下列 ...
- 软考 程序员教程-第二章 操作系统基础知识
软考 程序员教程-第二章 操作系统基础知识 第二章 操作系统基础知识 2.1.操作系统概述(第四版教程P44) 操作系统的4个特征:并发性.共享性.虚拟性.不确定性. 操作系统的5个功能:处理机管理. ...
- Cadence SIP Layout 简单教程-第二章
[从whp1920 网易博客迁移至CSDN] 第一章在正式布线之前做了必须做的准备工作,下面进入正题,打开Candence SIP RF Layout GXL软件. 第一节 导入外形尺寸 打开SIP设 ...
- 梯度下降法快速教程 | 第二章:冲量(momentum)的原理与Python实现
北京 | 深度学习与人工智能研修 12月23-24日 再设经典课程 重温深度学习阅读全文> 01 前言 梯度下降法(Gradient Descent)是机器学习中最常用的优化方法之一,常用来求解 ...
- 偏微分方程简明教程第二章部分答案
偏微分方程简明教程答案 第二章 二阶方程的特征理论与分类 习题2.1 2.1.2 习题2.2 2.2.1 2.2.3 第二章 二阶方程的特征理论与分类 习题2.1 2.1.2 2.求下列方程的特征线: ...
- 计算机原理简明教程第二章,《计算机原理简明教程》习题答案[参考].doc
<计算机原理简明教程>习题参考答案 第一章习题答案 1.1 答:是1946年在美国宾夕法尼亚大学诞生,称为ENIAC. 特点是由1800个电子管和1500个继电器组成,重30吨:功耗150 ...
最新文章
- opencv鼠标操作,画矩形
- Async/Await替代Promise的6个理由
- Call 从一个批处理程序调用另一个批处理程序,并且不终止父批处理程序。
- 【面试相关】python实现快速幂取余算法详解
- python中格式化字符串_Python中所有字符串格式化的指南
- 课外阅读(通讯技术的发展史)
- JavaScript密码复杂度
- 剖析Caffe源码之Blob
- 人脸识别使用base64的方式添加人脸
- C#三层架构通用数据库访问类SQLHerper总结
- 帝国CMS7.5二次元COS漫画分享漫展网站源码
- html编码写出滚动字幕,HTML滚动字幕代码及参数详解_html/css_WEB-ITnose
- 简单的自动化测试脚本
- 论现场跟客户演示软件产品
- 用PC机实现与ATV12变频器Modbus通信控制电机
- 如何开展业务是我在离开X网之后重新学的
- Webpack 搭建 Vue + ts + tsx
- 优柔但不寡断、柔弱绝不可欺、善良却不可骗、宽容而非懦弱
- Linux: 关于 SIGCHLD 的更多细节
- 华为c语言笔试形式,最新华为C语言笔试题目分享
热门文章
- php计算股票均线,移动平均线——Moving Average 平均线的计算公式
- python画鱼教程_Python Flask高级编程之从0到1开发《鱼书》精品项目 学习 教程??
- BZOJ4398: 福慧双修【二进制分组+最短路】
- c语言 数组 随机数 初始化
- java-接口之运动员教练员综合案例
- 「分布式架构」最终一致性:反熵
- Vue3 企业级网站建设
- wyt1210笔试、面试
- 二阶线性微分方程解的结构(齐次与非齐次)+ 常数变易法 | 高阶微分方程(二)
- 苹果新系统耗电过快怎么解决(解决方法)