
网游的老兵
能力说明:
具备数据库基础知识,了解数据库的分类,具备安装MySQL数据库的能力,掌握MySQL数据类型知识,基本了解常用SQL语句,对阿里云数据库产品有基本认知。
暂时未有相关云产品技术能力~
阿里云技能认证
详细说明Kratos微服务框架实现权鉴 - Zanzibar用户的权限管理对每个项目来说都至关重要。不同的业务场景决定了不同的权限管理需求,不同的技术栈也有不同的解决方案:如果你在写一个Ruby On Rails应用,那你可能会选择cancan;如果你在写一个Java Spring应用,那你可能会选择Spring Security 或者 Apache Shiro;如果你正在使用K8S,那你很可能需要与K8S的鉴权模块打交道。那如果你面对一个非常复杂的业务,需要实现极为灵活的权限配置,并且同时对接多个服务怎么办呢?谷歌的一致性全球授权系统Zanzibar可以帮到你。Google Zanzibar是谷歌2016年起上线的一致性全球授权系统。这套系统的主要功能是:储存来自各个服务的访问控制列表(Access Control Lists, ACLs),也就是所谓的权限(Permission)。根据储存的ACL,进行权限校验。这套系统上线后对接的Google服务有:Calendar、Cloud、Drive、Maps、Photos、YouTube等重要的服务。Google并没有对Zanzibar进行开源,只开放了论文。好在基于论文有一些优秀的开源实现,有两个开源实现值得推荐:Ory/KetoAuth0/OpenFGA为什么需要 Google Zanzibar?在 Zanzibar 论文中,谷歌列出了一些决定他们将从拥有权限服务中受益的原因:首先,作为一项服务,他们需要将代码重复和版本偏差的量降至最低。其次,谷歌拥有大量的应用程序和服务,他们经常需要检查一个应用程序在另一个应用程序中的资源之间的权限。例如,当您使用 Gmail 发送电子邮件时,它警告您收件人无法阅读电子邮件中链接的文档,这是有效的,因为 Gmail 正在询问 Zanzibar 关于链接的 Google 文档的权限。第三,谷歌在权限系统之上构建了通用基础设施,只有当您拥有全局一致的 API 来进行编程时,您才能做到这一点。最后,也是最重要的:鉴权很难。人们希望任何权鉴的实施都能够符合一些常见的要求。首先,它应该保证其正确性。有了权限,正确性就很容易定义了。所有授权用户都应该能够与受保护资源进行交互,并且不允许任何未经授权的用户与受保护资源进行交互。起初这似乎很容易,直到您开始考虑互联网应用所必须应对的挑战。诸如:网络延迟、节点故障和时钟同步之类的事情。其次,如果您打算对所有服务使用同一个权限系统,它应该合理地允许您对应用程序所需的所有不同类型的原语进行建模。在 Google 的案例中,他们至少具有以下权限模型:Docs 中的点对点共享、YouTube 中的公共/私人/非公开视频以及 Cloud IAM 中的 RBAC。Zanzibar 被设计得足够灵活,可以对不同类型的权限进行建模。通常来说,每一个请求都需要进行鉴权,并且没有收到肯定的权鉴成功消息的时候,必须被解释为拒绝,所以,您需要这个系统既快速又高度可靠。最后,由于谷歌的运营规模非常大,Zanzibar 还必须能够横向扩展,以应对每秒对数十亿用户和数万亿对象进行数百万次的权鉴操作。综合起来,这些需求几乎肯定只能通过某种大规模的分布式系统来解决。现在我们已经列出了一些要求,让我们来探索 Zanzibar 的 API 或程序员面临的经验。什么是Google Zanzibar?从开发人员的角度来看,Zanzibar 就是一个 API 而已,您可以将用户和数据关系托管给它,然后可以通过访问点做出快速、准确地权限决策。例如,当新用户注册时,您告知 Zanzibar。当该用户创建受保护的资源(例如文档、视频或银行帐户)时,您会告知 Zanzibar。当该用户与其他用户共享资源或创建相关资源时,您告知 Zanzibar。最后,在需要回答“X 是否允许 读取/写入/删除/更新 Y?”这个问题时,Zanzibar 已经具备了快速回答该问题所需的所有信息。Zanzibar 计算权限的方式比较新颖。应用程序开发人员写入服务的关系信息,用于构建用户、其他实体和资源之间关系的 有向图。一旦这个图可用,权限检查就变成了一个 图遍历问题。我们可以试图通过图表找到从请求的资源和关系(例如所有者、读者等)到用户(通常是发出请求的用户)的路径。通常,拥有一种关系暗示着同时拥有其他关系。例如,如果允许用户写入一段数据,则几乎(但不总是)意味着他们也可以读取相同的数据。为了减少必须存储的冗余信息量,Zanzibar 提供了一种称为关系重写(relationship rewrites)的机制,它描述了一种用于重新解释图当中某些边和关系的方法。重写的另一个例子是:“嵌套文档的文件夹的读者也应被视为文档的读者。” 。以这种方式消除冗余信息的过程,更正式地我们称之为:归一化。现在我们已经熟悉了 Zanzibar 的 API,让我们来看看 Zanzibar 是如何实现在大规模应用下实现低延迟的。Google Zanzibar 是如何实施的?因为鉴权服务需要不断被访问,并且处于服务请求的关键路径中,所以它必须要快。对于 Google 的 Zanzibar,对第20次和第99次的网络请求进行权限检查,他们延迟分别为 3 毫秒和 20 毫秒。同时为每秒来自世界各地的 2000 万个权限检查和读取请求提供服务。Zanzibar是如何实现如此低的延迟和高负载的?是通过运行 Zanzibar 服务的很多、很多的副本来实现高负载的:Zanzibar 将此负载分布在全球数十个集群中的 10,000 多台服务器上。每个集群的服务器数量从不到 100 台到超过 1,000 台不等,中位数接近 500 台。集群的大小与其地理区域的负载成比例。Zanzibar distributes this load across more than 10,000 servers organized in several dozen clusters around the world. The number of servers per cluster ranges from fewer than 100 to more than 1,000, with the median near 500. Clusters are sized in proportion to load in their geographic regions.全球分布是通过使用谷歌的全球数据库系统Spanner办到的。使用 Spanner,写入地球上任何地方的数据都可以立即使用,并且在外部保持一致。虽然它非常适合做权限系统的存储层,但这并不意味着存储在 Spanner 中的数据能够达到 Zanzibar 的延迟要求。F1(谷歌的另一项服务)从 Spanner 感知到的读取延迟中位数为 8.7 毫秒,标准差为 376.4 毫秒。Zanzibar 将经常需要多次往返于数据存储以计算单个权限检查。显然,如果没有一些严格的数据缓存,它不会达到99.9%的延迟只有20毫秒。Zanzibar 在服务的多个层级都有缓存。第一层缓存是服务级别。当服务收到它最近计算的权限检查请求时,并且结果仍然可以被认为是有效的(意味着计算它的时间不早于通过的 Zookie),可以直接地返回该值。这消除了到数据存储层的所有往返行程。服务级缓存是提高性能的有效方法,但以 Zanzibar 的运营规模来看,它本身并没有多大帮助。如果允许从任何缓存提供任何请求,流经Zanzibar的庞大数据量将导致非常低的命中率或过高的内存需求。为了提高命中率,Zanzibar 使用一致性哈希将请求(以及由此产生的缓存条目)分发到特定服务器。我们从中获得的第一个好处是缓存的命中率更高。如果我们期望特定类型的请求仅由 Zanzibar 的一小部分副本提供服务,则我们更有可能在缓存中拥有该值。第二个也是更微妙的改进是允许合并重复的请求,并且该值只计算一次并返回给所有调用者。在这种情况下,我们分摊后端数据存储往返于所有去重请求。Zanzibar 执行的服务器端缓存的最终形式是一种特定于 Google 用例的特殊非规范化。当工程师注意到组(如 Docs、Cloud IAM、产品组所使用的那样)通常是深度嵌套时,他们创建了一个名为 Leopard Indexing System 的服务。Leopard 保持内存中的传递闭包作为更高级别组的子组的所有组。默认情况下,Zanzibar 中的嵌套关系需要对支持 Spanner 数据库的多个串行请求,因为您需要加载直接子项才能计算它们的子项。通过将所有顶级和中间组的所有子组保存在内存中,Leopard 允许 Zanzibar 将所有嵌套组解析减少到对索引的单个调用。由于 Leopard 将数据存储在内存中,并作为独立于 Zanzibar 的服务运行,因此它使用本文第 2.4.3 节中的 watch API 来不断更新底层组结构数据的变化。Zanzibar 还使用了一个巧妙的技巧来减少尾部延迟:请求对冲。当 Zanzibar 检测到来自 Spanner 或 Leopard 的响应比平时花费的时间更长时,它会向其他一个或多个服务器发送完全相同数据的另一个请求,这些服务器有望会更快地响应。Google Zanzibar的概念与定义基于关系的访问控制 (ReBAC)Google Zanzibar所使用的授权模型是:基于关系的访问控制 (ReBAC)。基于关系的访问控制 (ReBAC) 定义了一种授权范例,其中主体访问资源的权限由这些主体与资源之间存在的关系来定义。通常,ReBAC 中的授权是通过遍历关系的有向图来执行的。该图的节点和边与资源描述框架 (RDF)数据格式中的三元组非常相似。ReBAC 系统允许关系的层次结构,有些系统允许更复杂的定义,包括关系上的代数运算符,例如并集、交集和差集。ReBAC 随着社交网络 Web 应用程序的兴起而流行起来,用户需要根据他们与数据接收者的关系而不是接收者的角色来控制他们的个人信息。与基于角色的访问控制 (RBAC)相比,它定义了角色,这些角色携带一组与其相关联的特定特权以及分配给哪些主题,ReBAC(如 ABAC)允许定义更细粒度的权限。例如,如果 ReBAC 系统定义了document类型的资源,它可以允许一个动作editor,如果系统包含关系('alice', 'editor', 'document:budget'),那么主题Alice可以编辑具体资源文件:预算. ReBAC 的缺点是,虽然它允许更细粒度的访问,但这意味着应用程序可能需要执行更多的授权检查。ReBAC 系统默认是拒绝的,并允许在它们之上构建 RBAC 系统。基于关系的访问控制 (ReBAC) 和 基于角色的访问控制 (RBAC) 本质上都是 基于属性的访问控制 (ABAC) 的一个子集关系元组(Relation Tuples)关系元组(Relation Tuples)是Zanzibar的核心概念。关系元组由:命名空间(Namespace),对象(Object),关系(Relation)和主题(Subject)/用户(User)组成。在关系被描述为关系元组,使用BNF语法描述,其形式如下:<tuple> ::= <object>'#'<relation>'@'<user> <object> ::= <namespace>':'<object_id> <user> ::= <user_id> | <userset> <userset> ::= <object>'#'<relation>这个定义不是容易理解的——让我们稍微分解一下。假定,有一个示例元组是issue:412#reporter@alice。在此:对象(Object) 是 issue:412。即,问题号 412。关系(Relation) 是 "reporter"。用户(User) 是 alice。总而言之,这个元组表示 Alice 是第 412 期的记者(reporter)。这个语法有点尴尬的部分是:user字段,它也可以是“userset(用户集)”。“userset(用户集)”,它是一组用户,即与某个对象有一定关系的所有用户。例如,team:eng#member将表示属于 eng 团队的所有用户的集合。使用它,可以写出repo:acme#maintainer@team:eng#member,即:“eng 团队的所有成员都是 Acme 存储库的维护者”。请注意,尝试从用户的角度来表达所有内容,这里存在一点差距。无法表示“acme 存储库是问题 412 的父级”。所以 Zanzibar 论文通过将它表示为issue:412#parent@repo:acme#来解决这个问题...。这里的问题是user必须是用户 ID,或者代表一组用户的东西。但我们的关系纯粹是资源对资源的关系。老实说,我不知道这是系统设计的缺陷,还是论文的代表性问题,还是别的什么。命名空间(Namepaces)、对象(Object)与主体(Subject)Zanzibar中的命名空间(Namespace)并不是起隔离作用的,就像上面的那个例子,在编写videosNamespace时也可以引用groupsNamespace。这里的命名空间概念更多是用来将数据分为同质的分块(并应用不同的配置),并且在储存层面上也是分离的。所以在多租户的使用场景中,用租户的UUID作为Namespace并不是一个好的选择,而应该使用tenants作为Namespace,从而实现:tenants:tenant-id-1#member@felix tenants:tenant-id-1#member@john这样的Relation Tuples,并且用tenants:tenant-id-1#member作为鉴权的subject_set。在命名方面,一般建议:Namespace使用单词的复数形式,而Object和Subject使用UUID。 将Relation Tuples转换为图有助于更好地理解object与subject之间的关系,考虑Keto官方文档上的以下例子:// user1 has access on dir1 dir1#access@user1 // Have a look on the subjects concept page if you don't know the empty relation. dir1#parent@(file1#) // Everyone with access to dir1 has access to file1. This would probably be defined // through a subject set rewrite that defines this inherited relation globally. // In this example, we define this tuple explicitly. file1#access@(dir1#access) // Direct access on file2 was granted. file2#access@user1 // user2 is owner of file2 file2#owner@user2 // Owners of file2 have access to it; possibly defined through subject set rewrites. file2#access@(file2#owner)将其转换为图可以得到:其中实线代表了直接定义的关系,而虚线代表了由Subject Set继承而来的关系。KetoOry/Keto 是谷歌Zanzibar的第一个开源实现。Keto用golang实现并兼容Zanzibar的概念,它作为一个单独的服务部署。相关网站:官方网站代码库官方文档API提供了两种调用方式:RestfulGrpc开放的端口:4466 读取4467 写入后端存储数据库可以使用:PostgreSQLMySQLCockroachDBSQLite(用于开发时,不能用于运行时)官方并未公布其具体的性能表现,但比起使用Spanner的Zanzibar来说,性能应该是差一些的。安装部署Keto服务具体的官方安装文档可见:https://www.ory.sh/docs/keto/install最基本配置keto.ymlversion: v0.10.0-alpha.0 log: level: debug namespaces: - id: 0 name: app serve: read: host: 0.0.0.0 port: 4466 write: host: 0.0.0.0 port: 4467 dsn: memory需要注意的是,新的版本当中,必须要有namespaces的定义,不然启动不了。Docker直接docker run启动docker pull oryd/keto:latest docker run -itd --name keto-server ` -p 4466:4466 -p 4467:4467 ` -v /d/keto.yml:/home/ory/keto.yml ` oryd/keto:latest serve -c /home/ory/keto.yml需要注意的是,我把宿主的keto.yml直接挂载上去了,不然启动不了。docker-compose启动version: "3" services: keto: image: oryd/keto:v0.10.0-alpha.0 ports: - "4466:4466" - "4467:4467" command: serve -c /home/ory/keto.yml restart: on-failure volumes: - type: bind source: . target: /home/oryLinuxbash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -d -b . keto v0.10.0-alpha.0 ./keto helpmacOSbrew install ory/tap/keto keto helpWindowsirm get.scoop.sh | iex scoop bucket add ory https://github.com/ory/scoop.git scoop install keto keto help我尝试了使用sqlite启动,结果说没有支持:could not create new connection: sqlite3 support was not compiled into the binary stack_traceKuberneteshelm repo add ory https://k8s.ory.sh/helm/charts helm repo update安装SDK安装gRPC APIgo get github.com/ory/keto/proto@v0.10.0-alpha.0安装REST APIgo get github.com/ory/keto-client-go@v0.10.0-alpha.0将Keto客户端实施封装package keto import ( "context" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "github.com/go-kratos/kratos/v2/log" client "github.com/ory/keto-client-go" acl "github.com/ory/keto/proto/ory/keto/relation_tuples/v1alpha2" ) type Client struct { checkServiceClient acl.CheckServiceClient readServiceClient acl.ReadServiceClient writeServiceClient acl.WriteServiceClient expandServiceClient acl.ExpandServiceClient readClient *client.APIClient writeClient *client.APIClient useGRPC bool } func NewClient(readUrl, writeUrl string, useGRPC bool) *Client { cli := &Client{ useGRPC: useGRPC, } if useGRPC { cli.createGrpcWriteClient(writeUrl) cli.createGrpcReadClient(readUrl) } else { cli.createRestWriteClient(writeUrl) cli.createRestReadClient(readUrl) } return cli } func (c *Client) GetCheck(ctx context.Context, namespace, object, relation, subject string) (bool, error) { if c.useGRPC { return c.grpcGetCheck(ctx, namespace, object, relation, subject) } else { return c.restGetCheck(ctx, namespace, object, relation, subject) } } func (c *Client) CreateRelationTuple(ctx context.Context, namespace, object, relation, subject string) error { if c.useGRPC { return c.grpcCreateRelationTuple(ctx, namespace, object, relation, subject) } else { return c.restCreateRelationTuple(ctx, namespace, object, relation, subject) } } func (c *Client) createGrpcReadClient(uri string) { conn, err := grpc.Dial(uri, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { panic("Encountered error: " + err.Error()) } c.checkServiceClient = acl.NewCheckServiceClient(conn) c.readServiceClient = acl.NewReadServiceClient(conn) c.expandServiceClient = acl.NewExpandServiceClient(conn) } func (c *Client) createGrpcWriteClient(uri string) { conn, err := grpc.Dial(uri, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { panic("Encountered error: " + err.Error()) } c.writeServiceClient = acl.NewWriteServiceClient(conn) } func (c *Client) createRestReadClient(uri string) { configuration := client.NewConfiguration() configuration.Servers = []client.ServerConfiguration{ { URL: uri, }, } c.readClient = client.NewAPIClient(configuration) } func (c *Client) createRestWriteClient(uri string) { configuration := client.NewConfiguration() configuration.Servers = []client.ServerConfiguration{ { URL: uri, }, } c.writeClient = client.NewAPIClient(configuration) } func (c *Client) restCreateRelationTuple(ctx context.Context, namespace, object, relation, subject string) error { relationQuery := *client.NewRelationQuery() relationQuery.SetNamespace(namespace) relationQuery.SetObject(object) relationQuery.SetRelation(relation) relationQuery.SetSubjectId(subject) _, r, err := c.writeClient.WriteApi.CreateRelationTuple(ctx).RelationQuery(relationQuery).Execute() if err != nil { log.Errorf("restCreateRelationTuple error: [%s][%v]", err.Error(), r) return err } return nil } func (c *Client) restGetCheck(ctx context.Context, namespace, object, relation, subject string) (bool, error) { check, r, err := c.readClient.ReadApi.GetCheck(ctx). Namespace(namespace). Object(object). Relation(relation). SubjectId(subject). Execute() if err != nil { log.Errorf("restGetCheck error: [%s][%v]", err.Error(), r) return false, err } return check.Allowed, nil } func (c *Client) grpcCreateRelationTuple(ctx context.Context, namespace, object, relation, subject string) error { response, err := c.writeServiceClient.TransactRelationTuples(ctx, &acl.TransactRelationTuplesRequest{ RelationTupleDeltas: []*acl.RelationTupleDelta{ { Action: acl.RelationTupleDelta_ACTION_INSERT, RelationTuple: &acl.RelationTuple{ Namespace: namespace, Object: object, Relation: relation, Subject: acl.NewSubjectID(subject), }, }, }, }) if err != nil { log.Errorf("grpcCreateRelationTuple error: [%s][%v]", err.Error(), response) } return err } func (c *Client) grpcGetCheck(ctx context.Context, namespace, object, relation, subject string) (bool, error) { response, err := c.checkServiceClient.Check(ctx, &acl.CheckRequest{ Tuple: &acl.RelationTuple{ Namespace: namespace, Object: object, Relation: relation, Subject: acl.NewSubjectID(subject), }, }) if err != nil { // If namespace doesn't exist, we'll catch the Not Round error. if status.Code(err) == codes.NotFound { return false, nil } log.Errorf("grpcGetCheck error: [%s][%v]", err.Error(), response) return false, err } return response.Allowed, nil }将Keto整合进Kratospackage middleware import ( "context" "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/middleware" "github.com/tx7do/kratos-authz/engine" ) const ( reason string = "FORBIDDEN" ) var ( ErrUnauthorized = errors.Forbidden(reason, "unauthorized access") ErrMissingClaims = errors.Forbidden(reason, "missing authz claims") ErrInvalidClaims = errors.Forbidden(reason, "invalid authz claims") ) func Server(authorizer engine.Authorizer, opts ...Option) middleware.Middleware { o := &options{} for _, opt := range opts { opt(o) } if authorizer == nil { return nil } return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { var ( allowed bool err error ) claims, ok := engine.AuthClaimsFromContext(ctx) if !ok { return nil, ErrMissingClaims } if claims.Subject == nil || claims.Action == nil || claims.Resource == nil { return nil, ErrInvalidClaims } var project engine.Project if claims.Project == nil { project = "" } else { project = *claims.Project } allowed, err = authorizer.IsAuthorized(ctx, *claims.Subject, *claims.Action, *claims.Resource, project) if err != nil { return nil, err } if !allowed { return nil, ErrUnauthorized } return handler(ctx, req) } } }OpenFGAOpenFGA是应用ReBAC概念的Fine-Grained Authorization的开源解决方案。它由Auth0 FGA团队创建,灵感来自Zanzibar。它专为大规模的可靠性和低延迟而设计。它提供了一个 HTTP API 和用于编程语言的 SDK,包括Node.js/JavaScript、GoLang、.NET和Python。未来计划提供更多 SDK 和集成,例如 Rego。相关网站:官方网站代码库官方文档API提供了两种调用方式:RestfulGrpc支持的数据存储引擎:PostgreSQLMySQLCCache(LRU Cache)内存开放的端口:8080 是GRPC的接口8081 是HTTP的接口3000 提供了playground:http://localhost:3000/playground3001 提供了性能探查器安装部署OpenFGA服务Dockerdocker pull openfga/openfga:latest docker run -itd --name openfga-server ` -p 8080:8080 ` -p 8081:8081 ` -p 3000:3000 ` openfga/openfga:latest runDocker Composecurl -LO https://openfga.dev/docker-compose.yaml docker compose up预编译二进制进入下载页面下载二进制包:https://github.com/openfga/openfga/releases/latest然后运行命令:./openfga run安装SDKgo get -u github.com/openfga/go-sdk将OpenFGA客户端实施封装package openfga import ( "context" "encoding/json" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" openfga "github.com/openfga/go-sdk" "github.com/openfga/go-sdk/credentials" ) type Client struct { apiClient *openfga.APIClient } func NewClient(scheme, host, storeId, token string) *Client { cli := &Client{} if cli.createApiClient(scheme, host, storeId, token) != nil { return nil } if cli.ensureStore(context.Background()) != nil { return nil } return cli } func (c *Client) ensureStore(ctx context.Context) error { stores, err := c.ListStore(context.Background()) if err != nil { return err } if stores == nil || len(*stores) == 0 { _uuid := uuid.New() storeName := _uuid.String() err = c.CreateStore(ctx, storeName) if err != nil { return err } } else { c.SetStoreId((*stores)[len(*stores)-1].GetId()) } return nil } func (c *Client) createApiClient(scheme, host, storeId, token string) error { rawConfig := openfga.Configuration{ ApiScheme: scheme, // optional, defaults to "https" ApiHost: host, // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example) StoreId: storeId, // not needed when calling `CreateStore` or `ListStores` } if token != "" { rawConfig.Credentials = &credentials.Credentials{ Method: credentials.CredentialsMethodApiToken, Config: &credentials.Config{ ApiToken: token, // will be passed as the "Authorization: Bearer ${ApiToken}" request header }, } } configuration, err := openfga.NewConfiguration(rawConfig) if err != nil { return err } c.apiClient = openfga.NewAPIClient(configuration) return nil } func (c *Client) GetCheck(ctx context.Context, object, relation, subject string) (bool, error) { body := openfga.CheckRequest{ TupleKey: &openfga.TupleKey{ User: openfga.PtrString(subject), Relation: openfga.PtrString(relation), Object: openfga.PtrString(object), }, } data, response, err := c.apiClient.OpenFgaApi.Check(ctx).Body(body).Execute() if err != nil { log.Errorf("GetCheck error: [%s][%v]", err.Error(), response) return false, err } return *data.Allowed, nil } func (c *Client) ListStore(ctx context.Context) (*[]openfga.Store, error) { stores, response, err := c.apiClient.OpenFgaApi.ListStores(ctx).Execute() if err != nil { log.Errorf("ListStore error: [%s][%v]", err.Error(), response) return nil, err } //log.Infof("%v", stores.Stores) return stores.Stores, nil } func (c *Client) GetStore(ctx context.Context) string { store, response, err := c.apiClient.OpenFgaApi.GetStore(ctx).Execute() if err != nil { log.Errorf("GetStore error [%s][%v]", err.Error(), response) return "" } return store.GetId() } func (c *Client) CreateStore(ctx context.Context, name string) error { store, response, err := c.apiClient.OpenFgaApi.CreateStore(ctx). Body(openfga.CreateStoreRequest{ Name: openfga.PtrString(name), }). Execute() if err != nil { log.Errorf("CreateStore error: [%s][%v]", err.Error(), response) return err } c.SetStoreId(store.GetId()) return nil } func (c *Client) DeleteStore() error { body := openfga.ApiDeleteStoreRequest{} response, err := c.apiClient.OpenFgaApi.DeleteStoreExecute(body) if err != nil { log.Errorf("DeleteStore error: [%s][%v]", err.Error(), response) return err } return nil } func (c *Client) SetStoreId(id string) { c.apiClient.SetStoreId(id) } func (c *Client) CreateRelationTuple(ctx context.Context, object, relation, subject string) error { body := openfga.WriteRequest{ Writes: &openfga.TupleKeys{ TupleKeys: []openfga.TupleKey{ { User: openfga.PtrString(subject), Relation: openfga.PtrString(relation), Object: openfga.PtrString(object), }, }, }, } _, response, err := c.apiClient.OpenFgaApi.Write(ctx).Body(body).Execute() if err != nil { log.Errorf("CreateRelationTuple error: [%s][%v]", err.Error(), response) return err } return nil } func (c *Client) DeleteRelationTuple(ctx context.Context, object, relation, subject string) error { body := openfga.WriteRequest{ Deletes: &openfga.TupleKeys{ TupleKeys: []openfga.TupleKey{ { User: openfga.PtrString(subject), Relation: openfga.PtrString(relation), Object: openfga.PtrString(object), }, }, }, } _, response, err := c.apiClient.OpenFgaApi.Write(ctx).Body(body).Execute() if err != nil { log.Errorf("DeleteRelationTuple error: [%s][%v]", err.Error(), response) return err } return nil } func (c *Client) ExpandRelationTuple(ctx context.Context, object, relation string) error { body := openfga.ExpandRequest{ TupleKey: &openfga.TupleKey{ Relation: openfga.PtrString(relation), Object: openfga.PtrString(object), }, } _, response, err := c.apiClient.OpenFgaApi.Expand(ctx).Body(body).Execute() if err != nil { log.Errorf("ExpandRelationTuple error: [%s][%v]", err.Error(), response) return err } return nil } func (c *Client) CreateAuthorizationModel(ctx context.Context, writeAuthorizationModelRequestString string) (string, error) { var body openfga.WriteAuthorizationModelRequest if err := json.Unmarshal([]byte(writeAuthorizationModelRequestString), &body); err != nil { return "", err } data, response, err := c.apiClient.OpenFgaApi.WriteAuthorizationModel(ctx).Body(body).Execute() if err != nil { log.Errorf("CreateAuthorizationModel error: [%s][%v]", err.Error(), response) return "", err } return data.GetAuthorizationModelId(), nil }将OpenFGA整合进Kratospackage middleware import ( "context" "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/middleware" "github.com/tx7do/kratos-authz/engine" ) const ( reason string = "FORBIDDEN" ) var ( ErrUnauthorized = errors.Forbidden(reason, "unauthorized access") ErrMissingClaims = errors.Forbidden(reason, "missing authz claims") ErrInvalidClaims = errors.Forbidden(reason, "invalid authz claims") ) func Server(authorizer engine.Authorizer, opts ...Option) middleware.Middleware { o := &options{} for _, opt := range opts { opt(o) } if authorizer == nil { return nil } return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { var ( allowed bool err error ) claims, ok := engine.AuthClaimsFromContext(ctx) if !ok { return nil, ErrMissingClaims } if claims.Subject == nil || claims.Action == nil || claims.Resource == nil { return nil, ErrInvalidClaims } var project engine.Project if claims.Project == nil { project = "" } else { project = *claims.Project } allowed, err = authorizer.IsAuthorized(ctx, *claims.Subject, *claims.Action, *claims.Resource, project) if err != nil { return nil, err } if !allowed { return nil, ErrUnauthorized } return handler(ctx, req) } } }相关代码相关代码已经开源,欢迎拉取参考学习:https://github.com/tx7do/kratos-authzhttps://gitee.com/tx7do/kratos-authz应用方面的代码,我开源了一个简单的CMS,完整的应用可在当中找到:https://github.com/tx7do/kratos-bloghttps://gitee.com/tx7do/kratos-blog参考资料Zanzibar: Google’s Consistent, Global Authorization SystemSpanner: Google's Globally-Distributed Database关系的访问控制 (ReBAC)基于属性的访问控制 (ABAC)详解微服务中的三种授权模式What is Relationship Based Access Control (ReBAC)?Relationship-Based Access Control (ReBAC)My Reading on Google Zanzibar: Consistent, Global Authorization SystemAuthZ: Carta’s highly scalable permissions systemZanzibar-style ACLs with OPA RegoZanzibar: A Global Authorization System - Presented by Auth0Building Zanzibar from ScratchWhat is Zanzibar?Building Zanzibar from ScratchGoogle Zanzibar In A NutshellThe Evolution of Ory Keto: A Global Scale Authorization SystemZANZIBAR与ORY/KETO: 权限管理服务简介OpenFGA : Auth0’s an open-source authorization solutionAnnouncing OpenFGA - Auth0’s Open Source Fine Grained Authorization System如何使用 Ory Kratos 和 Ory Keto 保护您的烧瓶应用程序
Kratos微服务工程Bazel构建指南Kratos是一个微服务框架,既然是微服务,那么一个工程下肯定会存在不少的服务,一个服务就是一个二进制可执行程序,那么我们将会面对一个问题:如何去构建(Build)这些服务程序。这件事情,通常都交由构建系统去做。我们能够选择的构建系统有很多:Make、CMake、Bazel……那么,我们又该如何选择一个构建系统呢?项目结构简单,服务少,我们完全可以使用Make来进行构建。要学会使用Make,您需要学会使用Makefile来编写构建脚本,如果整个构建只是组织一些简单的编译命令,那还好,学习和使用都会是简单轻松的事情。但是,理想很丰满,现实很骨感。在实际的工程实践中,一切都会朝着复杂的方向发展。服务的数量肯定不会少,工程的组织结构也肯定不会简单,那么,构建也就会变得相应的复杂起来,需要编写大量的Makefile,Makefile的复杂度也越来越大了。另外还有,构建环境的搭建问题,持续集成的问题,自动构建的问题,构建时间变长的问题……抱歉,面对这样复杂的工程环境,Make难以满足我们的需求。那么,要解决现实中这些问题,我们就需要一个合适的构建工具。这个工具也就是我们在本文要介绍的:Bazel。Bazel是谷歌开发的一个云构建系统,对于谷歌为什么要重新发明一个构建工具而不直接使用 Make,Google 认为 Make 控制得太细,最终结果完全要依靠开发人员能正确编写规则。很久以前,Google 使用自动生成的臃肿的 Makefile 来构建他们的软件,速度太慢,结果也不可靠,最终影响了研发人员的效率和公司的敏捷性。所以他们做了 Bazel。对于小型的项目,Bazel可能有点过于复杂,学习曲线也相对陡峭。但是,对于微服务这种拥有比较复杂的项目结构,众多服务的项目,就非常合适了,使用它就很值得。综上,我们可以选择Make和Bazel做我们Kratos微服务项目的构建工具:Make,适合规模小,服务少,项目结构固定的工程;Bazel,适合规模大,服务多,项目结构也复杂的工程。通俗来讲就是一个高低配。本文目标本文将要达成以下目标:学习使用Bazel构建Golang应用程序;学习使用Bazel构建Docker镜像;使用Bazel构建Kratos微服务项目实战。本文示例代码一个Bazel构建Golang应用程序的最简示例一个Bazel构建Golang应用程序并打包Docker镜像的示例一个Kratos微服务的CMS实战项目以上代码在Gitee上也同步有,只需要把github修改为gitee即可访问。代码库结构现在,代码库有两种风格:Monorepo和Polyrepo、Multirepos。Monorepo 意味着把所有项目的所有代码统一维护在一个单一的代码版本库中,和多代码库(Polyrepo、Multirepos)方案相比,两者各有优劣,需要根据公司文化和产品特性进行取舍。由于谷歌在 Monorepo 上的实践,Monorepo 受到了越来越多的关注。我们不能说因为有大厂商的背书,就不看具体情况的盲从。合适自己的,才是最好的。这两种风格,我们都要稍作了解,这样,当我们做选择的时候能够胸有成竹。本文所推崇的代码库结构为Monorepo,因为微服务的项目经常要去进行服务的拆分和组合,Monorepo就变得比较适合了,并且,本来服务之间就存在密不可分的交际,分到不同的代码库,也并不合适。什么是 单一代码库 (Monorepo) ?Monorepo 的意思是在版本控制系统的单个代码库里包含了许多项目的代码。这些项目虽然有可能是相关的,但通常在逻辑上是独立的,并由不同的团队维护。有些公司将所有代码存储在一个代码库中,由所有人共享,因此 Monorepos 可以非常大。例如,理论上谷歌拥有有史以来最大的代码库,每天有成百上千次提交,整个代码库超过 80 TB。其他已知运营大型单一代码库的公司还有微软、Facebook 和 Twitter。Monorepos 有时被称为单体代码库(monolithic repositories),但不应该与单体架构(monolithic architecture)相混淆,单体架构是一种用于编写自包含应用程序的软件开发实践。这方面的一个例子就是 Ruby on Rails,它可以处理 Web、API 和后端工作。什么是 多代码库 (Polyrepo、Multirepos) ?与单一代码库相反的是多代码库(multirepos),每个项目都储存在一个完全独立的、版本控制的代码库中。多代码库是很自然的选择——我们大多数人在开始一个新项目时都愿意开一个新的代码库,毕竟,谁都喜欢从 0 开始.从多代码库到单一代码库的变化就意味着将所有项目移到一个代码库中。多代码库不是微服务(MicroServices)的同义词,两者之间并没有耦合关系。事实上,我们稍后将讨论将单一代码库和微服务结合起来的例子。只要仔细设置用于部署的 CI/CD 流水线,单一代码库就可以托管任意数量的微服务。单一代码库(Monorepo)的好处乍一看,单一代码库和多代码库之间的选择似乎不是什么大问题,但这是一个会深刻影响到公司开发流程的决定。至于单一代码库的好处,可以列举如下:可见性(Visibility):每个人都可以看到其他人的代码,这样可以带来更好的协作和跨团队贡献——不同团队的开发人员都可以修复代码中的 bug,而你甚至都不知道这个 bug 的存在。更简单的依赖关系管理(Simpler dependency management):共享依赖关系很简单,因为所有模块都托管在同一个存储库中,因此都不需要包管理器。唯一依赖源(Single source of truth):每个依赖只有一个版本,意味着没有版本冲突,没有依赖地狱。一致性(Consistency):当你把所有代码库放在一个地方时,执行代码质量标准和统一的风格会更容易。共享时间线(Shared timeline):API 或共享库的变更会立即被暴露出来,迫使不同团队提前沟通合作,每个人都得努力跟上变化。原子提交(Atomic commits):原子提交使大规模重构更容易,开发人员可以在一次提交中更新多个包或项目。隐式 CI(Implicit CI):因为所有代码已经统一维护在一个地方,因此可以保证持续集成。统一的 CI/CD(Unified CI/CD):可以为代码库中的每个项目使用相同的 CI/CD 部署流程。统一的构建流程(Unified build process):代码库中的每个应用程序可以共享一致的构建流程。单一代码库(Monorepo)的缺陷随着单一代码库的发展,我们在版本控制工具、构建系统和持续集成流水线方面达到了设计极限。这些问题可能会让一家公司走上多代码库的道路:性能差(Bad performance):单一代码库难以扩大规模,像 git blame 这样的命令可能会不合理的花费很长时间执行,IDE 也开始变得缓慢,生产力受到影响,对每个提交测试整个 repo 变得不可行。破坏主线(Broken main/master):主线损坏会影响到在单一代码库中工作的每个人,这既可以被看作是灾难,也可以看作是保证测试既可以保持简洁又可以跟上开发的好机会。学习曲线(Learning curve):如果代码库包含了许多紧密耦合的项目,那么新成员的学习曲线会更陡峭。大量的数据(Large volumes of data):单一代码库每天都要处理大量的数据和提交。所有权(Ownership):维护文件的所有权更有挑战性,因为像 Git 或 Mercurial 这样的系统没有内置的目录权限。代码审查(Code reviews):通知可能会变得非常嘈杂。例如,GitHub 有有限的通知设置,不适合大量的 pull request 和 code review。Bazel是什么?Bazel 是一个构建工具,是 Google 为其内部软件开发的特点量身定制的工具,官方对其定位是:a fast, scalable, multi-language and extensible build system一款速度极快、可伸缩、跨语言并且可扩展的构建系统以下针对Bazel的四大特性进行分析,以更深入的理解Bazel:快 (Fast)Bazel 的构建过程很快,它集合了之前构建系统的加速的一些常见做法。包括:增量编译。只重新编译必须的部分,即通过依赖分析,只编译修改过的部分及其影响的路径。并行编译。将没有依赖的部分进行并行执行,可以通过 --jobs 来指定并行流的个数,一般可以是你机器 CPU 的个数。遇到大项目马力全开时,Bazel 能把你机器的 CPU 各个核都吃满。分布式 / 本地缓存。Bazel 将构建过程视为函数式的,只要输入给定,那么输出就是一定的。而不会随着构建环境的不同而改变(当然这需要做一些限制),这样就可以分布式的缓存 / 复用不同模块,这点对于超大项目的速度提升极为明显。可伸缩 (scalable)Bazel 号称无论什么量级的项目都可以应对,无论是超大型单体代码库(monorepo)、还是超多库的多代码库(multirepo)。在 Google,一个服务器软件有十万行代码是很常见的,在什么都不改的前提下重新构建这样一个项目,大概只需要 200 毫秒。Bazel 还可以很方便的集成 CD/CI ,并在云端利用分布式环境进行构建。Bazel 使用 沙箱机制 进行编译,即将所有编译依赖隔绝在一个沙箱中,比如编译 golang 项目时,不会依赖你本机的 GOPATH,从而做到同样源码、跨环境编译、输出相同,即构建的确定性。换言之,就是构建所需的构建环境,它也全包了。跨语言 (multi-language)如果一个项目不同模块使用不同的语言,利用 Bazel 可以使用一致的风格来管理项目外部依赖和内部依赖。典型的项目如 Ray。该项目使用 C++ 构建 Ray 的核心调度组件、通过 Python/Java 来提供多语言的 API,并将上述所有模块用单个 repo 进行管理。如此组织使其项目整合相当困难,但 Bazel 在此处理的游刃有余,大家可以去该 repo 一探究竟。可扩展 (extensible)Bazel 使用的语法是基于 Python 裁剪而成的一门语言:Starlark。其表达能力强大,往小了说,可以使用户自定义一些 rules (类似一般语言中的函数)对构建逻辑进行复用;往大了说,可以支持第三方编写适配新的语言或平台的 rules 集,比如 rules go。 Bazel 并不原生支持构建 golang 工程,但通过引入 rules go ,就能以比较一致的风格来管理 golang 工程。安装 Bazel如何安装Bazel的文档,官方提供的文档已经足够详细:https://bazel.build/install。Windows安装文档:https://bazel.build/install/windowsScoopscoop install bazel # include buildifier buildozer unused_deps scoop install bazel-buildtools scoop install msys2Chocolateychoco install bazel choco install buildifier choco install buildozer choco install msys2Windows因为不存在bash,会报错。所以需要另外,还需要安装MSYS2。新增一个环境变量BAZEL_SH,把变量值设置为MSYS2的usr\bin\bash.exe。Ubuntu安装文档:https://bazel.build/install/ubuntu先安装软件源和证书,此操作只需要做一次:sudo apt install apt-transport-https curl gnupg -y curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor >bazel-archive-keyring.gpg sudo mv bazel-archive-keyring.gpg /usr/share/keyrings echo "deb [arch=amd64 signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list接着就可以安装了:sudo apt update && sudo apt install bazelmacOS安装文档:https://bazel.build/install/os-xbrew install bazelBazel工程文件组成使用 Bazel 管理的项目一般包含以下几种 Bazel 相关的文件:WORKSPACE(.bazel)、BUILD(.bazel)、.bzl 和 .bazelrc 等。WORKSPACE(.bazel) 和 .bazelrc 必须要放置于项目的根目录下。BUILD(.bazel)必须要放在项目的每一个文件夹中去(包括项目根目录)。.bzl 文件可以根据用户喜好自由放置,一般可放在项目根目录下的某个专用文件夹(比如 build)中。其中,WORKSPACE(.bazel)和BUILD(.bazel)可以加.bazel后缀,也可以不加。WORKSPACE(.bazel)WORKSPACE(.bazel)文件 通常放置于工程的根目录下面,此文件用于:定义项目根目录和项目名。加载 Bazel 工具和 rule 集。管理项目外部依赖库。一个最小化的可用于构建golang语言项目的WORKSPACE(.bazel)文件大概是这样的:# 定义工作环境名称 workspace(name = "com_github_tx7do_bazel_golang_minimal_example") # 导入http_archive方法 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # 下载rules_go http_archive( name = "io_bazel_rules_go", sha256 = "56d8c5a5c91e1af73eca71a6fab2ced959b67c86d12ba37feedb0a2dfea441a6", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", "https://github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", ], ) ## 下载Gazelle http_archive( name = "bazel_gazelle", sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", ], ) ######################################### ## Go语言 规则集 初始化 ######################################### # 导入go_register_toolchains和go_rules_dependencies方法 load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") # 初始化go规则集的依赖项 go_rules_dependencies() # 注册go 1.19.5版本的工具链,包含下载安装go环境。 go_register_toolchains(version = "1.19.5") ######################################### ## Gazelle 规则集 初始化 ######################################### # 导入gazelle_dependencies和go_repository方法 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") # 初始化Gazelle规则集的依赖项 gazelle_dependencies()BUILD.bazel该文件主要针对其所在文件夹进行 依赖解析 和 构建目标定义。拿 go 来说,构建目标可以是 go_binary、go_test、go_library 等。Bazel 的之前版本用的文件名是 BUILD,但是在一些大小写不区分的系统上,它很容易跟 build 文件混淆,因此后来改为了显式的 BUILD.bazel。如果项目中同时存在两者,Bazel 更倾向于使用后者。对于所有的新项目,都推荐使用显式的 BUILD.bazel。github 上有一些讨论在这里。为了引用一个依赖,Bazel 使用 label 语法对所有的包进行唯一标识,其格式如下:@workerspace_name//path/of/package:target比如,go 中常用的一个日志库 logrus 的 label 为:@com_github_sirupsen_logrus//:go_default_library如果是本项目中的包路径,可以将 // 之前的 workspace 名字省去://:library一个最简单的Go项目的BUILD.bazel看起来是这样的:# 导入go_binary、go_test、go_library方法 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") # 构建二进制程序 go_binary( name = "hello", srcs = ["hello.go"], deps = [":greeter"], ) # 构建库 go_library( name = "greeter", importpath = "github.com/tx7do/bazel-golang-minimal-example/greeter", srcs = ["greeter.go"], ) # 构建单元测试 go_test( name = "greeter_test", srcs = [ "greeter_test.go" ], embed = [ ":greeter" ], )自定义 rule (*.bzl)如果你的项目有一些复杂构造逻辑、或者一些需要复用的构造逻辑,那么可以将这些逻辑以函数形式保存在 .bzl 文件,供 WORKSPACE 或者 BUILD 文件调用。其语法跟 Python 类似:def download_package(): # 下载 Bazel Go语言 规则集 if not native.existing_rule("io_bazel_rules_go"): http_archive( name = "io_bazel_rules_go", sha256 = "56d8c5a5c91e1af73eca71a6fab2ced959b67c86d12ba37feedb0a2dfea441a6", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", "https://github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", ], ) # 下载 Bazel Gazelle 规则集 if not native.existing_rule("bazel_gazelle"): http_archive( name = "bazel_gazelle", sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", ], ).bazelrc.bazelrc 是一个配置文件,熟悉Linux的同学一看就知道这是使用的.*rc的命名规则的配置文件。因为,Bazel是基于Java开发的,熟悉JVM的同学都知道,JVM配置过之后更香。使用UseParallelGC并行收集器,设置JVM的内存等。因为网络不好,Golang环境设置GOPROXY和GOSUMDB也是必须的,否则go依赖库的更新下载会让人崩溃死的。通常来说,我们的线上环境要么是Linux系统,要么是Docker——本质上,它还是Linux——所以,编译目标肯定就是Linux了,我们就需要进行交叉编译的配置,将目标系统配置为linux_amd64是必要的。这些配置,我们都可以写入到.bazelrc:# 设置JVM startup --host_jvm_args=-XX:+UseParallelGC --host_jvm_args=-Xmx6g --host_jvm_args=-Xms1g # 设置CoreDump startup --unlimit_coredumps # 设置GOPROXY test --action_env=GOPROXY=https://goproxy.cn build --action_env=GOPROXY=https://goproxy.cn run --action_env=GOPROXY=https://goproxy.cn # 设置GOSUMDB test --action_env=GOSUMDB=goproxy.cn/sumdb/sum.golang.org build --action_env=GOSUMDB=goproxy.cn/sumdb/sum.golang.org run --action_env=GOSUMDB=goproxy.cn/sumdb/sum.golang.org # 设置编译目标平台 build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 run --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64一个最简单的Golang程序构建最简单的Bazel构建文件只需要两个:WORKSPACE和BUILD.bazel。以下是项目的目录树:project ├─ BUILD.bazel ├─ WORKSPACE ├─ greeter_test.go ├─ greeter.go ├─ main.go三个go源码如下:greeter.gopackage greeter func Greet() string { return "Hello, Dear!" }greeter_test.gopackage greeter import ( "testing" ) func TestGreeter(t *testing.T) { got := Greet() want := "Hello, Dear!" if got != want { t.Errorf(`Greet() = %q, want %q`, got, want) } }main.gopackage main import ( "fmt" "github.com/tx7do/bazel-golang-minimal-example/greeter" ) func main() { fmt.Printf(greeter.Greet()) }两个Bazel配置文件如下:WORKSPACE# 定义工作环境名称 workspace(name = "com_github_tx7do_bazel_golang_minimal_example") # 导入http_archive方法 load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") # 下载rules_go http_archive( name = "io_bazel_rules_go", sha256 = "56d8c5a5c91e1af73eca71a6fab2ced959b67c86d12ba37feedb0a2dfea441a6", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", "https://github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", ], ) # 导入go_register_toolchains和go_rules_dependencies方法 load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") # 初始化go规则集的依赖项 go_rules_dependencies() # 注册go 1.19.5版本的工具链,包含下载安装go环境。 go_register_toolchains(version = "1.19.5")BUILD.bazel# 导入go_binary、go_test、go_library方法 load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") # 构建二进制程序 go_binary( name = "main", srcs = ["main.go"], deps = [":greeter"], ) # 构建库 go_library( name = "greeter", importpath = "github.com/tx7do/bazel-golang-minimal-example/greeter", srcs = ["greeter.go"], ) # 构建单元测试 go_test( name = "greeter_test", srcs = [ "greeter_test.go" ], embed = [ ":greeter" ], )在这个示例里面,我们只使用到了Bazel能够支持go语言的rules_go规则集。在BUILD.bazel里面,我们定义了3个构建目标://:main这是构建主程序二进制可执行程序的构建目标。//:greeter这是构建库文件的构建目标。//:greeter_test这是构建单元测试二进制可执行程序的构建目标。对于go来说,库的构建目标通常不是我们需要关注的。平时我们只需要关注主程序的构建和单元测试的构建。只是构建二进制可执行文件,我们只需要使用bazel build命令:bazel build //:greeter_test bazel build //:main我们要直接运行程序的话,那么可以使用bazel run命令,它将构建出二进制可执行文件,然后执行它:bazel run //:greeter_test bazel run //:main到这里,我们就完成了使用Bazel构建一个最简单golang程序的全过程。Bazel本身虽然很复杂,但是,上手使用还是很简单的。甚至比Make还要简单。何况Make还有个问题,在Windows下面使用极不友好,很多功能用不了。Bazel则不存在这样的问题,各操作系统都可以无障碍使用。完整代码请见:https://github.com/tx7do/bazel-golang-minimal-example使用Gazelle有了Bazel的使用基础,rules_go的使用基础。我们现在可以学习使用Bazel下的一个神器:Gazelle。Gazelle 是一个自动生成 Bazel 编译文件的工具,包括给 WORKSPACE 添加外部依赖、扫描源文件依赖自动生成BUILD.bazel文件等。Gazelle 原生支持Go和 protobuf。Gazelle 可以使用 bazel 命令结合 gazelle_rule 运行:bazel run //:gazelle。也可以下载使用单独的 Gazelle 的命令行工具:go install github.com/bazelbuild/bazel-gazelle/cmd/gazelle@latest。自动添加外部依赖Bazel是无法感知go.mod当中的golang依赖项的,但是,Bazel的沙箱是构建了一个全新的构建环境,所以,它必须要感知到go.mod当中的golang依赖项,不然Bazel无法进行拉取、管理和编译构建。Gazelle正好提供了相关的功能:首先是依赖库的导入:load("@bazel_gazelle//:deps.bzl", "go_repository") go_repository( name = "org_uber_go_zap", build_file_proto_mode = "disable", importpath = "go.uber.org/zap", sum = "h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=", version = "v1.24.0", )只要添加了以上代码之后,Bazel就能够拉取并构建Uber的zap库了。接着,就是从go.mod或者go.work中导入依赖项了:bazel run //:gazelle update-repos -from_file=go.mod bazel run //:gazelle update-repos -from_file=go.work或者gazelle update-repos -from_file=go.mod gazelle update-repos -from_file=go.work运行以上的命令之后,gazelle就会把依赖项都导入到WORKSPACE。如果你觉得go的依赖库太多,你不想要把依赖项导入到WORKSPACE,那么可以添加参数-to_macro=repositories.bzl%go_repositories,这样依赖项都会被导入到repositories.bzl文件里面去了,并且生成一个go_repositories方法,所有的go_repository方法将被置于go_repositories方法之下:load("@bazel_gazelle//:deps.bzl", "go_repository") def go_dependencies(): go_repository( name = "org_uber_go_zap", build_file_proto_mode = "disable", importpath = "go.uber.org/zap", sum = "h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=", version = "v1.24.0", )并且在WORKSPACE中添加调用方法:load("//:repos.bzl", "go_dependencies") # gazelle:repository_macro repositories.bzl%go_dependencies go_dependencies()导入和生成代码的命令现在就是:bazel run //:gazelle update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories bazel run //:gazelle update-repos -from_file=go.work -to_macro=repositories.bzl%go_repositories或者gazelle update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories gazelle update-repos -from_file=go.work -to_macro=repositories.bzl%go_repositories有的人可能会嫌弃写这么多的参数,累。那么,你可以在BUILD.bazel里面这样定义:gazelle( name = "gazelle-update-repos", args = [ "-from_file=go.mod", "-to_macro=repositories.bzl%go_dependencies", "-prune", "-build_file_proto_mode=disable", ], command = "update-repos", )现在你只需要执行以下命令就可以了:bazel run //:gazelle-update-repos自动生成构建文件在上一节里面我们可知,每一个源文件我们都需要通过go_binary、go_test、go_library方法引入到构建文件。文件少的情况下,勉强还能接受,一个项目成千上万的源文件,这无法接受。还好,gazelle能够帮我们做这脏活累活。我们只需要两步:向项目根目录下的BUILD.bazel添加以下代码:load("@bazel_gazelle//:def.bzl", "gazelle") # gazelle:prefix github.com/tx7do/bazel-containers-hasher-example gazelle(name = "gazelle")需要注意的是 # 后面的内容 gazelle:XXXX YYYYY 对于 Bazel 而言是注释,对于 Gazelle 来说却是一种 注解指令(Directive),会被 Gazelle 运行时所解析使用。执行命令生成:bazel run //:gazelle如何把Golang程序打包成Docker镜像要打包Docker镜像,我们只需要rules_docker规则包。在WORKSPACE中获取依赖:## 下载rules_docker http_archive( name = "io_bazel_rules_docker", sha256 = "b1e80761a8a8243d03ebca8845e9cc1ba6c82ce7c5179ce2b295cd36f7e394bf", urls = [ "https://github.com/bazelbuild/rules_docker/releases/download/v0.25.0/rules_docker-v0.25.0.tar.gz", ], ) # 导入container_repositories方法 load( "@io_bazel_rules_docker//repositories:repositories.bzl", container_repositories = "repositories", ) container_repositories() # 导入container_deps方法 load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps") container_deps() # 导入container_pull方法 load("@io_bazel_rules_docker//container:pull.bzl", "container_pull") # 拉取Alpine Linux # 该发行版使用musl libc,并且缺乏一些调试工具。 container_pull( name = "alpine_linux_amd64", registry = "index.docker.io", repository = "library/alpine", tag = "latest", )rules_docker规则包提供了两个方法container_image和container_push:container_image用于生成Docker镜像container_image( # 镜像名,可用于:编译目标名,镜像标签。 name = "image", base = "@alpine_linux_amd64//image", # https://docs.docker.com/engine/reference/builder/#entrypoint entrypoint = ["./api"], # 存放files/tars/debs文件的路径 directory = "/app/cmd", # https://docs.docker.com/engine/reference/builder/#workdir workdir = "/app/cmd", # 需要打包进镜像去的文件 files = [ ":api", ], # 资源库的用户名 repository = "tx7do", )container_push用于推送镜像到DockerHub# 最终产生的镜像,拉取命令为:docker pull tx7do/bazel-hasher:latest container_push( name = "image-push", # 镜像的格式,可选项:Docker、OCI;默认为:Docker。 format = "Docker", # 要被推送的镜像 image = ":image", # 镜像库的注册链接 registry = "index.docker.io", ## 目标镜像库中的镜像名 repository = "tx7do/bazel-hasher", # 镜像标签 tag = "latest", )现在,我们使用以下命令用于Docker镜像构建之上:bazel build //cmd/api:image该命令将会生成Docker镜像构成的文件:[name].tar、[name].digest、[name]-layer.tar等。bazel run //cmd/api:image该命令将会生成Docker镜像构成的文件,并且导入到本地Docker里。等同于docker load命令。我们可以在本地使用docker images命令查看。bazel run //cmd/api:image-push该命令将会生成Docker镜像构成的文件,并且推送到远端的DockerHub里去。等同于docker push命令。我们可以在https://hub.docker.com查看推送上去的镜像。到这里,有的同学会问到:Dockerfile在哪里?没错,我们不需要Dockerfile,只需要在Bazel构建文件里面添加这两个方法就搞定了。大大的简化了Docker打包的工作,而且比手打Dockerfile更可靠,不易出错。完整代码请见:https://github.com/tx7do/bazel-containers-hasher-exampleKratos微服务项目的构建我开源了一个基于Kratos开发的CMS项目:Kratos-Blog。它是一个Monorepo代码库的项目。我们基于这个项目来讲解Kratos微服务项目的Bazel构建。虽然,项目变大了。但是,大部分都是基于上面两节来做的。这一节就一些差异性来单独讲解一下。首先,我把规则包的下载提取到了DOWNLOAD.bzl:load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") def download_package(): # 下载 Bazel Go语言 规则集 if not native.existing_rule("io_bazel_rules_go"): http_archive( name = "io_bazel_rules_go", sha256 = "56d8c5a5c91e1af73eca71a6fab2ced959b67c86d12ba37feedb0a2dfea441a6", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", "https://github.com/bazelbuild/rules_go/releases/download/v0.37.0/rules_go-v0.37.0.zip", ], ) # 下载 Bazel Gazelle 规则集 if not native.existing_rule("bazel_gazelle"): http_archive( name = "bazel_gazelle", sha256 = "ecba0f04f96b4960a5b250c8e8eeec42281035970aa8852dda73098274d14a1d", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.29.0/bazel-gazelle-v0.29.0.tar.gz", ], ) # 下载 Bazel 工具方法集 if not native.existing_rule("bazel_skylib"): http_archive( name = "bazel_skylib", sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", "https://github.com/bazelbuild/bazel-skylib/releases/download/1.3.0/bazel-skylib-1.3.0.tar.gz", ], ) # 下载 Bazel Docker 规则集 if not native.existing_rule("io_bazel_rules_docker"): http_archive( name = "io_bazel_rules_docker", sha256 = "b1e80761a8a8243d03ebca8845e9cc1ba6c82ce7c5179ce2b295cd36f7e394bf", urls = [ "https://github.com/bazelbuild/rules_docker/releases/download/v0.25.0/rules_docker-v0.25.0.tar.gz" ], ) # 下载 Bazel Kubernetes 规则集 if not native.existing_rule("io_bazel_rules_k8s"): http_archive( name = "io_bazel_rules_k8s", sha256 = "ce5b9bc0926681e2e7f2147b49096f143e6cbc783e71bc1d4f36ca76b00e6f4a", strip_prefix = "rules_k8s-0.7", urls = ["https://github.com/bazelbuild/rules_k8s/archive/refs/tags/v0.7.tar.gz"], ) # 下载 Bazel 构建压缩包(tar、zip、deb 和 rpm) 规则集 if not native.existing_rule("rules_pkg"): http_archive( name = "rules_pkg", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_pkg/releases/download/0.8.0/rules_pkg-0.8.0.tar.gz", "https://github.com/bazelbuild/rules_pkg/releases/download/0.8.0/rules_pkg-0.8.0.tar.gz", ], sha256 = "eea0f59c28a9241156a47d7a8e32db9122f3d50b505fae0f33de6ce4d9b61834", ) # 下载 Bazel Buf 规则集 if not native.existing_rule("rules_buf"): http_archive( name = "rules_buf", sha256 = "523a4e06f0746661e092d083757263a249fedca535bd6dd819a8c50de074731a", strip_prefix = "rules_buf-0.1.1", urls = [ "https://github.com/bufbuild/rules_buf/archive/refs/tags/v0.1.1.zip", ], ) # 下载 Bazel Protobuf 规则集 if not native.existing_rule("rules_proto"): http_archive( name = "rules_proto", sha256 = "66bfdf8782796239d3875d37e7de19b1d94301e8972b3cbd2446b332429b4df1", strip_prefix = "rules_proto-4.0.0", urls = [ "https://mirror.bazel.build/github.com/bazelbuild/rules_proto/archive/refs/tags/4.0.0.tar.gz", "https://github.com/bazelbuild/rules_proto/archive/refs/tags/4.0.0.tar.gz", ], ) # 下载 Bazel gRPC 规则集 if not native.existing_rule("rules_proto_grpc"): http_archive( name = "rules_proto_grpc", sha256 = "fb7fc7a3c19a92b2f15ed7c4ffb2983e956625c1436f57a3430b897ba9864059", strip_prefix = "rules_proto_grpc-4.3.0", urls = [ "https://github.com/rules-proto-grpc/rules_proto_grpc/archive/4.3.0.tar.gz" ], ) # 下载 Bazel Protobuf 规则集 if not native.existing_rule("build_stack_rules_proto"): # Release: v2.0.1 # TargetCommitish: master # Date: 2022-10-20 02:38:27 +0000 UTC # URL: https://github.com/stackb/rules_proto/releases/tag/v2.0.1 # Size: 2071295 (2.1 MB) http_archive( name = "build_stack_rules_proto", sha256 = "ac7e2966a78660e83e1ba84a06db6eda9a7659a841b6a7fd93028cd8757afbfb", strip_prefix = "rules_proto-2.0.1", urls = [ "https://github.com/stackb/rules_proto/archive/v2.0.1.tar.gz" ], ) # 下载 Bazel protoc工具 if not native.existing_rule("com_google_protobuf"): http_archive( name = "com_google_protobuf", sha256 = "bc3dbf1f09dba1b2eb3f2f70352ee97b9049066c9040ce0c9b67fb3294e91e4b", strip_prefix = "protobuf-3.15.5", # latest, as of 2021-03-08 urls = [ "https://github.com/protocolbuffers/protobuf/archive/v3.15.5.tar.gz", "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v3.15.5.tar.gz", ], )然后在WORKSPACE当中调用:load("//:DOWNLOAD.bzl", "download_package") download_package()关于Docker打包这一块的功能,我提取出来一个方法publish_service到docker.bzl:load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_layer", "container_push") # 发布服务 def publish_service(service_name, repository_name = "", repository_version = "", publish = False): service_new_name = "{}-service".format(service_name) image_name = "{}-service-image".format(service_name) conf_file_group_name = "{}-service-configs".format(service_name) conf_layer_name = "{}-service-configs-layer".format(service_name) app_path = "/app/{}/service/bin".format(service_name) conf_path = "/app/{}/service/configs".format(service_name) if repository_version == "": repository_version = "{BUILD_TIMESTAMP}" # 为服务的编译目标定义一个别名 native.alias( name = service_new_name, actual = "//app/{}/service/cmd/server:server".format(service_name), visibility = ["//visibility:private"], ) # 将配置文件打包 native.filegroup( name = conf_file_group_name, srcs = native.glob(["app/{}/service/configs/**".format(service_name)]), visibility = ["//visibility:public"], ) container_layer( name = conf_layer_name, directory = "/{}".format(conf_path), files = [ "//:{}".format(conf_file_group_name), ], mode = "0o755", visibility = ["//visibility:public"], ) # 生成Docker镜像 container_image( # 镜像名,可用于:编译目标名,镜像标签。 name = image_name, # OS base = "@slim_linux_amd64//image", # 容器启动时运行的命令 # https://docs.docker.com/engine/reference/builder/#entrypoint entrypoint = [ "./server", "-conf", "../configs", "-chost", "host.docker.internal:8500", "-ctype", "consul", ], # 存放files/tars/debs文件的路径 directory = app_path, # https://docs.docker.com/engine/reference/builder/#workdir workdir = app_path, # https://docs.docker.com/engine/reference/builder/#user # user = "appuser", # 需要打包进镜像去的文件 files = [ "//:{}".format(service_new_name), ], layers = ["//:{}".format(conf_layer_name)], # 资源库的用户名 repository = repository_name, ) # 推送到DockerHub if publish: container_push( name = "{}-push".format(image_name), # 镜像的格式,可选项:Docker、OCI;默认为:Docker。 format = "Docker", # 要被推送的镜像 image = "//:{}".format(image_name), # 镜像库的注册链接 registry = "index.docker.io", ## 目标镜像库中的镜像名 repository = "{}/kratoscms-{}-service".format(repository_name, service_name), # 镜像标签 tag = repository_version, )此方法在根目录下的BUILD.bazel当中调用:load("//:docker.bzl", "publish_service") repository_name = "tx7do" repository_version = "latest" push_container = False publish_service("user", repository_name, repository_version, push_container) publish_service("file", repository_name, repository_version, push_container) publish_service("content", repository_name, repository_version, push_container) publish_service("comment", repository_name, repository_version, push_container) publish_service("admin", repository_name, repository_version, push_container)publish_service方法是需要重点讲一下的。alias是为服务的编译目标命名了一个别名,这样的话,之前编译的命令是:bazel build //app/admin/service/cmd/server:server,现在就简化成了:bazel build //:admin-service。filegroup可以把一些文件打包拷贝,在这里我是为了拷贝配置文件。接着,再把文件组使用container_layer打成一个容器层,使用container_layer有两个目的:一个是设置权限,一个是设置文件的路径。这一个容器层通过container_image方法的layers参数传入,打成一整个容器镜像。最开始的时候,我使用了Alpine Linux这个基础容器层,但是发现直接打包无法运行程序,后来改到了Debian-Slim就没问题了。拉取Linux镜像的Bazel代码附下:load("@io_bazel_rules_docker//container:pull.bzl", "container_pull") # 拉取Alpine Linux # 该发行版使用musl libc,并且缺乏一些调试工具。 container_pull( name = "alpine_linux_amd64", registry = "index.docker.io", repository = "library/alpine", tag = "latest", ) # 拉取Debian-Slim Linux container_pull( name = "slim_linux_amd64", registry = "index.docker.io", repository = "library/debian", tag = "stable-slim", ) # 拉取Centos Linux container_pull( name = "centos_linux_amd64", registry = "index.docker.io", repository = "library/centos", tag = "7", ) # 拉取Ubuntu Linux container_pull( name = "ubuntu_linux_amd64", registry = "index.docker.io", repository = "library/ubuntu", tag = "latest", )我们现在可以通过以下命令来构建某一个服务:bazel build //:admin-service bazel build //:comment-service bazel build //:content-service bazel build //:file-service bazel build //:user-service运行某一个服务:bazel run //:admin-service bazel run //:comment-service bazel run //:content-service bazel run //:file-service bazel run //:user-service生成服务的Docker镜像文件:bazel build //:admin-service-image bazel build //:comment-service-image bazel build //:content-service-image bazel build //:file-service-image bazel build //:user-service-image推送到DockerHub:bazel run //:admin-service-image-push bazel run //:comment-service-image-push bazel run //:content-service-image-push bazel run //:file-service-image-push bazel run //:user-service-image-push完整代码请见:https://github.com/tx7do/kratos-blog关于Protobuf的构建Bazel原生就支持Protobuf的构建,但是我用起来的时候发现有点麻烦,就暂时没有用了,我直接把生成的代码也一并提交到了代码库去了。我用了Gazelle的注解关闭掉了Protobuf协议的代码生成功能:# gazelle:proto disable # gazelle:exclude apigazelle:proto这个注解设置为disable关闭掉整个的代码生成。gazelle:exclude这个注解把Protobuf的协议所在文件夹排除构建范围。还有就是需要在bazel update-repos命令里面添加一个参数-build_file_proto_mode,将它设置为disable。参考资料Bazel - 官方网站Bazel - Github编译工具之Bazel vs Make5 分钟搞懂 MonorepoGolang with bazel: Part-1 SetupGolang with BazelBUILDING A GO PROJECT USING BAZELBUILDING GO APPLICATIONS WITH BAZELBazel 学习笔记 (四) 创建宏与规则使用genrule如何从makefile向bazel转变Bazel Build: 命令行Protobuf and gRPC rules for BazelProtocol Buffers in Bazel容器技术原理(一):从根本上认识容器镜像Bazel 构建 Golang 项目
Kratos微服务框架API工程化指南Kratos的RPC默认使用的是gRPC,与此同时我们还可以通过gRPC的grpc-gateway功能对RESTfull进行支持。这样,我们就可以同时支持gRPC和REST了。而这一切Kratos都已经封装好,无需知道底层的一切,用就好了。gRPC是基于Protobuf作为接口规范的描述语言(IDL,Interface Description Language)。换句通俗的话来说,gRPC使用Protobuf来设计和管理API。我们只需要编写一套Protobuf文件,就能够支持gRPC协议和RESTfull协议。Protobuf支持很多编程语言,比如:C++、Java、JavaScript、Python、Go、Ruby、Objective-C、C#……这也就意味着,它很适合多语言异构化架构,这样的场景在现实中是很稀松平常的,这使得Protobuf具有很强的实用性。Protobuf具有序列化后数据量更小、序列化/反序列化速度更快、更简单的特性;而JSON则相反,序列化后数据量较大,序列化和反序列化速度不优的特性,但是前端对JSON是原生支持,对前端极其友好。那么,我们可以在服务之间使用gRPC进行通讯,服务与前端之间可以通过RESTfull进行通讯。Protobuf和gRPC已经发展了许多年,极其稳定,生态链丰富。它具有强大的工具链可供使用,只要你想得到的,都能够找得到相对应的工具。没有合适的工具也没有关系,它的工具是使用插件方式来实现可扩展性的,因此我们可以容易的开发出自己的工具插件,Kratos就为此开发了自己的一系列的工具插件方便开发使用。综上,我们可知使用gRPC/protobuf的好处:一套proto,同时支持gRPC协议和RESTfull协议;支持多编程语言,适合多语言异构化架构;gRPC协议,数据量小、序列化/反序列化速度更快、更简单,适合服务之间通讯;RESTfull协议,数据量较大、序列化/反序列化速度较慢、前端原生支持JSON,适合同前端的通讯。强大的工具链,使用插件的方式实现强大的可扩展性,可方便的扩展。那么,这篇文章将会带来一些什么呢?Protobuf设计API的一丢丢基本知识;相关工具链的使用方法;如何实施工程化的方法。工具安装工欲善其事,必先利其器。让我们先安装所需要的工具。安装 protocprotoc是一款用C++编写的工具,其可以将proto文件翻译为指定语言的代码。具体用法可以使用protoc --help命令查看。goctl一键安装$ goctl env check -i -f --verbose [goctl-env]: preparing to check env [goctl-env]: looking up "protoc" [goctl-env]: "protoc" is not found in PATH [goctl-env]: preparing to install "protoc" "protoc" installed from cache [goctl-env]: "protoc" is already installed in "/Users/keson/go/bin/protoc"macOS安装brew install protobufUbuntu安装sudo apt update; sudo apt upgrade sudo apt install libprotobuf-dev protobuf-compiler非Windows系统源代码安装进入 protobuf release 下载页面下载;解压并进入文件夹:tar -xzvf protobuf-cpp-x.x.x.tar.gz cd protobuf-cpp-x.x.x设置编译目录./configure --prefix=/usr/local/protobuf安装检测make check安装及编译make && make install配置环境变量vim ~/.bash_profile在文件结尾添加环境变量export PROTOBUF=/usr/local/protobuf export PATH=$PATH:$PROTOBUF/bin使用source命令,使配置文件生效source ~/.bash_profile非Windows系统源二进制文件安装进入 protobuf release 下载页面,选择适合自己操作系统的压缩包文件下载;解压文件:tar -xzvf protoc-x.x.x-{OS}-x86_64.tar.gz拷贝protoc文件cd protoc-x.x.x-{OS}-x86_64/bin sudo chmod a+x protoc mv protoc /usr/local/bin拷贝头文件cd protoc-x.x.x-{OS}-x86_64/include cp google /usr/local/includeWindows安装在Windows下可以使用包管理器Choco和Scoop来安装。Chocochoco install protocScoopscoop bucket add extras scoop install protobuf后端工具后端工具都可以使用go install进行安装:用于生成struct代码:go install google.golang.org/protobuf/cmd/protoc-gen-go@latest用于生成grpc服务代码:go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest用于生成rest服务代码:go install github.com/go-kratos/kratos/cmd/protoc-gen-go-http/v2@latest用于生成kratos的错误定义代码:go install github.com/go-kratos/kratos/cmd/protoc-gen-go-errors/v2@latest用于生成消息验证器代码:go install github.com/envoyproxy/protoc-gen-validate@latest用于生成OpenAPI V2文档:go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest用于生成OpenAPI V3文档:go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest前端工具这是protobuf.js提供的一个Protobuf转换为Typescript的工具:pnpm i pbts -g另,我还找到一个基于pbts开发的在线工具:https://pb.brandonxiang.top/设计API在开始前,首先要说明的是,本文并不是一个Protobuf或者gRPC的教程,这方面,谷歌官方以及其他第三方(gRPC-Gateway)提供的资料已经足够详尽了:Protocol Buffers DocumentationgRPC DocumentationgRPC-Gateway DocumentationCURD在现实场景下,业务代码写得最多的恐怕还属CURD(增、删、改、查)了,不说多,80%是肯定有的,可以说,只要搞定了CURD,就搞定了大部分的业务代码的编写。以下是一个gRPC官方提供的示例,是一个书店的接口,里面包含了基本的Protobuf的语法和用法,以及gRPC服务和REST服务的设计。syntax = "proto3"; package endpoints.examples.bookstore; option java_multiple_files = true; option java_outer_classname = "BookstoreProto"; option java_package = "com.google.endpoints.examples.bookstore"; option go_package = "endpoints/examples/bookstore;bookstore"; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; // A simple Bookstore API. // // The API manages shelves and books resources. Shelves contain books. service Bookstore { // Returns a list of all shelves in the bookstore. rpc ListShelves(google.protobuf.Empty) returns (ListShelvesResponse) { // Define HTTP mapping. // Client example (Assuming your service is hosted at the given 'DOMAIN_NAME'): // curl http://DOMAIN_NAME/v1/shelves option (google.api.http) = { get: "/v1/shelves" }; } // Creates a new shelf in the bookstore. rpc CreateShelf(CreateShelfRequest) returns (Shelf) { // Client example: // curl -d '{"theme":"Music"}' http://DOMAIN_NAME/v1/shelves option (google.api.http) = { post: "/v1/shelves" body: "shelf" }; } // Returns a specific bookstore shelf. rpc GetShelf(GetShelfRequest) returns (Shelf) { // Client example - returns the first shelf: // curl http://DOMAIN_NAME/v1/shelves/1 option (google.api.http) = { get: "/v1/shelves/{shelf}" }; } // Deletes a shelf, including all books that are stored on the shelf. rpc DeleteShelf(DeleteShelfRequest) returns (google.protobuf.Empty) { // Client example - deletes the second shelf: // curl -X DELETE http://DOMAIN_NAME/v1/shelves/2 option (google.api.http) = { delete: "/v1/shelves/{shelf}" }; } } // A shelf resource. message Shelf { // A unique shelf id. int64 id = 1; // A theme of the shelf (fiction, poetry, etc). string theme = 2; } // Response to ListShelves call. message ListShelvesResponse { // Shelves in the bookstore. repeated Shelf shelves = 1; } // Request message for CreateShelf method. message CreateShelfRequest { // The shelf resource to create. Shelf shelf = 1; } // Request message for GetShelf method. message GetShelfRequest { // The ID of the shelf resource to retrieve. int64 shelf = 1; } // Request message for DeleteShelf method. message DeleteShelfRequest { // The ID of the shelf to delete. int64 shelf = 1; }需要说明的是,REST的接口是由google.api.http这个option提供的。上面这一套接口定义,既可以生成gRPC的服务,又可以生成REST的服务,而这是根据protoc调用的插件决定的,这方面内容不是这部分所要阐述的,暂且不表,且看后面部分。Kratos Errors在实际应用当中,存在着一个问题:gRPC状态码 和 REST HTTP状态码 是不一样的。为了解决这个问题,就需要一个映射表,用来互相转换状态码。以下就是一个映射表的示例:syntax = "proto3"; // 定义包名 package api.kratos.v1; import "errors/errors.proto"; // 多语言特定包名,用于源代码引用 option go_package = "kratos/api/helloworld;helloworld"; option java_multiple_files = true; option java_package = "api.helloworld"; enum ErrorReason { // 设置缺省错误码 option (errors.default_code) = 500; // 为某个枚举单独设置错误码 USER_NOT_FOUND = 0 [(errors.code) = 404]; CONTENT_MISSING = 1 [(errors.code) = 400]; }它利用了Protobuf的enum和option关键字实现了这样一个状态码的映射。再由protoc插件生成的代码实现映射和互换。Message Validator在实际应用当中,需要对接口的参数进行一些校验,比如:用户名的长度只能够大于或者小于某一个长度,身份证、手机号、EMail等特定格式的有效校验。其实,都不过是一些字符串、数字类型和布尔类型校验的简单规则。如果手写校验代码,都是一些机械无比的重复代码,而且要作修改起来也很痛苦。那么,有什么办法可以解决这个问题吗?必须有:规则写在Protobuf里面,利用proto-gen-validate插件生成代码,使用 Kratos Validate 中间件 作支持。以下是proto-gen-validate插件的示例接口:syntax = "proto3"; package examplepb; import "validate/validate.proto"; message Person { uint64 id = 1 [(validate.rules).uint64.gt = 999]; string email = 2 [(validate.rules).string.email = true]; string name = 3 [(validate.rules).string = { pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$", max_bytes: 256, }]; Location home = 4 [(validate.rules).message.required = true]; message Location { double lat = 1 [(validate.rules).double = {gte: -90, lte: 90}]; double lng = 2 [(validate.rules).double = {gte: -180, lte: 180}]; } }只需要利用validate.rulesoption就可以定义规则了,简单明了,又方便。OpenAPIOpenAPI是一个用于描述REST API的描述格式,包含端点、参数、输入输出格式、说明、认证等,本质上它是一个JSON或者YAML格式文档,而文件内的Schema则是有OpenAPI所定义的。以下是一个OpenAPI v3的JSON文件范例:{ "openapi": "3.0", "info": { "version": "1.0.0", "title": "OpenAPI Petstore", "license": { "name": "MIT" } }, "servers": [ { "url": "https://petstore.openapis.org/v1", "description": "Development server" } ], "paths": { "/pets": { "get": { "summary": "List all pets", "operationId": "listPets", "tags": [ "pets" ], "parameters": [ { "name": "limit", "in": "query", "description": "How many items to return at one time (max 100)", "required": false, "schema": { "type": "integer", "format": "int32" } } ], "responses": { "200": { "description": "An paged array of pets", "headers": { "x-next": { "schema": { "type": "string" }, "description": "A link to the next page of responses" } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pets" } } } }, "default": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } }, "post": { "summary": "Create a pet", "operationId": "createPets", "tags": [ "pets" ], "responses": { "201": { "description": "Null response" }, "default": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/pets/{petId}": { "get": { "summary": "Info for a specific pet", "operationId": "showPetById", "tags": [ "pets" ], "parameters": [ { "name": "petId", "in": "path", "required": true, "description": "The id of the pet to retrieve", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Expected response to a valid request", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pets" } } } }, "default": { "description": "unexpected error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } } }, "components": { "schemas": { "Pet": { "required": [ "id", "name" ], "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" }, "tag": { "type": "string" } } }, "Pets": { "type": "array", "items": { "$ref": "#/components/schemas/Pet" } }, "Error": { "required": [ "code", "message" ], "properties": { "code": { "type": "integer", "format": "int32" }, "message": { "type": "string" } } } } } }以及OpenAPI v3 的 YAML文件范例:openapi: "3.0" info: version: 1.0.0 title: OpenAPI Petstore license: name: MIT servers: - url: https://petstore.openapis.org/v1 description: Development server paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit in: query description: How many items to return at one time (max 100) required: false schema: type: integer format: int32 responses: "200": description: An paged array of pets headers: x-next: schema: type: string description: A link to the next page of responses content: application/json: schema: $ref: '#/components/schemas/Pets' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' post: summary: Create a pet operationId: createPets tags: - pets responses: "201": description: Null response default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' /pets/{petId}: get: summary: Info for a specific pet operationId: showPetById tags: - pets parameters: - name: petId in: path required: true description: The id of the pet to retrieve schema: type: string responses: "200": description: Expected response to a valid request content: application/json: schema: $ref: '#/components/schemas/Pets' default: description: unexpected error content: application/json: schema: $ref: '#/components/schemas/Error' components: schemas: Pet: required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string Pets: type: array items: $ref: '#/components/schemas/Pet' Error: required: - code - message properties: code: type: integer format: int32 message: type: string以上文本当中的Schema,有些可以望文生义,也有一些根本看不出来意义。可是,真要让人去阅读,只会有一个感受:头大。它主要还是给程序读取的,展现在UI之上,才能够让人感受到愉快。现在,市面上有非常非常多的工具可以读取OpenAPI JSON / YAML文档:Swagger UI / SwaggerHub / Swagger EditorRedoc / RedoclyStoplight Elements / StoplightReadMe DocumentationEolinkYApiPostmanApifox这些工具当中,最常见的是本家的Swagger UI(OpenAPI在成为开放标准之前是Swagger产品线当中的一部分),Kratos原生支持Swagger UI:https://github.com/go-kratos/swagger-api在本文接着后面,我要着重讲的,要推荐的是国产神器:Apifox。我这人对国产软件一向都是抱有藐视的态度,但是Apifox是真好使,绝对的开发利器,使得我一改对国产软件的态度,大力推荐。现在OpenAPI有两个版本:v2和v3。主流的protoc插件也刚好对应有两个:OpenAPI v2使用grpc-gateway出的protoc-gen-openapiv2;OpenAPI v3使用谷歌出品的gnostic下的protoc-gen-openapi。正常来说,只要是使用了google.api.http这个option定义的API,使用这两个插件就能够生成OpenAPI的文档。但是,实际应用中,我们还希望能够提供更多更丰富的一些信息,比如:描述信息、版本号、版权信息、认证信息……显然,光凭着google.api.http的定义是不够的。这两个插件提供了各自的option,可以定义这些信息。我们可以看一看都是怎样定义的:OpenAPI v2syntax = "proto3"; package grpc.gateway.examples.internal.proto.examplepb; import "protoc-gen-openapiv2/options/annotations.proto"; option go_package = "github.com/grpc-ecosystem/grpc-gateway/v2/examples/internal/proto/examplepb"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "A Bit of Everything"; version: "1.0"; contact: { name: "gRPC-Gateway project"; url: "https://github.com/grpc-ecosystem/grpc-gateway"; email: "none@example.com"; }; license: { name: "BSD 3-Clause License"; url: "https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt"; }; extensions: { key: "x-something-something"; value { string_value: "yadda"; } } }; // Overwriting host entry breaks tests, so this is not done here. external_docs: { url: "https://github.com/grpc-ecosystem/grpc-gateway"; description: "More about gRPC-Gateway"; } schemes: HTTP; schemes: HTTPS; schemes: WSS; consumes: "application/json"; consumes: "application/x-foo-mime"; produces: "application/json"; produces: "application/x-foo-mime"; security_definitions: { security: { key: "BasicAuth"; value: { type: TYPE_BASIC; } } security: { key: "ApiKeyAuth"; value: { type: TYPE_API_KEY; in: IN_HEADER; name: "X-API-Key"; extensions: { key: "x-amazon-apigateway-authtype"; value { string_value: "oauth2"; } } extensions: { key: "x-amazon-apigateway-authorizer"; value { struct_value { fields { key: "type"; value { string_value: "token"; } } fields { key: "authorizerResultTtlInSeconds"; value { number_value: 60; } } } } } } } security: { key: "OAuth2"; value: { type: TYPE_OAUTH2; flow: FLOW_ACCESS_CODE; authorization_url: "https://example.com/oauth/authorize"; token_url: "https://example.com/oauth/token"; scopes: { scope: { key: "read"; value: "Grants read access"; } scope: { key: "write"; value: "Grants write access"; } scope: { key: "admin"; value: "Grants read and write access to administrative information"; } } } } } security: { security_requirement: { key: "BasicAuth"; value: {}; } security_requirement: { key: "ApiKeyAuth"; value: {}; } } security: { security_requirement: { key: "OAuth2"; value: { scope: "read"; scope: "write"; } } security_requirement: { key: "ApiKeyAuth"; value: {}; } } responses: { key: "403"; value: { description: "Returned when the user does not have permission to access the resource."; } } responses: { key: "404"; value: { description: "Returned when the resource does not exist."; schema: { json_schema: { type: STRING; } } } } responses: { key: "418"; value: { description: "I'm a teapot."; schema: { json_schema: { ref: ".grpc.gateway.examples.internal.proto.examplepb.NumericEnum"; } } } } responses: { key: "500"; value: { description: "Server error"; headers: { key: "X-Correlation-Id" value: { description: "Unique event identifier for server requests" type: "string" format: "uuid" default: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$" } }; schema: { json_schema: { ref: ".grpc.gateway.examples.internal.proto.examplepb.ErrorResponse"; } } } } tags: { name: "echo rpc" description: "Echo Rpc description" extensions: { key: "x-traitTag"; value { bool_value: true; } } } extensions: { key: "x-grpc-gateway-foo"; value { string_value: "bar"; } } extensions: { key: "x-grpc-gateway-baz-list"; value { list_value: { values: { string_value: "one"; } values: { bool_value: true; } } } } }; message ErrorResponse { string correlationId = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$", title: "x-correlation-id", description: "Unique event identifier for server requests", format: "uuid", example: "\"2438ac3c-37eb-4902-adef-ed16b4431030\"" }]; ErrorObject error = 2; } message ErrorObject { int32 code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[0-9]$", title: "code", description: "Response code", format: "integer" }]; string message = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { pattern: "^[a-zA-Z0-9]{1, 32}$", title: "message", description: "Response message" }]; } // ABitOfEverything service is used to validate that APIs with complicated // proto messages and URL templates are still processed correctly. service ABitOfEverythingService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_tag) = { description: "ABitOfEverythingService description -- which should not be used in place of the documentation comment!" external_docs: { url: "https://github.com/grpc-ecosystem/grpc-gateway"; description: "Find out more about EchoService"; } }; // Create a new ABitOfEverything // // This API creates a new ABitOfEverything rpc Create(ABitOfEverything) returns (ABitOfEverything) { option (google.api.http) = { post: "/v1/example/a_bit_of_everything/{float_value}/{double_value}/{int64_value}/separator/{uint64_value}/{int32_value}/{fixed64_value}/{fixed32_value}/{bool_value}/{string_value=strprefix/*}/{uint32_value}/{sfixed32_value}/{sfixed64_value}/{sint32_value}/{sint64_value}/{nonConventionalNameValue}/{enum_value}/{path_enum_value}/{nested_path_enum_value}/{enum_value_annotation}" }; } rpc CreateBody(ABitOfEverything) returns (ABitOfEverything) { option (google.api.http) = { post: "/v1/example/a_bit_of_everything" body: "*" }; } }OpenAPI v3syntax = "proto3"; package tests.openapiv3annotations.message.v1; import "google/api/annotations.proto"; import "openapiv3/annotations.proto"; option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/openapiv3annotations/message/v1;message"; option (openapi.v3.document) = { info: { title: "Title from annotation"; version: "Version from annotation"; description: "Description from annotation"; contact: { name: "Contact Name"; url: "https://github.com/google/gnostic"; email: "gnostic@google.com"; } license: { name: "Apache License"; url: "https://github.com/google/gnostic/blob/master/LICENSE"; } } components: { security_schemes: { additional_properties: [ { name: "BasicAuth"; value: { security_scheme: { type: "http"; scheme: "basic"; } } } ] } } }; service Messaging1 { rpc UpdateMessage(Message) returns(Message) { option(google.api.http) = { patch: "/v1/messages/{message_id}" body: "*" }; option(openapi.v3.operation) = { security: [ { additional_properties: [ { name: "BasicAuth"; value: { value: [] } } ] } ] }; } } service Messaging2 { rpc UpdateMessage(Message) returns (Message) {} } message Message { option (openapi.v3.schema) = { title: "This is an overridden message schema title"; }; int64 id = 1; string label = 2 [ (openapi.v3.property) = { title: "this is an overriden field schema title"; max_length: 255; } ]; }管理生成APIProtobuf生成代码使用的工具是protoc,它是基于插件机制开发的,实际生成代码全靠插件,生成代码的命令如下所示:生成 go 代码(struct和enum等基础类型)protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto生成 grpc 服务代码protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto生成 rest 服务代码protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto生成 gRPC状态码映射代码protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto生成 消息参数校验代码protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto生成 OpenAPI v2 json文档protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto生成 OpenAPI v3 yaml文档protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto插件生成文件一览表插件名生成文件名protoc-gen-goXXXXX.pb.goprotoc-gen-go-grpcXXXXXX_grpc.pb.goprotoc-gen-go-httpXXXXXX_http.pb.goprotoc-gen-go-errorsXXXXXX_errors.pb.goprotoc-gen-validateXXXXXX.pb.validate.goprotoc-gen-openapiv2XXXXXX.swagger.jsonprotoc-gen-openapiopenapi.yaml这里要提醒一下,细心的你一定会发现,生成OpenAPI文档的参数里面各有一个--openapiv2_opt json_names_for_fields=true和--openapi_out=naming=json,这两个参数的作用是一样的,那么它们是做什么用的呢?我们先来看下面这个消息定义:// NonStandardMessageWithJSONNames maps odd field names to odd JSON names for maximum confusion. message NonStandardMessageWithJSONNames { // Id represents the message identifier. string id = 1 [json_name = "ID"]; int64 Num = 2 [json_name = "Num"]; int64 line_num = 3 [json_name = "LineNum"]; string langIdent = 4 [json_name = "langIdent"]; string STATUS = 5 [json_name = "status"]; int64 en_GB = 6 [json_name = "En_GB"]; string no = 7 [json_name = "yes"]; message Thing { message SubThing { string sub_value = 1 [json_name = "sub_Value"]; } SubThing subThing = 1 [json_name = "SubThing"]; } Thing thing = 8 [json_name = "Thingy"]; }你一定发现了json_name这个参数,没错,就是为了它,proto那两个参数就是它的开关。如果,字段定义了json_name参数之后,REST的JSON字段名便会采用json_name所定义的字段名。这是一个非常有用的特性,因为前后端的命名规则不一致是常态,golang用的是驼峰命名法,而前端用蛇形命名法的是很多,这就可以用上了。实施工程化好,我们现在已经知道如何去生成API的代码和文档了。但是,这还远远不够。因为我们不可能每次都去手打命令生成代码,这是不科学,不人道的,不现实的。我们需要工程化,使之可管理。CI/CD、自动化也能够实现。首先,我们把可用的方法列举出来,然后再一个个的讲解各个方法:BAT批处理脚本(Windows)或者Shell脚本(非Windows);Makefile;go:generate注解;buf.build。结论在前:推荐使用buf.buildBAT批处理脚本(Windows)或者Shell脚本(非Windows)BAT批处理脚本:: generate go struct code protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto :: generate grpc service code protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto :: generate rest service code protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto :: generate kratos errors code protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto :: generate message validator code protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto :: generate openapi v2 json doc protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto :: generate openapi v3 yaml doc protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.protoShell脚本#!/bin/bash # generate go struct code protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto # generate grpc service code protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto # generate rest service code protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto # generate kratos errors code protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto # generate message validator code protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto # generate openapi v2 json doc protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto # generate openapi v3 yaml doc protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto这个方法除了能用,没有别的好处了。它需要在每一组proto文件的同级目录下都冗余放一对脚本,如果要执行所有的生成脚本,另外还需要写一个脚本来调用生成脚本,维护起来很痛苦。2. MakefileKratos官方layout就是使用的Makefile的方法来生成代码的。它在根目录下的Makefile文件里:.PHONY: api # generate api proto api: protoc --proto_path=./api \ --proto_path=./third_party \ --go_out=paths=source_relative:./api \ --go-http_out=paths=source_relative:./api \ --go-grpc_out=paths=source_relative:./api \ --openapi_out=fq_schema_naming=true,default_response=false:. \ $(API_PROTO_FILES) .PHONY: conf # generate config define code conf: protoc --proto_path=. \ --proto_path=../../../third_party \ --go_out=paths=source_relative:. \ ./internal/conf/*.proto根目录下的Makefile由app\{服务名}\service\Makefile引用,调用者在服务目录app\{服务名}\service\下调用make api执行代码生成。这个方法很有局限性,掣手掣脚,你只能够依照严格的固定的项目结构来,只要有一些变动就完犊子了。MonoRepo的项目结构下,因为会有多个Makefile入口,所以没办法一键执行全部的Makefile,必须借助第三方工具,比如Shell脚本。偷懒如我,总觉得很麻烦。3. go:generate注解go1.4版本之后,可以通过go generate命令执行一些go:generate注解下的预处理命令,可以拿来生成API代码之用。因为在非Windows系统下,命令如果带通配符,会执行出错,需要加sh -c才行,而Windows系统不存在这样的问题,可以直接执行,所以需要使用go:build注解来区分操作系统,go generate命令会根据操作系统执行相对应的go代码文件。所以,我写了两个go文件:generate_windows.go//go:build windows // generate go struct code //go:generate protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto // generate grpc service code //go:generate protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto // generate rest service code //go:generate protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto // generate kratos errors code //go:generate protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto // generate message validator code //go:generate protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto // generate openapi v2 json doc //go:generate protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto // generate openapi v3 yaml doc //go:generate protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto package apigenerate_xnix.go//go:build !windows // +build !windows // generate go struct code //go:generate sh -c "protoc --proto_path=. --go_out=paths=source_relative:../ ./*.proto" // generate grpc service code //go:generate sh -c "protoc --proto_path=. --go-grpc_out=paths=source_relative:../ ./*.proto" // generate rest service code //go:generate sh -c "protoc --proto_path=. --go-http_out=paths=source_relative:../ ./*.proto" // generate kratos errors code //go:generate sh -c "protoc --proto_path=. --go-errors_out=paths=source_relative:../ ./*.proto" // generate message validator code //go:generate sh -c "protoc --proto_path=. --validate_out=paths=source_relative,lang=go:../ ./*.proto" // generate openapi v2 json doc //go:generate sh -c "protoc --proto_path=. --openapiv2_out=paths=source_relative:../ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=true ./*.proto" // generate openapi v3 yaml doc //go:generate sh -c "protoc --proto_path=. --openapi_out=naming=json=paths=source_relative:../ ./*.proto" package api它可以很好的完成生成代码的任务。主流的IDE(Goland、VSC)都可以很好的支持编辑界面执行注解。要自动化吧,也能实现,只要在项目根目录执行go generate ./...就能够执行整个项目的go:generate注解。但是,有一个很大的问题,它需要在每一组proto文件的同级目录下冗余一套go代码,维护起来就比较糟心了。4. buf.buildbuf.build是专门编译管理protobuf API的工具。它总共有3组配置文件:buf.work.yaml、buf.gen.yaml、buf.yaml。另外,还有一个buf.lock文件,但是它不需要进行人工配置,它是由buf mod update命令所生成。这跟前端的npm、yarn等的lock文件差不多,golang的go.sum也差不多。它的配置文件不多,也不复杂,维护起来非常方便,支持远程proto插件,支持远程第三方proto。对构建系统Bazel支持很好,对CI/CD系统也支持得很好。它还有很多优秀的特性。buf.build非常棒,用它,很方便。值得使用,值得推荐。buf.work.yaml它一般放在项目的根目录下面,它代表的是一个工作区,通常一个项目也就一个该配置文件。该配置文件最重要的就是directories配置项,列出了要包含在工作区中的模块的目录。目录路径必须相对于buf.work.yaml,像../external就是一个无效的配置。version: v1 directories: - api - third_partybuf.gen.yaml它一般放在buf.work.yaml的同级目录下面,它主要是定义一些protoc生成的规则和插件配置。# 配置protoc生成规则 version: v1 managed: enabled: false plugins: # generate go struct code - name: go out: gen/api/go opt: paths=source_relative # generate grpc service code - name: go-grpc out: gen/api/go opt: - paths=source_relative # generate rest service code - name: go-http out: gen/api/go opt: - paths=source_relative # generate kratos errors code - name: go-errors out: gen/api/go opt: - paths=source_relative # generate message validator code - name: validate out: gen/api/go opt: - paths=source_relative - lang=gobuf.yaml它放置的路径,你可以视之为protoc的--proto-path参数指向的路径,也就是proto文件里面import的相对路径。需要注意的是,buf.work.yaml的同级目录必须要放一个该配置文件。该配置文件的内容通常来说都是下面这个配置,不需要做任何修改,需要修改的情况不多。version: v1 deps: breaking: use: - FILE lint: use: - DEFAULT生成代码我有开源了一个Kratos的CMS项目kratos-blog,它是一个MonoRepo结构的项目,我们以它的项目结构来做讲解。下面的目录树,是我化简后的目录树。. ├── buf.work.yaml ├── buf.gen.yaml ├── buf.yaml ├── buf.lock ├── api │ ├── admin │ │ └── service │ │ └── v1 │ │ └── admin_errors.proto │ │ └── buf.openapi.gen.yaml │ │ └── i_user.proto │ └── buf.yaml └── third_party ├── errors │ └── errors.proto ├── google ├── openapiv3 ├── protoc-gen-openapiv2 ├── validate └── buf.yaml大家可以看到,我只在根目录、api、third_party放了3个buf.yaml,整体需求的配置文件并不多。buf.build使用buf generate命令进行构建,调用该命令必须在buf.work.yaml的同级目录下。执行了buf generate命令之后,将会在根目录下产生一个gen/api/go的文件夹,生成的代码都将被放在了这个目录下。细心的你肯定早就发现了在api/admin/service/v1下面有一个buf.openapi.gen.yaml的配置文件,这是什么配置文件呢?我现在把该配置文件放出来:# 配置protoc生成规则 version: v1 managed: enabled: false plugins: # generate openapi v2 yaml doc - name: openapi out: gen/api/go/admin/service/v1 opt: - naming=json - paths=source_relative没错,它是为了生成OpenAPI v3文档。我之前尝试了放在根目录下的buf.gen.yaml,但是产生了错误,因为OpenAPI v3文档,它全局只能产生一个openapi.yaml文件。所以,没辙,只能单独对待了。那么,怎么使用这个配置文件呢?还是使用buf generate命令,但是得带参数:buf generate --path api/admin/service/v1 --template api/admin/service/v1/buf.openapi.gen.yaml该命令还是在项目根目录下执行。与前端协同与前端协同,全靠一点:OpenAPI。前端只要拿到了OpenAPI的文档,他就可以开始上手干活了。在这里,我只介绍两个工具的使用:ApifoxpbtsApifox我在前面提到的那些支持OpenAPI的工具,随便拿出来一样都很好使。但本文只介绍国产神器Apifox。为什么要推荐它呢?有这么几点让我很爽:可以方便的导入导出OpenAPI文档;可以不需要配置Mock就可以使用MockServer,大部分的字段其实都不需要格外去配置,需要配置的字段其实只有微乎其微,而且就算是配置起来也很容易,这极大的提高了开发效率;MockServer支持本地和云端,当团队成员在异地的时候,当我们需要向客户或者领导演示的时候,云端Mock都很好使;同时还支持自动化测试。导入OpenAPI是很简单的,它支持手动和自动,手动就是自己拖动OpenAPI文档进来,一次性导入;自动就是通过url自动导入,它会定时导入,这样接口修改了也不用管了,像不像导弹的射后不管?手动导入的界面如下:自动导入的界面如下:pbtspbts是protobuf.js提供的一个Protobuf转Typescript的工具。对于自由惯了的前端程序员来说,这会让他很不解,难受,觉得束手束脚的——都已经有了OpenAPI了,还要这个作甚?要的就是约束。这么一个场景,我有一个协议做了修改,字段增删改了,但是,我代码里面依赖这个协议的地方很多,如果没有这个约束,改变了IDE也没有办法感知到,现在有了约束之后,IDE立马就可以感知到,并且提醒给前端程序员,循着这个提示去修改代码就变得很轻松了,不至于让一些隐藏很深的bug隐匿在深处,寻,也寻不到。pbts的功能其实还很不够,比如,无法把REST的路径导出。比如,生成协议的REST客户端代码。如果有这样一个工具,必将事半功倍。pbts命令的使用非常简单,就是只能一次处理一个proto,需要写一个脚本才好:pbts convert -i ./admin.proto -o ts/admin.d.ts参考资料mac安装包安装 protocOpenAPI 打通前後端任督二脈
怎么样在Windows下使用Make编译Golang程序GNU的Make是一个又古老又强大的构建工具,就Makefile的语法而言也不算复杂,没有特别复杂的需求的话,拿来做程序构建是一个好主意。更复杂一点的构建就可以选择Google的Bazel,但是通常的工程都没有这么复杂的需求。在Unix、Linux、BSD、macOS使用Make是很方便的,很自然的。可是,在Windows下面却存在着兼容性的问题,在其他操作系统可以顺利执行的Makefile,在Windows却跑不了。这体验很糟糕。虽然微软在努力解决不兼容的问题,比如最新的PowerShell,但是操作系统毕竟还是存在着巨大的差异,要完全解决几乎是不可能完成的任务。所以,在Makefile多少是需要做一些适配。安装Make常规的做法,大家都是使用安装MinGW包的方法来安装Make,但是这很繁琐,我并不推荐,我推荐使用Choco和Scoop来安装管理Make。ChocoPowerShell安装Choco:Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))管理员权限启动PowerShell,然后运行以下命令进行安装:choco install make安装其他有用的工具:choco install grep awk sed touchscoop安装Scoop:irm get.scoop.sh | iexscoop install make安装其他有用的工具:scoop install grep gawk sed touchMinGW下载安装MinGW:https://www.mingw-w64.org/downloads/下载好之后,解压,把bin文件夹加入到Windows的环境变量里面,使之可以全局运行。然后,还需要把mingw32-make.exe文件改名成make.exe。此方法比较繁琐,不如使用软件包管理器去直接安装make还方便多了。另,通过软件包管理也能够安装mingw:choco install mingw scoop install mingwWindows和Linux之间使用Make的区别在Windows下使用Make和在Linux下面使用Make是有区别的,而这个差异性倒不是来自于Make,而是来自于依赖的命令。Bash里面很多的命令,在CMD下都没有,PowerShell倒是增加了一些,像:man、ls、rm、pwd这些常用的倒是都已经有了,但是差异还是有,兼容性永远是个大问题。常用的Linux工具倒是有,像grep、awk、sed……都可以通过上面的软件管理器choco、scoop安装到。但是,像uname这些平台严重相关的命令是肯定没有办法的。简单举一些例子:文件路径分隔符,Windows是\,而Linux是/;Linux的mkdir是有-p选项的,而Windows没有。echo的行为也跟Linux的不同。现在PowerShell倒是在提高与Bash的兼容性,但是毕竟系统差异性太大,兼容性是肯定存在的,那么,怎么办呢?我们可以在Makefile里面判断操作系统的版本,来做差异化处理。简单的探测系统版本:detected_OS := ifeq ($(OS),Windows_NT) detected_OS := Windows else detected_OS := $(shell sh -c 'uname 2>/dev/null || echo Unknown') endif all: @echo $(detected_OS)使用Makefile编译Golang程序下面以一个简单的编译Golang程序的Makefile来讲解如何跨平台使用Makefile进行交叉编译:GOPATH=$(shell go env GOPATH) GOARCH?=$(shell go env GOARCH) ifeq ($(OS),Windows_NT) IS_WINDOWS := 1 endif BUILD_CMD = $(if $(IS_WINDOWS), \ SET CGO_ENABLED=0&SET GOOS=$(1)&SET GOARCH=$(2)&go build -o .\bin\$(1)_$(2)\$(3), \ CGO_ENABLED=0 GOOS=$(1) GOARCH=$(2) go build -o ./bin/$(1)_$(2)/$(3)) linux: $(call BUILD_CMD,linux,$(GOARCH),test) windows: echo $(IS_WINDOWS) $(call BUILD_CMD,windows,$(GOARCH),test.exe) mac: $(call BUILD_CMD,darwin,$(GOARCH),test)上面这段代码里面使用ifeq ($(OS),Windows_NT)来判断操作系统,得到一个IS_WINDOWS的变量。然后,定义了一个BUILD_CMD的函数,它调用了内置的if函数,它的语法是:$(if <condition>,<then-part>,<else-part>)第一个分支是走的Windows的编译,第二个分支是走的其他操作系统的编译。需要注意的是:路径分隔符,参数设置前面需要加SET,语句之间需要用&间隔。参考资料跟我一起写MakefileChocoScoopmingw
如何在Word文档中批量添加汉字注音所谓的汉字注音,就是给汉字上方加注拼音。在Office里面,这个功能叫做 “拼音指南”(Phonetic Guide)。拼音指南一次只能够处理最多30个字,一篇文章不可能只有30个字,上百个字是很正常的,人工处理就会很累。所以,需要做到自动化,做到自动化有两种方式可以做到:调用Office的功能;直接修改docx文档。调用Office的功能调用Office的功能又有两个途径:VBA;.net。其实,这两种途径最终都是调用的Office提供的API。VBA我查过了VBA的资料,总共有3个API可用:FormatPhoneticGuideRange.PhoneticGuide method (Word)Application.GetPhonetic method (Excel)网上最多的用是第一种,使用FormatPhoneticGuide宏,我试过是能用的,但是存在着一个很大的问题:它不能够定制拼音的样式。而且,相对来说不够稳定。'Word批量使用默认样式加注拼音 Sub BatchAddPinYinByDefaultStyle() On Error Resume Next Selection.WholeStory TextLength = Selection.Characters.Count Selection.EndKey For i = TextLength To 0 Step -30 If i < 30 Then Selection.MoveLeft Unit:=wdCharacter, Count:=i Selection.MoveRight(Unit:=wdCharacter, Count:=i,Extend:=wdExtend) Else Selection.MoveLeft Unit:=wdCharacter, Count:=30 Selection.MoveRight(Unit:=wdCharacter, Count:=30,Extend:=wdExtend) End If SendKeys "{Enter}" Application.Run "FormatPhoneticGuide" Next Selection.WholeStory End Sub另外还有一个清除注音的方法,用到了第二个API:'Word批量清除拼音 Sub CleanPinYin() Application.ScreenUpdating = False Selection.WholeStory TextLength = Selection.Characters.Count Selection.GoTo What:=wdGoToHeading, Which:=wdGoToAbsolute, Count:=1 For i = 0 To TextLength With Selection .Range.PhoneticGuide Text:="" End With Selection.MoveRight Unit:=wdCharacter, Count:=1 Next Selection.WholeStory Application.ScreenUpdating = True End Sub这一个API既可以清除注音,也可以标明注音。只需要给Text赋值拼音即可。这个API好在可以定制拼音的样式,麻烦的是需要自己去计算出拼音,本来是找到了一个计算拼音的内置方法:GetPhonetic,但是,它只存在于Excel里面,在Word里边无法进行调用。要实现内置的GetPhonetic,我在网上看到有两种实现方法:自行实现的VBA,但是实现不够完整:https://github.com/StinkCat/CH_TO_PY利用golang写了一个RestFull服务器提供服务,然后提供给VBA调用:https://github.com/yangjianhua/go-pinyin我们来讨论第二种方法,比较灵活。首先是golang的拼音计算服务:package main import ( "flag" "fmt" "strconv" "github.com/gin-gonic/gin" "github.com/mozillazg/go-pinyin" ) var a pinyin.Args func initPinyinArgs(arg int) { // arg should be pinyin.Tone, pinyin.Tone1, pinyin.Tone2, pinyin.Tone3, see go-pinyin doc a = pinyin.NewArgs() a.Style = arg } func getPinyin(c *gin.Context) { han := c.DefaultQuery("han", "") p := pinyin.Pinyin(han, a) c.JSON(200, gin.H{"code": 0, "data": p}) } func getPinyinOne(c *gin.Context) { han := c.DefaultQuery("han", "") p := pinyin.Pinyin(han, a) s := "" if len(p) > 0 { s = p[0][0] } c.JSON(200, gin.H{"code": 0, "data": s}) } func allowCors() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") if c.Request.Method == "OPTIONS" { c.AbortWithStatus(204) return } c.Next() } } func main() { // init pinyin output format initPinyinArgs(pinyin.Tone) fmt.Print("\n\nDEFAULT PORT: 8080, USING '-port portnum' TO START ANOTHER PORT.\n\n") port := flag.Int("port", 8080, "Port Number, default 8080") flag.Parse() sPort := ":" + strconv.Itoa(*port) // using gin as a web output r := gin.Default() r.Use(allowCors()) r.GET("/pinyin", getPinyin) // Call like GET http://localhost:8080/pinyin?han=我来了 r.GET("/pinyin1", getPinyinOne) r.Run(sPort) }接着,我们来封装自己的GetPhonetic:'从Json字符串中提取data字段的数据 Function getDataFromJSON(s As String) As String With CreateObject("VBScript.Regexp") .Pattern = """data"":""(.*)""" getDataFromJSON = .Execute(s)(0).SubMatches(0) End With End Function '使用http组件调用拼音转换服务获取拼音字符 Function GetPhonetic(strWord As String) As String Dim myURL As String Dim winHttpReq As Object Set winHttpReq = CreateObject("WinHttp.WinHttpRequest.5.1") myURL = "http://localhost:8080/pinyin1" myURL = myURL & "?han=" & strWord winHttpReq.Open "GET", myURL, False winHttpReq.Send GetPhonetic = getDataFromJSON(winHttpReq.responseText) End Function '测试GetPhonetic方法 Sub testGetPhonetic() ret = GetPhonetic("汗") MsgBox ret End Sub判定字符是否中文的方法:'判断传入的Unicode是否为中文字符 Function isChinese(uniChar As Integer) As Boolean isChinese = uniChar >= 19968 And uniChar <= 40869 End Function最后组装生成拼音注音的VBA脚本:'Word批量拼音注音 Sub BatchAddPinYin() Application.ScreenUpdating = False Dim SelectText As String Dim PinYinText As String Selection.WholeStory TextLength = Selection.Characters.Count Selection.GoTo What:=wdGoToHeading, Which:=wdGoToAbsolute, Count:=1 For i = 0 To TextLength Selection.MoveRight Unit:=wdCharacter, Count:=1 Selection.MoveLeft Unit:=wdCharacter, Count:=1, Extend:=wdExtend With Selection SelectText = .Text'基准文字 If isChinese(AscW(SelectText)) Then'判断是否为中文字符 PinYinText = GetPhonetic(SelectText)'基准文字 转换为 拼音文字 If PinYinText <> "" Then .Range.PhoneticGuide _ Text:=PinYinText, _'拼音文本 Alignment:=wdPhoneticGuideAlignmentCenter, _'对齐方式, see: https://learn.microsoft.com/en-us/office/vba/api/word.wdphoneticguidealignmenttype Raise:=0, _'偏移量(磅) FontSize:=10, _'字号(磅) FontName:="等线"'字体 End If End If End With Selection.MoveRight Unit:=wdCharacter, Count:=1 Next Selection.WholeStory Application.ScreenUpdating = True End Sub根据golang服务代码的提供者所说,它比较明显的缺点是对多音字的处理不如Word原来的拼音指南,所以需要后期进行手工校正。后期校正肯定是必须的,就好比古文里边还有一些通假字,发音是不一样的,这个,我想哪怕是拼音指南也做不好的吧。完整的BAS文件如下:'判断传入的Unicode是否为中文字符 Function isChinese(uniChar As Integer) As Boolean isChinese = uniChar >= 19968 And uniChar <= 40869 End Function '从Json字符串中提取data字段的数据 Function getDataFromJSON(s As String) As String With CreateObject("VBScript.Regexp") .Pattern = """data"":""(.*)""" getDataFromJSON = .Execute(s)(0).SubMatches(0) End With End Function '使用http组件调用拼音转换服务获取拼音字符 Function GetPhonetic(strWord As String) As String Dim myURL As String Dim winHttpReq As Object Set winHttpReq = CreateObject("WinHttp.WinHttpRequest.5.1") myURL = "http://localhost:8080/pinyin1" myURL = myURL & "?han=" & strWord winHttpReq.Open "GET", myURL, False winHttpReq.Send GetPhonetic = getDataFromJSON(winHttpReq.responseText) End Function '测试GetPhonetic方法 Sub testGetPhonetic() ret = GetPhonetic("汗") MsgBox ret End Sub 'Word批量拼音注音 Sub BatchAddPinYin() Application.ScreenUpdating = False Dim SelectText As String Dim PinYinText As String Selection.WholeStory TextLength = Selection.Characters.Count Selection.GoTo What:=wdGoToHeading, Which:=wdGoToAbsolute, Count:=1 For i = 0 To TextLength Selection.MoveRight Unit:=wdCharacter, Count:=1 Selection.MoveLeft Unit:=wdCharacter, Count:=1, Extend:=wdExtend With Selection SelectText = .Text'基准文字 If isChinese(AscW(SelectText)) Then'判断是否为中文字符 PinYinText = GetPhonetic(SelectText)'基准文字 转换为 拼音文字 If PinYinText <> "" Then .Range.PhoneticGuide _ Text:=PinYinText, _'拼音文本 Alignment:=wdPhoneticGuideAlignmentCenter, _'对齐方式, see: https://learn.microsoft.com/en-us/office/vba/api/word.wdphoneticguidealignmenttype Raise:=0, _'偏移量(磅) FontSize:=10, _'字号(磅) FontName:="等线"'字体 End If End If End With Selection.MoveRight Unit:=wdCharacter, Count:=1 Next Selection.WholeStory Application.ScreenUpdating = True End Sub 'Word批量使用默认样式加注拼音 Sub BatchAddPinYinByDefaultStyle() Application.ScreenUpdating = False On Error Resume Next Selection.WholeStory TextLength = Selection.Characters.Count Selection.EndKey For i = TextLength To 0 Step -30 If i <= 30 Then Selection.MoveLeft Unit:=wdCharacter, Count:=i SelectText = Selection.MoveRight(Unit:=wdCharacter, Count:=i,Extend:=wdExtend) Else Selection.MoveLeft Unit:=wdCharacter, Count:=30 SelectText = Selection.MoveRight(Unit:=wdCharacter, Count:=30,Extend:=wdExtend) End If SendKeys "{Enter}" Application.Run "FormatPhoneticGuide" Next Selection.WholeStory Application.ScreenUpdating = True End Sub 'Word批量清除拼音注音 Sub CleanPinYin() Application.ScreenUpdating = False Selection.WholeStory TextLength = Selection.Characters.Count Selection.GoTo What:=wdGoToHeading, Which:=wdGoToAbsolute, Count:=1 For i = 0 To TextLength With Selection .Range.PhoneticGuide Text:="" End With Selection.MoveRight Unit:=wdCharacter, Count:=1 Next Selection.WholeStory Application.ScreenUpdating = True End Sub.net它其实也是调用的Office的API,这个跟VBA调用API没有本质上的区别,是一样的。VS2022需要安装:Visual Studio Tools for Office(VSTO)然后,在项目当中引用程序集:Microsoft.Office.Interop.Word ,VS2022有14和15版本。我本机的是Office16,而vs2022并没有提供相关的程序集,所以我没有办法使用,也就没有做进一步的探索了。我查文档在Microsoft.Office.Interop.Word命名空间下,有一个Range.PhoneticGuide方法,接口看起来跟VBA调用的差不多,使用上应该也是差不太多的。直接修改docx文档docx的文档本质上是一个经过了zip压缩的OpenXML文档。基本上,主流的办公软件都支持这样一个标准:微软Office、苹果iWork、WPS Office、Google Docs。拼音指南在Office Open XML中的类型名是:CT_Ruby。Ruby,Wiki百科中解释为:注音,或称注音标识、加注音、标拼音、拼音指南。文档可见于:https://schemas.liquid-technologies.com/OfficeOpenXML/2006/?page=ct_ruby.htmlhttps://learn.microsoft.com/zh-cn/dotnet/api/documentformat.openxml.wordprocessing.rubyproperties?view=openxml-2.8.1我稍微研究了下,拼音指南的节点:<w:ruby>。其下面有若干个子节点:<w:rubyPr>是拼音指南的样式,<w:rt>是拼音指南的拼音文字,<w:rubyBase>是拼音指南的基准文字。一个比较完整的拼音指南的XML是这样的:<w:ruby> <w:rubyPr> <w:rubyAlign w:val="center"/> <w:hps w:val="26"/> <w:hpsRaise w:val="50"/> <w:hpsBaseText w:val="52"/> <w:lid w:val="zh-CN"/> </w:rubyPr> <w:rt> <w:r w:rsidR="00002ED0" w:rsidRPr="00002ED0"> <w:rPr> <w:rFonts w:ascii="等线" w:eastAsia="等线" w:hAnsi="等线"/> <w:color w:val="333333"/> <w:sz w:val="26"/> <w:shd w:val="clear" w:color="auto" w:fill="FFFFFF"/> </w:rPr> <w:t>diǎn</w:t> </w:r> </w:rt> <w:rubyBase> <w:r w:rsidR="00002ED0"> <w:rPr> <w:rFonts w:ascii="华文楷体" w:eastAsia="华文楷体" w:hAnsi="华文楷体"/> <w:color w:val="333333"/> <w:sz w:val="52"/> <w:shd w:val="clear" w:color="auto" w:fill="FFFFFF"/> </w:rPr> <w:t>点</w:t> </w:r> </w:rubyBase> </w:ruby>参考资料Office_Open_XML - WikiPedia求解MacroName:="FormatPhoneticGuide" '运行拼音指南 在vba中的初始化方法获取汉字拼音函数GetPhonetic的问题有没有给大批量中文加拼音的宏?Word批量加注拼音/清除拼音Add pinyin to all text using MS word.VBA实践+word快速全文加拼音
Entgo 实现 软删除(Soft Delete)我们在开发程序的过程中,会遇到一个常见的需求——删除表中的数据。但是有时候,业务需求要求不能永久删除数据库中的数据。比如一些敏感信息,我们需要留着以方便做历史追踪。这个时候,我们便会用到软删除。Entgo本身是不直接支持的,但是,要实现也并不是很难的事情。什么是软删除?软删除(Soft Delete) 是相对于 硬删除(Hard Delete) 来说的,它又可以叫做 逻辑删除 或者 标记删除。这种删除方式并不是真正地从数据库中把记录删除,而是通过特定的标记方式在查询的时候将此记录过滤掉。虽然数据在界面上已经看不见,但是数据库还是存在的。如何实现软删除?布尔类型字段标识时间戳字段标识将软删除的数据插入到另一个表中布尔类型字段、时间戳字段混合标识1. 布尔类型字段标识添加一个字段名为:is_deleted、is_active、is_archived等的布尔类型的字段,以此来标识该行是否已经删除。2. 时间戳字段标识添加一个字段名为:deleted_at、delete_time等的时间戳字段,null表示未删除,非null则表示已经删除,同时还能获取到删除的时间。3. 将软删除的数据插入到另一个表中举个例子,order表会有一个相应的order_deleted表,在删除order表中的数据,将数据复制到order_deleted表中。4. 布尔类型字段、时间戳字段混合标识使用时间戳的方式去标识,虽然可以在标识同时也可以获取到删除时间,但是在查询的时候,null值会导致查询全表扫描,导致查询的性能大打折扣。混合布尔类型和时间戳类型的字段来进行删除标识,虽然会多占用一点存储,但是可以带来更好的费效比。软删除使用场景我在网上搜索到了 Abel Avram 和 Udi Dahan 两个大佬关于要不要软删除的争论。存在的,就是有理的。软删除有其好处,也有其弊端。所以,不能够滥用,也不能完全否认它存在的意义。在数据库的领域里面,删除只有 Delete 的概念。但是,在业务的领域里面,删除其实是有很多现实意义的概念:员工的解雇、公民的故去、订单的取消、产品的停售……假设市场部要从商品目录中删除一样商品,那是不是说所有包含了该商品的旧订单都要一并消失?再级联下去,这些订单对应的所有发票也要删除吗?就这么一步步删下去,是不是公司的损益报表也要重做了?软删除,它就是后悔药,可以在历史追踪,审计等场景下发挥大作用。但是,必须要面对的是,留存大量的冗余数据,对于数据库的性能必然是不利的。Entgo中实现软删除(Soft Deletes)Ent框架暂时是不支持软删除的(当前版本:v0.11.4),但是实现起来也并不麻烦,代码修改量也并不大。本着偷懒的精神,我研究了一下怎么样让代码量更少的做法,但是我并没有找寻到——这还需要框架层的支持。创建创建删除标识字段在Ent中创建删除字段有两种方式:在Schema中创建删除标识(不通用);在Mixin中创建建删除标识(通用)。在Schema中创建删除标识在表里面添加字段:package schema func (User) Fields() []ent.Field { return []ent.Field{ ... field.Time("deleted_at").Optional().Nillable(), field.Bool("is_deleted").Optional().Nillable().Default(false), ... } }这种方式比较简单直观,但是,不够通用,需要在每一个Schema里面定义字段。在Mixin中创建建删除标识Mixin是Ent一个很重要也很有用的特性。我们可以把一些通用的字段提炼出来形成一个mixin包,这样这个mixin包里边的字段就可以复用了。例如,我们创建一个SoftDelete的mixin:package mixin type SoftDelete struct{} func (SoftDelete) Fields() []ent.Field { return []ent.Field{ // 删除时间 field.Time("deleted_at"). Comment("删除时间"). Optional(). Nillable(), // 删除标识 field.Bool("is_deleted"). Comment("删除标识"). Optional(). Nillable(). Default(false), } }然后在Schema当中引用mixin:package schema // Mixin of the User. func (User) Mixin() []ent.Mixin { return []ent.Mixin{ mixin.SoftDelete{}, } }执行查询查询之前的查询操作是这样的: users, err := client.Debug().User. Query(). Where(user.NameEQ("a8m")). Where(user.AgeEQ(18)). All(ctx)SELECT * FROM users WHERE name = ? AND age = ?现在变成这样: users, err := client.Debug().User. Query(). Where(user.NameEQ("a8m")). Where(user.AgeEQ(18)). Where(user.DeletedAtIsNil()). Where(user.IsDeletedEQ(false)). All(ctx)SELECT * FROM users WHERE (name = ? AND age = ?) AND deleted_at IS NULL AND is_deleted IS FALSe删除之前的删除操作是这样的:client.Debug().User. DeleteOneID(1). Exec(ctx)DELETE FROM users WHERE id = 1现在变成这样:_, err := client.Debug().User. UpdateOneID(id). SetDeletedAt(time.Now()). SetIsDeleted(true). Save(ctx)UPDATE users SET deleted_at = ?, is_deleted = true WHERE id = ?参考资料数据的软删除—什么时候需要?又如何去实现?Don’t Delete – Just Don’tDeleting Data Is Not a Recommended PracticeFeature Request: Soft Deletes[[HELP] Trying to implement soft delete logic using Hooks and Mixins](https://github.com/ent/ent/issues/2850)To Delete or to Soft Delete, That is the Question!
1. 列出WSL子系统wslconfig /list wsl --list wsl -l -v2. 关闭Ubuntu子系统wsl --terminate Ubuntu wsl -t Ubuntu3. 关闭WSLwsl --shutdown4. 启动WSLwsl
CLion 在头文件和源文件之间切换该快捷方式在键盘图中称为“相关符号”。MACCtrl + Cmd + UpWindows、LinuxCtrl + Alt + Home
Kratos微服务框架下实现Thrift服务什么是ThriftThrift是Facebook于2007年开发的跨语言的rpc服框架,提供多语言的编译功能,并提供多种服务器工作模式;用户通过Thrift的IDL(接口定义语言)来描述接口函数及数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。2007年由facebook贡献到apache基金,是apache下的顶级项目,具备如下特点:支持多语言:C、C++ 、C# 、D 、Delphi 、Erlang 、Go 、Haxe 、Haskell 、Java 、JavaScript、node.js 、OCaml 、Perl 、PHP 、Python 、Ruby 、SmallTalk消息定义文件支持注释,数据结构与传输表现的分离,支持多种消息格式包含完整的客户端/服务端堆栈,可快速实现RPC,支持同步和异步通信Thrift的优缺点Thrift的优点One-stop shop,相对于protobuf,序列化和RPC支持一站式解决,如果是pb的话,还需要考虑选择RPC框架,现在Google是开源了gRpc,但是几年以前是没有第一方的标准解决方案的特性丰富,idl层面支持map,protobuf应该是最近才支持的,map的key支持任意类型,avro只支持string,序列化支持自定义protocol, rpc支持thread pool, hsha, no blocking 多种形式,必有一款适合你,对于多语言的支持也非常丰富RPC和序列化性能都不错,这个到处都有benchmark,并不是性能最好的,但是基本上不会成为瓶颈或者短板有很多开源项目的周边支持都是thrift的,hbase提供thrift服务,hive,spark sql,cassandra等一系列对外的标准服务接口都是thrift的以支持多语言。Column Storage的话,parquet支持直接通过thrift idl转换,如果在Hadoop集群上存储数据,elephant-bird 支持得很好,你可以很方便地针对thrift的数据通过pig写dsl,如果你希望在rpc服务外做一系列工作,可以用finagle包装一层。不过,这部分对于protobuf和avro支持一般也不错Thrift的缺点基本没有官方文档RPC在 0.6.1 升级到 0.7.0 是不兼容的!这个对于早于 0.6.1 开始使用的用户来说是个大坑bug fix和更新不积极,好在序列化和RPC服务都不是太复杂的问题,需要考量的设计问题不多,自己维护patch的成本不高,如果我没有记错的话,0.6.1的java的ThreadPool Server是会有Thread死亡之后的Thread泄露问题的不支持双向通道,如果要支持双向通道比较麻烦rpc方法非线程安全,这就是为何很多时候服务器会被挂死,是因为客户端的并发rpc调用导致的,只需要客户端对rpc的调用进行串行化即可。统一服务器应答的时候,也需要串行化,否则有可能会把对方给挂死。特别是在多线程情况下。什么时候应该选择Thrift需要在非常多的语言间进行数据交换对CPU敏感协议层、传输层有多种控制要求需要稳定的版本不需要良好的文档和示例Thrift 网络栈架构TTransport 层TSocket :阻塞 SocketTFrameTransport :以 frame 为单位进行传输, 非阻塞式服务中使用TFileTransport : 以文件形式进行传输TProtocol 层代表 thrift 客户端和服务端之间的传输数据的协议,指的是客户端和服务端传输数据的格式,比如 Json, thrift 中有如下格式:TBinaryProtocol:二进制格式TCompactProtocol:压缩格式TJSONProtocol : Json格式TSimpleJsonProtocol:提供只写的 JSON 协议Thrift 支持的 Server 模型TSimpleServer :用于简单的单线程模型,常用于测试TThreadPoolServer :多线程模型,使用标准的阻塞 IOTNoBlockingServer: 多线程服务模型,使用非阻塞 IO,需要使用TFramedTransport 数据传输方式。THsHaServer : THsHa引入了线程池去处理,其模型读写任务放到线程池去处理,Half-sync/Half-async处理模式,Half-async是在处理IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步处理;Thrift支持的数据类型以及关键字基本数据类型类型说明byte有符号字节i1616 位有符号整数i3232 位有符号整数i6464 位有符号整数double64 位浮点数string字符串容器类型类型说明list一系列由 T 类型的数据组成的有序列表, 元素可以重复set一系列由 T 类型组成的无序集合,元素不可以重复map一个字典结构,Key 为 K 类型, Value 为 V 类型,和 Java 中的 HashMap 类似thrift 支持 struct 类型,可以将一些数据类型聚合到一块。Struct类型Struct:类似于C的struct,是一系列相关数据的封装,在OOP语言中会转换为类(class),struct的每个元素包括一个唯一的数字标识、一个数据类型、一个名称和一个可选的默认值。语法:struct People { 1:string name; 2:i32 age; 3:string gender; }Union类型union SomeUnion { 2: string string_thing, 3: i32 i32_thing }Enum类型Enum:Thrift枚举类型只支持单个32位int类型数据,第一个元素如果没有给值那么默认是0,之后的元素如果没有给值,则是在前一个元素基础上加1,语法:enum Gender { MALE, FEMALE }别名Typedef:Thrift 支持C/C++风格的类型自定义,语法:// typedef 原类型 自定义类型 typedef i32 int typedef i64 long常量Const:定义常量,Thrift允许使用JSON来定义复杂类型和struct类型,语法:// const 字段类型 名称标识 = 值 | 列表 const i32 MAX_RETRIES_TIME = 10; const string MY_WEBSITE = "http://facebook.com";Exception类型Exception:异常跟struct类似,会跟目标语言本地异常集成,语法:exception RequestException { 1:i32 code; 2:string reason; }Service类型Service:service是Thrift 服务器提供的一系列功能列表接口,在客户端就是调用这些接口来完成操作,语法:service HelloWorldService { // service中可以定义若干个服务,相当于Java Interface中定义的方法 string doAction(1:string name, 2:i32 age); }命名空间定义名称空间/包名/模块等等,可以使用编程语言名称规定某一特定语言的namespace,用*表示所有未匹配到的语言的namespace,语法// namespace [语言名称] 标识符 namespace cpp api namespace go api namespace d api namespace dart api namespace java api namespace php api namespace perl api namespace haxe api namespace netstd api // 用*表示所有未匹配到的语言的namespace namespace * api注释# shell风格注释 /* * 多行注释 */ // 单行注释包含引用Thrift Include:将所有声明包含的Thrift文档都包含到当前Thrift中来,语法:// 包含其他的thrift文件 // inlucde "文件名" include "other.thrift"C++ Include:将用户自己的C++头文件包含到当前Thrift中来,语法:// 将用户自己的C++头文件包含到当前Thrift中来 // cpp_include "头文件" cpp_include "string"安装编译器Linux安装编译器sudo apt install thrift-compilerWindows安装编译器先去官网下载编译器:https://thrift.apache.org/download然后把编译器放在一个全局可以运行的目录下面,比如:c:/Windows。Mac安装编译器brew install thrift编译Thrift文件# thrift --gen <language> <Thrift filename> # 如果有thrift文件中有包含其他thrift,可以使用递归生成命令 thrift -r --gen <language> <Thrift filename> # 示例 thrift -r -gen go tutorial.thrift开始在Kratos微服务框架下使用Thrift我封装了一个Thrift服务,可以在Kratos微服务框架下直接使用:https://github.com/tx7do/kratos-transport/tree/main/transport/thrift。实例程序的目标是从服务器获取温湿度信息,然后将温湿度信息发送给客户端。示例代码可以在单元测试里面找到。创建Thrift文件namespace go api struct Hygrothermograph { 1: optional double Humidity, 2: optional double Temperature, } service HygrothermographService { Hygrothermograph getHygrothermograph() }然后生成代码。生成的代码在thrift文件所在目录下的:gen-go/{namespace}之下。开发服务器首先,实现Handlertype HygrothermographHandler struct { } func NewHygrothermographHandler() *HygrothermographHandler { return &HygrothermographHandler{} } func (p *HygrothermographHandler) GetHygrothermograph(ctx context.Context) (_r *api.Hygrothermograph, _err error) { var Humidity = float64(rand.Intn(100)) var Temperature = float64(rand.Intn(100)) _r = &api.Hygrothermograph{ Humidity: &Humidity, Temperature: &Temperature, } fmt.Println("Humidity:", Humidity, "Temperature:", Temperature) return }Handler在Kratos里面的使用的语义是Service,实际应用的时候,将之实现为Service。实现服务端 ctx := context.Background() srv := NewServer( WithAddress(":7700"), WithProcessor(api.NewHygrothermographServiceProcessor(NewHygrothermographHandler())), ) if err := srv.Start(ctx); err != nil { panic(err) } defer func () { if err := srv.Stop(ctx); err != nil { t.Errorf("expected nil got %v", err) } }()使用WithProcessor方法我们把之前的Handler注册成Processor到服务器。开发客户端conn, err := Dial( WithEndpoint("localhost:7700"), ) if err != nil { t.Fatal(err) } defer conn.Close() client := api.NewHygrothermographServiceClient(conn.Client) reply, err := client.GetHygrothermograph(context.Background()) //t.Log(err) if err != nil { t.Errorf("failed to call: %v", err) } t.Log(*reply.Humidity, *reply.Temperature)Thrift客户端跟gRpc的客户端是一样的,使用Dial方法创建一个连接,然后直接调用RPC方法。参考文档Thrift官方网站Thrift各平台安装方法thrift 原理浅析Thrift入门基础知识-thrift文件(IDL)说明和生成目标语言源代码哪个互联网公司使用 facebook thrift 做底层架构,实现高性能、可扩展的web应用?引入thrift之后的优缺点是什么?一文搞懂gRPC和Thrift的基本原理和区别
Kratos微服务框架下实现GraphQL服务GraphQL 是一种用于应用编程接口(API)的查询语言和服务器端运行时,它可以使客户端准确地获得所需的数据,没有任何冗余。GraphQL 由 Facebook 开发,并于 2012 年首次应用于移动应用。GraphQL 规范于 2015 年实现开源。现在,它受 GraphQL 基金会监管。GraphQL有什么用?GraphQL 旨在让 API 变得快速、灵活并且为开发人员提供便利。它甚至可以部署在名为 GraphiQL 的集成开发环境(IDE)中。作为 REST 的替代方案,GraphQL 允许开发人员构建相应的请求,从而通过单个 API 调用从多个数据源中提取数据。此外,GraphQL 还可让 API 维护人员灵活地添加或弃用字段,而不会影响现有查询。开发人员可以使用自己喜欢的方法来构建 API,并且 GraphQL 规范将确保它们以可预测的方式在客户端发挥作用。GraphQL 的优缺点GraphQL 的优点GraphQL 模式会在 GraphQL 应用中设置单一事实来源。它为企业提供了一种整合其整个 API 的方法。一次往返通讯可以处理多个 GraphQL 调用。客户端可得到自己所请求的内容,不会超量。严格定义的数据类型可减少客户端与服务器之间的通信错误。GraphQL 具有自检功能。客户端可以请求一个可用数据类型的列表。这非常适合文档的自动生成。GraphQL 允许应用 API 进行更新优化,而无需破坏现有查询。许多开源 GraphQL 扩展可提供 REST API 所不具备的功能。GraphQL 不指定特定的应用架构。它能够以现有的 REST API 为基础,并与现有的 API 管理工具配合使用。GraphQL 的缺点即便是熟悉 REST API 的开发人员,也需要一定时间才能掌握 GraphQL。GraphQL 将数据查询的大部分工作都转移到服务器端,由此增加了服务器开发人员工作的复杂度。根据不同的实施方式,GraphQL 可能需要不同于 REST API 的 API 管理策略,尤其是在考虑速率限制和定价的情况下。缓存机制比 REST 更加复杂。API 维护人员还会面临编写可维护 GraphQL 模式的额外任务。GraphQL支持的数据类型以及关键字标量类型Int:带符号的32位整数,对应 JavaScript 的 NumberFloat:带符号的双精度浮点数,对应 JavaScript 的 NumberString:UTF-8字符串,对应 JavaScript 的 StringBoolean:布尔值,对应 JavaScript 的 BooleanID:ID 值,是一个序列化后值唯一的字符串,可以视作对应 ES 2015 新增的 Symbol高级类型接口类型Interface是包含一组确定字段的集合的抽象类型,实现该接口的类型必须包含interface定义的所有字段。比如:interface Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! } type Human implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! starships: [Starship] totalCredits: Int } type Droid implements Character { id: ID! name: String! friends: [Character] appearsIn: [Episode]! primaryFunction: String }联合类型Union类型非常类似于interface,但是他们在类型之间不需要指定任何共同的字段。通常用于描述某个字段能够支持的所有返回类型以及具体请求真正的返回类型。比如定义:union SearchResult = Human | Droid | Starship枚举类型又称Enums,这是一种特殊的标量类型,通过此类型,我们可以限制值为一组特殊的值。比如:enum Episode { NEWHOPE EMPIRE JEDI }输入类型input类型对mutations来说非常重要,在 GraphQL schema 语言中,它看起来和常规的对象类型非常类似,但是我们使用关键字input而非type,input类型按如下定义:input CommentInput { body: String! }为什么不直接使用Object Type呢?因为 Object 的字段可能存在循环引用,或者字段引用了不能作为查询输入对象的接口和联合类型。数组类型和非空类型使用[]来表示数组,使用!来表示非空。Non-Null强制类型的值不能为null,并且在请求出错时一定会报错。可以用于必须保证值不能为null的字段对象类型GraphQL schema最基本的类型就是Object Type。用于描述层级或者树形数据结构。比如:type Character { name: String! appearsIn: [Episode!]! }GraphQL查询语法GraphQL的一次操作请求被称为一份文档(document),即GraphQL服务能够解析验证并执行的一串请求字符串(Source Text)。完整的一次操作由操作(Operation)和片段(Fragments)组成。一次请求可以包含多个操作和片段。只有包含操作的请求才会被GraphQL服务执行。只包含一个操作的请求可以不带OperationName,如果是operationType是query的话,可以全部省略掉,即:{ getMessage { # query也可以拥有注释,注释以#开头 content author } }当query包含多个操作时,所有操作都必须带上名称。GraphQL中,我们会有这样一个约定,Query和与之对应的Resolver是同名的,这样在GraphQL才能把它们对应起来。QueryQuery用做读操作,也就是从服务器获取数据。以上图的请求为例,其返回结果如下,可以看出一一对应,精准返回数据{ "data": { "single": [ { "content": "test content 1", "author": "pp1" } ], "all": [ { "content": "test content", "author": "pp", "id": "0" }, { "content": "test content 1", "author": "pp1", "id": "1" } ] } }FieldField是我们想从服务器获取的对象的基本组成部分。query是数据结构的顶层,其下属的all和single都属于它的字段。字段格式应该是这样的:alias:name(argument:value)其中 alias 是字段的别名,即结果中显示的字段名称。name 为字段名称,对应 schema 中定义的 fields 字段名。argument 为参数名称,对应 schema 中定义的 fields 字段的参数名称。value 为参数值,值的类型对应标量类型的值。Argument和普通的函数一样,query可以拥有参数,参数是可选的或必须的。参数使用方法如上图所示。需要注意的是,GraphQL中的字符串需要包装在双引号中。Variables除了参数,query还允许你使用变量来让参数可动态变化,变量以$开头书写,使用方式如上图所示变量还可以拥有默认值:query gm($id: ID = 2) { # 查询数据 single: getMessage(id: $id) { ...entity } all: getMessage { ...entity id } }Allases别名,比如说,我们想分别获取全部消息和ID为1的消息,我们可以用下面的方法:query gm($id: ID = 2) { # 查询数据 getMessage(id: $id) { ...entity } getMessage { ...entity id } }由于存在相同的name,上述代码会报错,要解决这个问题就要用到别名了Allases。query gm($id: ID = 2) { # 查询数据 single: getMessage(id: $id) { ...entity } all: getMessage { ...entity id } }FragmentsFragments是一套在queries中可复用的fields。比如说我们想获取Message,在没有使用fragment之前是这样的:query gm($id: ID = 2) { # 查询数据 single: getMessage(id: $id) { content author } all: getMessage { content author id } }但是如果fields过多,就会显得重复和冗余。Fragments在此时就可以起作用了。使用了Fragment之后的语法就如上图所示,简单清晰。Fragment支持多层级地继承。DirectivesDirectives提供了一种动态使用变量改变我们的queries的方法。如本例,我们会用到以下两个directive:query gm($id: ID = 2, $isNotShowId: Boolean!, $showAuthor: Boolean!) { # 查询数据 single: getMessage(id: $id) { ...entity } all: getMessage { ...entity id @skip(if: $isNotShowId) } } fragment entity on Message { content author @include(if: $showAuthor) } ### 入参是: { "id": 1, "isNotShowId": true, "showAuthor": false }@include: 只有当if中的参数为true时,才会包含对应fragment或field;@skip:当if中的参数为true时,会跳过对应fragment或field;结果如下:{ "data": { "single": [ { "content": "test content 1" } ], "all": [ { "content": "test content" }, { "content": "test content 1" } ] } }Mutation传统的API使用场景中,我们会有需要修改服务器上数据的场景,mutations就是应这种场景而生。mutations被用以执行写操作,通过mutations我们会给服务器发送请求来修改和更新数据,并且会接收到包含更新数据的反馈。mutations和queries具有类似的语法,仅有些许的差别。operationType为mutation为了保证数据的完整性mutations是串形执行,而queries可以并行执行。SubscriptionSubscription是GraphQL最后一个操作类型,它被称为订阅。当另外两个由客户端通过HTTP请求发送,订阅是服务器在某个事件发生时将数据本身推送给感兴趣的客户端的一种方式。这就是GraphQL 处理实时通信的方式。编译GraphQL文件在graphql同级目录下创建一个配置文件,命名为:gqlgen.ymlschema: - "*.graphql"或者是指定导出models项:models: Todo: model: github.com/Laisky/laisky-blog-graphql.Todo然后在graphql文件同级目录下,使用命令行执行以下命令,即可生成go代码:# 直接运行 go run github.com/99designs/gqlgen # 安装 go get github.com/99designs/gqlgen go install github.com/99designs/gqlgen # 然后运行 gqlgen开始在Kratos微服务框架下使用GraphQL我基于gqlgen实现的GraphQL服务封装,它可以在Kratos微服务框架下直接使用:https://github.com/tx7do/kratos-transport/tree/main/transport/graphql。实例程序的目标是从服务器获取温湿度信息,然后将温湿度信息发送给客户端。示例代码可以在单元测试里面找到。编写GraphQL协议type Hygrothermograph { humidity: Float! temperature: Float! } type Query { hygrothermograph: Hygrothermograph! }编写Graphql服务器首先需要编写解析器,在Kratos里面,可以写成Service。type resolver struct{} func (r *resolver) Query() api.QueryResolver { return &queryResolver{} } type queryResolver struct{} func (r *queryResolver) Hygrothermograph(ctx context.Context) (*api.Hygrothermograph, error) { ret := &api.Hygrothermograph{ Humidity: float64(rand.Intn(100)), Temperature: float64(rand.Intn(100)), } fmt.Println("Humidity:", ret.Humidity, "Temperature:", ret.Temperature) return ret, nil }编写服务器ctx := context.Background() srv := NewServer( WithAddress(":8800"), ) srv.Handle("/query", api.NewExecutableSchema(api.Config{Resolvers: &resolver{}})) if err := srv.Start(ctx); err != nil { panic(err) } defer func() { if err := srv.Stop(ctx); err != nil { t.Errorf("expected nil got %v", err) } }()服务器本地访问地址为:http://localhost:8800/query测试如果要测试的话,推荐使用客户端:Altair。点击Docs按钮,可以查询到API文档,文档内容会显示在右侧编辑框,如下图所示:鼠标放到API上面会显示ADD QUERY按钮,点击就可以添加一个新的查询到最左边的编辑框中。点击右上角的Send Request按钮,可以发送请求。在中间的编辑框就可以看到请求的结果了。客户端工具AltairGraphQL EditorGraphQL-PlaygroundGraphiQLGraphQL Voyager客户端推荐使用Altair,我用着挺爽的。参考文档GraphQL中文站gqlgen官网GraphQL官网使用 gqlgen 编写 GraphQL 后端GraphQL学习之基础篇什么是 GraphQL?核心概念解析
Kratos微服务框架下实现分布式任务队列任务队列(Task Queue)一般用于线程或计算机之间分配工作的一种机制。其本质是生产者消费者模型,生产者发送任务到消息队列,消费者负责处理任务。提起分布式任务队列(Distributed Task Queue),就不得不提Python的Celery。而Asynq和Machinery就是GO当中类似于Celery的分布式任务队列。什么是任务队列消息队列(Message Queue),一般来说知道的人不少。比如常见的:kafka、Rabbitmq、RocketMQ等。任务队列(Task Queue),听说过这个概念的人不会太多,清楚它的概念的人怕是更少。这两个概念是有关系的,他们是怎样的关系呢?任务队列(Task Queue)是消息队列(Message Queue)的超集。任务队列是构建在消息队列之上的。消息队列是任务队列的一部分。下面我们来看Celery的架构图,以此来讲解。其他的任务队列也并不会与之有太大的差异性,至少原理是一致的。在 Celery 的架构中,由多台 Server 发起异步任务(Async Task),发送任务到 Broker 的队列中,其中的 Celery Beat 进程可负责发起定时任务。当 Task 到达 Broker 后,会将其分发给相应的 Celery Worker 进行处理。当 Task 处理完成后,其结果存储至 Backend。在上述过程中的 Broker 和 Backend,Celery 没有实现,而是使用了现有开源实现,例如 RabbitMQ 作为 Broker 提供消息队列服务,Redis 作为 Backend 提供结果存储服务。Celery 就像是抽象了消息队列架构中 Producer、Consumer 的实现,将消息队列中基本单位“消息”抽象成了任务队列中的“任务”,并将异步、定时任务的发起和结果存储等操作进行了封装,让开发者可以忽略 AMQP、RabbitMQ 等实现细节,为开发带来便利。综上所述,Celery 作为任务队列是基于消息队列的进一步封装,其实现依赖消息队列。任务队列的应用场景即时响应需求:网页的响应时间是用户体验的关键,Amazon 曾指出响应时间每提高 100ms,他们的收入便会增加 1%。对于一些需要长时间执行的任务,大多会采用异步调用的方式来释放用户操作。Celery 的异步调用特性,和前端使用 Ajax 异步加载类似,能够有效缩短响应时间。周期性任务需求(Periodic Task):对于心跳测试、日志归档、运维巡检这类指定时间周期执行的任务,可以应用任务队列的定时队列,支持 crontab 定时模式,简单方便。高并发及可扩展性需求:解耦应用程序最直接的好处就是可扩展性和并发性能的提高。支持并发执行任务,同时支持自动动态扩展。Kratos下实现分布式任务队列我们将分布式任务队列以transport.Server的形式整合进微服务框架Kratos。AsynqAsynq是一个go语言实现的分布式任务队列和异步处理库,基于Redis。类似于Python的Celery。作者Ken Hibino,任职于Google。特点保证至少执行一次任务任务写入Redis后可以持久化任务失败之后,会自动重试worker崩溃自动恢复可是实现任务的优先级任务可以进行编排任务可以设定执行时间或者最长可执行的时间支持中间件可以使用 unique-option 来避免任务重复执行,实现唯一性支持 Redis Cluster 和 Redis Sentinels 以达成高可用性作者提供了Web UI & CLI Tool让大家查看任务的执行情况安装命令行工具go install github.com/hibiken/asynq/tools/asynqDocker安装Web UIdocker pull hibiken/asynqmon:latest docker run -d \ --name asynq \ -p 8080:8080 \ hibiken/asynqmon:latest --redis-addr=host.docker.internal:6379管理后台:http://localhost:8080仪表盘任务视图性能创建Kratos服务import github.com/tx7do/kratos-transport/transport/asynq const ( localRedisAddr = "127.0.0.1:6379" testTask1 = "test_task_1" testDelayTask = "test_task_delay" testPeriodicTask = "test_periodic_task" ) ctx := context.Background() srv := asynq.NewServer( asynq.WithAddress(localRedisAddr), ) if err := srv.Start(ctx); err != nil { panic(err) } defer srv.Stop(ctx)创建新任务普通任务// 最多重试3次,10秒超时,20秒后过期 err = srv.NewTask(testTask1, []byte("test string"), asynq.MaxRetry(10), asynq.Timeout(10*time.Second), asynq.Deadline(time.Now().Add(20*time.Second)))延迟任务err = srv.NewTask(testDelayTask, []byte("delay task"), asynq.ProcessIn(3*time.Second))周期性任务// 每分钟执行一次 err = srv.NewPeriodicTask("*/1 * * * ?", testPeriodicTask, []byte("periodic task"))注册任务回调func handleTask(_ context.Context, task *asynq.Task) error { log.Infof("Task Type: [%s], Payload: [%s]", task.Type(), string(task.Payload())) return nil } func handleDelayTask(_ context.Context, task *asynq.Task) error { log.Infof("Delay Task Type: [%s], Payload: [%s]", task.Type(), string(task.Payload())) return nil } func handlePeriodicTask(_ context.Context, task *asynq.Task) error { log.Infof("Periodic Task Type: [%s], Payload: [%s]", task.Type(), string(task.Payload())) return nil } err := srv.HandleFunc(testTask1, handleTask) err = srv.HandleFunc(testTaskDelay, handleDelayTask) err = srv.HandleFunc(testPeriodicTask, handlePeriodicTask)示例代码示例代码可以在单元测试代码中找到:https://github.com/tx7do/kratos-transport/tree/main/transport/asynq/server_test.goMachinerygo machinery框架类似python中常用celery框架,主要用于异步任务和定时任务。特性任务重试机制延迟任务支持任务回调机制任务结果记录支持Workflow模式:Chain,Group,Chord多Brokers支持:Redis, AMQP, AWS SQS多Backends支持:Redis, Memcache, AMQP, MongoDB架构任务队列,简而言之就是一个放大的生产者消费者模型,用户请求会生成任务,任务生产者不断的向队列中插入任务,同时,队列的处理器程序充当消费者不断的消费任务。Server :业务主体,我们可以使用用server暴露的接口方法进行所有任务编排的操作。如果是简单的使用那么了解它就够了。Broker :数据存储层接口,主要功能是将数据放入任务队列和取出,控制任务并发,延迟也在这层。Backend:数据存储层接口,主要用于更新获取任务执行结果,状态等。Worker:数据处理层结构,主要是操作 Server、Broker、Backend 进行任务的获取,执行,处理执行状态及结果等。Task: 数据处理层,这一层包括Task、Signature、Group、Chain、Chord等结构,主要是处理任务编排的逻辑。任务编排Machinery一共提供了三种任务编排方式:Groups : 执行一组异步任务,任务之间互不影响。Chord:执行一组同步任务,执行完成后,在调用一个回调函数。Chain:执行一组同步任务,任务有次序之分,上个任务的出参可作为下个任务的入参。创建Kratos服务import github.com/tx7do/kratos-transport/transport/machinery const ( localRedisAddr = "127.0.0.1:6379" testTask1 = "test_task_1" testDelayTask = "test_delay_task" testPeriodicTask = "test_periodic_task" sumTask = "sum_task" ) ctx := context.Background() srv := machinery.NewServer( machinery.WithRedisAddress([]string{localRedisAddr}, []string{localRedisAddr}), ) if err := srv.Start(ctx); err != nil { panic(err) } defer srv.Stop(ctx)创建新任务普通任务var args = map[string]interface{}{} args["int64"] = 1 err = srv.NewTask(sumTask, args)延迟任务// 延迟5秒执行任务 var args = map[string]interface{}{} err = srv.NewTask(testDelayTask, args, WithDelayTime(time.Now().UTC().Add(time.Second*5)))周期性任务(需要注意的是,延迟任务的精度只能到秒级)var args = map[string]interface{}{} // 每分钟执行一次 err = srv.NewPeriodicTask("*/1 * * * ?", testPeriodicTask, args)注册任务回调 func handleTask(_ context.Context, task *asynq.Task) error { log.Infof("Task Type: [%s], Payload: [%s]", task.Type(), string(task.Payload())) return nil } func handleDelayTask(_ context.Context, task *asynq.Task) error { log.Infof("Task Type: [%s], Payload: [%s]", task.Type(), string(task.Payload())) return nil } func handleAdd(args ...int64) (int64, error) { sum := int64(0) for _, arg := range args { sum += arg } fmt.Printf("sum: %d\n", sum) return sum, nil } func handlePeriodicTask() error { fmt.Println("################ 执行周期任务PeriodicTask #################") return nil } err = srv.HandleFunc(testTask1, handleTask) err = srv.HandleFunc(testTaskDelay, handleDelayTask) err = srv.HandleFunc(testPeriodicTask, handlePeriodicTask) err = srv.HandleFunc(sumTask, handleAdd)示例代码示例代码可以在单元测试代码中找到:https://github.com/tx7do/kratos-transport/tree/main/transport/machinery/server_test.go参考资料Celery 简介分布式任务队列Celery的实践Asynq: Golang distributed task queue library异步任务处理系统,如何解决业务长耗时、高并发难题?分布式任务队列 CeleryCelery - GithubMachinery - GithubAsynq - Githubmachinery中文文档Go 语言分布式任务处理器 Machinery – 架构,源码详解篇Task orchestration in Go Machinery.go-machinery入门教程(异步任务队列)Asynq: simple, reliable & efficient distributed task queue for your next Go projectAsynq: Golang distributed task queue library
什么是ORM?面向对象编程和关系型数据库,都是目前最流行的技术,但是它们的模型是不一样的。面向对象编程把所有实体看成对象(object),关系型数据库则是采用实体之间的关系(relation)连接数据。很早就有人提出,关系也可以用对象表达,这样的话,就能使用面向对象编程,来操作关系型数据库。简单说,ORM 就是通过实例对象的语法,完成关系型数据库的操作的技术,是"对象-关系映射"(Object/Relational Mapping) 的缩写。ORM 把数据库映射成对象。数据库的表(table) --> 类(class)记录(record,行数据)--> 对象(object)字段(field)--> 对象的属性(attribute)举例来说,下面是一行 SQL 语句。SELECT id, first_name, last_name, phone, birth_date, sex FROM persons WHERE id = 10程序直接运行 SQL,操作数据库的写法如下。res = db.execSql(sql); name = res[0]["FIRST_NAME"];改成 ORM 的写法如下。p = Person.get(10); name = p.first_name;一比较就可以发现,ORM 使用对象,封装了数据库操作,因此可以不碰 SQL 语言。开发者只使用面向对象编程,与数据对象直接交互,不用关心底层数据库。ORM 有下面这些优点:数据模型都在一个地方定义,更容易更新和维护,也利于重用代码。ORM 有现成的工具,很多功能都可以自动完成,比如数据消毒、预处理、事务等等。它迫使你使用 MVC 架构,ORM 就是天然的 Model,最终使代码更清晰。基于 ORM 的业务代码比较简单,代码量少,语义性好,容易理解。你不必编写性能不佳的 SQL。ORM 也有很突出的缺点:ORM 库不是轻量级工具,需要花很多精力学习和设置。对于复杂的查询,ORM 要么是无法表达,要么是性能不如原生的 SQL。ORM 抽象掉了数据库层,开发者无法了解底层的数据库操作,也无法定制一些特殊的 SQL。什么是Ent?ent 是Facebook开源的一个简单但是功能强大的ORM框架,它可以轻松构建和维护具有大型数据模型的应用程序。它基于代码生成,并且可以很容易地进行数据库查询以及图遍历。它具有以下的特点:简单地使用数据库结构作为图结构。使用Go代码定义结构。基于代码生成的静态类型。容易地进行数据库查询和图遍历。容易地使用Go模板扩展和自定义。多存储驱动程序 - 支持MySQL、PostgreSQL、SQLite 和 Gremlin。如何去学习Ent?想要上手ent,需要学习和了解三个方面:entcSchemaCURD APIEnt因为是基于代码生成的,所以,首当其冲的,自然是要去了解其CLI工具,没有它,如何去生成代码?其次就是生成代码的模板:Schema。它主要是定义了表结构信息,至关重要的核心信息。生成数据库的结构和操作代码需要它,生成gRPC和GraphQL的接口也还是需要它。没它不行。最后,就是学习使用一些数据库的基本操作,比如:连接数据库,CURD API。从此往后,你就能够使用ent愉快的开始工作了。CLI工具使用以下命令安装entc工具:go install entgo.io/ent/cmd/ent@latestSchemaSchema相当于数据库的表。《道德经》说:道生一,一生二,二生三,三生万物。Schema,就是一切的起始点。只有定义了Schema,CLI才能够生成数据库表的结构和操作的相关代码,有了相关代码,才能够操作数据库表的数据。后面想要生成gRPC和GraphQL的接口定义,也还是需要Schema。创建一个Schema创建Schema有两个方法可以做到:使用 entc init 创建ent init User将会在 {当前目录}/ent/schema/ 下生成一个user.go文件,如果没有文件夹,则会创建一个:package schema import "entgo.io/ent" // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return nil } // Edges of the User. func (User) Edges() []ent.Edge { return nil }SQL转换Schema在线工具网上有人好心的制作了一个在线工具,可以将SQL转换成schema代码,实际应用中,这是非常方便的!SQL转Schema工具: https://printlove.cn/tools/sql2ent比如,我们有一个创建表的SQL语句:CREATE TABLE `user` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, `type` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = DYNAMIC;转换之后,生成如下的Schema代码:package schema import ( "entgo.io/ent" "entgo.io/ent/dialect" "entgo.io/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int32("id").SchemaType(map[string]string{ dialect.MySQL: "int(10)UNSIGNED", // Override MySQL. }).NonNegative().Unique(), field.String("email").SchemaType(map[string]string{ dialect.MySQL: "varchar(50)", // Override MySQL. }), field.String("type").SchemaType(map[string]string{ dialect.MySQL: "varchar(20)", // Override MySQL. }), field.Time("created_at").SchemaType(map[string]string{ dialect.MySQL: "timestamp", // Override MySQL. }).Optional(), field.Time("updated_at").SchemaType(map[string]string{ dialect.MySQL: "timestamp", // Override MySQL. }).Optional(), } } // Edges of the User. func (User) Edges() []ent.Edge { return nil }Mixin复用字段在实际应用中,我们经常需要会有一些通用的字段,比如:id、created_at、updated_at等等。那么,我们就一直的复制粘贴?这显然很是不优雅。entgo能够让我们复用这些字段吗?答案显然是,没问题。Mixin,就是办这个事儿的。好,我们现在需要复用时间相关的字段:created_at和updated_at,那么我们可以:package mixin import ( "time" "entgo.io/ent" "entgo.io/ent/schema/field" "entgo.io/ent/schema/mixin" ) type TimeMixin struct { mixin.Schema } func (TimeMixin) Fields() []ent.Field { return []ent.Field{ field.Time("created_at"). Immutable(). Default(time.Now), field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now), field.Bool("deleted").Default(false), } }然后,我们就可以在Schema当中应用了,比如User,我们为它添加一个Mixin方法:func (User) Mixin() []ent.Mixin { return []ent.Mixin{ mixin.TimeMixin{}, } }生成代码再看,就有这3个字段了。生成代码有了以上的Schema,我们就可以生成代码了。生成代码只能够官方提供的CLI工具ent来生成。而使用CLI有两种途径可以走:直接使用命令行执行命令,还有一种就是利用了go的go:generate特性。命令行直接执行命令生成我们可以命令行进入ent文件夹,然后执行以下命令:ent generate ./schema通过 generate.go 生成直接运行命令看起来是没有问题,但是在我们实际应用当中,直接使用命令行的方式进行代码生成是很不方便的。为什么呢?ent命令是有参数的,而在正常情况下,都是需要携带一些参数的:比如:--feature sql/modifier,具体文档在:特性开关。这时候,我们必须在某一个地方记录这些命令,而后续会有同事需要接手这个项目呢?他又从何而知?在这个时候就徒增了不少麻烦。好在go有一个很赞的特性go:generate,可以完美的解决这样一个问题。命令可以以代码的形式被记录下来,方便的重复使用。通常我们都会把ent相关的代码放置在ent文件夹下面,因此我们在ent文件夹下面创建一个generate.go文件:package ent //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature privacy --feature sql/modifier --feature entql --feature sql/upsert ./schema接着,我们可以在项目的根目录或者ent文件夹下,执行以下命令:go generate ./...以上的命令会遍历执行当前以及所有子目录下面的go:generate。如果您使用的是Goland或者VSC,则可以在IDE中直接运行go:generate命令。ent的一些数据库基本操作因为数据库是复杂的,SQL是复杂的,复杂到能够出好几本书,所以是绝不可能在简单的篇幅里面讲完整,只能够将常用的一些操作(连接数据库、CURD)拿来举例讲讲。连接数据库SQLite3import ( _ "github.com/mattn/go-sqlite3" ) client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") if err != nil { log.Fatalf("failed opening connection to sqlite: %v", err) } defer client.Close()MySQL/MariaDBTiDB 高度兼容MySQL 5.7 协议ClickHouse 支持MySQL wire通讯协议import ( _ "github.com/go-sql-driver/mysql" ) client, err := ent.Open("mysql", "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True") if err != nil { log.Fatalf("failed opening connection to mysql: %v", err) } defer client.Close()PostgreSQLCockroachDB 兼容PostgreSQL协议import ( _ "github.com/lib/pq" ) client, err := ent.Open("postgresql", "host=<host> port=<port> user=<user> dbname=<database> password=<pass>") if err != nil { log.Fatalf("failed opening connection to postgres: %v", err) } defer client.Close()Gremlinimport ( "<project>/ent" ) client, err := ent.Open("gremlin", "http://localhost:8182") if err != nil { log.Fatalf("failed opening connection to gremlin: %v", err) } defer client.Close()自定义驱动sql.DB有以下两种途径可以达成:package main import ( "time" "<your_project>/ent" "entgo.io/ent/dialect/sql" ) func Open() (*ent.Client, error) { drv, err := sql.Open("mysql", "<mysql-dsn>") if err != nil { return nil, err } // Get the underlying sql.DB object of the driver. db := drv.DB() db.SetMaxIdleConns(10) db.SetMaxOpenConns(100) db.SetConnMaxLifetime(time.Hour) return ent.NewClient(ent.Driver(drv)), nil }第二种是:package main import ( "database/sql" "time" "<your_project>/ent" entsql "entgo.io/ent/dialect/sql" ) func Open() (*ent.Client, error) { db, err := sql.Open("mysql", "<mysql-dsn>") if err != nil { return nil, err } db.SetMaxIdleConns(10) db.SetMaxOpenConns(100) db.SetConnMaxLifetime(time.Hour) // Create an ent.Driver from `db`. drv := entsql.OpenDB("mysql", db) return ent.NewClient(ent.Driver(drv)), nil }自动迁移 Automatic Migrationif err := client.Schema.Create(context.Background(), migrate.WithForeignKeys(false)); err != nil { l.Fatalf("failed creating schema resources: %v", err) }增 Createpedro := client.Pet. Create(). SetName("pedro"). SaveX(ctx)删 Deleteerr := client.User. DeleteOneID(id). Exec(ctx)改 Updatepedro, err := client.Pet. UpdateOneID(id). SetName("pedro"). SetOwnerID(owner). Save(ctx)查 Readnames, err := client.Pet. Query(). Select(pet.FieldName). Strings(ctx)事务 Transaction事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。封装一个方法WithTx,利用匿名函数来调用被事务管理的Insert、Update、Delete语句:package data func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error { tx, err := client.Tx(ctx) if err != nil { return err } defer func() { if v := recover(); v != nil { tx.Rollback() panic(v) } }() if err := fn(tx); err != nil { if rerr := tx.Rollback(); rerr != nil { err = fmt.Errorf("%w: rolling back transaction: %v", err, rerr) } return err } if err := tx.Commit(); err != nil { return fmt.Errorf("committing transaction: %w", err) } return nil }使用方法:func createUser(tx *ent.Tx, u UserData) *ent.UserCreate { return tx.User.Create(). SetName(u.Name). SetNillableNickName(u.NickName) } func updateUser(tx *ent.Tx, u UserData) *ent.UserUpdate { return tx.User.Update(). Where( user.Name(u.Name), ). SetNillableNickName(u.NickName) } func deleteUser(tx *ent.Tx, u UserData) *ent.UserDelete { return tx.User.Delete(). Where( user.Name(u.Name), ) } func batchCreateUser(tx *ent.Tx, users []UserData) error { userCreates := make([]*ent.UserCreate, 0) for _, u := range users { userCreates = append(userCreates, createUser(tx, u)) } if _, err := tx.User.CreateBulk(userCreates...).Save(r.ctx); err != nil { return err } return nil } func DoBatchCreateUser(ctx context.Context, client *ent.Client) { if err := WithTx(ctx, client, func(tx *ent.Tx) error { return batchCreateUser(tx, users) }); err != nil { log.Fatal(err) } }创建gRPC接口如果你已经有了数据库的表结构,当你开始初始化一个项目的时候,你不必写任何一行代码,就完成了从ent的数据库定义,到网络API定义的全流程。接着,你需要做的,也就是微调,然后开始撸业务逻辑代码了。不要太开心!现在不都流行所谓的“低代码”吗?这不就是吗!安装protoc插件:go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest go install entgo.io/contrib/entproto/cmd/protoc-gen-entgrpc@latest向项目添加依赖库:go get -u entgo.io/contrib/entproto向Schema添加entproto.Message()和entproto.Service()方法:func (User) Annotations() []schema.Annotation { return []schema.Annotation{ entproto.Message(), entproto.Service( entproto.Methods(entproto.MethodCreate | entproto.MethodGet | entproto.MethodList | entproto.MethodBatchCreate), ), } }其中,entproto.Message()将会导致生成Protobuf的message;entproto.Service()将会导致生成gRPC的service。使用entproto.Field()方法向表字段添加Protobuf的字段索引号:func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). Unique(). Annotations( entproto.Field(2), ), field.String("email_address"). Unique(). Annotations( entproto.Field(3), ), } }向generate.go添加entgo.io/contrib/entproto/cmd/entproto命令:package ent //go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema //go:generate go run -mod=mod entgo.io/contrib/entproto/cmd/entproto -path ./schema执行生成命令:go generate ./...将会生成以下文件:ent/proto/entpb ├── entpb.pb.go ├── entpb.proto ├── entpb_grpc.pb.go ├── entpb_user_service.go └── generate.go生成的entpb.proto文件生成的内容可能会是这样的:// Code generated by entproto. DO NOT EDIT. syntax = "proto3"; package entpb; option go_package = "ent-grpc-example/ent/proto/entpb"; message User { int32 id = 1; string user_name = 2; string email_address = 3; } service UserService { rpc Create ( CreateUserRequest ) returns ( User ); rpc Get ( GetUserRequest ) returns ( User ); rpc Update ( UpdateUserRequest ) returns ( User ); rpc Delete ( DeleteUserRequest ) returns ( google.protobuf.Empty ); rpc List ( ListUserRequest ) returns ( ListUserResponse ); rpc BatchCreate ( BatchCreateUsersRequest ) returns ( BatchCreateUsersResponse ); }与Kratos携起手来官方推荐的包结构是这样的:|- data |- biz |- service |- server 那么,我们可以把ent放进data文件夹下面去:|- data | |- ent |- biz |- service |- server需要说明的是,项目的结构、命名的规范这些并不在本文阐述的范围之内。并非说非要如此,这个可以根据各自的情况来灵活设计。我使用这样的项目结构和命名规范,仅仅是为了方便讲清楚如何在Kratos中去引用Ent。创建数据库客户端在data/data.go文件中添加创建数据库客户端的代码,并将之注入到ProviderSet:package data // ProviderSet is data providers. var ProviderSet = wire.NewSet( NewEntClient, ... ) // Data . type Data struct { db *ent.Client } // NewEntClient 创建数据库客户端 func NewEntClient(conf *conf.Data, logger log.Logger) *ent.Client { l := log.NewHelper(log.With(logger, "module", "ent/data")) client, err := ent.Open( conf.Database.Driver, conf.Database.Source, ) if err != nil { l.Fatalf("failed opening connection to db: %v", err) } // 运行数据库迁移工具 if true { if err := client.Schema.Create(context.Background(), migrate.WithForeignKeys(false)); err != nil { l.Fatalf("failed creating schema resources: %v", err) } } return client }需要说明的是数据库迁移工具,如果数据库中不存在表,迁移工具会创建一个;如果字段存在改变,迁移工具会对字段进行修改。创建UseCase在biz文件夹下创建user.go:package biz type UserRepo interface { List(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) Get(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) Create(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) Update(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) Delete(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) } type UserUseCase struct { repo UserRepo log *log.Helper } func NewUserUseCase(repo UserRepo, logger log.Logger) *UserUseCase { l := log.NewHelper(log.With(logger, "module", "user/usecase")) return &UserUseCase{repo: repo, log: l} } func (uc *UserUseCase) List(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) { return uc.repo.ListUser(ctx, req) } func (uc *UserUseCase) Get(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) { return uc.repo.GetUser(ctx, req) } func (uc *UserUseCase) Create(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) { return uc.repo.CreateUser(ctx, req) } func (uc *UserUseCase) Update(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) { return uc.repo.UpdateUser(ctx, req) } func (uc *UserUseCase) Delete(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) { return uc.repo.DeleteUser(ctx, req) }注入到biz.ProviderSetpackage biz // ProviderSet is biz providers. var ProviderSet = wire.NewSet( NewUserUseCase, ... )创建Repo在data文件夹下创建user.go文件,实际操作数据库的操作都在此处。package data var _ biz.UserRepo = (*UserRepo)(nil) type UserRepo struct { data *Data log *log.Helper } func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo { l := log.NewHelper(log.With(logger, "module", "User/repo")) return &UserRepo{ data: data, log: l, } } func (r *userRepo) Delete(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) { err := r.data.db.User. DeleteOneID(req.GetId()). Exec(ctx) return err != nil, err } ...注入到data.ProviderSetpackage data // ProviderSet is data providers. var ProviderSet = wire.NewSet( NewUserRepo, ... )在Service中调用package service type UserService struct { v1.UnimplementedUserServiceServer uc *biz.UserUseCase log *log.Helper } func NewUserService(logger log.Logger, uc *biz.UserUseCase) *UserService { l := log.NewHelper(log.With(logger, "module", "service/user")) return &UserService{ log: l, uc: uc, } } // ListUser 列表 func (s *UserService) ListUser(ctx context.Context, req *pagination.PagingRequest) (*v1.ListUserResponse, error) { return s.uc.List(ctx, req) } // GetUser 获取 func (s *UserService) GetUser(ctx context.Context, req *v1.GetUserRequest) (*v1.User, error) { return s.uc.Get(ctx, req) } // CreateUser 创建 func (s *UserService) CreateUser(ctx context.Context, req *v1.CreateUserRequest) (*v1.User, error) { return s.uc.Create(ctx, req) } // UpdateUser 更新 func (s *UserService) UpdateUser(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) { return s.uc.Update(ctx, req) } // DeleteUser 删除 func (s *UserService) DeleteUser(ctx context.Context, req *v1.DeleteUserRequest) (*emptypb.Empty, error) { _, err := s.uc.Delete(ctx, req) if err != nil { return nil, err } return &emptypb.Empty{}, nil }结语Ent是一个优秀的ORM框架。基于模板进行代码生成,相比较利用反射等方式,在性能上的损耗更少。并且,模板的使用使得扩展系统变得简单容易。它不仅能够很对传统的关系数据库(MySQL、PostgreSQL、SQLite)方便的进行查询,并且可以容易的进行图遍历——常用的譬如像是:菜单树、组织树……这种数据查询。Ent的工具链完整。对gRPC和GraphQL也支持的极好,也有相应的一系列工具链进行支持。从数据库表可以用工具转换成Ent的Schema,从Schema可以生成gRPC和GraphQL的API的接口。Kratos的RPC就是基于的gRPC,也支持GraphQL,简直就是为Kratos量身定做的。相比较其他的ORM框架,Ent对工程化的支持是极佳的,这对于开发维护的效率将会有极大的提升,几个项目下来,受益良多。个人而言,我是极力推崇的。参考资料官方网站: https://entgo.io/官方文档: https://entgo.io/zh/docs/getting-started/代码仓库: https://github.com/ent/entSQL转Schema在线工具: https://printlove.cn/tools/sql2entORM 实例教程 - 阮一峰: http://www.ruanyifeng.com/blog/2019/02/orm-tutorial.html
什么是依赖注入?依赖注入 (Dependency Injection,缩写为 DI),是一种软件设计模式,也是实现控制反转(Inversion of Control)的其中一种技术。这种模式能让一个物件接收它所依赖的其他物件。“依赖”是指接收方所需的对象。“注入”是指将“依赖”传递给接收方的过程。在“注入”之后,接收方才会调用该“依赖”。此模式确保了任何想要使用给定服务的物件不需要知道如何建立这些服务。取而代之的是,连接收方物件(像是 client)也不知道它存在的外部代码(注入器)提供接收方所需的服务。依赖注入涉及四个概念:服务:任何类,提供了有用功能。客户:使用服务的类。接口:客户不应该知道服务实现的细节,只需要知道服务的名称和 API。注入器:Injector,也称 assembler、container、provider 或 factory。负责把服务引入给客户。依赖注入把对象构建与对象注入分开。因此创建对象的 new 关键字也可消失了。Golang 的依赖注入框架有两类:通过反射在运行时进行依赖注入,典型代表是 Uber 开源的 dig;通过 generate 进行代码生成,典型代表是 Google 开源的 wire。使用 dig 功能会强大一些,但是缺点就是错误只能在运行时才能发现,这样如果不小心的话可能会导致一些隐藏的 bug 出现。使用 wire 的缺点就是功能限制多一些,但是好处就是编译的时候就可以发现问题,并且生成的代码其实和我们自己手写相关代码差不太多,更符合直觉,心智负担更小,所以更加推荐 wire。什么是Wire?wire 是由 google 开源的一个供 Go 语言使用的依赖注入代码生成工具。它能够根据你的代码,生成相应的依赖注入 go 代码。与其他依赖注入工具不同,比如 Uber 的 Dig 和 Facebook 的 Inject,这 2 个工具都是使用反射实现的依赖注入,而且是运行时注入(runtime dependency injection)。wire 是编译代码时生成代码的依赖注入,是编译期间注入依赖代码(compile-time dependency injection)。而且代码生成期间,如果依赖注入有问题,生成依赖代码时就会出错,就可以报出问题来,而不必等到代码运行时才暴露出问题。Provider 和 Injector首先,需要理解 wire 的 2 个核心概念:provider 和 injector。从上面 Java 模拟依赖注入的例子中,可以简化出依赖注入的步骤:第一:需要 New 出一个类实例第二:把这个 New 出来的类实例通过构造函数或者其他方式“注入”到需要使用它的类中第三:在类中使用这个 New 出来的实例从上面步骤来理解 wire 的 2 个核心概念 provider 和 injector。provider 就相当于上面 New 出来的类实例。injector 就相当于“注入”动作前,把所需依赖函数进行聚合,根据这个聚合的函数生成依赖关系。provider:提供一个对象。 injector:负责根据对象依赖关系,生成新程序。ProviderProvider 是一个普通的 Go 函数 ,可以理解为是一个对象的构造函数。为下面生成 Injector 函数提供”构件“。下面的 NewUserStore() 函数可以看作是一个 provider。这个函数需要传入 *Config 和 *mysql.DB 2 个参数。// NewUserStore 是一个 provider for *UserStore,*UserStore 依赖 *Config,*mysql.DB func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {... ...} // NewDefaultConfig 是一个 provider for *Config,没有任何依赖 func NewDefaultConfig() *Config {...} // NewDB 是 *mysql.DB 的一个 provider ,依赖于数据库连接信息 *ConnectionInfo func NewDB(info *ConnectionInfo) (*mysql.DB, error){...}provider 可以组合成一组 provider set。对于经常在一起使用的 providers 来说,这个非常有用。使用 wire.NewSet 方法可以把他们组合在一起,var SuperSet = wire.NewSet(NewUserStore, NewDefaultConfig)你也可以把其他的 provider sets 加入一个 provider set,import ( “example.com/some/other/pkg” ) // ... ... var MegaSet = wire.NewSet(SuperSet, pkg.OtherSet)wire.NewSet() 函数: 这个函数可以把相关的 provider 组合在一起然后使用。当然也可以单独使用,如 var Provider = wire.NewSet(NewDB)。 这个 NewSet 函数的返回值也可以作为其他 NewSet 函数的参数使用,比如上面的 SuperSet 作为参数使用。Injector我们编写程序把这些 providers 组合起来(比如下面例子 initUserStore() 函数),wire 里的 wire 命令会按照依赖顺序调用 providers 生成更加完整的函数,这个就是 injector。首先,编写生成 injector 的签名函数,然后用 wire 命令生成相应的函数。例子如下:// +build wireinject func initUserStore(info *ConnectionInfo) (*UserStore, error) { wire.Build(SuperSet, NewDB) // 声明获取 UserStore 需要调用哪些 provider 函数 return nil, nil }然后用 wire 命令把上面的 initUserStore 函数生成 injector 函数,生成的函数对应文件名 wire_gen.go。wire 命令:You can generate the injector by invoking Wire in the package directory。 直接在生成 injector 函数的包下,使用 wire 命令,就可以生成 injector 代码。wire.Build() 函数:它的参数可以是 wire.NewSet() 组织的一个或多个 provider,也可以直接使用 provider。与Kratos携起手来Wire命令行工具安装使用以下命令将Wire的命令行工具安装在全局路径下,用于代码的生成。go install github.com/google/wire/cmd/wire@latest场景代码在这里,我们做一个“用户服务”。根据Kratos的官方推荐Layout,我们将服务分为以下几层:server、service、biz、data。package server func NewHTTPServer(c *conf.Server, ac *conf.Auth, logger log.Logger, userSvc *service.UserService) *http.Server { var opts = []http.ServerOption{} if c.Http.Network != "" { opts = append(opts, http.Network(c.Http.Network)) } if c.Http.Addr != "" { opts = append(opts, http.Address(c.Http.Addr)) } if c.Http.Timeout != nil { opts = append(opts, http.Timeout(c.Http.Timeout.AsDuration())) } srv := http.NewServer(opts...) v1.RegisterUserServiceHTTPServer(srv, userSvc) return srv }package service type UserService struct { v1.UnimplementedUserServiceServer uc *biz.UserUseCase log *log.Helper } func NewUserService(logger log.Logger, uc *biz.UserUseCase) *UserService { l := log.NewHelper(log.With(logger, "module", "service/user")) return &UserService{ log: l, uc: uc, } }package biz type UserRepo interface { Create(ctx context.Context, req *v1.RegisterRequest) (*v1.User, error) Update(ctx context.Context, req *v1.UpdateUserRequest) (*v1.User, error) Delete(ctx context.Context, req *v1.DeleteUserRequest) (bool, error) } type UserUseCase struct { repo UserRepo log *log.Helper } func NewUserUseCase(repo UserRepo, logger log.Logger) *UserUseCase { l := log.NewHelper(log.With(logger, "module", "user/usecase")) return &UserUseCase{ repo: repo, log: l, } }package data var _ biz.UserRepo = (*UserRepo)(nil) type UserRepo struct { data *Data log *log.Helper } func NewUserRepo(data *Data, logger log.Logger) biz.UserRepo { l := log.NewHelper(log.With(logger, "module", "user/repo")) return &UserRepo{ data: data, log: l, } }没有Wire,我们该如何组装代码?现在,我们需要把上面这几个包组合起来,常规都是这样写的:package main func main() { userRepo := data.NewUserRepo(dataData, logger) userUseCase := biz.NewUserUseCase(userRepo, logger) userService := service.NewUserService(logger, userUseCase) httpSrv := server.NewHTTPServer(confServer, auth, logger, userService) app := kratos.New( kratos.Name("http"), kratos.Server( httpSrv, ), ) if err := app.Run(); err != nil { log.Error(err) } }唔,看起来好像也没有什么啊,我觉着这么写也没啥问题啊。是的,如果项目的规模很小的时候,这样写也没啥毛病,而且看起来还挺清晰的。那么,我的项目没有这么简单了,突然爆炸了:client := data.NewEntClient(confData, logger) redisClient := data.NewRedisClient(confData, logger) dataData, cleanup, err := data.NewData(client, redisClient, logger) if err != nil { return nil, nil, err } userRepo := data.NewUserRepo(dataData, logger) userUseCase := biz.NewUserUseCase(userRepo, logger) userTokenRepo := data.NewUserTokenRepo(dataData, auth, logger) userTokenUseCase := biz.NewUserAuthUseCase(userTokenRepo) userService := service.NewUserService(logger, userUseCase, userTokenUseCase) postRepo := data.NewPostRepo(dataData, logger) postUseCase := biz.NewPostUseCase(postRepo, logger) postService := service.NewPostService(logger, postUseCase) linkRepo := data.NewLinkRepo(dataData, logger) linkUseCase := biz.NewLinkUseCase(linkRepo, logger) linkService := service.NewLinkService(logger, linkUseCase) categoryRepo := data.NewCategoryRepo(dataData, logger) categoryUseCase := biz.NewCategoryUseCase(categoryRepo, logger) categoryService := service.NewCategoryService(logger, categoryUseCase) commentRepo := data.NewCommentRepo(dataData, logger) commentUseCase := biz.NewCommentUseCase(commentRepo, logger) commentService := service.NewCommentService(logger, commentUseCase) tagRepo := data.NewTagRepo(dataData, logger) tagUseCase := biz.NewTagUseCase(tagRepo, logger) tagService := service.NewTagService(logger, tagUseCase) attachmentRepo := data.NewAttachmentRepo(dataData, logger) attachmentUseCase := biz.NewAttachmentUseCase(attachmentRepo, logger) attachmentService := service.NewAttachmentService(logger, attachmentUseCase) httpServer := server.NewHTTPServer(confServer, auth, logger, userService, postService, linkService, categoryService, commentService, tagService, attachmentService) registrar := server.NewRegistrar(registry)现在,你再来看。我就问你,头大不大?脑壳晕不晕?心情美不美丽?这是一个圆环套圆环的游戏,你不仅需要手写这么多的代码,而且,还需要管理他们之间的依赖关系,要小心翼翼的别把传入参数搞错、创建的顺序别搞错。这时候,我要:增加一个方法,减少一个方法;增加一个变量,减少一个变量。都是很奔溃的事情。哪怕你再小心翼翼,也保不齐自己不出错。有了Wire,我们可以如何组装代码?首先需要在上面4个包下面声明4个ProviderSet变量:package server import ( "github.com/google/wire" ) // ProviderSet is server providers. var ProviderSet = wire.NewSet(NewHTTPServer)package service import ( "github.com/google/wire" ) // ProviderSet is service providers. var ProviderSet = wire.NewSet( NewUserService, )package biz import "github.com/google/wire" // ProviderSet is biz providers. var ProviderSet = wire.NewSet( NewUserUseCase, )package data // ProviderSet is data providers. var ProviderSet = wire.NewSet( NewData, NewEntClient, NewRedisClient, NewUserRepo, )而现在,main包下面,我需要两个go文件:main.gopackage main func newApp(logger log.Logger, hs *http.Server, rr registry.Registrar) *kratos.App { return kratos.New( kratos.ID(Service.GetInstanceId()), kratos.Name(Service.Name), kratos.Version(Service.Version), kratos.Metadata(Service.Metadata), kratos.Logger(logger), kratos.Server( hs, ), kratos.Registrar(rr), ) } func main() { app, cleanup, err := initApp(bc.Server, rc, bc.Data, bc.Auth, logger) if err != nil { panic(err) } defer cleanup() // start and wait for stop signal if err := app.Run(); err != nil { fmt.Println(err) panic(err) } }wire.go//go:build wireinject // +build wireinject package main import ( "/internal/biz" "/internal/conf" "/internal/data" "/internal/server" "/internal/service" "github.com/go-kratos/kratos/v2" "github.com/go-kratos/kratos/v2/log" "github.com/google/wire" ) // initApp init kratos application. func initApp(*conf.Server, *conf.Registry, *conf.Data, *conf.Auth, log.Logger) (*kratos.App, func(), error) { panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp)) }然后在main包路径下直接运行wire命令:$ wire wire: XXXX: wrote XXXX\wire_gen.go该命令将会在main包路径下生成一个wire_gen.go文件:// Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject package main // Injectors from wire.go: // initApp init kratos application. func initApp(confServer *conf.Server, registry *conf.Registry, confData *conf.Data, auth *conf.Auth, logger log.Logger) (*kratos.App, func(), error) { client := data.NewEntClient(confData, logger) redisClient := data.NewRedisClient(confData, logger) dataData, cleanup, err := data.NewData(client, redisClient, logger) if err != nil { return nil, nil, err } userRepo := data.NewUserRepo(dataData, logger) userUseCase := biz.NewUserUseCase(userRepo, logger) userService := service.NewUserService(logger, userUseCase) httpServer := server.NewHTTPServer(confServer, auth, logger, userService) registrar := server.NewRegistrar(registry) app := newApp(logger, httpServer, registrar) return app, func() { cleanup() }, nil }明眼人的你一看就明白了:那些初始化依赖的代码全部都在生成的代码当中了。从此,圆环套圆环,你调用我我调用你,依赖管理的这些脏活累活,你再也不需要接触,再也不需要干了,全部都丢给了Wire。从此往后,你需要做什么呢?维护每一个依赖包下面的ProviderSet,然后运行wire命令。比如,我现在需要增加一个GRPC服务器,只需要在ProviderSet里边添加NewGRPCServer方法:var ProviderSet = wire.NewSet(NewHTTPServer, NewGRPCServer, NewRegistrar)然后运行wire命令,这时候wire_gen.go文件里边就会增加NewGRPCServer方法的调用。再比如,我现在需要在NewHTTPServer方法增加一个变量,ProviderSet此时倒是不需要动的。但是,必须要执行wire命令,重新生成代码。注意事项wire 不允许不同的注入对象拥有相同的类型。google 官方认为这种情况,是设计上的缺陷。这种情况下,可以通过类型别名来将对象的类型进行区分。func NewRegistrar(conf *conf.Registry) registry.Registrar var ProviderSet = wire.NewSet(NewRegistrar, NewRegistrar)以上的代码是不合法的,会报错ProviderSet has multiple bindings for ***。我们可以用下面的方法规避,但是,不建议这么做:type RegistrarB registry.Registrar func NewRegistrarA(conf *conf.Registry) registry.Registrar func NewRegistrarB(conf *conf.Registry) RegistrarB var ProviderSet = wire.NewSet(NewRegistrarA, NewRegistrarB)结语Wire 是一个强大的依赖注入工具。项目工程化过程中,Wire 可以很好的帮助我们管理依赖关系,协助我们完成复杂对象的构建组装。与此同时,Wire与 Inject 、Dig 等不同的是,Wire只生成代码,而不是使用反射在运行时注入,因此不需要担心会有性能损耗。参考资料Wire GithubDig GithubGo工程化(三) 依赖注入框架 wire理解一下依赖注入,以及如何用 wire依赖注入 - 维基百科Dependency Injection DemystifiedGo Cloud Wire:编译时依赖注入详解golang常用库包:Go依赖注入(DI)工具-wire使用Golang依赖注入框架wire使用详解
ASIO的定时器ASIO现在提供的定时器有以下几种:boost::asio::deadline_timer boost::asio::steady_timer boost::asio::high_resolution_timer boost::asio::system_timerhigh_resolution_timer就是system_timer,也是精度最高的定时器,精度为:纳秒。steady_timer、system_timer都是模板basic_waitable_timer<>的特例化:typedef basic_waitable_timer<chrono::system_clock> system_timer; typedef basic_waitable_timer<chrono::steady_clock> steady_timer;deadline_timer是basic_deadline_timer<>的特例化,它使用的计量时间是系统时间,修改系统时间会对它造成影响:typedef basic_deadline_timer<boost::posix_time::ptime> deadline_timer;下面是一个完整的定时器应用示例:#include <boost/asio.hpp> #include <iostream> namespace asio = boost::asio; using error_code = boost::system::error_code; auto now() { return std::chrono::high_resolution_clock::now(); } void async_wait(asio::high_resolution_timer& timer, std::chrono::high_resolution_clock::time_point& lastTime) { timer.expires_after(std::chrono::seconds(1)); timer.async_wait([&](error_code ec) { if (ec == asio::error::operation_aborted) { return; } auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now() - lastTime).count(); lastTime = now(); std::cout << elapsed << "\n"; async_wait(timer, lastTime); }); } int main() { asio::io_context ctx; asio::high_resolution_timer timer(ctx); auto lastTime = now(); async_wait(timer, lastTime); ctx.run(); }定时器的到期时间可以用持续时间:timer.expires_after(std::chrono::seconds(1));也可以用绝对时间去定时:timer.expires_at(std::chrono::high_resolution_clock::now() + std::chrono::seconds(1));如果要获取到期的绝对时间值,可以调用expiry()成员方法:std::chrono::high_resolution_clock::time_point time_point = timer.expiry();如果要取消当前定时器,可以调用cancel成员方法,它将触发一个错误码为boost::asio::error::operation_aborted的回调:timer.async_wait([&] (error_code error) { if(error == boost::asio::error::operation_aborted) { std::cout << "The timer is cancelled\n"; } }); timer.cancel();
PostgreSQL查询交叉表什么是交叉表?交叉表(Cross Tabulations) 是一种常用的分类汇总表格。利用交叉表查询数据非常直观明了,被广泛应用。交叉表查询也是数据库的一个特点。概念在统计学中,交叉表是矩阵格式的一种表格,显示变量的(多变量)频率分布。交叉表被广泛用于调查研究,商业智能,工程和科学研究。它们提供了两个变量之间的相互关系的基本画面,可以帮助他们发现它们之间的相互作用。卡尔·皮尔逊(Karl Pearson)首先在“关于应变的理论及其关联理论与正常相关性”中使用了交叉表。多元统计学的一个关键问题是找到高维应变表中包含的变量的(直接)依赖结构。如果某些有条件的独立性被揭示,那么甚至可以以更智能的方式来完成数据的存储。为了做到这一点,可以使用信息理论概念,它只能从概率分布中获得信息,这可以通过相对频率从交叉表中容易地表示。举例假设我们有两个变量,性别(男性或女性)和手性(右或左手)。 进一步假设,从非常大的人群中随机抽取100个人,作为对手性的性别差异研究的一部分。 可以创建一个应变表来显示男性和右撇子,男性和左撇子,女性和右撇子以及女性和左撇子的个人数量。 这样的应变表如下所示。Gender\HandednessRight HandedLeft HandedTotalMale43952Female44448Total8713100男性,女性以及右撇子和左撇子个体的数量称为边际总数。总计(即应急表中所代表的个人总数)是右下角的数字。这张表格让我们一目了然地看到,右撇子男子的比例与右撇子女性的比例大致相同。两种比例差异的意义可以通过各种统计检验来评估,包括Pearson的卡方检验,G检验,Fisher精确检验和巴纳德检验,条件是表中的条目代表从人口我们想得出结论。如果不同列中的个体的比例在行之间变化很大(反之亦然),则我们说两个变量之间存在偶然性。换句话说,这两个变量不是独立的。如果没有偶然性,我们说这两个变量是独立的。上面的例子是最简单的交叉表,每个变量只有两个级别的表:这被称为2×2交叉表。原则上可以使用任何数量的行和列。也可能有两个以上的变量,但较高阶的偶然事件表难以在视觉上表示。序数变量之间或序数变量与分类变量之间的关系也可以用交叉表来表示,尽管这种做法很少见。交叉报表交叉报表是报表当中常见的类型,属于基本的报表,是行、列方向都有分组的报表。这里牵涉到另外一个概念即分组报表。这是所有报表当中最普通,最常见的报表类型,也是所有报表工具都支持的一种报表格式。从一般概念上来讲,分组报表就是只有纵向的分组。传统的分组报表制作方式是把报表划分为条带状,用户根据一个数据绑定向导指定分组,汇总字段,生成标准的分组报表。转换查询交叉表列式数据,即原始数据如下:NamesubjectscoreLucyEnglish100LucyPhysics90LucyMath85LilyEnglish95LilyPhysics81LilyMath84DavidEnglish100DavidPhysics86DavidMath89SimonEnglish90SimonPhysics76SimonMath79行数据查询结果:NameEnglishPhysicsMathSimon907679Lucy1009085Lily958184David1008689创建测试表CREATE TABLE IF NOT EXISTS score ( name VARCHAR, subject VARCHAR, score FLOAT );插入测试数据TRUNCATE TABLE score; INSERT INTO score (name, subject, score) VALUES ('Lucy', 'English', 100), ('Lucy', 'Physics', 90), ('Lucy', 'Math', 85), ('Lily', 'English', 95), ('Lily', 'Physics', 81), ('Lily', 'Math', 84), ('David', 'English', 100), ('David', 'Physics', 86), ('David', 'Math', 89), ('Simon', 'English', 90), ('Simon', 'Physics', 76), ('Simon', 'Math', 79);1. 标准聚合函数查询SELECT name, sum(CASE WHEN subject = 'English' THEN score ELSE 0 END) AS "English", sum(CASE WHEN subject = 'Physics' THEN score ELSE 0 END) AS "Physics", sum(CASE WHEN subject = 'Math' THEN score ELSE 0 END) AS "Math" FROM score GROUP BY name ORDER BY name DESC;2. PostgreSQL聚合函数查询SELECT name, split_part(split_part(tmp, ',', 1), ':', 2) AS "English", split_part(split_part(tmp, ',', 2), ':', 2) AS "Physics", split_part(split_part(tmp, ',', 3), ':', 2) AS "Math" FROM (SELECT name, string_agg(subject || ':' || score, ',') AS tmp FROM score GROUP BY name ORDER BY name DESC) AS T;3. crosstab交叉函数查询首先需要安装tablefunc扩展,才能够使用crosstab函数。CREATE EXTENSION IF NOT EXISTS tablefunc;SELECT * FROM crosstab('SELECT name, subject, score FROM score ORDER BY name DESC', $$values ('English'::text),('Physics'::text),('Math'::text)$$ ) AS score(name text, English int, Physics int, Math int);参考资料PostgreSQL实现交叉表(行列转换)的五种方法PostgreSQL行转列交叉表
ASIO的post和dispatch方法关于这两个方法,我去网上找了一大堆资料,都没有讲清楚是怎么一回事。还是读了ASIO的源代码这才理解。要提到这两个方法,不得不提一下Windows的两个API:SendMessage和PostMessage。io_context::post跟PostMessage的行为差不多,投递完消息立即返回,Handler的执行跟它没有半毛钱的关系。io_context::dispatch可以认为是SendMessage的超集,SendMessage是阻塞的,必须要在消息处理完成之后才返回,当io_context::dispatch在io_context的工作线程中被调用的时候,io_context::dispatch的行为和SendMessage是一致的,必须要在Handler调用完成之后才返回。但是,如果不是io_context的工作线程中调用,则执行了io_context::post一样的行为:将Handler投递到io_context的事件队列中去。我下面用伪代码来描述其功能:void post(Handler handler) { _queue.push(handler); } void dispatch(Handler handler) { if (can_execute()) handler(); else post(handler); } void run() { _work_thrd_id = std::this_thread::get_id(); while (!_queue.empty()) { auto handler = _queue.front(); _queue.pop(); handler(); } } bool can_execute() { return _work_thrd_id == std::this_thread::get_id(); }
Golang的func中分配的变量通过参数传递出函数域之后变nil的问题最近在Go上面碰到了一个传出参数的问题:func testOutString(out *string) { if out == nil { // str := "hellow" // out = &str out = new(string) } *out = "hello" } func main() { var str *string testOutString(str) fmt.Println(str) }不管是用的堆分配out = new(string), 还是栈上内存分配str := "hellow", 以上代码打印出来的必然是nil。然后下面的代码则不会:func testOutString(out *string) { *out = "hello" } func main() { var str = new(string) testOutString(str) fmt.Println(str) }没错,在调用func之前,传入的参数必须先初始化。其实,仔细想一想这个问题,要理解也是容易的:因为Go语言是带GC的语言,内存是在func域中分配的,所以在func域中分配的内存,在func结束之后,会被GC所回收。换言之,也就是在func中创建出来的变量具有一个引用计数,创建之后引用计数+1。出域的时候,引用计数-1,引用计数就变成了0,GC回收内存也就是很自然的事情了。而返回值的话则不会出现这样的问题,很显然在用返回值传递的时候,引用计数并没有归零,如以下代码:func testOutString() *string { str := "hellow" return &str } func main() { var str *string str = testOutString() fmt.Println(*str) }在这里得提一提golang里面的一个概念:变量逃逸关于变量逃逸,简单来说,就是原本应该分配在函数栈上的局部变量,因为其生命周期超出了所在函数的生命周期,所以编译器将其由栈分配改为堆分配,也就是我们通常所讲的“变量逃逸到了堆上”。上面这个例子实际上就是一种变量逃逸的例子。 而我上面产生问题的代码则并没有产生变量逃逸。照理来说,按照我曾经C和C++的经验,在函数域里边new出来的变量应该是可以传递出来的,直觉上是没问题的,然而就是这个直觉并不是正确的。这样可以产生变量逃逸:func testOutString(out **string) { var n = "hellow" *out = &n } func main() { var str *string testOutString(&str) fmt.Println(*str) }还有全局变量也能产生变量逃逸:var str *string func testOutString() { var n = new(string) *n = "hello" str = n } func main() { testOutString() fmt.Println(*str) }
golang软件包列表软件包样例格式ksuid0pPKHjWprnVxGH7dEsAoXX2YQvU4 bytes of time (seconds) + 16 random bytesxidb50vl5e54p1000fo3gh04 bytes of time (seconds) + 3 byte machine id + 2 byte process id + 3 bytes randombetterguid-N-35bz_wVbhxzTZD2wO8 bytes of time (milliseconds) + 9 random bytessonyflake402329004549341446~6 bytes of time (10 ms) + 1 byte sequence + 2 bytes machine idsnowflake15120524612469719041 Bit Unused + 41 Bit Timestamp + 10 Bit NodeID + 12 Bit Sequence IDsnowflake15120524612469719041 Bit Unused + 41 Bit Timestamp + 10 Bit NodeID + 12 Bit Sequence IDulid01BJMVNPBBZC3E36FJTGVF0C4S6 bytes of time (milliseconds) + 8 bytes randomsid1JADkqpWxPx-4qaWY47~FqI8 bytes of time (ns) + 8 random bytesshortuuiddwRQAc68PhHQh4BUnrNsoSUUIDv4 or v5, encoded in a more compact waygo.uuid5b52d72c-82b3-4f8e-beb5-437a974842cUUIDv4 from RFC 4112 for comparisongoogle.uuiddd5f48eb-1722-4e5f-9d56-dcaf0aae1026UUIDv4bomberman.uuid330806cf-684c-43f7-85aa-79939ed3415dUUIDv4MongoDB ObjectID624ee0fb37583f00042dc3094-byte timestamp + 5-byte random value + 3-byte incrementing counter代码package main import ( "fmt" "log" "math/rand" "time" "github.com/bwmarrin/snowflake" "github.com/chilts/sid" goSnowflake "github.com/godruoyi/go-snowflake" guuid "github.com/google/uuid" "github.com/kjk/betterguid" "github.com/lithammer/shortuuid" "github.com/oklog/ulid" pborman "github.com/pborman/uuid" "github.com/rs/xid" goUUID "github.com/satori/go.uuid" "github.com/segmentio/ksuid" "github.com/sony/sonyflake" "go.mongodb.org/mongo-driver/bson/primitive" ) func genShortUUID() { id := shortuuid.New() fmt.Printf("github.com/lithammer/shortuuid: %s\n", id) } func genXid() { id := xid.New() fmt.Printf("github.com/rs/xid: %s\n", id.String()) } func genKsuid() { id := ksuid.New() fmt.Printf("github.com/segmentio/ksuid: %s\n", id.String()) } func genBetterGUID() { id := betterguid.New() fmt.Printf("github.com/kjk/betterguid: %s\n", id) } func genUlid() { t := time.Now().UTC() entropy := rand.New(rand.NewSource(t.UnixNano())) id := ulid.MustNew(ulid.Timestamp(t), entropy) fmt.Printf("github.com/oklog/ulid: %s\n", id.String()) } func genSonyflake() { flake := sonyflake.NewSonyflake(sonyflake.Settings{}) id, err := flake.NextID() if err != nil { log.Fatalf("flake.NextID() failed with %s\n", err) } // Note: this is base16, could shorten by encoding as base62 string fmt.Printf("github.com/sony/sonyflake: %d\n", id) } func genSnowflake() { nodeId := rand.Int63() % 1023 node, err := snowflake.NewNode(nodeId) if err != nil { log.Fatalf("snowflake.NewNode() failed with %s\n", err) } id := node.Generate().Int64() fmt.Printf("github.com/bwmarrin/snowflake: %d\n", id) } func genGoSnowflake() { id := goSnowflake.ID() fmt.Printf("github.com/godruoyi/go-snowflake: %d\n", id) } func genSid() { id := sid.Id() fmt.Printf("github.com/chilts/sid: %s\n", id) } func genUUIDv4() { id := goUUID.NewV4() fmt.Printf("github.com/satori/go.uuid: %s\n", id) } func genUUID() { id := guuid.New() fmt.Printf("github.com/google/uuid: %s\n", id.String()) } func genPbormanUUID() { id := pborman.NewRandom() fmt.Printf("github.com/pborman/uuid: %s\n", id.String()) } func genMongoDBObjectID() { id := primitive.NewObjectID() fmt.Printf("go.mongodb.org/mongo-driver/bson/primitive: %s\n", id.Hex()) } func main() { genXid() genKsuid() genBetterGUID() genUlid() genSonyflake() genSnowflake() genGoSnowflake() genSid() genShortUUID() genUUIDv4() genUUID() genPbormanUUID() genMongoDBObjectID() }参考资料Generating good unique ids in GoGenerate a UUID/GUID in Go (Golang)
导航快捷键快捷键说明Ctrl + Tab(^ Tab)切换标签页(还要进行此选择,效率差些)Ctrl + E(⌘ E)查看最近打开的文件Ctrl+B 或 Ctrl+单击 (⌘ B 或 ⌘ + 单击)立即跳转到符号的定义Ctrl + Alt + B立即跳转到符号的实现Ctrl + Shift + T (⇧ ⌘ T)跳转至测试Ctrl + Alt + F7 (⌘ ⌥ F7)显示用例Shift + Shift(⇧⇧)快速查找任意内容Ctrl + Shift + A快速查找并使用编辑器所有功能(必记)Ctrl + N (⌘ O)快速查找类Ctrl + Shift + N (⌘ ⇧ O)通过文件名快速查找工程内的文件(必记)Ctrl + Shift + Alt + N (⌘ ⇧ ⌥ O)通过一个字符快速查找位置(必记)编辑快捷键快捷键说明Ctrl + / (⌘ /)单行 注释 or 取消注释(//)Ctrl + Shift + /(⌘ ⇧ /)多行 注释 or 取消注释(/…/ )Ctrl + D (⌘ D)复制当前行Ctrl + X (⌘ X)删除当前行并保存到剪切板Ctrl + Y (⌘ ⌫)删除当前行Ctrl + Delete删除到单词结束Ctrl + Backspace删除到单词开始Ctrl + W (⌥ ↑)快速展开所选区域(不断按,不断扩大选中区域)Ctrl + Shift + W (⌥ ↓)快速收起所选区域(不断按,不断缩小选中区域)Ctrl + Space (^ Space)基本代码补全Alt + Enter (⌥ Enter)显示意图Shift + Enter (⇧ Enter)在当前行的下一行起一行新的空行(无论光标在哪个位置)Ctrl + Alt + Enter (⌘ ⌥ Enter)在当前行的上一行起一行新的空行(无论光标在哪个位置)Ctrl + Shift + Enter (⌘ ⇧ Enter)补全语句Ctrl + Alt + T用 if, else, try, catch, for 等来围绕选中的代码块Ctrl + Shift + F7(⌘ ⇧ F7)高亮显示选中文本,按Esc高亮消失Ctrl + F12 (⌘ F12)结构视图Ctrl + G(⌘ L)跳转至行F2 或 Shift + F2 (F2)高亮错误或警告快速定位Alt + 点击 (⌥ + 点击)多重文本光标Ctrl + Alt + Shift + Insert (⇧ ⌘ N)临时文件可以在 IDE 中快速创建代码示例或笔记,而不对项目文件产生影响Ctrl + Shift + ↑/↓代码向上/下移动。Alt + ↑/↓跳转到上一个/下一个方法Ctrl + R替换文本Ctrl + Shift + R指定目录内代码批量替换Ctrl + F查找文本Ctrl + Shift + F指定目录内代码批量查找Ctrl + Shift + U切换光标所选中单词的大小写Ctrl + Alt + L格式化代码Ctrl + ]/[光标到移动到代码块的前面或后面Ctrl + Shift + I打开定义快速查找F3查找下一个Shift + F3查找上一个Alt + F1查找代码所在位置Ctrl + P方法参数提示重构快捷键快捷键说明Ctrl + Alt + M (⌘ ⌥ M)提取方法Shift + F6 (⇧ F6)重命名Ctrl + Alt + V (⌘ ⌥ V)提取变量Ctrl + Alt + Shift + T (⌘ ⌥ V)快速访问所选代码的可用重构列表其他快捷键(摘录)快捷键说明Alt + Shift + F将当前文件加入收藏夹Ctrl + Alt + S打开配置窗口F11切换书签,就是 sublime text 的F2Alt + Shift + F添加至收藏夹Alt + [0-9]快速拆合功能界面模块Ctrl + Shift + F12最大区域显示代码(会隐藏其他的功能界面模块)Alt + ←/→切换代码选项卡Ctrl + F4关闭当前代码选项卡Ctrl + ←/→以单词作为边界跳光标位置Alt + Insert新建一个文件或其他Shift + Tab/Tab减少/扩大缩进(可以在代码中减少行缩进)Alt + F1查找代码在其他界面模块的位置,颇为有用Ctrl + Shift + F12开启或关闭最大化编辑Ctrl + Alt + F11开启或关闭全屏模式参考文章JetBrains IDE 基本快捷键十大必会 WebStorm 快捷键
Docker的容器当中一般是没有安装任何编辑器的,vi和vim神马的都没有.如果想要在容器中使用编辑器,需要自己去安装.但是,在 Docker 中执行:apt-get update报错:E: List directory /var/lib/apt/lists/partial is missing. - Acquire (13: Permission denied)这是因为在Docker Desktop中启动命令行时并没有以管理员身份启动,而是以普通用户的身份启动的,权限不足.要解决这个问题,需要用以下命令启动 :docker exec -u 0 -it {容器id} /bin/bash其中-u 0代表是以root用户启动Docker的命令行,再执行更新命令就可以了.apt-get install vim这就行了,再到Docker Desktop中启动容器的命令行,这时候已经有vim了.
今天Windows11升级重启了,我启动RabbitMQ,然后提示端口被占用,而无法启动Docker. 提示信息如下:listen tcp 0.0.0.0:1883: bind: An attempt was made to access a socket in a way forbidden by its access permissions.解决办法打开管理员模式 Windows PowerShell,执行下面命令重启 NAT 驱动服务即可net stop winnat net start winnat
什么是消息队列MQ就是消息队列,是Message Queue的缩写。消息队列是一种通信方式。消息的本质就是一种数据结构。因为MQ把项目中的消息集中式的处理和存储,所以MQ主要有解耦,并发,和削峰的功能。为什么要使用消息队列1. 异步通常的微服务实现里面,都是通过RPC进行微服务之间的相互调用,这是同步的。如果消息队列的话,可以实现异步的调用。至于异步有啥好处呢,主要是为了削峰。2. 削峰同步的调用会带来一个问题:瞬时流量。客户的调用同步接口节奏,你是无法把控的,流量将会是忽高忽低的,猛的来一波,搞不好系统就崩了溃了。如果消息队列的话,可以实现异步的调用,并且可以实现削峰,请求进来,我先放到消息队列里面去,慢慢消化掉,不至于猛的来一下,把系统击垮。3. 解耦通常的微服务实现里面,都是通过RPC进行微服务之间的相互调用,那么意味着,你要做到一件事情,你必须要知道做事情的对方是谁。在微服务的世界里面,如果设计得不好,那就是一团糟的相互调用网络,看得你晕晕的,运维会疯,后面接手的开发人员也得疯。应用了消息队列,你就只需要跟消息队列这个代理打交道,单线联系,关系简单。我们只需要生产消息,消费消息,至于是谁消费的,谁生产的,完全不用去管它。架构上,就会清爽多了。所以,要对服务进行解耦,消息队列是一个很好的选择。Kratos与消息队列Kratos现在的版本(v2.2.1)中,还没有对消息队列的直接支持,但是要运用还是容易的。官方有一个空壳示例代码BeerShop,可以看到,在data层,使用Kafka的痕迹。对于在Kratos微服务框架里面应用消息队列,我认为有两种方式可以实现:在data层,使用消息队列,但是在这个层,你只能在那生产消息,而不好消费消息。将消息队列的客户端实现为微服务的一个Server,然后在微服务的Service中消费消息和生产消息。第一个方式的应用面不广,更多的时候,第二种方式的应用面会更广一些,我选择了第二种方式。但是,Kratos官方并没有支持这一种方式。故而,我只能够自己动手实现了,我从另外一个微服务Go-Micro里面提取了其Broker的实现,并且将其实现为Kratos框架里面的一个Server。事实证明,这样是可行的,并且很好使。你可能会问,为什么我不直接使用Go-Micro呢?因为Go-Micro是一个很重的微服务框架,尽管它的功能很丰富,几乎支持了大部分的微服务需求。但是对于一个应用来说,我并不需要使用所有的技术栈、中间件,我只需要部分的技术栈。所以,我宁愿做加法,也不愿意去做减法。对于服务端来说,可控、可用、可维护是最重要的。极简,是一个很好的选择。另外,我还要腹诽一点,我从Go-Micro提取出来的Broker在测试的过程中发现,都有一些瑕疵。我实现的代码,我放到了github:https://github.com/tx7do/kratos-transport。它所支持的协议和消息队列有:KafkaRabbitMQNATSRedisMQTTWebSocket基本上是够用了。kratos-transport的应用它主要分为了3个部分:1. Codec 编解码器这一块和Broker都是从Go-Micro提取出来的,但是它对我的应用来说,还并没有什么用处,因为我的编解码器是很独特的,需要定制的。所以,我暂时还没用上这一块。2. Broker 消息队列可以直接拿来用,我拿Kafka举例:package kafka import ( "context" "fmt" "github.com/tx7do/kratos-transport/broker" "os" "os/signal" "syscall" "testing" ) func TestSubscribe(t *testing.T) { interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) ctx := context.Background() b := NewBroker( broker.Addrs("127.0.0.1:9092"), broker.OptionContext(ctx), ) _, _ = b.Subscribe("logger.sensor.ts", receive, broker.SubscribeContext(ctx), broker.Queue("fx-group"), ) <-interrupt } func receive(event broker.Event) error { fmt.Println("Topic: ", event.Topic(), " Payload: ", string(event.Message().Body)) //_ = event.Ack() return nil } func TestPublish(t *testing.T) { ctx := context.Background() b := NewBroker( broker.Addrs("127.0.0.1:9092"), broker.OptionContext(ctx), ) var msg broker.Message msg.Body = []byte(`{"Humidity":60, "Temperature":25}`) _ = b.Publish("logger.sensor.ts", &msg) }3. Server 封装给Kratos的Server实现还是拿Kafka举例:package main import ( "fmt" "github.com/go-kratos/kratos/v2" "log" "github.com/tx7do/kratos-transport/broker" "github.com/tx7do/kratos-transport/transport/kafka" ) func main() { //ctx := context.Background() kafkaSrv := kafka.NewServer( kafka.Address("127.0.0.1:9092"), kafka.Subscribe("test_topic", "a-group", receive), ) app := kratos.New( kratos.Name("kafka"), kratos.Server( kafkaSrv, ), ) if err := app.Run(); err != nil { log.Println(err) } } func receive(event broker.Event) error { fmt.Println("Topic: ", event.Topic(), " Payload: ", string(event.Message().Body)) return nil } func sendData(sendData []byte) error { var msg broker.Message msg.Body = sendData kafkaSrv.Publish(topic, &msg) }另外再看一个例子,是Websocket的,它的应用其实也是很广的:package main import ( "fmt" "log" "github.com/go-kratos/kratos/v2" "github.com/tx7do/kratos-transport/transport/websocket" ) func main() { //ctx := context.Background() wsSrv := websocket.NewServer( websocket.Address(":8800"), websocket.ReadHandle("/ws", handleMessage), websocket.ConnectHandle(handleConnect), ) app := kratos.New( kratos.Name("websocket"), kratos.Server( wsSrv, ), ) if err := app.Run(); err != nil { log.Println(err) } } func handleConnect(connectionId string, register bool) { if register { fmt.Printf("%s connected\n", connectionId) } else { fmt.Printf("%s disconnect\n", connectionId) } } func handleMessage(connectionId string, message *websocket.Message) (*websocket.Message, error) { fmt.Printf("[%s] Payload: %s\n", connectionId, string(message.Body)) var relyMsg websocket.Message relyMsg.Body = []byte("hello") return &relyMsg, nil }具体的应用实例我写了两个实例代码,并且都已经提交到了Kratos的examples代码仓库中去了。这两个例子都是物联网方面的应用。kratos-cqrs这是一个简单的CQRS的实现,主要就是拿了Kafka来消费来自于传感器的遥感数据,然后把数据存储到数据库中去。需要注意的是,这个实例并不够完整,我并没有实现MQTT的消费,没有实现前端页面等等。只实现了对Kafka的消费。kratos-realtimemap这是一个完整的例子,有前端,有后端,可以完整的跑起来看。通过MQTT接收一个开放的公交遥测数据源,然后通过REST和Websocket向前端发送数据,在地图上展现出来车辆的轨迹、车辆的位置、车辆的速度、开关门状态等等。
命令查询的责任分离Command Query Responsibility Segregation 通常被简化为 命令查询分离,即读写分离。在特定的场景下,它可以提供更好的性能。但是,在强一致性方面,它并不能够保证。而且,还会带来认知负担。所以,实际运用上,需要谨慎。什么是 CQRS这个概念出自于 命令与查询分离(CQS, Command Query Separation),出自于1987 年 Bertrand Meyer 的 《面向对象软件构造》一书,其原始概念是我们可以把对象操作分为:命令(Command)和 查询(Query)两种形式。命令(Command):在执行之后,会改变对象的状态。查询(Query):仅仅是查看对象的数据,而不会对对象产生改变。而 命令查询的责任分离Command Query Responsibility Segregation (简称CQRS)模式是一种架构体系模式,能够使改变模型的状态的命令和模型状态的查询实现分离。在单体应用时代,它是读写分离:而在微服务的时代,就变成了命令查询的责任分离:读写分离解决了什么?数据库的读写分离就是:将数据库分为了主从库,一个主库用于写数据,多个从库完成读数据的操作,主从库之间通过某种机制进行数据的同步。大多数互联网业务,往往读多写少。这时候,数据库的读会首先成为数据库的瓶颈。这时,如果我们希望能够线性的提升数据库的读性能,消除读写锁冲突从而提升数据库的写性能,那么就可以使用读写分离的架构:主从,主主等。MySQL用的最多的就是主从,主数据库通过BinLog同步到从数据库。这就产生了一个问题,数据不一致问题。如果写数据的压力很大,binlog就会拥塞,从库数据更新不及时,就会读到老旧的脏数据。所以这个方案局限了它的应用范围:只有对一致性要求不高的场景才好使。比如,日志查询,报表等。实现CQRS在这里讨论是物联网的时序数据的存取场景。我们分为两个微服务:日志查询服务(kratos.logger.service)主要是开放了API用于查询数据库,获取日志数据。日志写入服务(kratos.logger.job)订阅Kafka的日志数据写入Topic,写入到时序数据库中去。Docker部署开发服务器TimeScaleDBdocker pull timescale/timescaledb:latest-pg14 docker pull timescale/timescaledb-postgis:latest-pg13 docker pull timescale/pg_prometheus:latest-pg11 docker run -itd \ --name timescale-test \ -p 5432:5432 \ -e POSTGRES_PASSWORD=123456 \ timescale/timescaledb-postgis:latest-pg13Kafkadocker pull bitnami/kafka:latest docker pull bitnami/zookeeper:latest docker pull hlebalbau/kafka-manager:latest docker run -itd \ --name zookeeper-test \ -p 2181:2181 \ -e ALLOW_ANONYMOUS_LOGIN=yes \ bitnami/zookeeper:latest docker run -itd \ --name kafka-standalone \ --link zookeeper-test \ -p 9092:9092 \ -v /home/data/kafka:/bitnami/kafka \ -e KAFKA_BROKER_ID=1 \ -e KAFKA_LISTENERS=PLAINTEXT://:9092 \ -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \ -e KAFKA_ZOOKEEPER_CONNECT=zookeeper-test:2181 \ -e ALLOW_PLAINTEXT_LISTENER=yes \ --user root \ bitnami/kafka:latest docker run -itd \ -p 9000:9000 \ -e ZK_HOSTS="localhost:2181" \ hlebalbau/kafka-manager:latestConsuldocker pull bitnami/consul:latest docker run -itd \ --name consul-server-standalone \ -p 8300:8300 \ -p 8500:8500 \ -p 8600:8600/udp \ -e CONSUL_BIND_INTERFACE='eth0' \ -e CONSUL_AGENT_MODE=server \ -e CONSUL_ENABLE_UI=true \ -e CONSUL_BOOTSTRAP_EXPECT=1 \ -e CONSUL_CLIENT_LAN_ADDRESS=0.0.0.0 \ bitnami/consul:latestJaegerdocker pull jaegertracing/all-in-one:latest docker run -d \ --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:latest测试下载工具PostmanOffset Explorer进行测试测试写使用Offset Explorer 模拟设备,向 Topic logger.sensor.ts 发送JSON数据:[{"ts": 1646409307, "sensor_id": 1, "temperature":30, "cpu":20}]测试读使用Postman向日志服务发起gRPC请求进行查询。技术栈KratosTimeScaleDBKafkaConsulJaegerEntgo实例代码Kratos Examples参考资料淺談 CQRS 的實現方法淺談微服務拆分原理详解 CQRS 架构模式
从单体应用到微服务架构,优势很多,但是并不是代表着就没有一点缺点了。微服务架构,意味着每个服务都是松散耦合的。因此,作为软件工程师和架构师,我们在分布式架构中面临着安全挑战。微服务对外开放的断点,我们称之为:API。单体应用只需要保护自己就可以了,而微服务的攻击面则很大,这意味着越多的服务将会带来更大的风险,每个服务都得保证其安全性。在单体架构中,组件之间通过方法来相互调用。而微服务则是依靠开放的API来相互调用,除了要保证其安全性,还得保障其可用性。因此,根据上述的安全挑战,我们可以得出一个结论:微服务与单体应用有着不同的安全处理方式。认证和授权的区别我们在谈论应用程序安全的时候,总是会提到:认证 (Authentication)和 鉴权 (Authorization)这两个术语。但是,总有人很容易混淆概念。在 身份验证(Authentication) 的过程当中,我们需要检查用户的身份以提供对系统的访问。在这个过程当中验证的是 “你是谁?” 。故而,用户需要提供登陆所需的详细信息以供身份的验证。授权(Authorization) ,是通过了身份验证之后的用户,系统是否授权给他访问特定信息(读)或者执行特定的操作(写)的过程。此过程确定了 用户拥有哪些权限。微服务下的认证和授权策略我可以想到的解决方案有以下这么几种:无API网关 1.1 每个服务各自为政,各自进行认证和鉴权 1.2 拆分出 认证授权服务 进行全局的认证和鉴权有API网关 2.1 在网关上进行全局的认证,每个服务各自鉴权 2.2 在网关上进行全局的认证和鉴权 2.3 拆分出 认证服务 进行全局的认证,在网关上进行鉴权我比较推崇 2.3 这种策略,为什么呢?认证对于鉴权来说,是频度较低的服务:登陆不常有,鉴权则发生在每一个API调用上;往往认证会相对复杂,具有特异性,难以做到通用化。而鉴权不会特别复杂,容易做到通用化。有状态和无状态身份验证当一个设备(客户端)向一个设备(服务端)发送请求的时候,服务端如何判断这个客户端是谁?传统意义上的认证方式又两种:有状态认证、无状态认证。有状态认证和无状态认证最大的区别就是服务器会不会保存客户端的信息。有状态身份验证有状态认证,以cookie-session模型为例,当客户端第一次请求服务端的时候,服务端会返回客户端一个唯一的标识(默认在cookie中),并保存对应的客户端信息,客户端接受到唯一标识之后,将标识保存到本地cookie中,以后的每次请求都携带此cookie,服务器根据此cookie标识就可以判断请求的用户是谁,然后查到对应用户的信息。无状态身份验证无状态的认证,客户端在提交身份信息,服务端验证身份后,根据一定的算法生成一个token令牌返回给客户端,之后每次请求服务端,客户端都需要携带此令牌,服务器接受到令牌之后进行校验,校验通过后,提取令牌的信息用来区别用户。开始实施在具体的技术选择上:微服务框架使用Kratos;认证使用JWT;鉴权使用Casbin。JWT在微服务架构下,无状态的身份验证是更为合适的方式,其中以 JWT 为代表,最为流行。什么是JWT?Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准 RFC 7519 该token被设计为紧凑且安全的,特别适用于分布式站点的 单点登录(SSO) 场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。JWT需要注意的点JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。JWT 不加密的情况下,不能将秘密数据写入 JWT。JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。CasbinCasbin 根本上是依托规则引擎做的软件设计,抽取出来的模型可以做到通用化,能够轻松的使用多种不同的控制模型:ACL, RBAC, ABAC等。什么是Casbin?Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。目前这个框架的生态已经发展的越来越好了。提供了各种语言的类库,自定义的权限模型语言,以及模型编辑器。Casbin 可以支持自定义请求的格式,默认的请求格式为{subject, object, action}。具有访问控制模型model和策略policy两个核心概念。支持RBAC中的多层角色继承,不止主体可以有角色,资源也可以具有角色。支持内置的超级用户 例如:root 或 administrator。超级用户可以执行任何操作而无需显式的权限声明。支持多种内置的操作符,如 keyMatch,方便对路径式的资源进行管理,如 /foo/bar 可以映射到 /foo*Casbin 不能身份认证 authentication(即验证用户的用户名和密码),Casbin 只负责访问控制。应该有其他专门的组件负责身份认证,然后由 Casbin 进行访问控制,二者是相互配合的关系。管理用户列表或角色列表。 Casbin 认为由项目自身来管理用户、角色列表更为合适, 用户通常有他们的密码,但是 Casbin 的设计思想并不是把它作为一个存储密码的容器。 而是存储RBAC方案中用户和角色之间的映射关系。KratosKratos是B站开源出来的一个微服务框架,我在做技术选型的时候,横向的对比了市面上的主流几款微服务架构,总结下来,还是Kratos更加适合我使用,于是就选择了它。Kratos的认证和权鉴都是依托中间件来实现的。认证方面,Kratos官方已经支持了Jwt中间件 。鉴权方面,Kratos官方还没有对此的支持,于是我就自己简单的实现了一个Casbin中间件 ,简单封装,足够使用就是了。实现SecurityUserSecurityUser用于创建Jwt的令牌,以及后面Casbin解析和存取权鉴相关的数据,需要实现它。并且实现一个SecurityUserCreator注册进中间件。const ( ClaimAuthorityId = "authorityId" ) type SecurityUser struct { Path string Method string AuthorityId string } func NewSecurityUser() authzM.SecurityUser { return &SecurityUser{} } func (su *SecurityUser) ParseFromContext(ctx context.Context) error { if claims, ok := jwt.FromContext(ctx); ok { su.AuthorityId = claims.(jwtV4.MapClaims)[ClaimAuthorityId].(string) } else { return errors.New("jwt claim missing") } if header, ok := transport.FromServerContext(ctx); ok { su.Path = header.Operation() su.Method = "*" } else { return errors.New("jwt claim missing") } return nil } func (su *SecurityUser) GetSubject() string { return su.AuthorityId } func (su *SecurityUser) GetObject() string { return su.Path } func (su *SecurityUser) GetAction() string { return su.Method } func (su *SecurityUser) CreateAccessJwtToken(secretKey []byte) string { claims := jwtV4.NewWithClaims(jwtV4.SigningMethodHS256, jwtV4.MapClaims{ ClaimAuthorityId: su.AuthorityId, }) signedToken, err := claims.SignedString(secretKey) if err != nil { return "" } return signedToken } func (su *SecurityUser) ParseAccessJwtTokenFromContext(ctx context.Context) error { claims, ok := jwt.FromContext(ctx) if !ok { return errors.New("no jwt token in context") } if err := su.ParseAccessJwtToken(claims); err != nil { return err } return nil } func (su *SecurityUser) ParseAccessJwtTokenFromString(token string, secretKey []byte) error { parseAuth, err := jwtV4.Parse(token, func(*jwtV4.Token) (interface{}, error) { return secretKey, nil }) if err != nil { return err } claims, ok := parseAuth.Claims.(jwtV4.MapClaims) if !ok { return errors.New("no jwt token in context") } if err := su.ParseAccessJwtToken(claims); err != nil { return err } return nil } func (su *SecurityUser) ParseAccessJwtToken(claims jwtV4.Claims) error { if claims == nil { return errors.New("claims is nil") } mc, ok := claims.(jwtV4.MapClaims) if !ok { return errors.New("claims is not map claims") } strAuthorityId, ok := mc[ClaimAuthorityId] if ok { su.AuthorityId = strAuthorityId.(string) } return nil }JWT中间件创建白名单在白名单下的API将会被忽略认证和权限验证需要注意的是:这里面注册的是 操作名(operation),而非是API的URL。具体的操作名是什么,可以在Protoc生成的 *_grpc.pb.go 和 *_http.pb.go 找到。// NewWhiteListMatcher 创建白名单 func NewWhiteListMatcher() selector.MatchFunc { whiteList := make(map[string]bool) whiteList["/admin.v1.AdminService/Login"] = true return func(ctx context.Context, operation string) bool { if _, ok := whiteList[operation]; ok { return false } return true } }创建中间件// NewMiddleware 创建中间件 func NewMiddleware(logger log.Logger) http.ServerOption { return http.Middleware( recovery.Recovery(), tracing.Server(), logging.Server(logger), selector.Server( jwt.Server( func(token *jwtV4.Token) (interface{}, error) { return []byte(ac.ApiKey), nil }, jwt.WithSigningMethod(jwtV4.SigningMethodHS256), ), ). Match(NewWhiteListMatcher()).Build(), ) }注册中间件var opts = []http.ServerOption{ NewMiddleware(logger), http.Filter(handlers.CORS( handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"}), handlers.AllowedOrigins([]string{"*"}), )), }前端发送Token前端只需要在HTTP的Header里面加入以下数据即可:键值AuthorizationBearer {JWT Token}export default function authHeader() { const userStr = localStorage.getItem("user"); let user = null; if (userStr) user = JSON.parse(userStr); if (user && user.token) { return { Authorization: 'Bearer ' + user.token }; } else { return {}; } }Casbin中间件Casbin的模型和策略配置读取,我简化的使用了读取本地配置文件。通常来说,模型文件变化不大,放本地配置文件或者直接硬代码都没问题。变化的通常都是策略配置,通常做法都是放置在数据库里面,方便通过后台去进行编辑改变。// NewMiddleware 创建中间件 func NewMiddleware(ac *conf.Auth, logger log.Logger) http.ServerOption { m, _ := model.NewModelFromFile("../../configs/authz/authz_model.conf") a := fileAdapter.NewAdapter("../../configs/authz/authz_policy.csv") return http.Middleware( recovery.Recovery(), tracing.Server(), logging.Server(logger), selector.Server( casbinM.Server( casbinM.WithCasbinModel(m), casbinM.WithCasbinPolicy(a), casbinM.WithSecurityUserCreator(myAuthz.NewSecurityUser), ), ). Match(NewWhiteListMatcher()).Build(), ) }开始登陆吧func (s *AdminService) Login(_ context.Context, req *v1.LoginReq) (*v1.User, error) { fmt.Println("Login", req.UserName, req.Password) var id uint64 = 10 var email = "hello@kratos.com" var roles []string switch req.UserName { case "admin": roles = append(roles, "ROLE_ADMIN") case "moderator": roles = append(roles, "ROLE_MODERATOR") } var securityUser myAuthz.SecurityUser securityUser.AuthorityId = req.GetUserName() token := securityUser.CreateAccessJwtToken([]byte(s.auth.GetApiKey())) return &v1.User{ Id: &id, UserName: &req.UserName, Token: &token, Email: &email, Roles: roles, }, nil }流程简要说明前端发送登陆请求登陆请求处理 2.1 验证用户名密码 2.2 securityUser.CreateAccessJwtToken生成Jwt的Token 2.3 返回token给前端其他正常的请求 3.1 Jwt中间件进行令牌进行认证信息校验 3.2 Casbin中间件解析Jwt中间件的Payload信息,根据用户信息以及操作名进行权鉴。技术栈GolangReactKratosConsulJaegerJWTCasbin实例代码Kratos CasbinKratos Examples
什么是Traefik?Traefik 是一款开源的反向代理与负载均衡工具,它监听后端的变化并自动更新服务配置。Traefik 最大的优点是能够与常见的微服务系统直接整合,可以实现自动化动态配置。目前支持 Docker、Swarm,Marathon、Mesos、Kubernetes、Consul、Etcd、Zookeeper、BoltDB 和 Rest API 等后端模型。什么是微服务网关?微服务网关是整个微服务API请求的入口,可以实现过滤Api接口。并且可以实现用户的验证登录、解决跨域、日志拦截、权限控制、限流、熔断、负载均衡、黑名单与白名单机制等。Docker部署服务器Consul (测试的版本为v1.11.4)docker pull bitnami/consul:latest docker pull bitnami/consul-exporter:latest docker run -itd \ --name consul-server-standalone \ -p 8300:8300 \ -p 8500:8500 \ -p 8600:8600/udp \ -e CONSUL_BIND_INTERFACE='eth0' \ -e CONSUL_AGENT_MODE=server \ -e CONSUL_ENABLE_UI=true \ -e CONSUL_BOOTSTRAP_EXPECT=1 \ -e CONSUL_CLIENT_LAN_ADDRESS=0.0.0.0 \ bitnami/consul:latestTraefik (测试的版本为v2.5.6)docker pull traefik:latest docker run -itd ` --name traefik-server ` --link consul-server-standalone ` --add-host=host.docker.internal:host-gateway ` -p 8080:8080 ` -p 80:80 ` -v /var/run/docker.sock:/var/run/docker.sock ` traefik:latest --api.insecure=true --providers.consul.endpoints="consul-server-standalone:8500"管理后台Consul: http://localhost:8500Traefik:http://localhost:8080加入路由配置在这里我使用了Consul作为远程配置中心,配置以KV的方式存储,可登陆consul的管理后台添加配置,Traefik默认是监控配置改变的。键值traefik/http/routers/myrouter-1/rulePathPrefix('/')traefik/http/routers/myrouter-1/entryPoints/0httptraefik/http/routers/myrouter-1/servicemyservice-1traefik/http/services/myservice-1/loadbalancer/servers/0/urlhttp://host.docker.internal:8100简单的Go服务示例package main import ( "fmt" "net/http" ) func HelloHandle(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprint(w, "hello kitty") } func main() { http.HandleFunc("/hello", HelloHandle) if e := http.ListenAndServe(":8100", nil); e!= nil{ panic(e.Error()) }原始服务器的访问地址是:http://localhost:8100/hello通过网关访问的地址是:http://localhost/hello注意的点在这里我使用了Consul作为远程配置中心,另外Etcd等也可以。因为我网关跑在了Docker下,而http服务器跑在了宿主机上,因此需要--add-host=host.docker.internal:host-gateway以期Traefik能够访问宿主机。
Pinia的Store的ID是全局唯一的标识符,但是在实际应用当中,我可能需要复用这一个State,举一个例子:我们现在有一个用户详情页,它下面有多个Tab页,那么我就定一个Store去在组件之间去共享数据,如果只能够开一个用户的详情页,那也就罢了,但是,假如我现在需要多开用户详情页,那么就不好玩了,在这个时候,我需要id可变,就像这样的:user-detail-{userId}.import { defineStore } from 'pinia'; interface CounterState { counter: number; } export const useCounterStore = function(id: string) { return defineStore(id, { state: (): CounterState => ({ counter: 0, }), getters: { getCounter(): number { return this.counter; } }, actions: { increment() { this.counter++ }, randomizeCounter(){ this.counter = Math.round(100 * Math.random()) }, }, })(); }使用:import { useCounterStore } from '@/stores/test'; const store = useCounterStore("main-1") console.log('before', store.getCounter); store.randomizeCounter(); console.log('after', store.getCounter);
什么是地理围栏(Geo-fencing)?地理围栏(Geo-fencing)是LBS的一种新应用,就是用一个虚拟的栅栏围出一个虚拟地理边界。当手机进入、离开某个特定地理区域,或在该区域内活动时,手机可以接收自动通知和警告。有了地理围栏技术,位置社交网站就可以帮助用户在进入某一地区时自动登记。地理坐标系我们通常用经纬度来表示一个地理位置,但是由于一些原因,我们从不同渠道得到的经纬度信息可能并不是在同一个坐标系下。高德地图、腾讯地图以及谷歌中国区地图使用的是GCJ-02坐标系百度地图使用的是BD-09坐标系底层接口(HTML5 Geolocation或ios、安卓API)通过GPS设备获取的坐标使用的是WGS-84坐标系不同的坐标系之间可能有几十到几百米的偏移,所以在开发基于地图的产品,或者做地理数据可视化时,我们需要修正不同坐标系之间的偏差。WGS-84 - 世界大地测量系统WGS-84(World Geodetic System, WGS)是使用最广泛的坐标系,也是世界通用的坐标系,GPS设备得到的经纬度就是在WGS84坐标系下的经纬度。通常通过底层接口得到的定位信息都是WGS84坐标系。GCJ-02 - 国测局坐标GCJ-02(G-Guojia国家,C-Cehui测绘,J-Ju局),又被称为火星坐标系,是一种基于WGS-84制定的大地测量系统,由中国国测局制定。此坐标系所采用的混淆算法会在经纬度中加入随机的偏移。国家规定,中国大陆所有公开地理数据都需要至少用GCJ-02进行加密,也就是说我们从国内公司的产品中得到的数据,一定是经过了加密的。绝大部分国内互联网地图提供商都是使用GCJ-02坐标系,包括高德地图,谷歌地图中国区等。BD-09 - 百度坐标系BD-09(Baidu, BD)是百度地图使用的地理坐标系,其在GCJ-02上多增加了一次变换,用来保护用户隐私。从百度产品中得到的坐标都是BD-09坐标系。不同坐标系下的点在百度地图上会有偏移地理坐标系列表坐标系坐标格式说明WGS84[lng,lat]WGS-84坐标系,GPS设备获取的经纬度坐标GCJ02[lng,lat]GCJ-02坐标系,google中国地图、SoSo地图、AliYun地图、MapAbc地图和高德地图所用的经纬度坐标BD09[lng,lat]BD-09坐标系,百度地图采用的经纬度坐标BD09LL[lng,lat]同BD09BD09MC[x,y]BD-09米制坐标,百度地图采用的米制坐标,单位:米BD09Meter[x,y]同BD09MCBaidu[lng,lat]百度坐标系,BD-09坐标系别名,同BD-09BMap[lng,lat]百度地图,BD-09坐标系别名,同BD-09AMap[lng,lat]高德地图,同GCJ-02WebMercator[x,y]Web Mercator投影,墨卡托投影,同EPSG3857,单位:米WGS1984[lng,lat]WGS-84坐标系别名,同WGS-84EPSG4326[lng,lat]WGS-84坐标系别名,同WGS-84EPSG3857[x,y]Web Mercator投影,同WebMercator,单位:米EPSG900913[x,y]Web Mercator投影,同WebMercator,单位:米地理围栏 GeoJSON 数据地理围栏或地理围栏集的数据由 rfc7946 中定义的、采用 GeoJSON 格式的 Feature 对象和 FeatureCollection 对象表示。 除此之外:GeoJSON 对象类型可以是 Feature 对象或 FeatureCollection 对象。几何对象类型可以是 Point、MultiPoint、LineString、MultiLineString、Polygon、MultiPolygon 和 GeometryCollection。所有特征属性应该包含用于标识地理围栏的 geometryId。具有 Point、MultiPoint、LineString、MultiLineString 的特征必须在属性中包含 radius。 radius 值的计量单位为米,radius 值的范围为 1 到 10000。具有 polygon 和 multipolygon 几何类型的特征没有半径属性。validityTime 是可选属性,可让用户为地理围栏数据设置过期时间和有效时间。 如果未指定该属性,则数据永不过期,而是一直有效。expiredTime 是地理围栏数据的过期日期和时间。 如果请求中 userTime 的值晚于此值,则将相应的地理围栏数据视为过期的数据,且不会查询这些数据。 基于这一点,此地理围栏数据的 geometryId 将包含在地理围栏响应中的 expiredGeofenceGeometryId 数组内。validityPeriod 是地理围栏有效时段的列表。 如果请求中 userTime 的值超出有效时段,则将相应的地理围栏数据视为无效,且不会查询这些数据。 此地理围栏数据的 geometryId 包含在地理围栏响应中的 invalidPeriodGeofenceGeometryId 数组内。 下表显示了 validityPeriod 元素的属性。名称类型必需说明startTimedatetime是有效时段的开始日期时间。endTimedatetime是有效时段的结束日期时间。recurrenceType字符串false时段的重复类型。 值可为 Daily、Weekly、Monthly 或 Yearly。 默认值为 Daily。businessDayOnlyBooleanfalse指示数据是否仅在工作日有效。 默认值为 false。所有坐标值都表示为中定义的 “经度,纬度” WGS84 。对于包含 MultiPoint、MultiLineString、MultiPolygon 或 GeometryCollection 的每个特征,属性将应用到所有元素。 例如:中的所有点 MultiPoint 都将使用相同的半径形成多个圆形地域隔离区内。标准GeoJSONGeoJSON 规范仅支持以下几何图形:点 (Point)线 (LineString)多边形 (Polygon)点集合 (MultiPoint)线集合 (MultiLineString)多边形集合 (MultiPolygon)空间数据集合 (GeometryCollection)点 (Point){ "type": "Feature", "geometry": { "type": "Point", "coordinates": [125.6, 10.1] }, "properties": { "name": "Dinagat Islands" } }线 (LineString){ "type": "LineString", "coordinates": [ [30.0, 10.0], [10.0, 30.0], [40.0, 40.0] ] }多边形 (Polygon){ "type": "Polygon", "coordinates": [ [[30.0, 10.0], [40.0, 40.0], [20.0, 40.0], [10.0, 20.0], [30.0, 10.0]] ] }{ "type": "Polygon", "coordinates": [ [[35.0, 10.0], [45.0, 45.0], [15.0, 40.0], [10.0, 20.0], [35.0, 10.0]], [[20.0, 30.0], [35.0, 35.0], [30.0, 20.0], [20.0, 30.0]] ] }点集合 (MultiPoint){ "type": "MultiPoint", "coordinates": [ [10.0, 40.0], [40.0, 30.0], [20.0, 20.0], [30.0, 10.0] ] }线集合 (MultiLineString){ "type": "MultiLineString", "coordinates": [ [[10.0, 10.0], [20.0, 20.0], [10.0, 40.0]], [[40.0, 40.0], [30.0, 30.0], [40.0, 20.0], [30.0, 10.0]] ] }多边形集合 (MultiPolygon){ "type": "MultiPolygon", "coordinates": [ [ [[30.0, 20.0], [45.0, 40.0], [10.0, 40.0], [30.0, 20.0]] ], [ [[15.0, 5.0], [40.0, 10.0], [10.0, 20.0], [5.0, 10.0], [15.0, 5.0]] ] ] }{ "type": "MultiPolygon", "coordinates": [ [ [[40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0]] ], [ [[20.0, 35.0], [10.0, 30.0], [10.0, 10.0], [30.0, 5.0], [45.0, 20.0], [20.0, 35.0]], [[30.0, 20.0], [20.0, 15.0], [20.0, 25.0], [30.0, 20.0]] ] ] }空间数据集合 (GeometryCollection){ "type": "GeometryCollection", "geometries": [ { "type": "Point", "coordinates": [40.0, 10.0] }, { "type": "LineString", "coordinates": [ [10.0, 10.0], [20.0, 20.0], [10.0, 40.0] ] }, { "type": "Polygon", "coordinates": [ [[40.0, 40.0], [20.0, 45.0], [45.0, 30.0], [40.0, 40.0]] ] } ] }扩展GeoJSON圆形 (Circle)Circle GeoJSON 规范不支持该几何图形。我们使用 GeoJSON Point Feature 对象来表示圆。Circle使用对象表示的几何图形 GeoJSON Feature 必须 包含以下坐标和属性:圆心 (Center)圆心使用 GeoJSON Point 对象表示。半径 (Radius)圆形的半径 radius 使用 GeoJSON Feature 的属性表示。 半径值以米为单位,并且其类型必须为 double 。子类型 (subType)圆形几何图形还必须包含 subType 属性。 此属性必须是的属性的一部分 GeoJSON Feature ,并且其值应为 Circle{ "type": "Feature", "geometry": { "type": "Point", "coordinates": [-122.126986, 47.639754] }, "properties": { "subType": "Circle", "radius": 500 } }矩形 (Rectangle)Rectangle GeoJSON 规范不支持该几何图形。我们使用 GeoJSON Polygon Feature 对象来表示矩形。 矩形扩展主要由 Web SDK 的 “绘图工具” 模块使用。Rectangle使用对象表示的几何图形 GeoJSON Polygon Feature 必须 包含以下坐标和属性:内角使用对象的坐标表示矩形的角 GeoJSON Polygon 。 应该有五个坐标,每个角一个。 与第五个坐标相同,用于关闭多边形环。 假定这些坐标对齐,开发人员可以根据需要对其进行旋转。子类型矩形几何图形还必须包含 subType 属性。 此属性必须是的属性的一部分 GeoJSON Feature ,并且其值应为 Rectangle{ "type": "Feature", "geometry": { "type": "Polygon", "coordinates": [[[5,25],[14,25],[14,29],[5,29],[5,25]]] }, "properties": { "subType": "Rectangle" } }高德地图高德地图提供了以下几组API可用于地理围栏:GeoJSON工具类 AMap.GeoJSON编辑器工具类 AMap.PolyEditor AMap.CircleEditor AMap.RectangleEditor AMap.EllipseEditor AMap.BezierCurveEditor鼠标工具插件 AMap.MouseTool1. GeoJSON工具类它可以用来解析标准的GeoJSON,但是需要注意的是:它只支持FeatureCollection形式的数据.const geoJson = new AMap.GeoJSON({ geoJSON: geoJsonObject, getMarker: function (geoJson, lngLats) { console.log('点', lngLats); }, getPolyline: function (geoJson, lngLats) { console.log('线', lngLats); }, getPolygon: function (geoJson, lngLats) { console.log('面', lngLats); }, });2. 编辑器工具类它支持:圆形,折线,多边形,贝瑟尔曲线,椭圆,矩形.虽然说,圆形,矩形绘制简单.但是,真正实用的只有:多边形.// 编辑多边形 function editPolygon(path: any, open: boolean) { closePolygonEditor(); const polygon = createPolygon(path); // 缩放地图到合适的视野级别 map.setFitView(); // 创建编辑器 polygonEditor = new AMap.PolyEditor(map, polygon); // 吸附功能 polygonEditor.addAdsorbPolygons(polygon); // 设置编辑目标 polygonEditor.setTarget(polygon); // 监听事件 // polygonEditor.on('addnode', function (event) {}); // polygonEditor.on('adjust', function (event) {}); // polygonEditor.on('removenode', function (event) {}); polygonEditor.on('end', function (event) { const paths = event.target.getPath(); console.log('结束多边形编辑', event.target, paths); }); // 打开编辑 if (open) polygonEditor.open(); }需要注意的是:绘制出来的路径需要倒序读取,因为标准GeoJson使用的是右手法则.绘制出来的路径并没有封口,即起始点和结束点必须是同一个点.如果需要被AMap.GeoJSON所解析,GeoJson的数据必须封装成FeatureCollection.3. 鼠标工具插件并不实用,弃用.参考资料地理围栏技术GeoJSON WikiGeoJSON Viewergeojson.io高德参考手册高德JS API 示例
什么是MinIO?MinIO 是一款高性能、分布式的对象存储系统. 它是一款软件产品, 可以100%的运行在标准硬件。即X86等低成本机器也能够很好的运行MinIO。本地Docker部署测试服务器docker pull bitnami/minio:latest # MINIO_ROOT_USER最少3个字符 # MINIO_ROOT_PASSWORD最少8个字符 # 第一次运行的时候,服务会自动关闭,手动再次启动就可以正常运行了. docker run -itd \ --name minio-server \ -p 9000:9000 \ -p 9001:9001 \ --env MINIO_ROOT_USER="root" \ --env MINIO_ROOT_PASSWORD="123456789" \ --env MINIO_DEFAULT_BUCKETS='images' \ --env MINIO_FORCE_NEW_KEYS="yes" \ --env BITNAMI_DEBUG=true \ bitnami/minio:latest上传的API它有3个API可供调用:putObject 从流上传fPutObject 从文件上传presignedPutObject 提供一个临时的上传链接以供上传使用1和2的方式的话,在前端需要暴露出连接MinIO的访问密钥,很不安全,而且官方的Js客户端压根就没想过开放给浏览器.而3的话,可以由服务端生成一个临时的上传链接提供给前端上传之用,而无需要暴露访问MinIO的密钥,非常的安全,我采用的是第三种方式.第三种方式,官方有一篇文章: Upload Files Using Pre-signed URLsTypeScript实现在TypeScript下,我们可用的有三种方式实现文件上传:XMLHttpRequestFetch APIAxios需要注意的是: 事实上,后两种API都是封装的XMLHttpRequest.1. XMLHttpRequestfunction xhrUploadFile(file: File, url: string) { const xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.send(file); xhr.onload = () => { if (xhr.status === 200) { console.log(`${file.name} 上传成功`); } else { console.error(`${file.name} 上传失败`); } }; }2. Fetch APIfunction fetchUploadFile(file: File, url: string) { fetch(url, { method: 'PUT', body: file, }) .then((response) => { console.log(`${file.name} 上传成功`, response); }) .catch((error) => { console.error(`${file.name} 上传失败`, error); }); }3. Axiosfunction axiosUploadFile(file: File, url: string) { const instance = axios.create(); instance .put(url, file, { headers: { 'Content-Type': file.type, }, }) .then(function (response) { console.log(`${file.name} 上传成功`, response); }) .catch(function (error) { console.error(`${file.name} 上传失败`, error); }); }从后端获取临时上传链接function retrieveNewURL(file: File, cb: (file: File, url: string) => void) { const url = `http://localhost:8080/presignedUrl/${file.name}`; axios.get(url) .then(function (response) { cb(file, response.data.data.url); }) .catch(function (error) { console.error(error); }); }上传文件function onXhrUploadFile(file?: File) { console.log('onXhrUploadFile', file); if (file) { retrieveNewURL(file, (file, url) => { xhrUploadFile(file, url); }); } }踩过的坑1. presignedPutObject方式上传提交的方法必须得是PUT我试过了用POST去上传文件,但是显然的是:我失败了.必须得用PUT去上传.2. 直接发送File即可看了不少文章都是这么干的: 构造一个FormData,然后把文件打进去,如果用putObject和fPutObject这两种方式上传,这是没问题的,但是使用presignedPutObject则是不行的,直接发送File就可以了.fileUpload(file) { const url = 'http://example.com/file-upload'; const formData = new FormData(); formData.append('file', file) const config = { headers: { 'content-type': 'multipart/form-data' } } return post(url, formData,config) }如果使用以上的方式上传,文件头会被插入一段数据,看起来像是这样子的:------WebKitFormBoundaryaym16ehT29q60rUx Content-Disposition: form-data; name="file"; filename="webfonts.zip" Content-Type: application/zip它是遵照了 rfc1867 定义的协议.3. 使用Axios上传的时候,需要自己把Content-Type填写成为file.type直接使用XMLHttpRequest和Fetch API都会自动填写成为文件真实的Content-Type.而Axios则不会,需要自己填写进去,或许是我不会使用Axios,但是这是一个需要注意的地方,否则在MinIO里边的Content-Type会被填写成为Axios默认的Content-Type,或者是你自己指定的.示例代码Github: https://github.com/tx7do/minio-typescript-example后端采用go+gin实现,用于调用MinIO的APIpresignedPutObject获取临时上传Url.前端有React和Vue的实现,要实现进度条和多文件上传也是容易的.
算法列表顺序查找(Sequential Search)二叉树查找(Binary Search)三叉树查找(Ternary Search)插值查找(Interpolation Search)斐波那契查找(Fibonacci Search)指数查找(Exponential Search)树表查找(Tree table lookup)分块查找(Blocking Search)哈希查找(Hash Search)算法实现顺序查找(Sequential Search)func SequentialSearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } for i := 0; i < len(array); i++ { if array[i] == target { return i } } return -1 }二叉树查找(Binary Search)基本二分查找func BinarySearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } low := 0 high := len(array) - 1 mid := 0 for low <= high { //mid = low + (high-low)/2 mid = low + (high-low)>>1 if array[mid] > target { high = mid - 1 } else if array[mid] < target { low = mid + 1 } else { return mid } } return -1 }二分查找第一个元素的位置func LowerBound(array []int, target int) int { low, high, mid := 0, len(array)-1, 0 for low <= high { //mid = low + (high-low)/2 mid = low + (high-low)>>1 if array[mid] >= target { high = mid - 1 } else { low = mid + 1 } } return low }二分查找第一个大于该元素的位置func UpperBound(array []int, target int) int { low, high, mid := 0, len(array)-1, 0 for low <= high { //mid = low + (high-low)/2 mid = low + (high-low)>>1 if array[mid] > target { high = mid - 1 } else { low = mid + 1 } } return low }三叉树查找(Ternary Search)func TernarySearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } low, high := 0, len(array)-2 mid1, mid2 := 0, 0 for low <= high { mid1 = low + (high-low)/3 mid2 = high + (high-low)/3 if array[mid1] == target { return mid1 } else if array[mid2] == target { return mid2 } if target < array[mid1] { high = mid1 - 1 } else if target > array[mid2] { low = mid2 + 1 } else { low = mid1 + 1 high = mid2 - 1 } } return -1 }插值查找(Interpolation Search)func InterpolationSearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } low, high := 0, len(array)-1 var mid = 0 for array[low] < target && array[high] > target { //mid = low + (high-low)/2 mid = low + (high-low)>>1 if array[mid] < target { low = mid + 1 } else if array[mid] > target { high = mid - 1 } else { return mid } } if array[low] == target { return low } else if array[high] == target { return high } else { return -1 } }斐波那契查找(Fibonacci Search)在是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。注意同时属于一种有序查找算法// FibonacciSearch 斐波那查找 func FibonacciSearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } high := len(array) - 1 max := array[high] fibMMm2 := 0 // (m-2)'th Fibonacci No. fibMMm1 := 1 // (m-1)'th Fibonacci No. fibM := fibMMm2 + fibMMm1 // m'th Fibonacci for fibM < max { fibMMm2 = fibMMm1 fibMMm1 = fibM fibM = fibMMm2 + fibMMm1 } var mid, offset = 0, -1 for fibM > 1 { mid = algorithm.Min(offset+fibMMm2, high) if array[mid] < target { fibM = fibMMm1 fibMMm1 = fibMMm2 fibMMm2 = fibM - fibMMm1 offset = mid } else if array[mid] > target { fibM = fibMMm2 fibMMm1 = fibMMm1 - fibMMm2 fibMMm2 = fibM - fibMMm1 } else { return mid } } if offset < high && (array[offset+1] == target) { return offset + 1 } return -1 } // fibonacciRecursion 递归求斐波那数 // f(n) = f(n-1) + f(n-2), n >= 2 func fibonacciRecursion(n int) int { if n == 0 { return 0 } else if n == 1 { return 1 } return fibonacciRecursion(n-1) + fibonacciRecursion(n-2) } // fibonacci 求斐波那数 func fibonacci(n int) int { a := 0 b := 1 for i := 0; i < n; i++ { temp := a a = b b = temp + a } return a }指数查找(Exponential Search)func ExponentialSearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } if array[0] == target { return 0 } length := len(array) if array[length-1] == target { return length - 1 } searchRange := 1 for searchRange < length && array[searchRange] <= target { //searchRange = searchRange * 2 searchRange = searchRange << 1 } //startIndex := searchRange / 2 startIndex := searchRange >> 1 endIndex := algorithm.Min(searchRange, length) bi := BinarySearch(array[startIndex:endIndex], target) if bi == -1 { return -1 } else { return bi + startIndex } }树表查找(Tree table lookup)分块查找(Blocking Search)func JumpSearch(array []int, target int) int { if array == nil || len(array) == 0 { return -1 } length := len(array) step := int(math.Sqrt(float64(length))) prev := 0 for array[algorithm.Min(step, length)-1] < target { prev = step step += int(math.Sqrt(float64(length))) if prev >= length { return -1 } } for array[prev] < target { prev++ if prev == algorithm.Min(step, length) { return -1 } } if array[prev] == target { return prev } return -1 }哈希查找(Hash Search)
关于Unix时间戳(Unix timestamp)时间戳(Timestamp) 也被称作为 Unix时间戳(Unix timestamp),或称Unix时间(Unix time)、POSIX时间(POSIX time),是一种时间表示方式,定义为从世界协调时间(Coordinated Universal Time,即UTC)或称 格林威治时间的 1970年01月01日00时00分00秒(00:00:00 GMT) 起至现在的总秒数。Unix时间戳不仅被使用在Unix系统、类Unix系统中,也在许多其他操作系统中被广泛采用。具体数值实例2022-03-07 08:49:15 秒: 1646614155 毫秒: 1646614155112 微秒: 1646614155112986 纳秒: 1646614155112986400Go// 秒 fmt.Printf("时间戳(秒):%v;\n", time.Now().Unix()) fmt.Printf("时间戳(纳秒转换为秒):%v;\n", time.Now().UnixNano()/1e9) // 毫秒 fmt.Printf("时间戳(毫秒):%v;\n", time.Now().UnixMilli()) fmt.Printf("时间戳(纳秒转换为毫秒):%v;\n", time.Now().UnixNano()/1e6) // 微秒 fmt.Printf("时间戳(微秒):%v;\n", time.Now().UnixMicro()) fmt.Printf("时间戳(纳秒转换为微秒):%v;\n", time.Now().UnixNano()/1e3) // 纳秒 fmt.Printf("时间戳(纳秒):%v;\n", time.Now().UnixNano())PostgreSQL-- 秒 select (extract(EPOCH FROM CURRENT_TIMESTAMP))::bigint; -- 毫秒 select (extract(EPOCH FROM CURRENT_TIMESTAMP)*1000)::bigint; -- 微秒 select (extract(EPOCH FROM CURRENT_TIMESTAMP)*1000*1000)::bigint; -- 纳秒 select (extract(EPOCH FROM CURRENT_TIMESTAMP)*1000*1000*1000)::bigint;在PostgreSQL里面要实现更新表立即更新update_time/updated_at字段,要比MySQL麻烦一点点,需要自己实现一个函数,并且绑定一个触发器到表上.不像MySQL开箱即用.不过,也算不上特别的麻烦.-- 创建测试用的表 CREATE TABLE IF NOT EXISTS test ( id BIGSERIAL NOT NULL, title VARCHAR(255), create_time BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::BIGINT, update_time BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::BIGINT, create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT test_pkey PRIMARY KEY (id) ); -- 创建函数 CREATE OR REPLACE FUNCTION upd_timestamp() RETURNS TRIGGER AS $$ BEGIN new.updated_at := CURRENT_TIMESTAMP; new.update_time := (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000)::BIGINT; RETURN new; END; $$ LANGUAGE 'plpgsql'; -- 创建触发器 CREATE TRIGGER t_test BEFORE UPDATE ON test FOR EACH ROW EXECUTE PROCEDURE upd_timestamp();MySQLMySQL最高只支持到微秒-- 秒 SELECT UNIX_TIMESTAMP(CURRENT_TIMESTAMP); -- 毫秒 SELECT UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)); -- 微秒 SELECT UNIX_TIMESTAMP(CURRENT_TIMESTAMP(6));MySQL/MariaDB下面ON UPDATE UNIX_TIMESTAMP语法不合法,也跟PostgreSQL一样,需要用触发器的方式达成.-- 创建测试用的表 CREATE TABLE IF NOT EXISTS test ( id BIGINT NOT NULL, title VARCHAR(255), create_time BIGINT NOT NULL DEFAULT UNIX_TIMESTAMP(CURRENT_TIMESTAMP(6)), update_time BIGINT NOT NULL DEFAULT UNIX_TIMESTAMP(CURRENT_TIMESTAMP(6)), create_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), CONSTRAINT test_pkey PRIMARY KEY (id) );
Bash的换行符为 \\CMD的换行符为 \^Powershell的换行符为 \`关系型数据库MySQLdocker pull bitnami/mysql:latest docker run -itd \ --name mysql-test \ -p 3306:3306 \ -e ALLOW_EMPTY_PASSWORD=yes \ -e MYSQL_ROOT_PASSWORD=123456 \ bitnami/mysql:latestMariaDBdocker pull bitnami/mariadb:latest docker run -itd \ --name mariadb-test \ -p 3306:3306 \ -e ALLOW_EMPTY_PASSWORD=yes \ -e MARIADB_ROOT_PASSWORD=123456 \ bitnami/mariadb:latestPostgresSQLdocker pull bitnami/postgresql:latest docker pull bitnami/postgresql-repmgr:latest docker pull bitnami/pgbouncer:latest docker pull bitnami/pgpool:latest docker pull bitnami/postgres-exporter:latest docker run -itd \ --name postgres-test \ -p 5432:5432 \ -e POSTGRES_PASSWORD=123456 \ bitnami/postgresql:latest docker exec -it postgres-test "apt update"CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "postgis"; SELECT version(); SELECT postgis_full_version();SQLServerdocker pull mcr.microsoft.com/mssql/server:2019-latest docker run -itd \ --name MSSQL_1433 \ -m 512m \ -e "ACCEPT_EULA=Y" \ -e "SA_PASSWORD=Abcd123456789*" \ -p 1433:1433 \ mcr.microsoft.com/mssql/server:2019-latestTiDBdocker pull pingcap/tidb:latest docker pull pingcap/tikv:latest docker pull pingcap/pd:latest docker run -itd \ --name tidb-test \ -v /data/tidb/data:/tmp/tidb \ --privileged=true \ -p 4000:4000 \ -p 10080:10080 \ pingcap/tidb:latest图数据库Neo4Jdocker pull bitnami/neo4j:latest docker run -itd \ --name neo4j-test \ -p 7473:7473 \ -p 7687:7687 \ -p 7474:7474 \ -e NEO4J_PASSWORD=bitnami \ bitnami/neo4j:latest时序型数据库InfluxDBdocker pull bitnami/influxdb:latest docker run -itd \ --name influxdb-test \ -p 8083:8083 \ -p 8086:8086 \ -e INFLUXDB_HTTP_AUTH_ENABLED=true \ -e INFLUXDB_ADMIN_USER=admin \ -e INFLUXDB_ADMIN_USER_PASSWORD=123456789 \ -e INFLUXDB_ADMIN_USER_TOKEN=admintoken123 \ -e INFLUXDB_DB=my_database \ bitnami/influxdb:latestcreate user "admin" with password '123456789' with all privileges管理后台: http://localhost:8086/TimescaleDBdocker pull timescale/timescaledb:latest-pg14 docker pull timescale/timescaledb-postgis:latest-pg13 docker pull timescale/pg_prometheus:latest-pg11 docker run -itd \ --name timescale-test \ -p 5432:5432 \ -e POSTGRES_PASSWORD=123456 \ timescale/timescaledb-postgis:latest-pg13OpenTSDBdocker pull petergrace/opentsdb-docker:latest docker run -itd \ --name opentsdb-test \ -p 4242:4242 \ petergrace/opentsdb-docker:latest管理后台 http://localhost:4242QuestDBdocker pull questdb/questdb:latest docker run -itd \ --name questdb-test \ -p 9000:9000 \ -p 8812:8812 \ -p 9009:9009 \ questdb/questdb:latestTDenginedocker pull tdengine/tdengine:latest docker run -itd \ --name tdengine-test \ -p 6030-6041:6030-6041 \ -p 6030-6041:6030-6041/udp \ tdengine/tdengine:latestElasticSearchdocker pull bitnami/elasticsearch:latest docker run -itd \ --name elasticsearch-test \ -p 9200:9200 \ -p 9300:9300 \ -e ELASTICSEARCH_USERNAME=elastic \ -e ELASTICSEARCH_PASSWORD=elastic \ -e xpack.security.enabled=true \ -e discovery.type=single-node \ -e http.cors.enabled=true \ -e http.cors.allow-origin=http://localhost:13580,http://127.0.0.1:13580 \ -e http.cors.allow-headers=X-Requested-With,X-Auth-Token,Content-Type,Content-Length,Authorization \ -e http.cors.allow-credentials=true \ bitnami/elasticsearch:latest docker pull appbaseio/dejavu:latest docker run -itd \ --name dejavu-test \ -p 13580:1358 \ appbaseio/dejavu:latest http://localhost:13580/Clickhousedocker pull yandex/clickhouse-server:latest # 8123为http接口 9000为tcp接口 9004为mysql接口 # 推荐使用DBeaver作为客户端 docker run -itd \ --name clickhouse-server-test \ -p 8123:8123 \ -p 9000:9000 \ -p 9004:9004 \ --ulimit \ nofile=262144:262144 \ yandex/clickhouse-server:latestNoSQL数据库MongoDBdocker pull bitnami/mongodb:latest docker pull bitnami/mongodb-exporter:latest docker run -itd \ --name mongodb-test \ -p 27017:27017 \ -e MONGODB_ROOT_USER=root \ -e MONGODB_ROOT_PASSWORD=123456 \ -e MONGODB_USERNAME=test \ -e MONGODB_PASSWORD=123456 \ -e MONGODB_DATABASE=test \ bitnami/mongodb:latestRedisdocker pull bitnami/redis:latest docker pull bitnami/redis-exporter:latest docker run -itd \ --name redis-test \ -p 6379:6379 \ -e ALLOW_EMPTY_PASSWORD=yes \ bitnami/redis:latestMemcacheddocker pull bitnami/memcached:latest docker pull bitnami/memcached-exporter:latest docker run -itd \ --name memcached-test \ -p 11211:11211 \ bitnami/memcached:latestCouchDBdocker pull bitnami/couchdb:latest docker run -itd \ --name couchdb-test \ -p 5984:5984 \ -p 9100:9100 \ -e COUCHDB_PORT_NUMBER=5984 -e COUCHDB_CLUSTER_PORT_NUMBER=9100 -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=couchdb bitnami/couchdb:latestCassandradocker pull bitnami/cassandra:latest docker pull bitnami/cassandra-exporter:latest docker run -itd \ --name cassandra-test \ -p 7000:7000 \ -p 9042:9042 \ -e CASSANDRA_USER=cassandra \ -e CASSANDRA_PASSWORD=cassandra \ bitnami/cassandra:latest服务发现注册etcddocker pull bitnami/etcd:latest docker run -itd \ --name etcd-standalone \ -p 2379:2379 \ -p 2380:2380 \ -e ETCDCTL_API=3 \ -e ALLOW_NONE_AUTHENTICATION=yes \ -e ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379 \ bitnami/etcd:latest管理工具: etcd-managerNacosdocker pull nacos/nacos-server:latest docker run -itd \ --name nacos-standalone \ -e MODE=standalone \ -p 8849:8848 \ nacos/nacos-server:latest管理后台: http://localhost:8849/nacos/index.htmlConsuldocker pull bitnami/consul:latest docker pull bitnami/consul-exporter:latest docker run -itd \ --name consul-server-standalone \ -p 8300:8300 \ -p 8500:8500 \ -p 8600:8600/udp \ -e CONSUL_BIND_INTERFACE='eth0' \ -e CONSUL_AGENT_MODE=server \ -e CONSUL_ENABLE_UI=true \ -e CONSUL_BOOTSTRAP_EXPECT=1 \ -e CONSUL_CLIENT_LAN_ADDRESS=0.0.0.0 \ bitnami/consul:latest管理后台: http://localhost:8500Apollo注意,先要导入SQL数据!docker pull apolloconfig/apollo-portal:latest docker pull apolloconfig/apollo-configservice:latest docker pull apolloconfig/apollo-adminservice:latest # docker run -itd \ --name apollo-configservice \ -p 8080:8080 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://127.0.0.1:3306/ApolloConfigDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=root \ -e SPRING_DATASOURCE_PASSWORD=123456 \ -v /tmp/logs:/opt/logs \ apolloconfig/apollo-configservice:latest docker run -itd \ --name apollo-adminservice \ -p 8090:8090 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://127.0.0.1:3306/ApolloConfigDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=root \ -e SPRING_DATASOURCE_PASSWORD=123456 \ -v /tmp/logs:/opt/logs \ apolloconfig/apollo-adminservice:latest docker run -itd \ --name apollo-portal \ -p 8070:8070 \ -e SPRING_DATASOURCE_URL="jdbc:mysql://127.0.0.1:3306/ApolloPortalDB?characterEncoding=utf8" \ -e SPRING_DATASOURCE_USERNAME=root \ -e SPRING_DATASOURCE_PASSWORD=123456 \ -e APOLLO_PORTAL_ENVS=dev \ -e DEV_META=http://127.0.0.1:8080 \ -v /tmp/logs:/opt/logs \ apolloconfig/apollo-portal:latestEureka管理后台: Apollo管理后台: 账号密码: apollo / admin消息队列RabbitMQdocker pull bitnami/rabbitmq:latest docker run -itd \ --hostname localhost \ --name rabbitmq-test \ -p 15672:15672 \ -p 5672:5672 \ -p 1883:1883 \ -p 15675:15675 \ -e RABBITMQ_PLUGINS=rabbitmq_top,rabbitmq_mqtt,rabbitmq_web_mqtt,rabbitmq_prometheus,rabbitmq_stomp \ bitnami/rabbitmq:latest rabbitmq-plugins --offline enable rabbitmq_peer_discovery_consul # rabbitmq_mqtt 提供与后端服务交互使用,端口1883 rabbitmq-plugins enable rabbitmq_mqtt # rabbitmq_web_mqtt 提供与前端交互使用,端口15675 rabbitmq-plugins enable rabbitmq_web_mqtt管理后台: http://localhost:15672默认账号: user默认密码: bitnamiKafkadocker pull bitnami/kafka:latest docker pull bitnami/zookeeper:latest docker pull bitnami/kafka-exporter:latest docker run -itd \ --name zookeeper-test \ -p 2181:2181 \ -e ALLOW_ANONYMOUS_LOGIN=yes \ bitnami/zookeeper:latest docker run -itd \ --name kafka-standalone \ --link zookeeper-test \ -p 9092:9092 \ -v /home/data/kafka:/bitnami/kafka \ -e KAFKA_BROKER_ID=1 \ -e KAFKA_LISTENERS=PLAINTEXT://:9092 \ -e KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \ -e KAFKA_ZOOKEEPER_CONNECT=zookeeper-test:2181 \ -e ALLOW_PLAINTEXT_LISTENER=yes \ --user root \ bitnami/kafka:latest管理工具: Offset ExplorerNSQdocker pull nsqio/nsq:latest # nsqlookupd docker run -d \ --name lookupd \ -p 4160:4160 \ -p 4161:4161 \ nsqio/nsq:latest \ /nsqlookupd # nsqd docker run -itd \ --name lookupd \ -p 4160:4160 \ -p 4161:4161 \ nsqio/nsq:latest \ /nsqd --lookupd-tcp-address=nsqlookupd:4160 #nsqadmin docker run run -itd \ --name nsqadmin \ -p 4171:4171 \ nsqio/nsq:latest \ /nsqadmin --lookupd-http-address=nsqlookupd:4161管理后台: http://127.0.0.1:4171NATSdocker pull bitnami/nats:latest docker pull bitnami/nats-exporter:latest docker run -itd \ --name nats-server \ --p 4222:4222 \ --p 6222:6222 \ --p 8000:8222 \ -e NATS_HTTP_PORT_NUMBER=8222 \ bitnami/nats:latest管理后台: https://127.0.0.1:8000mosquittodocker pull eclipse-mosquitto:latest # 1883 tcp # 9001 websockets docker run -itd \ --name mosquitto-test \ -p 1883:1883 \ -p 9001:9001 \ eclipse-mosquitto:latestEMXdocker pull emqx/emqx:latest docker run -itd \ --name emqx-test \ -p 18083:18083 \ -p 1883:1883 \ emqx/emqx:latest管理后台: http://localhost:18083默认账号: admin默认密码: publicPulsardocker pull apachepulsar/pulsar-manager:latest docker pull apachepulsar/pulsar:latest docker run -itd \ -p 6650:6650 \ -p 8080:8080 \ --name pulsar-standalone \ apachepulsar/pulsar:latest bin/pulsar standalone docker run -itd \ -p 9527:9527 \ -p 7750:7750 \ -e SPRING_CONFIGURATION_FILE=/pulsar-manager/pulsar-manager/application.properties \ apachepulsar/pulsar-manager:latest管理后台 http://localhost:9527运维监控Jaegerdocker pull jaegertracing/all-in-one:latest docker run -d \ --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -p 5775:5775/udp \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 14268:14268 \ -p 14250:14250 \ -p 9411:9411 \ jaegertracing/all-in-one:latest管理后台: http://localhost:16686Kibanadocker pull bitnami/kibana:latest docker run -d \ --name kibana \ -p 5601:5601 \ --name kibana \ -e KIBANA_ELASTICSEARCH_URL=elasticsearch \ -e KIBANA_ELASTICSEARCH_PORT_NUMBER=9200 bitnami/kibana:latestPrometheusdocker pull bitnami/prometheus:latest docker pull bitnami/pushgateway:latest docker run -d \ --name=prometheus-gateway \ -p 5051:9091 \ bitnami/pushgateway docker run -d \ --name=prometheus \ -p 5050:9090 \ bitnami/prometheus:latestPrometheus后台: http://localhost:5050Pushgateway后台: http://localhost:5051Grafanadocker pull bitnami/grafana:latest docker run -d \ --name grafana \ -p 3000:3000 \ bitnami/grafana:latestLogstashdocker pull bitnami/logstash:latest docker pull bitnami/logstash-exporter:latest docker run -d \ -p 8080:8080 \ bitnami/logstash:latestFluentddocker pull bitnami/fluentd:latest docker pull bitnami/fluentd-exporter:latest docker run -d \ -p 24224:24224 \ -p 24224:24224/udp \ -v /data:/opt/bitnami/fluentd/log \ bitnami/fluentd:latest流式计算Sparkdocker pull bitnami/spark:latest docker run -itd \ --name spark-standalone \ -p 6066:6066 \ -p 7077:7077 \ -p 8080:8080 \ -p 50070:50070 \ -e SPARK_MODE=master \ -e SPARK_WORKER_CORES=1 \ -e SPARK_WORKER_MEMORY=2g \ bitnami/spark:latesthdfs的web界面:http://localhost:50070spark界面:http://localhost:8080Flinkdocker pull flink:latest docker network create flink-network docker run -itd \ --name flink-jobmanager \ --network flink-network \ -p 8081:8081 \ --env FLINK_PROPERTIES="jobmanager.rpc.address: flink-jobmanager" \ flink:latest jobmanager docker run -itd \ --name flink-taskmanager \ --network flink-network \ --env FLINK_PROPERTIES="jobmanager.rpc.address: flink-jobmanager" \ flink:latest taskmanager管理后台: http://localhost:8081其他Miniodocker pull bitnami/minio:latest docker run -itd \ --name minio-server \ --env MINIO_ACCESS_KEY="minio-access-key" \ --env MINIO_SECRET_KEY="minio-secret-key" \ bitnami/minio:latest机器学习TensorFlowdocker pull bitnami/tensorflow-resnet:latest docker pull bitnami/tensorflow-serving:latest docker pull bitnami/tensorflow-inception:latest docker network create app-tier --driver bridge docker run -d --name tensorflow-serving \ --volume /tmp/model-data:/bitnami/model-data \ --network app-tier \ bitnami/tensorflow-serving:latest docker run -d --name tensorflow-resnet \ --volume /tmp/model-data:/bitnami/model-data \ --network app-tier \ bitnami/tensorflow-resnet:latestPyTorchdocker pull bitnami/pytorch:latestAPI网关HAProxydocker pull bitnami/haproxy:latestKongdocker pull bitnami/kong:latestNginxdocker pull bitnami/nginx:latestEnvoydocker pull bitnami/envoy:latestAPISIXdocker pull apache/apisix:latest docker pull apache/apisix-dashboard:latest管理后台: http://127.0.0.1:8080/apisix/dashboardTykdocker pull tykio/tyk-gateway:latestGraviteedocker pull graviteeio/apim-gateway:latest docker pull graviteeio/apim-management-ui:latest docker pull graviteeio/apim-portal-ui:latest docker run \ --publish 82:8082 \ --name gateway \ --env GRAVITEE_MANAGEMENT_MONGODB_URI=mongodb://username:password@mongohost:27017/dbname \ --detach \ graviteeio/apim-gateway:latest docker run \ --publish 80:8080 \ --env MGMT_API_URL=http://localhost:81/management/organizations/DEFAULT/environments/DEFAULT \ --name management-ui \ --detach \ graviteeio/apim-management-ui:latest docker run \ --publish 80:8080 \ --env PORTAL_API_URL=http://localhost:81/portal/environments/DEFAULT \ --name portal-ui \ --detach \ graviteeio/apim-portal-ui:latest参考资料https://docs.emqx.cn/broker/v4.3/#%E6%B6%88%E6%81%AF%E6%A1%A5%E6%8E%A5https://github.com/lf-edge/ekuiper/blob/master/README-CN.mdhttps://db-engines.com/en/ranking/time+series+dbms
打开模块支持go env -w GO111MODULE=on取消代理go env -w GOPROXY=direct取消校验go env -w GOSUMDB=off设置不走 proxy 的私有仓库或组,多个用逗号相隔(可选)go env -w GOPRIVATE=git.mycompany.com,github.com/my/private设置代理国内常用代理列表提供者地址官方全球代理https://proxy.golang.com.cn七牛云https://goproxy.cn阿里云https://mirrors.aliyun.com/goproxy/GoCenterhttps://gocenter.io百度https://goproxy.bj.bcebos.com/“direct” 为特殊指示符,用于指示 Go 回源到模块版本的源地址去抓取(比如 GitHub 等),当值列表中上一个 Go module proxy 返回 404 或 410 错误时,Go 自动尝试列表中的下一个,遇见 “direct” 时回源,遇见 EOF 时终止并抛出类似 “invalid version: unknown revision…” 的错误。官方全球代理go env -w GOPROXY=https://proxy.golang.com.cn,direct go env -w GOPROXY=https://goproxy.io,direct go env -w GOSUMDB=gosum.io+ce6e7565+AY5qEHUk/qmHc5btzW45JVoENfazw8LielDsaI+lEbq6 go env -w GOSUMDB=sum.golang.google.cn七牛云go env -w GOPROXY=https://goproxy.cn,direct go env -w GOSUMDB=goproxy.cn/sumdb/sum.golang.org阿里云go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/,direct # GOSUMDB 不支持GoCentergo env -w GOPROXY=https://gocenter.io,direct # 不支持 GOSUMDB百度go env -w GOPROXY=https://goproxy.bj.bcebos.com/,direct # 不支持 GOSUMDBGoland参考资料goproxy.io文档goproxy.io文档七牛云文档阿里云文档开发者头条文档百度文档Go 国内加速:Go 国内加速镜像
DROP PROCEDURE IF EXISTS SPLIT_STRING; DELIMITER // CREATE PROCEDURE SPLIT_STRING ( IN fullstr VARCHAR(1024), IN delim VARCHAR(1) ) SQL SECURITY INVOKER BEGIN DECLARE inipos INTEGER; DECLARE endpos INTEGER; DECLARE maxlen INTEGER; DECLARE item VARCHAR(1024); SET inipos = 1; SET fullstr = CONCAT(fullstr, delim); SET maxlen = LENGTH(fullstr); REPEAT SET endpos = LOCATE(delim, fullstr, inipos); SET item = SUBSTR(fullstr, inipos, endpos - inipos); IF item <> '' AND item IS NOT NULL THEN SELECT item; END IF; SET inipos = endpos + 1; UNTIL inipos >= maxlen END REPEAT; END// DELIMITER ; SET @agg = "G1;G2;G3;G4;" ; call SPLIT_STRING(@agg, ';');
代码是自己写的,写得烂,有很多改进的地方.起始字节-结束字节 1-4 64 00 00 00 文件头 5-8 4字节,文件个数 (把一个pkg文件后面的文件列表数了下,确实是文件个数) 9-12 4字节,文件列表区的偏移地址(通过比较两个pkg文件,确实是偏移地址) 13-16 4字节,文件列表区的大小,其实就是从上一面的偏移地址到文件末尾 分析文件列表区数据,文件名长度是不等长的,这样读取文件列表时,就从头读过去,根据标志位来确定。下面是每条文件记录的各项属性: 1 - 2 接下来的字符串的长度,也就是文件名路径的字符串长度 3 - X 带相对路径的文件名称(说明:X是文件名称结束的偏移位置) X - X+4 00 00 00 00 识别标志 X+5 - X+8 文件起始偏移 X+9 - X+12 原始文件大小 X+13 - X+16 文件大小 说明:直接从PKG里面解出来的图片文件(主要是MIF格式的)是经过zlib压缩过的,必须要解压后才能使用。解压我们只要用到了zlib的uncompress函数就行了。 工具源代码下载 : PkgManager.rar
1、如何在窗体关闭前自行判断是否可关闭答:重新实现这个窗体的closeEvent()函数,加入判断操作 void MainWindow::closeEvent(QCloseEvent *event){ if (maybeSave()) { writeSettings(); event->accept(); } else { event->ignore(); }} 2、如何用打开和保存文件对话答:使用QFileDialog 打开文件 QString fileName = QFileDialog::getOpenFileName(this); if (!fileName.isEmpty()) { loadFile(fileName); } 保存文件 QString fileName = QFileDialog::getSaveFileName(this); if (fileName.isEmpty()) { return false; } 3、如何创建Actions(可在菜单和工具栏里使用这些Action) 答: newAct = new QAction(QIcon(":/images/new.png"), tr("&New"), this); newAct->setShortcut(tr("Ctrl+N")); newAct->setStatusTip(tr("Create a new file")); connect(newAct, SIGNAL(triggered()), this, SLOT(newFile())); openAct = new QAction(QIcon(":/images/open.png"), tr("&Open"), this); openAct->setShortcut(tr("Ctrl+O")); openAct->setStatusTip(tr("Open an existing file")); connect(openAct, SIGNAL(triggered()), this, SLOT(open())); saveAct = new QAction(QIcon(":/images/save.png"), tr("&Save"), this); saveAct->setShortcut(tr("Ctrl+S")); saveAct->setStatusTip(tr("Save the document to disk")); connect(saveAct, SIGNAL(triggered()), this, SLOT(save())); saveAsAct = new QAction(tr("Save &As"), this); saveAsAct->setStatusTip(tr("Save the document under a new name")); connect(saveAsAct, SIGNAL(triggered()), this, SLOT(saveAs())); exitAct = new QAction(tr("E&xit"), this); exitAct->setShortcut(tr("Ctrl+Q")); exitAct->setStatusTip(tr("Exit the application")); connect(exitAct, SIGNAL(triggered()), this, SLOT(close())); cutAct = new QAction(QIcon(":/images/cut.png"), tr("Cu&t"), this); cutAct->setShortcut(tr("Ctrl+X")); cutAct->setStatusTip(tr("Cut the current selection's contents to the " "clipboard")); connect(cutAct, SIGNAL(triggered()), textEdit, SLOT(cut())); copyAct = new QAction(QIcon(":/images/copy.png"), tr("&Copy"), this); copyAct->setShortcut(tr("Ctrl+C")); copyAct->setStatusTip(tr("Copy the current selection's contents to the " "clipboard")); connect(copyAct, SIGNAL(triggered()), textEdit, SLOT(copy())); pasteAct = new QAction(QIcon(":/images/paste.png"), tr("&Paste"), this); pasteAct->setShortcut(tr("Ctrl+V")); pasteAct->setStatusTip(tr("Paste the clipboard's contents into the current " "selection")); connect(pasteAct, SIGNAL(triggered()), textEdit, SLOT(paste())); aboutAct = new QAction(tr("&About"), this); aboutAct->setStatusTip(tr("Show the application's About box")); connect(aboutAct, SIGNAL(triggered()), this, SLOT(about())); aboutQtAct = new QAction(tr("About &Qt"), this); aboutQtAct->setStatusTip(tr("Show the Qt library's About box")); connect(aboutQtAct, SIGNAL(triggered()), qApp, SLOT(aboutQt())); 4、如何创建主菜单答:采用上面的QAction的帮助,创建主菜单 fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(newAct); fileMenu->addAction(openAct); fileMenu->addAction(saveAct); fileMenu->addAction(saveAsAct); fileMenu->addSeparator(); fileMenu->addAction(exitAct); editMenu = menuBar()->addMenu(tr("&Edit")); editMenu->addAction(cutAct); editMenu->addAction(copyAct); editMenu->addAction(pasteAct); menuBar()->addSeparator(); helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(aboutAct); helpMenu->addAction(aboutQtAct); 5、如何创建工具栏 答:采用上面的QAction的帮助,创建工具栏 fileToolBar = addToolBar(tr("File"));fileToolBar->addAction(newAct);fileToolBar->addAction(openAct);fileToolBar->addAction(saveAct);editToolBar = addToolBar(tr("Edit"));editToolBar->addAction(cutAct);editToolBar->addAction(copyAct);editToolBar->addAction(pasteAct); 6、如何使用配置文件保存配置 答:使用QSettings类 QSettings settings("Trolltech", "Application Example");QPoint pos = settings.value("pos", QPoint(200, 200)).toPoint();QSize size = settings.value("size", QSize(400, 400)).toSize(); QSettings settings("Trolltech", "Application Example");settings.setValue("pos", pos());settings.setValue("size", size()); 7、如何使用警告、信息等对话框答:使用QMessageBox类的静态方法 int ret = QMessageBox::warning(this, tr("Application"), tr("The document has been modified.\n" "Do you want to save your changes?"), QMessageBox::Yes | QMessageBox::Default, QMessageBox::No, QMessageBox::Cancel | QMessageBox::Escape); if (ret == QMessageBox::Yes) return save(); else if (ret == QMessageBox::Cancel) return false; 8、如何使通用对话框中文化答:对话框的中文化 比如说,QColorDialog的与文字相关的部分,主要在qcolordialog.cpp文件中,我们可以从qcolordialog.cpp用 lupdate生成一个ts文件,然后用自定义这个ts文件的翻译,再用lrelease生成一个.qm文件,当然了,主程序就要改变要支持多国语言了,使用这个.qm文件就可以了。 另外,还有一个更快的方法,在源代码解开后有一个目录translations,下面有一些.ts, .qm文件,我们拷贝一个: cp src/translations/qt_untranslated.ts ./qt_zh_CN.ts 然后,我们就用Linguist打开这个qt_zh_CN.ts,进行翻译了,翻译完成后,保存后,再用lrelease命令生成qt_zh_CN.qm,这样,我们把它加入到我们的qt project中,那些系统的对话框,菜单等等其它的默认是英文的东西就能显示成中文了。9、在Windows下Qt里为什么没有终端输出? 答:把下面的配置项加入到.pro文件中 win32:CONFIG += console 10、Qt 4 for X11 OpenSource版如何静态链接? 答:编译安装的时候加上-static选项 ./configure -static //一定要加static选项gmakegmake install 然后,在Makefile文件中加 static 选项或者在.pro文件中加上QMAKE_LFLAGS += -static,就可以连接静态库了。11、想在源代码中直接使用中文,而不使用tr()函数进行转换,怎么办? 答:在main函数中加入下面三条语句,但并不提倡 QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));QTextCodec::setCodecForCStrings(QTextCodec::codecForName("UTF-8"));QTextCodec::setCodecForTr(QTextCodec::codecForName("UTF-8")); 或者 QTextCodec::setCodecForLocale(QTextCodec::codecForName("GBK"));QTextCodec::setCodecForCStrings(QTextCodec::codecForName("GBK"));QTextCodec::setCodecForTr(QTextCodec::codecForName("GBK")); 使用GBK还是使用UTF-8,依源文件中汉字使用的内码而定 这样,就可在源文件中直接使用中文,比如: QMessageBox::information(NULL, "信息", "关于本软件的演示信息", QMessageBox::Ok, QMessageBox::NoButtons); 12、为什么将开发的使用数据库的程序发布到其它机器就连接不上数据库? 答:这是由于程序找不到数据库插件而致,可照如下解决方法: 在main函数中加入下面语句: QApplication::addLibraryPath(strPluginsPath"); strPluginsPath是插件所在目录,比如此目录为/myapplication/plugins 则将需要的sql驱动,比如qsqlmysql.dll, qsqlodbc.dll或对应的.so文件放到 /myapplication/plugins/sqldrivers/ 目录下面就行了 这是一种解决方法,还有一种通用的解决方法,即在可执行文件目录下写qt.conf文件,把系统相关的一些目录配置写到qt.conf文件里,详细情况情参考Qt Document Reference里的qt.conf部分13、如何创建QT使用的DLL(.so)以及如何使用此DLL(.so) 答:创建DLL时其工程使用lib模板 TEMPLATE=lib 而源文件则和使用普通的源文件一样,注意把头文件和源文件分开,因为在其它程序使用此DLL时需要此头文件 在使用此DLL时,则在此工程源文件中引入DLL头文件,并在.pro文件中加入下面配置项: LIBS += -Lyourdlllibpath -lyourdlllibname Windows下和Linux下同样(Windows下生成的DLL文件名为yourdlllibname.dll而在Linux下生成的为libyourdlllibname.so。注意,关于DLL程序的写法,遵从各平台级编译器所定的规则。14、如何启动一个外部程序答: 1、使用QProcess::startDetached()方法,启动外部程序后立即返回; 2、使用QProcess::execute(),不过使用此方法时程序会最阻塞直到此方法执行的程序结束后返回,这时候可使用QProcess和QThread这两个类结合使用的方法来处理,以防止在主线程中调用而导致阻塞的情况 先从QThread继承一个类,重新实现run()函数: class MyThread : public QThread{public: void run();};void MyThread::run(){ QProcess::execute("notepad.exe");} 这样,在使用的时候则可定义一个MyThread类型的成员变量,使用时调用其start()方法: class {..MyThread thread;};thread.start(); 15、如何打印报表 答:Qt目前对报表打印支持的库还很少,不过有种变通的方法,就是使用XML+XSLT+XSL-FO来进行报表设计,XML输出数据,用XSLT将XML数据转换为XSL-FO格式的报表,由于现在的浏览器不直接支持XSL-FO格式的显示,所以暂时可用工具(Apache FOP, Java做的)将XSL-FO转换为PDF文档来进行打印,转换和打印由FOP来做,生成XSL-FO格式的报表可以由Qt来生成,也可以由其它内容转换过来,比如有工具(html2fo)将HTML转换为XSL-FO。16、如何在系统托盘区显示图标答:在4.2及其以上版本中使用QSystemTrayIcon类来实现17、怎样将日志输出到文件中 答:(myer提供) void myMessageOutput( QtMsgType type, const char *msg ){ switch ( type ) { case QtDebugMsg: //写入文件; break; case QtWarningMsg: break; case QtFatalMsg: abort(); }}int main( int argc, char** argv ){ QApplication app( argc, argv ); qInstallMsgHandler( myMessageOutput ); return app.exec();} qDebug(), qWarning(), qFatal()分别对应以上三种type。18、如何将图像编译到可执行程序中去 答:使用.qrc文件 写.qrc文件,例如: res.qrc <!DOCTYPE RCC><RCC version="1.0"><qresource> <file>images/copy.png</file> <file>images/cut.png</file> <file>images/new.png</file> <file>images/open.png</file> <file>images/paste.png</file> <file>images/save.png</file></qresource></RCC> 然后在.pro中加入下面代码: RESOURCES = res.qrc 在程序中使用: :images/copy.png 19、如何制作不规则形状的窗体或部件 答:请参考下面的帖子http://www.qtcn.org/bbs/read.php?tid=868120、删除数据库时出现"QSqlDatabasePrivate::removeDatabase: connection 'xxxx' is still in use, all queries will cease to work"该如何处理 答:出现此种错误是因为使用了连接名字为xxxx的变量作用域没有结束,解决方法是在所有使用了xxxx连接的数据库组件变量的作用域都结束后再使用QSqlDatabase::removeDatabae("xxxx")来删除连接。21、如何显示一个图片并使其随窗体同步缩放答:下面给出一个从QWidget派生的类ImageWidget,来设置其背景为一个图片,并可随着窗体改变而改变,其实从下面的代码中可以引申出其它许多方法,如果需要的话,可以从这个类再派生出其它类来使用。 头文件: ImageWidget.hpp #ifndef IMAGEWIDGET_HPP#define IMAGEWIDGET_HPP#include <QtCore>#include <QtGui>class ImageWidget : public QWidget{ Q_OBJECTpublic: ImageWidget(QWidget *parent = 0, Qt::WindowFlags f = 0); virtual ~ImageWidget();protected: void resizeEvent(QResizeEvent *event);private: QImage _image;};#endif CPP文件: ImageWidget.cpp #include "ImageWidget.hpp"ImageWidget::ImageWidget(QWidget *parent, Qt::WindowFlags f) : QWidget(parent, f){ _image.load("image/image_background"); setAutoFillBackground(true); // 这个属性一定要设置 QPalette pal(palette()); pal.setBrush(QPalette::Window, QBrush(_image.scaled(size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation))); setPalette(pal);}ImageWidget::~ImageWidget(){}// 随着窗体变化而设置背景void ImageWidget::resizeEvent(QResizeEvent *event){ QWidget::resizeEvent(event); QPalette pal(palette()); pal.setBrush(QPalette::Window, QBrush(_image.scaled(event->size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation))); setPalette(pal);} 22、Windows下如何读串口信息答:可通过注册表来读qt4.1.0 读取注册表得到 串口信息的方法!23、如何使用WebKit查看的网页进入编辑状态答:在你的HTML网页代码的HTML元素节点上增加一个属性contenteditable就可以使QWebView中查看的网页进入编辑状态了。
I learned Bezier curve and surface in 2001 when I studied the Advanced Computer Graphics paper in The University of Auckland. I drew a teapot lid. One year later, Richard wrote "The teapot is the classic icon of computer graphics. To find out more about its history, check out http://sjbaker.org/teapot/index.html" in an assignment specification that was for the Computer Graphics paper. I was so interested in this that I checked the "A Brief History of The Teapot" web page by Steve Baker, and knew the teapot is from Martin Newell and his wife, and got the entire Newell teapot data set from the web page linker. I have already known what the teapot looks like, but wondered what the Newell tea cup and spoon look like. I asked Steve Baker, who said in his email that "I wonder how many years it's been since the less famous parts of the tea set have been rendered? The only images of the cup and spoon that I've ever seen were Newell's original rendering." I became fussy with Newell cup and spoon, so I decided to render them myself. First I implemented a template using VS .Net MFC and OpenGL, which can render any Bezier patches if the patches and vertices are in array format. I tested my template using the teapot data set from Richard's assignment, which works fine. Then I moved on rendering the tea cup and spoon. To render the cup and spoon, the data set needs to be changed in array format. In order to that, a c++ program ReadAndWrite.cpp was coded using iosteam, which reads the data from the original text files and writes the data to three text files in array format. Then the array data is copied to NewellTeasetData.h header file. I rendered the cup and spoon data and got very funny images. I assumed the data my programm retrieves from the arrays was not correct. After testing and checking the data many times, I found that the vertices' index stored in Newell's cup and spoon patches arrays start from 1, not from 0. Finally, I knew what Newell's tea cup and spoon look like. Here is the screen shot of my template, you can download the execute file to play with it. Here are some images from Newell tea set rendering using my MFC+Open Bezier curve template Newell tea cup from different views Newell tea spoon from different views
构建理想的模块自测结构 温辉敏(wenhm@sina.com) 软件测试的工作量很大(业界统计达到40% 到60%的总开发时间),而又有很大部分适于自动化,因此,测试的改进会对整个开发工作的质量、成本和周期带来非常显著的效果。 极限编程(XP)中推荐的自动化单元测试,是指在编写代码之前先写好测试代码,代码编到一定阶段就用写好的测试代码进行测试。自动化单元测试可以带来如下好处: l 由于测试代码(测试用例)的不断增加,这些测试总让我们写出的代码往正确的方向靠拢,使我们不至于重犯以前的错误。 l 由于可以实现自动测试,所以可以很好的解决人工很难进行的回归测试问题。 我们沿着“建立测试=>令测试通过=>再建立测试=>再令测试通过”的模式,一步一步地把整个程序正确地开发出来。 1.测试代码的几个关键环节 1)将测试数据和测试结果从测试代码中分离出来 在采用CXXUNIT系列测试工具开发测试代码时,发现一般编程人员都是测试用例和测试代码混杂在一起,同样测试结果也是和测试代码混杂在一起,这样就导致测试用例和测试结果的管理非常困难,因为要管理每个用例的数据和结果实际上就是去管理这些代码。而且对于一个函数(或功能) 每增加新测试用例,就要多出一份类似的代码,代码的逻辑实际上都是一致的,和以前测试代码的不同点就是在初始化数据、测试结果的不同,这实际上也导致了代码的重复。 我们可以将测试数据和测试结果从测试代码中分离出来,使得某一个函数(或功能)的测试代码就一份,这一份测试代码应可以进行多组测试数据的测试,可以进行多组测试结果的验证。 2)将测试数据和测试结果放入文件中,并按目录存放 将测试数据和测试结果从测试代码中分离出来是为了更好的管理代码和测试数据,将每个测试用例的数据和结果都放入到一个文件中,文件名字或文件所在目录起上能表明测试用例含义的名字,这样管理起来就方便多了,见图1-2。 图1-2 测试用例文件及目录结构图 由图1-2中可以看出此时测试用例非常直观,从目录名就可以知道该目录下的为那个功能的测试用例,从测试用例文件名就可以知道这个测试用例测的哪一个方面。当一个功能的测试用例非常多可以分成许多类别时我们还可以在下面再创建测试用例分类子目录,使得不同类型的测试用例能分隔开方便管理(详见图2-1)。 3)将测试数据和测试结果绑定在一起 当每个测试用例对应的测试结果都一样时可以将测试结果嵌入到测试代码中,当不同的测试用例要对应不同的测试结果时,根据1)中论述应该将测试结果也从测试代码中分离出来。为了不混淆测试数据和测试结果之间的关系,将它们放在同一个文件中。这里举个例子,图1-2中的“闰年2月份的测试用例.ini”文件中内容如图1-3: 图1-3 测试用例结构图 由图1-3可以看到,测试数据和测试结果放在一起,增加了可读性和维护性。 4) 一份测试代码来运行多份测试用例 怎样让一份测试程序可以进行多组数据的测试和结果的比较呢?3)中已经将测试用例分门别类,并由相应的目录结构组织起来。此时测试程序只需每次从测试目录中取出一个测试用例文件,进行初始化,然后执行测试,最后比较测试结果;测试完一个用例文件,再取下一个文件进行测试,如此循环直到所有的用例文件都测了一遍,详见图1-4。 图1-4一份测试代码测试多个测试用例的流程图 2.测试用例管理方案设计 若再加上边界数据要测试的数据组数就更多了,一般CXXUNIT系列编写的测试代码是每组测试数据(其实一组数据就对应一个测试用例)都要编写初始化代码,然后调用相应功能函数测试。这样导致: 在自动测试的整个过程中,测试用例的可维护性会影响到将来测试用例增加的难易度,良好的自测程序应能很方便的扩充测试用例。 在采用CXXUNIT系列测试工具开发测试代码时,对于一些简单的测试可以测试用例就嵌在测试代码中。但当某一个功能或函数要进行很多组数据(如边界数据)的测试时使用这种方法就得重复编写测试代码,可能每增加一个测试用例就要编写大量的重复测试代码。 举例:要测试周期会议预约功能的代码,要测试以下几组数据: 1)每日召开的周期会议 1.1)按召开次数预约的周期会议 1.2)按开始时间、终止时间预约的周期会议 2)每周召开的周期会议 ……(内容和1.1、1.2一致) 3)每月召开的周期会议 ……(内容和1.1、1.2一致) 4)每年召开的周期会议 ……(内容和1.1、1.2一致) l 随着测试数据组数的增加,将出现大量做重复动作的测试代码,这些测试代码之间唯一的不同是由于初始化的数据不一样而已。 l 当一个功能只需一两个测试用例时,测试用例嵌入在测试代码中可以进行控制和管理,当一个功能测试时需要大量的测试用例时(见举例),大量的测试用例嵌在代码中将很难管理,你要知道某个用例是否已经有了还得去遍历测试代码比较麻烦。 2.1测试用例目录结构 为此很有必要设计一套能良好管理和添加测试用例的体系结构。 当一个功能有很多组测试数据时,我们可以将测试用例数据全部存放在文件中,使测试用例和测试代码分离开。由于测试用例脱离测试代码而存在,可以很方便的进行管理和维护。我们可以为每个要测试的函数(或功能)建个目录,每个测试用例放在一个单独的文本文件中,将所有对应于该函数的测试数据文件全部放入该目录下,当测试数据量很大时还可以在目录下再创建相应的子目录,分类进行管理。这样可以方便测试用例的管理,而测试程序也只用专注于测试逻辑。还以测试周期会议预约功能的例子为基础进行讨论。 图2-1 测试用例目录结构图 由图2-1上可以看到,当某个函数(或功能)的测试用例很多时,还可以在“某功能测试用例总目录”下再使用子目录来划分,若觉得划分不够细还可以继续加深目录层次,直到分类比较清晰为止。 由于测试用例数据和测试代码进行了分离,以后对于某个函数(或功能)有了新的测试用例时不用再去修改测试代码,只需在该功能的测试数据目录下添加新的测试用例文件即可。该功能的测试程序每次运行时对于相应测试数据目录下的每个测试用例文件(包括各级子目录下的用例文件)都要执行一遍。 使用了目录结构对它们进行了分门别类方便了以后的管理,正如良好的程序应具有好的可读性和维护性一样,良好的测试数据也应具有好的可读性和维护性。 2.2测试用例文件结构 测试用例文件中存放测试用例初始化数据和测试完毕后的验证数据。数据的结构采用一般配置文件的格式,详见图2-2。 图2-2 测试用例文件结构图 1)段名 配置文件中使用了段的概念,段相当于C++中的NameSpace,每个段内的关键字(Key)与其它段内的关键字互不影响,Section即为段名。 2)关键字 关键字用来标志不同元素的值,Key即为关键字的名字,关键字的名字不区分大小。 3)值 每个关键字都对应一个值,Value 即为值,值要区分大小。 4)注释 支持单行注释,字符“#”后面的内容为注释。 5)行结构 一行的结构只能是以下几种: l [Section] l Key=Value l 空行 l 以上三中情况之一加上注释 2.3测试程序通用库 测试数据从测试代码中分离出来后,增加了管理测试数据文件和解析测试数据文件的代码,这部分代码是通用的,可以将它们组织成库供开发测试程序时使用。 图2-3 测试程序和测试通用库关系图 2.3.1管理测试数据文件的库 管理测试数据文件的库的功能如下: 1)获取目录下所有测试文件路径的接口 能找出某个目录下 (包括该目录下所有子目录)所有的测试文件(如“*.ini”文件)的路径并存起来。 2)获取下一个测试文件的接口 向用户提供下一个测试文件的路径,若已没有下一测试文件则返回空。 2.3.2解析测试文件的库 解析测试文件的库的功能如下: 1)文件解析接口 按照 2.2测试用例文件结构 中的文件结构解析出一个测试文件。 2)获取值的接口 向用户提供测试文件中某个段内某个关键字对应的值。 由于测试文件的结构和一般配置文件结构一致,可以使用已有的库来实现(如PWLIB中配置文件解析类)。 3.总结 一个完整的测试用例包含测试数据和测试代码,当测试数据和代码混在一起时给测试用例的维护带来了很大困难,而且给测试代码带来许多冗余。本文提出了将测试数据和测试代码分离的想法,并对怎样进行分离进行了阐述。测试数据从测试代码中分离出来后,使得测试数据维护简单、方便。
开始学习3D了,很早就想学了,怎奈数学不好,一直畏惧,可是如果不直面它,就永远的害怕,永远的逃避,这是在很糟.于是,我终于开始了我的3D之旅.在学校时候就看上了OGRE,十几万行代码的图形引擎,它给我的感觉很好.不过奇怪的是国内似乎根本没有OGRE的社区,曾经是在91看到的,可是那边早已经荒废了.我使用的是OGRE的 1.2.4,VC是VC.net 2003,也就是VC7.1.我是下载源代码,自行编译的,编译需要两个包,我选择了以下两个包:OgreDependencies_VC71_1.2.0p2.zipogre-win32-v1-2-4.zip第二个包是引擎的源码包,解压缩之.第一个包是引擎编译的依存库,里面有两个文件夹,Samples和Dependencies.Samples是依存库的DLL文件,而Dependencies是依存库的头文件和Lib文件.这两个文件夹只要覆盖掉源码相同的文件夹就可以了.接下来就开始进行编译,编译的过程是缓慢的,总共得耗费半个小时左右.我还把Debug和Release都编译出来了,那真是痛苦的过程啊...引擎我放到了G:\OGRE\OGRE1_2_4,编译完了引擎的开发包之后,下面开始设置VC环境了.Tool->Options->Projects->VC++ Directories.设置头文件包含路径:1.在Show Directories for选择Include Files;2.加入:G:\OGRE\OGRE1_2_4\Dependencies\include,G:\OGRE\OGRE1_2_4\OgreMain\include设置Lib库包含路径:1.在Show Directories for选择Librarys Files;2.加入G:\OGRE\OGRE1_2_4\OgreMain\lib\Release,G:\OGRE\OGRE1_2_4\OgreMain\lib\DebugG:\OGRE\OGRE1_2_4\Dependencies\lib\ReleaseG:\OGRE\OGRE1_2_4\Dependencies\lib\Debug这个时候就可以开始我们的第一个OGRE了.其实,OGRE的引擎源码包里面已经自带了很多的例子,不过看起来相当之难看,反正我觉得很难读,于是,我自己按照自己较为熟悉的风格修改了Demo,弄了一个名为Empty的项目,里面什么都没有做,就是一个空白的窗口(准确说是纯黑的窗口).对于我们这些初学者来说,必须得弄清楚Samples\Common\include下面的三个头文件:ExampleApplication.hExampleFrameListener.hExampleLoadingBar.h其实这三个文件和MFC有点形似:ExampleApplication.h里面的类就是应用程序类,它是一个基类;ExampleFrameListener.h里面的类算是一个窗口类,鼠标,键盘的检测都在这里,还有渲染窗口的刷新等;ExampleLoadingBar.h其实有没有也没什么关系,粗略看了看,它只是一个加载条的类.Empty这个项目有以下文件:/main.cpp/Empty.h/Empty.cpp/Common/ExampleApplication.h/Common/ExampleApplication.cpp/Common/ExampleFrameListener.h/Common/ExampleFrameListener.cpp/Common/ExampleLoadingBar.h/Common/ExampleLoadingBar.cpp我把上面的三个公用头文件拆分出了一个cpp,把实现都丢到了cpp里面,之前它们都是直接写在h里面的,对于学习来说,极其难看,就拆开了.我先把三个项目文件内容列出来:main.cpp #include <windows.h>#include "Empty.h"// =============================================================================// WinMain// -----------------------------------------------------------------------------///// =============================================================================INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT ){ // 创建应用程序对象 EmptyApplication app; // 进入应用程序循环 try { app.go(); } catch( Ogre::Exception& e ) { MessageBox( NULL, e.getFullDescription().c_str(), "应用程序出现异常!", MB_OK | MB_ICONERROR | MB_TASKMODAL ); } return 0;} Empty.h #include "ExampleApplication.h"class EmptyApplication : public ExampleApplication{public: EmptyApplication(); ~EmptyApplication();protected: void createScene(void);}; Empty.cpp #include <windows.h>#include "Empty.h"// =============================================================================// EmptyApplication// -----------------------------------------------------------------------------///// =============================================================================EmptyApplication::EmptyApplication(){} // =============================================================================// ~EmptyApplication// -----------------------------------------------------------------------------///// =============================================================================EmptyApplication::~EmptyApplication(){} // =============================================================================// createScene// -----------------------------------------------------------------------------///// =============================================================================void EmptyApplication::createScene(void){} 可以看到,根本什么都没有做,呵呵.当你第一次运行OGRE的Demo时,你可以看到,每次运行的时候都会弹出一个配置对话框出来,一两次还没什么,多了就很让人厌烦,恨不得马上把它踢掉,OK,我终于把它给踢掉了!不过在此之前,你必须确定你程序运行的当前目录下有ogre.cfg这个文件,它就是那个配置对话框所配置的东西.内容大致如下: Render System=OpenGL Rendering Subsystem[Direct3D9 Rendering Subsystem]Allow NVPerfHUD=NoAnti aliasing=NoneFloating-point mode=FastestFull Screen=NoRendering Device=Intel(R) 82915G/GV/910GL Express Chipset FamilyVSync=NoVideo Mode=800 x 600 @ 32-bit colour[OpenGL Rendering Subsystem]Colour Depth=32Display Frequency=N/AFSAA=0Full Screen=NoRTT Preferred Mode=FBOVSync=NoVideo Mode=800 x 600 如果没有这个文件,程序是会挂掉D.啊,现在我要去掉它,是的,现在我要把这个该死的对话框去掉,打开ExampleApplication.h吧,找到configure()这个函数,你将看到的函数应该是这个样子的. /** Configures the application - returns false if the user chooses to abandon configuration. */ virtual bool configure(void) { // Show the configuration dialog and initialise the system // You can skip this and use root.restoreConfig() to load configuration // settings if you were sure there are valid ones saved in ogre.cfg if(mRoot->showConfigDialog()) { // If returned true, user clicked OK so initialise // Here we choose to let the system create a default rendering window by passing 'true' mWindow = mRoot->initialise(true); return true; } else { return false; } } 看到了第二到第四行的注释没有?如果你英语还好,就看懂了,知道怎么做了,如果看不懂,就解释一下,大致意思是:显示配置对话框,并初始化系统.你可以忽略掉此对话框,使用 root.restoreConfig() 加载配置.不过你这么做之前必须确定你设置并且保存了一个ogre.cfg文件.明白了吧,下面给出我修改后的代码: bool ExampleApplication::configure(){ // 显示配置对话框,并初始化系统. // 你可以忽略掉此对话框,使用 root.restoreConfig() 加载配置. // 不过你这么做之前必须确定你设置并且保存了一个ogre.cfg文件. //if(mRoot->showConfigDialog()) if(mRoot->restoreConfig()) { // If returned true, user clicked OK so initialise // Here we choose to let the system create a default rendering window by passing 'true' mWindow = mRoot->initialise(true); return true; } else { return false; }} 非常之简单,就是把mRoot->showConfigDialog()修改成了mRoot->restoreConfig(),直接加载配置文件.啊,你应该已经发现了我的修改以后的代码和之前的代码不一样,嗯,是的.因为这个是在cpp里面的实现.翻了一下<Pro OGRE 3D Programming>其实还有更好的代码,如下: bool ExampleApplication::configure(){ // 显示配置对话框,并初始化系统. // 你可以忽略掉此对话框,使用 root.restoreConfig() 加载配置. // 不过你这么做之前必须确定你设置并且保存了一个ogre.cfg文件. //if(mRoot->showConfigDialog()) if(mRoot->restoreConfig()) { // If returned true, user clicked OK so initialise // Here we choose to let the system create a default rendering window by passing 'true' mWindow = mRoot->initialise(true,"渲染窗口"); return true; } else { if (mRoot->showConfigDialog() ) { mWindow = mRoot->initialise(true); return true; } else { return false; } }} 如果有ogre.cfg,就读取,如果没有的话就新建一个.OGRE修改窗口标题OGRE默认的窗口标题为"OGRE Render Window",在哪修改它哪?远在天边,近在眼前.就在上面代码中的一个函数:mRoot->initialise();看到了吧,mWindow = mRoot->initialise(true,"渲染窗口");这样,窗口标题就变成了 "渲染窗口"了.
一、 Code Review 简介 1 Code Review 的目的 凡事知其然还要知其所以然 , 我们首先需要知道什么是 Code Review 和我们使用它的目的是什么。 Code Review 是一种用来确认方案设计和代码实现的质量保证机制,通过这个机制我们可以对代码,测试过程和注释进行检查。 Code Review 主要用来在软件工程过程中改进代码质量,通过 Code Review 可以达到如下目的: 在项目早期就能够发现代码中的BUG 帮助初级开发人员学习高级开发人员的经验,达到知识共享 避免开发人员犯一些很常见,很普通的错误 保证项目组人员的良好沟通 项目或产品的代码更容易维护 2 Code Review的前提 知道了 Code Review 的目的,我们就可以看看如何做 Code Review 了,但在做 Code Review 前我们还有事要做,所谓预则立,不预则废,就是说如果在进入 Code Review 之前我们不做些准备工作, Code Review 很容易就变得没有意义或是流于形式,这在我们周围是有很多例子的啊。进入 Code Review需要检查的条件如下: a) Code Review 人员是否理解了 Code Review 的概念和 Code Review 将做什么 如果做 Code Review 的人员不能理解 Code Review 对项目成败和代码质量的重要程度,他们的做法可能就会是应付了事。 b) 代码是否已经正确的 build , build 的目的使得代码已经不存在基本语法错误 我们总不希望高级开发人员或是主管将时间浪费在检查连编译都通不过的代码上吧。 c) 代码执行时功能是否正确 Code Review 人员也不负责检查代码的功能是否正确,也就是说,需要复查的代码必须由开发人员或质量人员负责该代码的功能的正确性。 d) Review 人员是否理解了代码 做复查的人员需要对该代码有一个基本的了解,其功能是什么,是拿一方面的代码,涉及到数据库或是通讯,这样才能采取针对性的检查 e) 开发人员是否对代码做了单元测试 这一点也是为了保证 Code Review 前一些语法和功能问题已经得到解决, Code Review 人员可以将精力集中在代码的质量上。 3 Code Review 需要做什么 好了,进入条件准备好了,有人在这些条件中看到 Code Review 这也不负责,那也不检查,不禁会问, Code Review 到底做什么?其实 Code Review主要检查代码中是否存在以下方面问题:代码的一致性、编码风格、 代码的安全问题、代码冗余、是否正确设计以满足需求(性能、功能等等),下边我们一一道来。以下内容参考了 《 Software Quality Assurance: Documentation and Reviews 》一文中的代码检查部分。 3 . 1 完整性检查( Completeness ) 代码是否完全实现了设计文档中提出的功能需求 代码是否已按照设计文档进行了集成和 Debug 代码是否已创建了需要的数据库 , 包括正确的初始化数据 代码中是否存在任何没有定义或没有引用到的变量、常数或数据类型 3.2一致性检查(Consistency) 代码的逻辑是否符合设计文档 代码中使用的格式、符号、结构等风格是否保持一致 3.3正确性检查(Correctness) 代码是否符合制定的标准 所有的变量都被正确定义和使用 所有的注释都是准确的 所有的程序调用都使用了正确的参数个数 3 . 4 可修改性检查( Modifiability ) 代码涉及到的常量是否易于修改 ( 如使用配置、定义为类常量、使用专门的常量类等 ) 代码中是否包含了交叉说明或数据字典,以描述程序是如何对变量和常量进行访问的 代码是否只有一个出口和一个入口(严重的异常处理除外) 3.5可预测性检查(Predictability) 代码所用的开发语言是否具有定义良好的语法和语义 是否代码避免了依赖于开发语言缺省提供的功能 代码是否无意中陷入了死循环 代码是否是否避免了无穷递归 3.6健壮性检查(Robustness) 代码是否采取措施避免运行时错误(如数组边界溢出、被零除、值越界、堆栈溢出等) 3.7结构性检查(Structuredness) 程序的每个功能是否都作为一个可辩识的代码块存在 循环是否只有一个入口 3.8可追溯性检查(Traceability) 代码是否对每个程序进行了唯一标识 是否有一个交叉引用的框架可以用来在代码和开发文档之间相互对应 代码是否包括一个修订历史记录,记录中对代码的修改和原因都有记录 是否所有的安全功能都有标识 3.9可理解性检查(Understandability) 注释是否足够清晰的描述每个子程序 是否使用到不明确或不必要的复杂代码,它们是否被清楚的注释 使用一些统一的格式化技巧(如缩进、空白等)用来增强代码的清晰度 是否在定义命名规则时采用了便于记忆,反映类型等方法 每个变量都定义了合法的取值范围 代码中的算法是否符合开发文档中描述的数学模型 3.10可验证性检查(Verifiability) 代码中的实现技术是否便于测试 二、 Code Review 经验检查项 以下是在实践中建立的检查列表 ( checklist ), 通过分类和有针对性的检查项 , 保证了 Code Review 可以有的放矢。 1 JAVA 编码规范方面检查项 检查项参照 JAVA 编码规范执行 , 见《 JAVA 编码规范 ( Java Code Conventions ) 》 2 面向对象设计方面检查项 这几点的范围都很大,不可能在本文展开讨论,有专门的书籍介绍这方面问题,当然在 Code Review 中主要靠经验来判断。 A) 类设计和抽象是否合适 B) 是否符合面向接口编程的思想 C) 是否采用合适的设计范式 3 性能方面检查项 性能检查在大多数代码中都是需要严重关注的方面,也是最容易出现问题的方面,常常有程序员写出了功能和语法没有丝毫问题的代码后,正式运行时却在性能上表现不佳,从而不得不做大量的返工,甚至是推倒重来。 A) 在海量数据出现时,队列,表,文件,在传输,upload等方面是否会出现问题,有无控制,如分配的内存块大小,队列长度等控制参数 B) 对hashtable,vector等集合类数据结构的选择和设置是否合适,如正确设置capacity,load factor等参数,数据结构的是否是同步的 C) 有无滥用String对象的现象 D) 是否采用通用的线程池、对象池模块等cache技术以提高性能 E) 类的接口是否定义良好,如参数类型等,避免内部转换 F) 是否采用内存或硬盘缓冲机制以提高效率 G) 并发访问时的应对策略 H) I/O方面是否使用了合适的类或采用良好的方法以提高性能(如减少序列化,使用buffer类封装流等) I) 同步方法的使用是否得当,是否过度使用 J) 递归方法中的叠代次数是否合适,应该保证在合理的栈空间范围内 K) 如果调用了阻塞方法,是否考虑了保证性能的措施 L) 避免过度优化,对性能要求高的代码是否使用profile工具,如Jprobe等 4 资源泄漏处理方面检查项 对于 JAVA 来说由于存在垃圾收集机制,所以内存泄漏不是太明显,但使用不当,仍然存在内存泄漏的问题。而对于其它的语言,如 C++ 等在这方面就要严重关注了。当然数据库连接资源不释放的问题也是广大程序员最常见的,相信有很多的 PM 被这个问题折磨的死去活来。 A) 分配的内存是否释放,尤其在错误处理路径上(对非 JAVA 类) B) 错误发生时是否所有的对象被释放,如数据库连接、 Socket 、文件等 C) 是否同一个对象被释放多次(对非 JAVA 类) D) 代码是否保存准确的对象 reference 计数(对非 JAVA 类) 5 线程安全方面检查项 线程安全问题实际涉及两个方面,一个是性能,另一个是资源的一致性,我们需要在这两方面做个权衡,现在就是到了权衡利弊的时候了。 A) 代码中所有的全局变量是否是线程安全的 B) 需要被多个线程访问的对象是否线程安全,检查有无通过同步方法保护 C) 同步对象上的锁是否按相同的顺序获得和释放以避免死锁,注意错误处理代码 D) 是否存在可能的死锁或是竞争,当用到多个锁时,避免出现类似情况:线程 A 获得锁 1 ,然后锁 2 ,线程 B 获得锁 2 ,然后锁 1 E) 在保证线程安全的同时,要注意避免过度使用同步,导致性能降低 6 程序流程方面检查项 A) 循环结束条件是否准确 B) 是否避免了死循环的产生 C) 对循环的处理是否合适,如循环变量,局部对象,循环次数等能够考虑到性能方面的影响 7 数据库处理方面 很多 Code Review 人员在面对代码中涉及到的数据库可移植性和提高数据库性能方面的冲突时表现的无所适从,凡事很难两全其美的啊。 A) 数据库设计或SQL语句是否便于移植(注意和性能方面会存在冲突) B) 数据库资源是否正常关闭和释放 C) 数据库访问模块是否正确封装,便于管理和提高性能 D) 是否采用合适的事务隔离级别 E) 是否采用存储过程以提高性能 F) 是否采用PreparedStatement以提高性能 8 通讯方面检查项 A) socket通讯是否存在长期阻塞问题 B) 发送接收的数据流是否采用缓冲机制 C) socket超时处理,异常处理 D) 数据传输的流量控制问题 9 JAVA 对象处理方面检查项 这个检查项的基础是对 JAVA 对象有较深的理解,但现实是很多看过《 Thinking in Java 》的程序员,仍然在程序中无法区分传值和传引用,以及对象和reference 的区别。这或许就是理论和实践难以结合的问题啊。正所谓知而不行,非真知也。 A) 对象生命周期的处理,是否对象的reference已经失效,能够设置为null,并被回收 B) 在对象的传值和传参方面有无问题,对象的clone方法使用是否过度 C) 是否 大量经常的创建临时对象 D) 是否尽量使用局部对象(堆栈对象) E) 在只需要对象reference的地方是否创建了新的对象实例 10 异常处理方面检查项 JAVA 中提供了方便的异常处理机制,但普遍存在的是异常被捕获,但并没有得到处理。我们可以打开一段代码,最常见的现象是进入某个方法后,一个大的 try/catch 将所有代码行括住,然后在 catch 中将异常打印到控制台,而且该异常是 Exception 对象。 A) 每次当方法返回时是否正确处理了异常,如最简单的处理,记录日志到日志文件中 B) 是否对数据的值和范围是否合法进行校验,包括采用断言( assertion ) C) 在出错路径上是否所有的资源和内存都已经释放 D) 所有抛出的异常都得到正确的处理,特别是对子方法抛出的异常,在整个调用栈中必须能够被捕捉并处理 E) 当调用导致错误发生时,方法的调用者应该得到一个通知 F) 不要忘了对错误处理部分的代码进行测试,很多代码在正常情况下执行良好,而一旦出错,整个系统就崩溃了 11 方法(函数)方面检查项 A) 方法的参数是否都做了校验 B) 数组类结构是否做了边界校验 C) 变量在使用前是否做了初始化 D) 返回堆对象的 reference ,不要返回栈对象的 reference E) 方法 API 是否被良好定义,即是否尽量面向接口编程,便于维护和重构 12 安全方面检查项 A) 对命令行执行的代码,需要详细检查命令行参数 B) WEB 类程序检查是否对访问参数进行合法性验证 C) 重要信息的保存是否选用合适的加密算法 D) 通讯时考虑是否选用安全的通讯方式 13 其他 A) 日志是否正常输出和控制 B) 配置信息如何获得,是否有硬编码 三、 总结 通过在项目中实施 Code Review 将为我们带来多方面的好处,表现在提高代码质量,保证项目或产品的稳定性,开发经验的积累等,具体的实施当然也要看项目的实际情况,因为 Code Review也是需要成本的,这方面属于 Code Review 过程的问题,将在其他文章中进行探讨。
由于我们的前台使用C语言编写CGI,如果对方提供XML接口给我们传递数据,就必须有解析的程序,这也可能是今后数据接口的最通用的办法。经过研究,正如使用C语言来生成页面一样,显然使用C语言解析XML要比PHP和ASP要麻烦很多。同其它语言一样,解析的方法一般都是调用现有的解析器,因为这样省时省力。PHP4是内置的EXPAT,PHP5是内置的LIBXML2,WIN平台可以调用MSXML。FREEBSD上使用C语言,最流行的就是调用EXPAT和LIBXML2,由于PHP基于某些原因放弃了EXPAT,所以我主要试用了LIBXML2。 LIBXML2主页是http://xmlsoft.org安装过程:(需要ROOT权限)gunzip -c libxml2-2.6.22.tar.gz | tar xvf -cd libxml2-2.6.22./configuremakesumake installexit安装完成后就可以使用简单的代码解析XML文件,包括本地和远程的文件,但是在编码上有一些问题。LIBXML默认只支持UTF-8的编码,无论输入输出都是UTF-8,所以如果你解析完一个XML得到的结果都是UTF-8的,如果需要输出GB2312或者其它编码,需要ICONV来做转码(生成UTF-8编码的文件也可以用它做)。ICONV的安装过程和LIBXML2一样。下面是一些例子,包括解析XML和转码 # i nclude <stdio.h># i nclude <string.h># i nclude <stdlib.h># i nclude <libxml/xmlmemory.h># i nclude <libxml/parser.h>#i nclude <iconv.h>//***********************************************************************////* d_ConvertCharset: 编码转换函数,可以转换任意两种编码格式//* ddr/2005-11-10//* 此函数需要库libiconv,编译时需加-liconv,如果找不到库,编译时加-L/usr/local/lib// 其中/usr/local/lib为安装库文件的目录// 使用时需要#i nclude <iconv.h>,如果找不到此头文件请在编译时加-I/usr/local/include// 其中/usr/local/include为安装头文件的目录//* 需要使用static变量作为输出的缓冲区,这里设置的最大长度是1024,可以根据需要修改,以避免溢出// 由于使用了static变量,所以这个函数是不可重入的,非线程安全的// 可以改用new的方式来实现可重入//***********************************************************************//static char s_strBufOut[1024];char *d_ConvertCharset(char *cpEncodeFrom, char *cpEncodeTo, const char *cpInput){ char *cpOut; size_t iInputLen, iOutLen, iReturn; iconv_t c_pt; if ((c_pt = iconv_open(cpEncodeTo, cpEncodeFrom)) == (iconv_t)-1) { printf("iconv_open failed!\n"); return NULL; } iconv(c_pt, NULL, NULL, NULL, NULL); iInputLen = strlen(cpInput) + 1; iOutLen = 1024; cpOut = s_strBufOut; iReturn = iconv(c_pt, &cpInput, &iInputLen, &cpOut, &iOutLen); if (iReturn == -1) { return NULL; } iconv_close(c_pt); return s_strBufOut;}//输出每一项的内容,使用GB2312编码输出void parseItem (xmlDocPtr doc, xmlNodePtr cur) { xmlChar *key; cur = cur->xmlChildrenNode; while (cur != NULL) { if ((!xmlStrcmp(cur->name, (const xmlChar *)"songname"))) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); printf("songname: %s\n", d_ConvertCharset("utf-8", "gb2312", (char *)key)); xmlFree(key); } else if ((!xmlStrcmp(cur->name, (const xmlChar *)"songurl"))) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); printf("songurl: %s\n", d_ConvertCharset("utf-8", "gb2312", (char *)key)); xmlFree(key); } else if ((!xmlStrcmp(cur->name, (const xmlChar *)"singer"))) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); printf("singer: %s\n", d_ConvertCharset("utf-8", "gb2312", (char *)key)); xmlFree(key); } else if ((!xmlStrcmp(cur->name, (const xmlChar *)"singerurl"))) { key = xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); printf("singerurl: %s\n", d_ConvertCharset("utf-8", "gb2312", (char *)key)); xmlFree(key); } cur = cur->next; } return;}void parseDoc(char *docname) { xmlDocPtr doc; //解析树 xmlNodePtr cur; //当前节点 doc = xmlParseFile(docname); if (doc == NULL ) { fprintf(stderr,"Document not parsed successfully. \n"); return; } //得到根节点 cur = xmlDocGetRootElement(doc); if (cur == NULL) { fprintf(stderr,"empty document\n"); xmlFreeDoc(doc); return; } //判断根节点是不是mp3 if (xmlStrcmp(cur->name, (const xmlChar *) "mp3")) { fprintf(stderr,"document of the wrong type, root node != mp3"); xmlFreeDoc(doc); return; } //得到当前节点的第一个子节点,即第一个ITEM cur = cur->xmlChildrenNode; while (cur != NULL) { if ((!xmlStrcmp(cur->name, (const xmlChar *)"item"))) { //输出每个ITEM parseItem (doc, cur); } cur = cur->next; } xmlFreeDoc(doc); return;}//入参可以是一个文件,也可以是一个URL,要求必须是UTF-8编码int main(int argc, char **argv) { char *docname; if (argc <= 1) { printf("Usage: %s docname\n", argv[0]); return(0); } docname = argv[1]; parseDoc (docname); return 0;}
一、ACE综述 ACE自适配通信环境(ADAPTIVE Communication Environment)是可以自由使用、开放源码的面向对象(OO)框架(Framework),在其中实现了许多用于并发通信软件的核心模式。ACE提供了一组丰富的可复用C++ Wrapper Facade(包装外观)和框架组件,可跨越多种平台完成通用的通信软件任务,其中包括:事件多路分离和事件处理器分派、信号处理、服务初始化、进程间通信、共享内存管理、消息路由、分布式服务动态(重)配置、并发执行和同步,等等。 ACE的目标用户是高性能和实时通信服务和应用的开发者。它简化了使用进程间通信、事件多路分离、显式动态链接和并发的OO网络应用和服务的开发。此外,通过服务在运行时与应用的动态链接,ACE还使系统的配置和重配置得以自动化。 ACE正在进行持续的改进。Riverace公司(http://www.riverace.com)采用开放源码商业模式对ACE进行商业支持。此外,ACE开发组的许多成员目前正在进行The ACE ORB(TAO,http://www.cs.wustl.edu/~schmidt/TAO.html)的开发工作。 二、使用ACE的好处使用ACE的好处有: 。增强可移植性:在ACE组件的帮助下,很容易在一种OS平台上编写并发网络应用,然后快速地将它们移植到各种其他的OS平台上。而且,因为ACE是开放源码的自由软件,你无需担心被锁定在特定的操作系统平台或编译器上。 。更好的软件质量:ACE的设计使用了许多可提高软件质量的关键模式,这些质量因素包括通信软件灵活性、可扩展性、可复用性和模块性。 。更高的效率和可预测性:ACE经仔细设计,支持广泛的应用服务质量(QoS)需求,包括延迟敏感应用的低响应等待时间、高带宽应用的高性能,以及实时应用的可预测性。 。更容易转换到标准的高级中间件:TAO使用了ACE提供的可复用组件和模式。它是CORBA的开发源码、遵循标准的实现,并为高性能和实时系统作了优化。为此,ACE和TAO被设计为能良好地协同工作,以提供全面的中间件解决方案。 三、ACE的结构和功能下图显示了ACE中的关键组件以及它们的层次关系: 图中的结构和各层的组成部分描述如下。 四、ACE OS适配层该层直接位于用C写成的本地OS API之上。它提供轻型的类POSIX OS适配层,将ACE中的其他层及组件和以下与OS API相关联的平台专有特性屏蔽开来: 。并发和同步:ACE的适配层封装了用于多线程、多进程和同步的OS API。 。进程间通信(IPC)和共享内存:ACE的适配层封装了用于本地和远地IPC、以及共享内存的OS API。 。事件多路分离机制:ACE的适配层封装了用于对基于I/O、定时器、信号和同步的事件进行同步和异步多路分离的OS API。 。显式动态链接:ACE的适配层封装了用于显式动态链接的OS API。显式动态链接允许在安装时或运行时对应用服务进行配置。 。文件系统机制:ACE的适配层封装了用于操作文件和目录的OS文件系统API。 ACE OS适配层的可移植性使得ACE可运行在许多操作系统上。ACE已在广泛的OS平台上进行了移植和测试,包括Win32(也就是,在Intel和Alpha平台,使用MSVC++、Borland C++ Builder和IBM Visual Age的WinNT 3.5.x、4.x、2000、Win95/98和WinCE)、Mac OS X、大多数版本的UNIX(例如,SPARC和Intel上的Solaris 1.x和2.x、SGI IRIX 5.x和6.x、DG/UX、HP-UX 9.x、10.x和11.x、DEC/Compaq UNIX 3.x和4.x、AIX 3.x和4.x、UnixWare、SCO,以及可自由使用的UNIX实现,比如Debian Linux 2.x、RedHat Linux 5.2、6.x和7.x、FreeBSD和NetBSD)、实时操作系统(比如,LynxOS、VxWorks、Chorus ClassiX 4.0、QnX Neutrino、RTEMS和PSoS)、MVS OpenEdition和CRAY UNICOS。 由于ACE的OS适配层所提供的抽象,所有这些平台使用同一棵代码树。这样的设计极大地增强了ACE的可移植性和可维护性。此外,还有Java版本的ACE可用(http://www.cs.wustl.edu/~eea1/JACE.html)。 五、OS接口的C++ Wrapper Facade可以直接在ACE OS适配层之上编写高度可移植的C++应用。但是,大多数ACE开发者使用的是上图中所示的C++ Wrapper Facade层。通过提供类型安全的C++接口(这些接口封装并增强本地的OS并发、通信、内存管理、事件多路分离、动态链接和文件系统API),ACE Wrapper Facade简化了应用的开发。应用可以通过有选择地继承、聚合和/或实例化下面的组件来组合和使用这些包装: 。并发和同步组件:ACE对像互斥体和信号量这样的本地OS多线程和多进程机制进行抽象,以创建高级的OO并发抽象,像主动对象(Active Object)和多态期货(Polymorphic Future)。 。IPC和文件系统组件:ACE C++包装对本地和/或远地IPC机制进行封装,比如socket、TLI、UNIX FIFO和STREAM管道,以及Win32命名管道。此外,ACE C++包装还封装了OS文件系统API。 。内存管理组件:ACE内存管理组件为管理进程间共享内存和进程内堆内存的动态分配和释放提供了灵活和可扩展的抽象。 ACE C++包装提供了许多与ACE OS适配层一样的特性。但是,这些特性是采用C++类和对象、而不是独立的C函数来构造的。这样的OO包装有助于减少正确地学习和使用ACE所需的努力。 例如,C++的使用提高了应用的健壮性,因为C++包装是强类型的。所以,编译器可在编译时、而不是运行时检测类型系统违例。相反,不到运行时,不可能检测像socket或文件系统I/O这样的C一级OS API的类型系统违例。 ACE采用了许多技术来降低或消除额外的性能开销。例如,ACE大量地使用C++内联来消除额外的方法调用开销;这样的开销可由OS适配层和C++包装所提供的额外的类型安全和抽象层次带来。此外,对于性能要求很高的包装,比如socket和文件I/O的send/recv方法,ACE会避免使用虚函数。 六、框架ACE还含有一个高级的网络编程框架,集成并增强了较低层次的C++ Wrapper Facade。该框架支持将并发分布式服务动态配置进应用。ACE的框架部分包含以下组件: 。事件多路分离组件:ACE Reactor(反应器)和Proactor(前摄器)是可扩展的面向对象多路分离器,它们分派应用特有的处理器,以响应多种类型的基于I/O、定时器、信号和同步的事件。 。服务初始化组件:ACE Acceptor(接受器)和Connector(连接器)组件分别使主动和被动的初始化任务与初始化一旦完成后通信服务所执行的应用特有的任务去耦合。 。服务配置组件:ACE Service Configurator(服务配置器)支持应用的配置,这些应用的服务可在安装时和/或运行时动态装配。 。分层的流组件:ACE Stream组件简化了像用户级协议栈这样的由分层服务组成的通信软件应用的开发。 。ORB适配器组件:通过ORB适配器,ACE可以与单线程和多线程CORBA实现进行无缝集成。 ACE框架组件便利了通信软件的开发,它们无需修改、重编译、重链接,或频繁地重启运行中的应用,就可被更新和扩展。在ACE中,这样的灵活性是通过结合以下要素来获得的:(1)C++语言特性,比如模板、继承和动态绑定,(2)设计模式,比如抽象工厂、策略和服务配置器,以及(3)OS机制,比如显式动态链接和多线程。 七、分布式服务和组件除了OS适配层、C++ Wrapper Facade和框架组件,ACE还提供了包装成自包含组件的标准分布式服务库。尽管这些服务组件并不是ACE框架库的严格组成部分,它们在ACE中扮演了两种角色: 。分解出可复用分布式应用的“积木”:这些服务组件提供通用的分布式应用任务的可复用实现,比如名字服务、事件路由、日志、时间同步和网络锁定。 。演示ACE组件的常见用例:这些分布式服务还演示了怎样用像Reactor、Service Configurator、Acceptor和Connector、Active Object,以及IPC包装这样的ACE组件来有效地开发灵活、高效和可靠的通信软件。 八、高级分布式计算中间件组件即使使用像ACE这样的通信框架,开发健壮、可扩展和高效的通信应用仍富有挑战性。特别是,开发者必须掌握许多复杂的OS和通信的概念,比如: 。网络寻址和服务标识。 。表示转换,比如加密、压缩和在异种终端系统间的字节序转换。 。进程和线程的创建和同步。 。本地和远地进程间通信(IPC)机制的系统调用和库例程。 通过采用像CORBA、DCOM或Java RMI这样的高级分布式计算中间件,可以降低开发通信应用的复杂性。高级分布式计算中间件驻留在客户端和服务器之间,可自动完成分布式应用开发的许多麻烦而易错的方面,包括: 。认证、授权和数据安全。 。服务定位和绑定。 。服务注册和启用。 。事件多路分离和分派。 。在像TCP这样的面向字节流的通信协议之上实现消息帧。 。涉及网络字节序和参数整编(marshaling)的表示转换问题。 为给通信软件的开发者提供这些特性,在ACE中绑定了下面的高级中间件应用: 。The ACE ORB(TAO):TAO是使用ACE提供的框架组件和模式构建的CORBA实时实现,包含有网络接口、OS、通信协议和CORBA中间件组件等特性。TAO基于标准的OMG CORBA参考模型,并进行了增强的设计,以克服传统的用于高性能和实时应用的ORB的缺点。TAO像ACE一样,也是可自由使用的开放源码软件。 。JAWS:JAWS是高性能、自适配的Web服务器,使用ACE提供的框架组件和模式构建。JAWS被构造成“框架的框架”。JAWS的总体框架含有以下组件和框架:事件多路分派器、并发策略、I/O策略、协议管道、协议处理器和缓存虚拟文件系统。每个框架都被构造成一组协作对象,通过组合和扩展ACE中的组件来实现。JAWS也是可自由使用的开放源码软件。 九、主页ACE的主页为:http://www.cs.wustl.edu/~schmidt/ACE.html,在这里可获得最新版本的ACE以及其他相关资源。
老实说,对这个操作系统还是一无所知,只是今天看了一点资料,才知道有这么个东西,国内某互联网大头就是用的这个操作系统作为服务器端的。 英文官方:http://www.slackware.com/ 中文社区:http://www.slack.cn/ http://slack.linuxsir.org/main/ 没有使用过这个操作系统,所以不好对之进行任何的评论, 不过我不会拒绝使用它的, 既然有人那么偏好它, 自然有其过人之处了。 以下是我刚找到的一个它的FAQ: 走近Slackware - SlackFiles的FAQ 作者:Danil de Kok 来自:www.slackfiles.org 翻译:windrose 什么是Slackware Linux? SlackwareLinux是由PatrickVolkerding开发的GNU/Linux发行版。与很多其他的发行版不同,它坚持KISS(KeepItSimpleStupid)的原则,就是说没有任何配置系统的图形界面工具。一开始,配置系统会有一些困难,但是更有经验的用户会喜欢这种方式的透明性和灵活性。 SlackwareLinux的另一个突出的特性也符合KISS原则:Slackware没有如RPM之类的成熟的软件包管理器。Slackware的软件包都是通常的tgz(tar/gzip)格式文件再加上安装脚本。Tgz对于有经验的用户来说,比RPM更为强大,并避免了RPM之类管理器的依赖性问题。Slackware还有一个众所周知的特性就是BSD风格的初始化脚本。Slackware对所有的运行级(runlevel)/任务都用同一个脚本,而不是在不同的运行级中建立一堆脚本的链接(译注:详见)。这样让你不必自己写新的脚本就能很容易地调整系统。 Slackware Linux难学吗? 与多数发行版相比,Slackware的学习曲线会陡峭一点,你要准备好多用一些时间。一旦你开始了解到这个发行版,你很可能会发现它比很多其他发行版更容易调整。拿做饭作比方:微波炉餐是很容易做的。你把东西放到微波炉里,等几分钟就做好了。不用微波炉做饭需要更多训练,你必须熟悉原料和烹饪技巧。但是,一旦你学到了烹饪术,就很容易做出比微波炉餐好吃得多的饭菜。 从哪里可以得到Slackware Linux? 有几种途径可以得到Slackware。首先,可以从FTP镜像站点下载。镜像站的列表可以在Slackware的网站:http://www.slackware.com/找到。尽管Slackware可以免费得到,但购买官方CD也是个好主意。PatrickVolkerding在开发Slackware上面花了许许多多时间,购买官方CD你就是在支持Slackware的开发。 Slackware Linux安装的系统要求如何? 这取决于你打算怎么用Slackware。一台16M内存的486刚好能够用一个轻量级的窗口管理器,如BlackBox或Windowmaker,来运行XFree86和轻量级的X程序。这样的机器用来做简单的web服务器或ftp服务器也足够了。一台更少内存(例如8M)的机器也能够用于做路由器或防火墙。KDE和GNOME这种重量级的桌面环境,要求更快的机器,至少是32M内存的奔腾级,但是如果你想做更有用的事情,很可能需要64M内存。Linux的优势在于像vi、gcc和apache等Unix类的程序在旧机器上也能运行得很快。多数其他发行版也有这些软件,但是重量级的安装和配置工具会造成在旧机器上运行Linux非常痛苦。不推荐在386机器上运行最新的Slackware,尽管在一些ftp站上有旧版本的Slackware(甚至可回溯到1994年)能在386上运行得很好。(译注:这一段的信息有些过时,但仍有参考价值) Slackware是基于源码的发行版吗? 与LinuxFromScratch或Gentoo不同,你不必编译整个系统。上述版本的支持者相信可以通过例如针对CPU的优化得到速度的大幅提高。实际上,速度的提高很小,除了几个程序(例如MPEG-2解码器,也可能是KDE之类的桌面环境),你可能感受不到(速度的提高)。Slackware一般是编译好的,但假如你需要你也可以自行用Slackware的源码和编译脚本编译各个部分。与源码发行版相比,Slackware的优点在于你不必编译整个系统,这样有更多的灵活性,并很可能得到一个更稳定的系统(因为有些优化会坏事)。
Introduction A Windows service is an EXE specially designed to communicate with the SCM (Service Control Manager) of Windows NT/2000. The Service Control Manager (SCM) maintains a database of installed services and driver services, and provides a unified and secure means of controlling them. SCM is started at system boot and it is a remote procedure call (RPC) server. As a developer to try a simple service, we can divide the program into four parts. Main program of Win32 / Console Application. A so called ServiceMain(), main program of Service. Entry point of a service. A Service Control Handler, a function to communicate with SCM. A Service Installer/ Uninstaller, to register an EXE as a Service. Firstly, let us take a look at the Main program of the Console application (it can also be a WinMain()). #include " Winsvc.h " // Header file for Services. main() { SERVICE_TABLE_ENTRY Table[] = { { " Service1 " ,ServiceMain} , {NULL,NULL} } ; StartServiceCtrlDispatcher(Table);} The only thing done by the main() is to fill a SERVICE_TABLE_ENTRY array. The position [0][0] contains the name of the Service (any string you like). Position [0][1] contains the name of the Service Main function, I specified in the list earlier. It actually is a function pointer to the Service main function. The name can be any thing. Now we start the first step to a service by calling StartServiceCtrlDispatcher() with the SERVICE_TABLE_ENTRY array. Note that the function signature should be of the form. The [1][0] and [1][1] positions are NULL, just to say the end of the array (not a must). We can add more entries to the list if we have more than one service running from the same EXE. The declaration of a typical ServiceMain(): void WINAPI ServiceMain(DWORD argc, LPTSTR * argv) </ PRE > Now, let us analyze our ServiceMain function. The main steps of this function are: Fill the SERVICE_STATUS structure with appropriate values to communicate with the SCM. Register the Service Control Handler function said earlier in the list. Call the actual processing functions. For proceeding, we need two global variables here: SERVICE_STATUS m_ServiceStatus; SERVICE_STATUS_HANDLE m_ServiceStatusHandle; The ServiceMain() can accept command line arguments just as any C++ main() function. The first parameter contains the number of arguments being passed to the service. There will always be at least one argument. The second parameter is a pointer to an array of string pointers. The first item in the array always points to the service name. The SERVICE_STATUS data structure is used to fill the current state of the Service and notify it to the SCM. We use an API function SetServiceStatus() for the purpose. The data members of SERVICE_STATUS to look for are: < PRE > dwServiceType = SERVICE_WIN32; dwCurrentState = SERVICE_START_PENDING; // Means Trying To Start(Initially)</PRE> dwControlsAccepted = SERVICE_ACCEPT_STOP; accepts Stop/Start only in Service control program, usually in the Control Panel (NT) / Administrative tools (2000). We can also set our service to accept PAUSE and CONTINUE functionality. In the beginning of the ServiceMain(), we should set the dwCurrentState of SERVICE_STATUS to SERVICE_START_PENDING. This signals the SCM that the service is starting. If any error occurs in the way, we should notify the SCM by passing SERVICE_STOPPED. By default, the SCM will look for an activity from the service and if it fails to show any progress within 2 minutes, SCM kills that service. The API function RegisterServiceCtrlHandler() is used to set the Service Control Handler Function of the Service with the SCM. The function takes two parameters as earlier, one service name (string) and the pointer to the Service Control Handler Function. That function should be with the signature. Once we get till here, we now set dwCurrentState as SERVICE_RUNNING to notify that the service has started to function. The next step is to call the actual processing steps. Now, let us analyze our Service Control Handler function: The Service Control Handler function is used by the SCM to communicate to the Service program about a user action on the service, like a start, stop, pause or continue. It basically contains a switch statement to deal with each case. Here, we will call appropriate steps to clean up and terminate the process. This function receives an opcode which can have values like SERVICE_CONTROL_PAUSE, SERVICE_CONTROL_CONTINUE, SERVICE_CONTROL_STOP, SERVICE_CONTROL_INTERROGATE etc. We have to write appropriate steps on each. Now Service Installer/ Uninstaller For installing a service, we need to make some entries in the system registry. Windows has some APIs to do these steps, instead of using the registry functions. They are CreateService() and DeleteService(). For both these functions, we need to open the SCM database with appropriate rights. I prefer SC_MANAGER_ALL_ACCESS. For installing a service, first open the SCM by OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS). Then invoke the CreateService() with appropriate binary file path of our service. Here also, we have to give the name of our service. We need this name if we want to delete a particular service. In deleting a service, we need to open the specific service first by its name and then invoke the DeleteService() on it. That’s all what we need. Take a look at the code given with it for more details. Thank You Anish C.V. The Code Goes Here: #include " stdafx.h " #include " Windows.h " #include " Winsvc.h " #include " time.h " SERVICE_STATUS m_ServiceStatus;SERVICE_STATUS_HANDLE m_ServiceStatusHandle;BOOL bRunning = true ; void WINAPI ServiceMain(DWORD argc, LPTSTR * argv); void WINAPI ServiceCtrlHandler(DWORD Opcode);BOOL InstallService();BOOL DeleteService(); int main( int argc, char * argv[]) { if (argc > 1 ) { if (strcmp(argv[ 1 ], " -i " ) == 0 ) { if (InstallService()) printf( " \n\nService Installed Sucessfully\n " ); else printf( " \n\nError Installing Service\n " ); } if (strcmp(argv[ 1 ], " -d " ) == 0 ) { if (DeleteService()) printf( " \n\nService UnInstalled Sucessfully\n " ); else printf( " \n\nError UnInstalling Service\n " ); } else { printf( " \n\nUnknown Switch Usage\n\nFor Install use Srv1 - i\n\nFor UnInstall use Srv1 - d\n " ); } } else { SERVICE_TABLE_ENTRY DispatchTable[] = { { " Service1 " ,ServiceMain} , {NULL,NULL} } ; StartServiceCtrlDispatcher(DispatchTable); } return 0 ;} void WINAPI ServiceMain(DWORD argc, LPTSTR * argv) { DWORD status; DWORD specificError; m_ServiceStatus.dwServiceType = SERVICE_WIN32; m_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; m_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; m_ServiceStatus.dwWin32ExitCode = 0 ; m_ServiceStatus.dwServiceSpecificExitCode = 0 ; m_ServiceStatus.dwCheckPoint = 0 ; m_ServiceStatus.dwWaitHint = 0 ; m_ServiceStatusHandle = RegisterServiceCtrlHandler( " Service1 " , ServiceCtrlHandler); if (m_ServiceStatusHandle == (SERVICE_STATUS_HANDLE) 0 ) { return ; } m_ServiceStatus.dwCurrentState = SERVICE_RUNNING; m_ServiceStatus.dwCheckPoint = 0 ; m_ServiceStatus.dwWaitHint = 0 ; if ( ! SetServiceStatus (m_ServiceStatusHandle, & m_ServiceStatus)) { } bRunning = true ; while (bRunning) { Sleep( 3000 ); // Place Your Code for processing here. } return ;} void WINAPI ServiceCtrlHandler(DWORD Opcode) { switch (Opcode) { case SERVICE_CONTROL_PAUSE: m_ServiceStatus.dwCurrentState = SERVICE_PAUSED; break ; case SERVICE_CONTROL_CONTINUE: m_ServiceStatus.dwCurrentState = SERVICE_RUNNING; break ; case SERVICE_CONTROL_STOP: m_ServiceStatus.dwWin32ExitCode = 0 ; m_ServiceStatus.dwCurrentState = SERVICE_STOPPED; m_ServiceStatus.dwCheckPoint = 0 ; m_ServiceStatus.dwWaitHint = 0 ; SetServiceStatus (m_ServiceStatusHandle, & m_ServiceStatus); bRunning = false ; break ; case SERVICE_CONTROL_INTERROGATE: break ; } return ;} BOOL InstallService() { char strDir[ 1024 ]; HANDLE schSCManager,schService; GetCurrentDirectory( 1024 ,strDir); strcat(strDir, " \\Srv1.exe " ); schSCManager = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if (schSCManager == NULL) return false ; LPCTSTR lpszBinaryPathName = strDir; schService = CreateService(schSCManager, " Service1 " , " The Display Name Needed " , // service name to display SERVICE_ALL_ACCESS, // desired access SERVICE_WIN32_OWN_PROCESS, // service type SERVICE_DEMAND_START, // start type SERVICE_ERROR_NORMAL, // error control type lpszBinaryPathName, // service's binary NULL, // no load ordering group NULL, // no tag identifier NULL, // no dependencies NULL, // LocalSystem account NULL); // no password if (schService == NULL) return false ; CloseServiceHandle(schService); return true ;} BOOL DeleteService() { HANDLE schSCManager; SC_HANDLE hService; schSCManager = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if (schSCManager == NULL) return false ; hService = OpenService(schSCManager, " Service1 " ,SERVICE_ALL_ACCESS); if (hService == NULL) return false ; if (DeleteService(hService) == 0 ) return false ; if (CloseServiceHandle(hService) == 0 ) return false ; return true ;} About C.V Anish A Developer from India. Concentrating on the Microsoft Technologies. VC++ and VB. Click here to view C.V Anish's
The following example uses the LogonUser function to start a new logon session for a client. The example gets the logon SID from the client's access token, and uses it to add access control entries (ACEs) to the discretionary access control list (DACL) of the interactive window station and desktop. The ACEs allow the client access to the interactive desktop for the duration of the logon session. Next, the example calls the ImpersonateLoggedOnUserfunction to ensure that it has access to the client's executable file. A call to the CreateProcessAsUser function creates the client's process, specifying that it run in the interactive desktop. Note that your process must have the SE_ASSIGNPRIMARYTOKEN_NAME and SE_INCREASE_QUOTA_NAME privileges for successful execution of CreateProcessAsUser. Before the function returns, it calls the RevertToSelf function to end the caller's impersonation of the client. This example calls the GetLogonSID and FreeLogonSID functions described in Getting the Logon SID in C++. #define DESKTOP_ALL (DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | \ DESKTOP_CREATEMENU | DESKTOP_HOOKCONTROL | DESKTOP_JOURNALRECORD | \DESKTOP_JOURNALPLAYBACK | DESKTOP_ENUMERATE | DESKTOP_WRITEOBJECTS | \DESKTOP_SWITCHDESKTOP | STANDARD_RIGHTS_REQUIRED) #define WINSTA_ALL (WINSTA_ENUMDESKTOPS | WINSTA_READATTRIBUTES | \ WINSTA_ACCESSCLIPBOARD | WINSTA_CREATEDESKTOP | WINSTA_WRITEATTRIBUTES | \WINSTA_ACCESSGLOBALATOMS | WINSTA_EXITWINDOWS | WINSTA_ENUMERATE | \WINSTA_READSCREEN | STANDARD_RIGHTS_REQUIRED) #define GENERIC_ACCESS (GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | \ GENERIC_ALL)BOOL AddAceToWindowStation(HWINSTA hwinsta, PSID psid);BOOL AddAceToDesktop(HDESK hdesk, PSID psid);BOOL StartInteractiveClientProcess ( LPTSTR lpszUsername, // client to log on LPTSTR lpszDomain, // domain of client's account LPTSTR lpszPassword, // client's password LPTSTR lpCommandLine // command line to execute ) { HANDLE hToken; HDESK hdesk = NULL; HWINSTA hwinsta = NULL, hwinstaSave = NULL; PROCESS_INFORMATION pi; PSID pSid = NULL; STARTUPINFO si; BOOL bResult = FALSE; // Log the client on to the local computer. if ( ! LogonUser( lpszUsername, lpszDomain, lpszPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, & hToken) ) { goto Cleanup; } // Save a handle to the caller's current window station. if ( (hwinstaSave = GetProcessWindowStation() ) == NULL) goto Cleanup; // Get a handle to the interactive window station. hwinsta = OpenWindowStation( " winsta0 " , // the interactive window station FALSE, // handle is not inheritable READ_CONTROL | WRITE_DAC); // rights to read/write the DACL if (hwinsta == NULL) goto Cleanup; // To get the correct default desktop, set the caller's // window station to the interactive window station. if ( ! SetProcessWindowStation(hwinsta)) goto Cleanup; // Get a handle to the interactive desktop. hdesk = OpenDesktop( " default " , // the interactive window station 0 , // no interaction with other desktop processes FALSE, // handle is not inheritable READ_CONTROL | // request the rights to read and write the DACL WRITE_DAC | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS); // Restore the caller's window station. if ( ! SetProcessWindowStation(hwinstaSave)) goto Cleanup; if (hdesk == NULL) goto Cleanup; // Get the SID for the client's logon session. if ( ! GetLogonSID(hToken, & pSid)) goto Cleanup; // Allow logon SID full access to interactive window station. if ( ! AddAceToWindowStation(hwinsta, pSid) ) goto Cleanup; // Allow logon SID full access to interactive desktop. if ( ! AddAceToDesktop(hdesk, pSid) ) goto Cleanup; // Impersonate client to ensure access to executable file. if ( ! ImpersonateLoggedOnUser(hToken) ) goto Cleanup; // Initialize the STARTUPINFO structure. // Specify that the process runs in the interactive desktop. ZeroMemory( & si, sizeof (STARTUPINFO)); si.cb = sizeof (STARTUPINFO); si.lpDesktop = TEXT( " winsta0\\default " ); // Launch the process in the client's logon session. bResult = CreateProcessAsUser( hToken, // client's access token NULL, // file to execute lpCommandLine, // command line NULL, // pointer to process SECURITY_ATTRIBUTES NULL, // pointer to thread SECURITY_ATTRIBUTES FALSE, // handles are not inheritable NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE, // creation flags NULL, // pointer to new environment block NULL, // name of current directory & si, // pointer to STARTUPINFO structure & pi // receives information about new process ); // End impersonation of client. RevertToSelf(); if (bResult && pi.hProcess != INVALID_HANDLE_VALUE) { WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); } if (pi.hThread != INVALID_HANDLE_VALUE) CloseHandle(pi.hThread); Cleanup: if (hwinstaSave != NULL) SetProcessWindowStation (hwinstaSave); // Free the buffer for the logon SID. if (pSid) FreeLogonSID( & pSid); // Close the handles to the interactive window station and desktop. if (hwinsta) CloseWindowStation(hwinsta); if (hdesk) CloseDesktop(hdesk); // Close the handle to the client's access token. if (hToken != INVALID_HANDLE_VALUE) CloseHandle(hToken); return bResult;} BOOL AddAceToWindowStation(HWINSTA hwinsta, PSID psid) { ACCESS_ALLOWED_ACE * pace; ACL_SIZE_INFORMATION aclSizeInfo; BOOL bDaclExist; BOOL bDaclPresent; BOOL bSuccess = FALSE; DWORD dwNewAclSize; DWORD dwSidSize = 0 ; DWORD dwSdSizeNeeded; PACL pacl; PACL pNewAcl; PSECURITY_DESCRIPTOR psd = NULL; PSECURITY_DESCRIPTOR psdNew = NULL; PVOID pTempAce; SECURITY_INFORMATION si = DACL_SECURITY_INFORMATION; unsigned int i; __try { // Obtain the DACL for the window station. if ( ! GetUserObjectSecurity( hwinsta, & si, psd, dwSidSize, & dwSdSizeNeeded) ) if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { psd = (PSECURITY_DESCRIPTOR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, dwSdSizeNeeded); if (psd == NULL) __leave; psdNew = (PSECURITY_DESCRIPTOR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, dwSdSizeNeeded); if (psdNew == NULL) __leave; dwSidSize = dwSdSizeNeeded; if ( ! GetUserObjectSecurity( hwinsta, & si, psd, dwSidSize, & dwSdSizeNeeded) ) __leave; } else __leave; // Create a new DACL. if ( ! InitializeSecurityDescriptor( psdNew, SECURITY_DESCRIPTOR_REVISION) ) __leave; // Get the DACL from the security descriptor. if ( ! GetSecurityDescriptorDacl( psd, & bDaclPresent, & pacl, & bDaclExist) ) __leave; // Initialize the ACL. ZeroMemory( & aclSizeInfo, sizeof (ACL_SIZE_INFORMATION)); aclSizeInfo.AclBytesInUse = sizeof (ACL); // Call only if the DACL is not NULL. if (pacl != NULL) { // get the file ACL size info if ( ! GetAclInformation( pacl, (LPVOID) & aclSizeInfo, sizeof (ACL_SIZE_INFORMATION), AclSizeInformation) ) __leave; } // Compute the size of the new ACL. dwNewAclSize = aclSizeInfo.AclBytesInUse + ( 2 * sizeof (ACCESS_ALLOWED_ACE)) + ( 2 * GetLengthSid(psid)) - ( 2 * sizeof (DWORD)); // Allocate memory for the new ACL. pNewAcl = (PACL)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, dwNewAclSize); if (pNewAcl == NULL) __leave; // Initialize the new DACL. if ( ! InitializeAcl(pNewAcl, dwNewAclSize, ACL_REVISION)) __leave; // If DACL is present, copy it to a new DACL. if (bDaclPresent) { // Copy the ACEs to the new ACL. if (aclSizeInfo.AceCount) { for (i = 0 ; i < aclSizeInfo.AceCount; i ++ ) { // Get an ACE. if ( ! GetAce(pacl, i, & pTempAce)) __leave; // Add the ACE to the new ACL. if ( ! AddAce( pNewAcl, ACL_REVISION, MAXDWORD, pTempAce, ((PACE_HEADER)pTempAce) -> AceSize) ) __leave; } } } // Add the first ACE to the window station. pace = (ACCESS_ALLOWED_ACE * )HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof (ACCESS_ALLOWED_ACE) + GetLengthSid(psid) - sizeof (DWORD)); if (pace == NULL) __leave; pace -> Header.AceType = ACCESS_ALLOWED_ACE_TYPE; pace -> Header.AceFlags = CONTAINER_INHERIT_ACE | INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE; pace -> Header.AceSize = sizeof (ACCESS_ALLOWED_ACE) + GetLengthSid(psid) - sizeof (DWORD); pace -> Mask = GENERIC_ACCESS; if ( ! CopySid(GetLengthSid(psid), & pace -> SidStart, psid)) __leave; if ( ! AddAce( pNewAcl, ACL_REVISION, MAXDWORD, (LPVOID)pace, pace -> Header.AceSize) ) __leave; // Add the second ACE to the window station. pace -> Header.AceFlags = NO_PROPAGATE_INHERIT_ACE; pace -> Mask = WINSTA_ALL; if ( ! AddAce( pNewAcl, ACL_REVISION, MAXDWORD, (LPVOID)pace, pace -> Header.AceSize) ) __leave; // Set a new DACL for the security descriptor. if ( ! SetSecurityDescriptorDacl( psdNew, TRUE, pNewAcl, FALSE) ) __leave; // Set the new security descriptor for the window station. if ( ! SetUserObjectSecurity(hwinsta, & si, psdNew)) __leave; // Indicate success. bSuccess = TRUE; } __finally { // Free the allocated buffers. if (pace != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)pace); if (pNewAcl != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)pNewAcl); if (psd != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)psd); if (psdNew != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)psdNew); } return bSuccess;} BOOL AddAceToDesktop(HDESK hdesk, PSID psid) { ACL_SIZE_INFORMATION aclSizeInfo; BOOL bDaclExist; BOOL bDaclPresent; BOOL bSuccess = FALSE; DWORD dwNewAclSize; DWORD dwSidSize = 0 ; DWORD dwSdSizeNeeded; PACL pacl; PACL pNewAcl; PSECURITY_DESCRIPTOR psd = NULL; PSECURITY_DESCRIPTOR psdNew = NULL; PVOID pTempAce; SECURITY_INFORMATION si = DACL_SECURITY_INFORMATION; unsigned int i; __try { // Obtain the security descriptor for the desktop object. if ( ! GetUserObjectSecurity( hdesk, & si, psd, dwSidSize, & dwSdSizeNeeded)) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { psd = (PSECURITY_DESCRIPTOR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, dwSdSizeNeeded ); if (psd == NULL) __leave; psdNew = (PSECURITY_DESCRIPTOR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, dwSdSizeNeeded); if (psdNew == NULL) __leave; dwSidSize = dwSdSizeNeeded; if ( ! GetUserObjectSecurity( hdesk, & si, psd, dwSidSize, & dwSdSizeNeeded) ) __leave; } else __leave; } // Create a new security descriptor. if ( ! InitializeSecurityDescriptor( psdNew, SECURITY_DESCRIPTOR_REVISION) ) __leave; // Obtain the DACL from the security descriptor. if ( ! GetSecurityDescriptorDacl( psd, & bDaclPresent, & pacl, & bDaclExist) ) __leave; // Initialize. ZeroMemory( & aclSizeInfo, sizeof (ACL_SIZE_INFORMATION)); aclSizeInfo.AclBytesInUse = sizeof (ACL); // Call only if NULL DACL. if (pacl != NULL) { // Determine the size of the ACL information. if ( ! GetAclInformation( pacl, (LPVOID) & aclSizeInfo, sizeof (ACL_SIZE_INFORMATION), AclSizeInformation) ) __leave; } // Compute the size of the new ACL. dwNewAclSize = aclSizeInfo.AclBytesInUse + sizeof (ACCESS_ALLOWED_ACE) + GetLengthSid(psid) - sizeof (DWORD); // Allocate buffer for the new ACL. pNewAcl = (PACL)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, dwNewAclSize); if (pNewAcl == NULL) __leave; // Initialize the new ACL. if ( ! InitializeAcl(pNewAcl, dwNewAclSize, ACL_REVISION)) __leave; // If DACL is present, copy it to a new DACL. if (bDaclPresent) { // Copy the ACEs to the new ACL. if (aclSizeInfo.AceCount) { for (i = 0 ; i < aclSizeInfo.AceCount; i ++ ) { // Get an ACE. if ( ! GetAce(pacl, i, & pTempAce)) __leave; // Add the ACE to the new ACL. if ( ! AddAce( pNewAcl, ACL_REVISION, MAXDWORD, pTempAce, ((PACE_HEADER)pTempAce) -> AceSize) ) __leave; } } } // Add ACE to the DACL. if ( ! AddAccessAllowedAce( pNewAcl, ACL_REVISION, DESKTOP_ALL, psid) ) __leave; // Set new DACL to the new security descriptor. if ( ! SetSecurityDescriptorDacl( psdNew, TRUE, pNewAcl, FALSE) ) __leave; // Set the new security descriptor for the desktop object. if ( ! SetUserObjectSecurity(hdesk, & si, psdNew)) __leave; // Indicate success. bSuccess = TRUE; } __finally { // Free buffers. if (pNewAcl != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)pNewAcl); if (psd != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)psd); if (psdNew != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)psdNew); } return bSuccess;}
A logon security identifier (SID) identifies the logon session associated with an access token. A typical use of a logon SID is in an ACE that allows access for the duration of a client's logon session. For example, a Windows service can use the LogonUser function to start a new logon session. The LogonUser function returns an access token from which the service can extract the logon SID. The service can then use the SID in an ACE that allows the client's logon session to access the interactive window station and desktop. The following example gets the logon SID from an access token. It uses the GetTokenInformation function to fill a TOKEN_GROUPS buffer with an array of the group SIDs from an access token. This array includes the logon SID, which is identified by the SE_GROUP_LOGON_ID attribute. The example function allocates a buffer for the logon SID; it is the caller's responsibility to free the buffer. BOOL GetLogonSID (HANDLE hToken, PSID * ppsid) { BOOL bSuccess = FALSE; DWORD dwIndex; DWORD dwLength = 0 ; PTOKEN_GROUPS ptg = NULL; // Verify the parameter passed in is not NULL. if (NULL == ppsid) goto Cleanup; // Get required buffer size and allocate the TOKEN_GROUPS buffer. if ( ! GetTokenInformation( hToken, // handle to the access token TokenGroups, // get information about the token's groups (LPVOID) ptg, // pointer to TOKEN_GROUPS buffer 0 , // size of buffer & dwLength // receives required buffer size )) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) goto Cleanup; ptg = (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); if (ptg == NULL) goto Cleanup; } // Get the token group information from the access token. if ( ! GetTokenInformation( hToken, // handle to the access token TokenGroups, // get information about the token's groups (LPVOID) ptg, // pointer to TOKEN_GROUPS buffer dwLength, // size of buffer & dwLength // receives required buffer size )) { goto Cleanup; } // Loop through the groups to find the logon SID. for (dwIndex = 0 ; dwIndex < ptg -> GroupCount; dwIndex ++ ) if ((ptg -> Groups[dwIndex].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID) { // Found the logon SID; make a copy of it. dwLength = GetLengthSid(ptg -> Groups[dwIndex].Sid); * ppsid = (PSID) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); if ( * ppsid == NULL) goto Cleanup; if ( ! CopySid(dwLength, * ppsid, ptg -> Groups[dwIndex].Sid)) { HeapFree(GetProcessHeap(), 0 , (LPVOID) * ppsid); goto Cleanup; } break ; } bSuccess = TRUE;Cleanup: // Free the buffer for the token groups. if (ptg != NULL) HeapFree(GetProcessHeap(), 0 , (LPVOID)ptg); return bSuccess;} The following function frees the buffer allocated by the GetLogonSID example function. VOID FreeLogonSID (PSID *ppsid) { HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid);}
可以使用 WinInet 添加 FTP 支持以从应用程序内下载文件和上载文件。可以重写 OnStatusCallback 并使用 dwContext 参数在搜索和下载文件时向用户提供进度信息。 本文包含以下主题: 创建一个非常简单的浏览器 下载 Web 页 FTP 文件 检索 Gopher 目录 传输文件时显示进度信息 以下摘录的代码说明如何创建一个简单的浏览器、下载 Web 页、FTP 文件和搜索 gopher 文件。它们并不代表完整的示例,并且不都包含异常处理功能。 创建一个非常简单的浏览器 #include <afxinet.h> //assumes URL names have been initialized CInternetSession session("My Session"); CStdioFile* pFile = NULL; //use a URL and display a Web page while (lpszURL = DisplayPage()) { pFile = session.OpenURL(lpszURL); while (pFile->Read(szBuff,1024) > 0) { //read file } delete pFile; } session.Close(); 下载 Web 页 //this code excerpt also demonstrates try/catch exception handling #include <afxinet.h> //assumes server, port, and URL names have been initialized CInternetSession session("My Session"); CHttpConnection* pServer = NULL; CHttpFile* pFile = NULL; try { CString strServerName; INTERNET_PORT nPort; pServer = session.GetHttpConnection(strServerName, nPort); pFile = pServer->OpenRequest(CHttpConnection::HTTP_VERB_GET, strObject); pFile->AddRequestHeaders(szHeaders); pFile->SendRequest(); pFile->QueryInfoStatusCode(dwRet); if (dwRet == HTTP_STATUS_OK) { UINT nRead = pFile->Read(szBuff, 1023); while (nRead > 0) { //read file } } delete pFile; delete pServer; } catch (CInternetException* pEx) { //catch errors from WinInet } session.Close(); FTP 文件 #include <afxinet.h> //assumes server and file names have been initialized CInternetSession session("My FTP Session"); CFtpConnection* pConn = NULL; pConn = session.GetFtpConnection(lpszServerName); //get the file if (!pConn->GetFile(pstrRemoteFile, pstrLocalFile)) //display an error delete pConn; session.Close(); 检索 Gopher 目录 #include <afxinet.h> //assumes file name has been initialized CInternetSession session("My Gopher Session"); CGopherConnection* pConn = NULL; CGopherFileFind* pFile; pConn = session.GetGopherConnection("gopher.yoursite.com"); pFile = new CGopherFileFind(pConn); BOOL bFound = pFile->FindFile(lpszFileToFind); while (bFound) { bFound = pFile->FindNextFile(); //retrieve attributes of found file } delete pFile; delete pConn; session.Close(); 使用 OnStatusCallback 使用 WinInet 类时,可以使用应用程序的 CInternetSession 对象的 OnStatusCallback 成员来检索状态信息。如果您派生自己的 CInternetSession 对象、重写 OnStatusCallback 并启用状态回调,MFC 将调用 OnStatusCallback 函数并提供那个 Internet 会话中所有活动的进度信息。 由于单个会话可能会支持若干个连接(这些连接在它们的生存期内可能执行许多不同的独特操作),因此 OnStatusCallback 需要一个机制用特定的连接或事务来标识每个状态更改。该机制由分配给 WinInet 支持类中的许多成员函数的上下文 ID 参数提供。该参数的类型总是 DWORD 并且总是命名为 dwContext。 分配给具体某个 Internet 对象的上下文只用于标识此对象在 CInternetSession 对象的 OnStatusCallback 成员中导致的活动。对 OnStatusCallback 的调用将接收几个参数;这些参数共同工作以通知应用程序哪个事务和连接的进度是多少。 当创建 CInternetSession 对象时,可以指定构造函数的 dwContext 参数。CInternetSession 本身不使用上下文 ID,而是将上下文 ID 传递给 InternetConnection 派生的任何对象,这些对象不显式获得它们自己的上下文 ID。反过来,如果您不显式指定不同的上下文 ID,则那些 CInternetConnection 对象将上下文 ID 继续传递给它们创建的 CInternetFile 对象。另一方面,如果您确实指定了自己的特定上下文 ID,对象和它所做的任何工作将与那个上下文 ID 关联。可以使用上下文 ID 来标识 OnStatusCallback 函数中为您提供的状态信息。 传输文件时显示进度信息 例如,如果编写一个应用程序来创建两个连接,一个连到 FTP 服务器以读取文件,一个连到 HTTP 服务器以获取 Web 页,那么,您将有一个 CInternetSession 对象、两个 CInternetConnection 对象(一个是 CFtpSession,另一个是 CHttpSession)和两个 CInternetFile 对象(分别用于两个连接)。假如对 dwContext 参数使用了默认值,将不能区分指示 FTP 连接进度的 OnStatusCallback 调用和指示 HTTP 连接进度的调用。如果指定以后可在 OnStatusCallback 中测试的 dwContext ID,您将知道是哪个操作生成的回调
一、ADO简介 ADO(ActiveX Data Object)是Microsoft数据库应用程序开发的新接口,是建立在OLE DB之上的高层数据库访问技术,请不必为此担心,即使你对OLE DB,COM不了解也能轻松对付ADO,因为它非常简单易用,甚至比你以往所接触的ODBC API、DAO、RDO都要容易使用,并不失灵活性。本文将详细地介绍在VC下如何使用ADO来进行数据库应用程序开发,并给出示例代码。 本文示例代码 二、基本流程 万事开头难,任何一种新技术对于初学者来说最重要的还是“入门”,掌握其要点。让我们来看看ADO数据库开发的基本流程吧! (1)初始化COM库,引入ADO库定义文件 (2)用Connection对象连接数据库 (3)利用建立好的连接,通过Connection、Command对象执行SQL命令,或利用Recordset对象取得结果记录集进行查询、处理。 (4)使用完毕后关闭连接释放对象。 准备工作: 为了大家都能测试本文提供的例子,我们采用Access数据库,您也可以直接在我们提供的示例代码中找到这个test.mdb。 下面我们将详细介绍上述步骤并给出相关代码。 【1】COM库的初始化 我们可以使用AfxOleInit()来初始化COM库,这项工作通常在CWinApp::InitInstance()的重载函数中完成,请看如下代码: BOOL CADOTest1App::InitInstance() { AfxOleInit(); ...... 【2】用#import指令引入ADO类型库 我们在stdafx.h中加入如下语句:(stdafx.h这个文件哪里可以找到?你可以在FileView中的Header Files里找到) #import "c:\program files\common files\system\ado\msado15.dll" no_namespace rename("EOF","adoEOF") 这一语句有何作用呢?其最终作用同我们熟悉的#include类似,编译的时候系统会为我们生成msado15.tlh,ado15.tli两个C++头文件来定义ADO库。 几点说明: (1) 您的环境中msado15.dll不一定在这个目录下,请按实际情况修改 (2) 在编译的时候肯能会出现如下警告,对此微软在MSDN中作了说明,并建议我们不要理会这个警告。 msado15.tlh(405) : warning C4146: unary minus operator applied to unsigned type, result still unsigned 【3】创建Connection对象并连接数据库 首先我们需要添加一个指向Connection对象的指针: _ConnectionPtr m_pConnection; 下面的代码演示了如何创建Connection对象实例及如何连接数据库并进行异常捕捉。 BOOL CADOTest1Dlg::OnInitDialog() { CDialog::OnInitDialog(); HRESULT hr; try { hr = m_pConnection.CreateInstance("ADODB.Connection");///创建Connection对象 if(SUCCEEDED(hr)) { hr = m_pConnection->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=test.mdb","","",adModeUnknown); ///连接数据库 ///上面一句中连接字串中的Provider是针对ACCESS2000环境的,对于ACCESS97,需要改为:Provider=Microsoft.Jet.OLEDB.3.51; } } catch(_com_error e)///捕捉异常 { CString errormessage; errormessage.Format("连接数据库失败!\r\n错误信息:%s",e.ErrorMessage()); AfxMessageBox(errormessage);///显示错误信息 } 在这段代码中我们是通过Connection对象的Open方法来进行连接数据库的,下面是该方法的原型 HRESULT Connection15:: Open (_bstr_t ConnectionString, _bstr_t UserID, _bstr_t Password, long Options ) ConnectionString为连接字串,UserID是用户名, Password是登陆密码,Options是连接选项,用于指定Connection对象对数据的更新许可权, Options可以是如下几个常量: adModeUnknown:缺省。当前的许可权未设置 adModeRead:只读 adModeWrite:只写 adModeReadWrite:可以读写 adModeShareDenyRead:阻止其它Connection对象以读权限打开连接 adModeShareDenyWrite:阻止其它Connection对象以写权限打开连接 adModeShareExclusive:阻止其它Connection对象打开连接 adModeShareDenyNone:允许其它程序或对象以任何权限建立连接 我们给出一些常用的连接方式供大家参考: (1)通过JET数据库引擎对ACCESS2000数据库的连接 m_pConnection->Open("Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\\test.mdb","","",adModeUnknown); (2)通过DSN数据源对任何支持ODBC的数据库进行连接: m_pConnection->Open("Data Source=adotest;UID=sa;PWD=;","","",adModeUnknown); (3)不通过DSN对SQL SERVER数据库进行连接: m_pConnection-> Open( "driver={SQLServer};Server=127.0.0.1;DATABASE=vckbase;UID=sa;PWD=139","","",adModeUnknown ); 其中Server是SQL服务器的名称,DATABASE是库的名称 Connection对象除Open方法外还有许多方法,我们先介绍Connection对象中两个有用的属性ConnectionTimeOut与State ConnectionTimeOut用来设置连接的超时时间,需要在Open之前调用,例如: m_pConnection->ConnectionTimeout = 5;///设置超时时间为5秒 m_pConnection->Open("Data Source=adotest;","","",adModeUnknown); State属性指明当前Connection对象的状态,0表示关闭,1表示已经打开,我们可以通过读取这个属性来作相应的处理,例如: if(m_pConnection->State) m_pConnection->Close(); ///如果已经打开了连接则关闭它 【4】执行SQL命令并取得结果记录集 为了取得结果记录集,我们定义一个指向Recordset对象的指针:_RecordsetPtr m_pRecordset; 并为其创建Recordset对象的实例: m_pRecordset.CreateInstance("ADODB.Recordset"); SQL命令的执行可以采用多种形式,下面我们一进行阐述。 (1)利用Connection对象的Execute方法执行SQL命令 Execute方法的原型如下所示: _RecordsetPtr Connection15:: Execute ( _bstr_t CommandText, VARIANT * RecordsAffected, long Options ) 其中CommandText是命令字串,通常是SQL命令。参数RecordsAffected是操作完成后所影响的行数, 参数Options表示CommandText中内容的类型,Options可以取如下值之一: adCmdText:表明CommandText是文本命令 adCmdTable:表明CommandText是一个表名 adCmdProc:表明CommandText是一个存储过程 adCmdUnknown:未知 Execute执行完后返回一个指向记录集的指针,下面我们给出具体代码并作说明。 _variant_t RecordsAffected; ///执行SQL命令:CREATE TABLE创建表格users,users包含四个字段:整形ID,字符串username,整形old,日期型birthday m_pConnection-> Execute("CREATE TABLE users(ID INTEGER,username TEXT,old INTEGER,birthday DATETIME)",&RecordsAffected,adCmdText); ///往表格里面添加记录 m_pConnection-> Execute("INSERT INTO users(ID,username,old,birthday) VALUES (1, 'Washington',25,'1970/1/1')",&RecordsAffected,adCmdText); ///将所有记录old字段的值加一 m_pConnection-> Execute("UPDATE users SET old = old+1",&RecordsAffected,adCmdText); ///执行SQL统计命令得到包含记录条数的记录集 m_pRecordset = m_pConnection-> Execute("SELECT COUNT(*) FROM users",&RecordsAffected,adCmdText); _variant_t vIndex = (long)0; _variant_t vCount = _pRecordset-> GetCollect(vIndex); ///取得第一个字段的值放入vCount变量 m_pRecordset->Close(); ///关闭记录集 CString message; message.Format("共有%d条记录",vCount.lVal); AfxMessageBox(message); ///显示当前记录条数 (2)利用Command对象来执行SQL命令 _CommandPtr m_pCommand; m_pCommand.CreateInstance("ADODB.Command"); _variant_t vNULL; vNULL.vt = VT_ERROR; vNULL.scode = DISP_E_PARAMNOTFOUND; ///定义为无参数 m_pCommand->ActiveConnection = m_pConnection; ///非常关键的一句,将建立的连接赋值给它 m_pCommand->CommandText = "SELECT * FROM users"; ///命令字串 m_pRecordset = m_pCommand->Execute(&vNULL,&vNULL,adCmdText); ///执行命令,取得记录集 在这段代码中我们只是用Command对象来执行了SELECT查询语句,Command对象在进行存储过程的调用中能真正体现它的作用。下次我们将详细介绍。 如果是使用智能指针_RecordsetPtr定义的记录集,那么可以使用 RecordCount 方法得到记录集记录数。
在第一阶段的检测(BroadPhase)中所需要的算法就是Sweep and Prune,因为从未接触过此类的东西,所以不知道到底是个什么东西,今天终于找到具体资料了,一看,晕倒掉了.原来就是<游戏编程精粹2>里面所提及到的 逐维递归分组法...貌似如果有人搜索相关词汇是能够搜索到我的blog的,特别留下此文以防止有哥们走我同样的弯路了...顺便放一个英文东西:来自于:http://parallel.vub.ac.be/documentation/pvm/Example/Marc_Ramaekers/node3.htmlSweep and PruneGiven a number N of objects, O(N2) object pairs have to be checked for collision. In general, the objects in most of the pairs aren't even close to each other so we should be able to eliminate them quickly. To do this we use a technique called Sweep and Prune ([CLMP95]). In this section I will briefly introduce this technique. To determine whether two objects are close enough to potentially collide, the Sweep and Prune checks whether the axis aligned bounding boxes of the respective objects overlap. If they do, further investigation is necessary. If not, the objects can't possibly collide and the algorithm can move on. To determine whether two bounding boxes overlap, the algorithm reduces the 3D problem to three simpler 1D problems. It does so by determining the intervals occupied by the bounding volume along each of the x,y and z axes. If and only if the intervals of two bounding volumes overlap in all of the three dimensions, the objects corresponding to these bounding volumes must overlap. To determine which intervals of the objects along an axis overlap, the list of the intervals is sorted. Normally, using quick-sort, this would be an process. However, by exploiting frame coherence (the similarity between situations in two subsequent frames) we can sort the lists in an expected (O(N), using insertion sort. Another difficult part in the Sweep and Prune approach is the maintenance of the bounding volume. If the objects in the scene move or rotate, the previously calculated bounding boxes are invalid. It is important to be able to update the boxes as quickly as possible. Again, we can do this by exploiting frame coherence. The algorithm's performance is of course dependent on the application and the typical situations that occur in that application. Many variations exists, such as reducing the overlap problem by only 1 dimension and using a rectangle intersection test. It is also possible to choose other types of bounding volumes that might be faster to update but produce a less accurate approximation of the object.
I did a quick look through your most recent code and was kind of shocked to find out you still didn’t have a more efficient Broad Phase. So I decided to write one for your engine during my break. I tried to make it simple instead of optimized for ease of understanding. public class SweepAndPrune{ delegate void CollisionCallback(Wrapper w1, Wrapper w2); class Wrapper { public Node xBegin; public Node xEnd; public Node yBegin; public Node yEnd; public Geometry geometry; public List<Geometry> colliders; public Wrapper(Geometry geometry) { this.geometry = geometry; this.colliders = new List<Geometry>(); this.xBegin = new Node(this, true); this.xEnd = new Node(this, false); this.yBegin = new Node(this, true); this.yEnd = new Node(this, false); } public void Update() { colliders.Clear(); xBegin.value = geometry.AABB.Min.X; xEnd.value = geometry.AABB.Max.X; yBegin.value = geometry.AABB.Min.Y; yEnd.value = geometry.AABB.Max.Y; } } class Node { public bool begin; public float value; public Wrapper wrapper; public Node(Wrapper wrapper, bool begin) { this.wrapper = wrapper; this.begin = begin; } } List<Wrapper> wrappers = new List<Wrapper>(); List<Node> xList = new List<Node>(); List<Node> yList = new List<Node>(); public void AddGeometry(Geometry item) { Wrapper wrapper = new Wrapper(item); wrappers.Add(wrapper); xList.Add(wrapper.xBegin); xList.Add(wrapper.xEnd); yList.Add(wrapper.yBegin); yList.Add(wrapper.yEnd); } public void RemoveDisposed() { if (wrappers.RemoveAll(delegate(Wrapper w) { return w.geometry.Body.IsDisposed; }) > 0) { xList.RemoveAll(delegate(Node n) { return n.wrapper.geometry.Body.IsDisposed; }); yList.RemoveAll(delegate(Node n) { return n.wrapper.geometry.Body.IsDisposed; }); } } public void Run() { Update(); RunAxis(xList, HandleFirstCollision); RunAxis(yList, HandleSecondCollision); } /// <summary> /// Updates the nodes and sorts them. /// </summary> void Update() { foreach (Wrapper wrapper in wrappers) { wrapper.Update(); } xList.Sort(delegate(Node l, Node r) { return l.value.CompareTo(r.value); }); yList.Sort(delegate(Node l, Node r) { return l.value.CompareTo(r.value); }); } /// <summary> /// Runs the collision detection on a axis /// </summary> void RunAxis(List<Node> list, CollisionCallback callback) { LinkedList<Wrapper> proximityList = new LinkedList<Wrapper>(); foreach (Node node in list) { if (node.begin) { foreach (Wrapper wrapper in proximityList) { callback(node.wrapper, wrapper); } proximityList.AddLast(node.wrapper); } else { proximityList.Remove(node.wrapper); } } } /// <summary> /// when there is a collsion along the first axis /// and if there is no early fail conditions then they are /// added to each others colliders list /// </summary> void HandleFirstCollision(Wrapper w1, Wrapper w2) { //if(early fail conditions) {return;} w1.colliders.Add(w2.geometry); w2.colliders.Add(w1.geometry); } /// <summary> /// when there is a collision along the second axis then /// it checks to see if there was a collision along the first. /// if there is then the 2 geometries bounding boxes are colliding. /// </summary> void HandleSecondCollision(Wrapper w1, Wrapper w2) { if (w1.colliders.Contains(w2.geometry)) { //this is a confirmed broadphase collision //so add a new arbiter or something for //w1.geometry and w2.geometry } }}
Here is an implementation of a list class. Lists are another way to store data. Lists have very fast inserts and deletes however iterating thru the elements in the list is not as fast as iterating thru a data vector. template class ZList { public: class ListNode; private: DWORD m_dwSize; bool bValid; ZVector m_Offsets; public: T AllocItem() { T ret; ret.Initialize(); push_back(ret); return ret; } DWORD GetSize(){ return size(); } DWORD size() { return m_dwSize; } inline bool IsEmpty(){ return m_pHead==NULL;} class ListNode { friend class ZList; public: T m_Data; ListNode* m_pNext; ListNode* m_pPrev; public: inline operator T&() { return m_Data; } ListNode(T pData) : m_pNext(0), m_pPrev(0) { m_Data = pData; } ListNode() : m_pNext(0), m_pPrev(0){} }; class Iterator { ListNode* m_pCurrent; bool m_bFirst; public: Iterator(ListNode* pBegin) : m_pCurrent(pBegin), m_bFirst(true) {} operator T&(){ return m_pCurrent->m_Data;} ListNode* Next() { if(m_bFirst) { m_bFirst = false; return m_pCurrent; &nb%
2023年02月
2022年11月
2022年10月
2022年06月
2022年04月
2022年03月