《第四章.数据编码与演化》 作者: nbboy 时间: 2022-05-17 分类: 软件架构,软件工程,设计模式,Golang 评论 ### 数据编码用处 数据编码主要被用在数据传输和存储上,至于数据在内存中是不需要特定的数据编码的,用某种结构就可以表示(比如数组,链表,数,图等等),平时接触最多的可能就从数据库反射回来的数据模型对象。 ### 数据编码困境 数据编码即可以是通用的数据编码也可以是语言专用的数据编码,比如大家都知道的XML,JSON,PB是通用的数据编码,任何的语言基本都可以编码和解码。对于专有数据编码,比如Python Pickle文件和Java序列化等都属于这类,一般被用来存储的用途。 但是无论对于哪种数据编码,主要需要解决的问题是: - 保持双向兼容(前向和后向) - 快速的编码和解码 - 数据是否紧凑 - 最好对人类友好,即便于调试 以比较熟悉的JSON和PB进行描述和对比,JSON并没有保持双向兼容的能力,需要程序去自行处理。从性能上来说,相对于其他二进制编码,它无疑编解码也是比较慢的,而且因为是文本格式,所以数据也不具有太好的压缩特性。但是因此带来的好处是方便我们去调试,比如可以很方面的截获一段HTTP报文,我们完全不借助工具就可以阅读他的内容,这是其他二进制编码格式做不到的。所以正是这些因素,外网常用的接口都以Restful风格+JSON编码去传输。但是,PB正好相反,它可以预定义模式协议文件(pb文件),而且支持可选和必选,这样就为双向兼容提供了方便。PB编解码性能比JSON快了非常多,另外数据更加紧凑,不过由于以上特性,如果不借助工具,不方便调试。 以上描述的两种编码格式比较常见,也比较有代表性。不存在最好的和具备所有优点的编解码方式,需要我们根据具体场景去选择,比如目前比较好的实践方式是,走外网的接口通常用JSON去传输,内网的RPC服务用的更多的还是PB格式。 ### 数据流模式 在Web应用开发中,一般和数据库,消息队列,调用第三方服务打交道,书中总结抽象为数据流。 - 数据库也可以看成编解码的过程,把SQL的记录行组装成模型对象(不是领域对象)称为解码,把模型对象拼装成SQL语句执行的过程称为编码。消息队列和服务调用过程中更加少不了编解码。 - RPC和Restful在交互过程中对内容进行编解码 - 异步消息在投递到消息代理过程中会进行编解码 ### 总结 数据在存储和传输过程中需要进行编解码,在一些特殊的场景会自己定义一套编解码方式,目标是更好的性能和更具经凑的格式。当然当然在Web开发中,用的最多的还是JSON和PB,以后肯定会有更多的方案出现,但是都应该围绕上面提到的几个目标去设计。
TimeWheel一种实现方式 作者: nbboy 时间: 2022-02-23 分类: 默认分类,软件架构,Golang 评论 > 时间轮是一种应用于定时器的有效数据结构,只需要一个系统时钟定时器就搞定非常多的定时策略,常常应用于**延时任务**等场景 现在实现上主要有两种时间轮,Hash时间轮和分层时间轮,Hash时间轮又有可以实现为排序和非排序的,但是主要原理基本一致。 ### Hash时间轮 hash时间轮可以用hash加上双向链表来实现,当然双向链表可以替换成其他类似的数据结构,比如数组,有序列表等等。其大致结构如下图所示: 每个系统时钟周期内都走动一个hash槽,在这个槽内task都是以双链表形式构建,并且每个task中有代表第几轮的circle和代表在第几个hash槽中的slot。如果circle是0,那就意味着当前可以调度,反之就该把circle减去1。具体看代码可能比较清楚。 ```go for e := slot.Front(); e != nil; e = e.Next() { tasker := e.Value.(*task) if tasker.circle > 0 { tasker.circle -= 1 continue } else { tw.removeTime(&removingTasker{key: tasker.key}) go func() { defer func() { if err := recover(); err != nil { log.Printf("tasker error: %v\n", err) } }() tasker.fn() }() } } ``` ### 分层时间轮 分层时间轮是hash时间轮的一种优化,在原来论文里写得也比较清楚,假如说定时时间跨度很大,则采用hash时间轮就不是很理想。分层时间轮基于相当于十进制中的进位理念一样,或者类似现实中的时钟工作机制。结构如下图所示。 hierarchical-timing-wheel.pn ###### 设置定时时间 先计算出,给定的定时时间需要用几个轮子来表示,当前轮子不能表示就再加入一个。 ###### 周期调度 每个系统时钟周期都是调度低位的轮子,走动一个时间槽,当走完当前轮的所有时间槽,则在比其高一级的轮子中走动一个时间槽。走动的时间槽里包含设定的定时时间,则把该时间移动到下一级轮子中,其位置很容易计算,和最开始设定定时时间的计算方式是一样的。当然,如果当前级别的轮子是最后一级,则需要去调度了。 ### 总结 实现上,可以采用很多的实现方式,可以参考kafka,libevent,nginx等等源码都有实现,看具体需求做trade-off,我实现了简单的hash时间轮体验了一把,它存放在:https://github.com/x-debug/tw
重读《Unix编程艺术》笔记 作者: nbboy 时间: 2021-10-01 分类: 软件架构,软件工程,设计模式,C,Golang 1 条评论 第一次记不清什么时候读的了,大概是在刚毕业那会,也没读懂多少,但是印象中只记得一句话:**一种是设计的极为简洁,以至于无法看到明显的缺陷;另外一种是设计的极为复杂,以至于有缺陷也看不出来。**作者引用这句的意思是尽量把软件设计得简洁,这样才能尽量减少BUG。 作者水平非常牛,可以看下参考资料里的链接,作者写过大量的开源软件,所以他说的有一定的参考意义。对书中我比较感兴趣的是设计原则部分和软件复杂度部分,就针对这两块记录下我的总结和理解。 ### Unix软件设计原则 1. 简洁原则:其实就是我们常说的KISS原则(Keep It Simple, Stupid) 2. 模块原则,组合原则:作者提倡用清晰的接口+简单的模块组合成复杂的软件 3. 清晰原则:有时候,尽量写一些很轻松就可以阅读的代码,而不是过度优化的代码,即使性能比前者好 4. 分离原则:容易变的和不容易变的需要分离 5. 透明原则:给模块或者程序预留测试,统计,监控接口,一切都在掌控之中。 6. 表示原则:尽量用数据结构去驱动,而不是逻辑 7. 通俗原则,默认原则:现在流行的说法就是约定优于配置,Java的SpringBoot就是把这种思想推到了极致,让新手入门比较简单 8. 补救原则:常说的Fail-Fast思想,作者还说了我们应该遵循宽容地收,谨慎地发的思想,这个想法不单单用在网络程序上,我们平时写方法也可以把它作为准则之一。 9. 经济原则:类似清晰原则,不要去写晦涩的优化代码,除非在性能严格场景,或者可以封装复杂度 10. 生成原则:尽量利用自动代码生成来代替手工代码 11. 优化原则:先设计一个简单的可以实现的原型,然后再去优化代码性能 12. 多样原则:更加包容的心态去接受新事物 13. 扩展原则:为将来可能的扩展预留接口 ### 好的程序特征 - **首先应该为程序选择恰当的数据结构,其次是算法,因为数据结构比算法更有表现力。** - 不需要过早的优化。 - 数据结构和算法应该尽量简单,拿不准就用枚举法。 - 紧凑性:减少冗余设计,可靠精简的核心算法,可以避免出现一些边界情况。 - 正交性:只做一件事情,减少副作用。 - SPOT原则:更具体了紧凑原则,对于数据模型尽量要简化,抽象/模型尽量和现实/需求一一对应。 - 分层原则:好的软件是分层设计的,编程习惯上一般可以分为自顶向下和自底向上方式。 - **衡量是否一个好程序,不在无可增加,而在无可删减** - 一个模块不应该暴露过多的API/全局变量,也需要控制函数的体积 - 透明原则:添加过度的抽象层/过度保护底层细节会让实现变得不透明,保持简单是实行透明性的一个方法 ### 复杂度来源 全书说得最多的是简洁性,那对于软件复杂性的来源可以归结为3类: - 接口(可以理解为用户界面)的复杂性 - 实现(代码的实现细节)的复杂性 - 代码行数带来的复杂性 ### 复杂度种类 作者主要认为复杂度主要分为3种,即**偶然复杂度,选择复杂度,本质复杂度**:  偶然复杂度也是最好控制的,属于代码层面的复杂度,利用重构能一定程度减轻偶然复杂度。选择性复杂度主要产生于需要做的功能,比如对于财务报表功能和对于飞机自动化控制程序的功能显然是完全不是一个等级的。本质性复杂度主要根据开发工具,选取的核心数据结构,和真正的功能本身逻辑复杂度都可能影响到。 那这些复杂度怎么破?作者也只是给出了一些指导性建议: - 对于偶然性复杂度,通过设计更加简单的程序(其实之前讲的一些原则都是可以用来消除本类复杂度的)能够带来一些帮助。 - 对于选择性复杂度,通过选择要做哪些功能来权衡功能和复杂度之间的利弊。 - 对于本质性复杂度,只有对要解决的程序问题进行深入思考,设计合适的数据结构才能降低此类复杂度。 ### 总结 编程属于实践性活动,作者的大量经验教训也是在爬了很多坑才总结出来的,即使到现在也在提交代码。所以以上的编程原则,以及对复杂度的讨论,都需要笔者和读者去实践中应用和领悟才行。 ### 参考 [作者简介](https://zh.wikipedia.org/wiki/%E5%9F%83%E9%87%8C%E5%85%8B%C2%B7%E9%9B%B7%E8%92%99 "作者简介") [知乎上的评价](https://zhuanlan.zhihu.com/p/25040637 "知乎上的评价") [作者维护的软件](http://www.catb.org/~esr/software.html "作者维护的软件")
Golang HttpClient调用过程浅析 作者: nbboy 时间: 2021-09-26 分类: 默认分类,软件架构,软件工程,设计模式,Golang 评论 Golang Client的实现高低版本的实现方式上存在一些有意思的差异,本文主要分析1.0版本和1.2版本,因为1.2版本之后的版本和1.2版本模型上基本是一致的。 先看下1.0版本的调用流程图:  get请求最后会调用核心的RoundTrip,其完成实际的httpclient链接池请求。 ```go func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) ``` 在继续分析剩下的流程之前,先看下主要的Transport结构: ```go type Transport struct { lk sync.Mutex idleConn map[string][]*persistConn ... } // persistConn wraps a connection, usually a persistent one // (but may be used for non-keep-alive requests as well) type persistConn struct { t *Transport cacheKey string // its connectMethod.String() conn net.Conn br *bufio.Reader // from conn bw *bufio.Writer // to conn reqch chan requestAndChan // written by roundTrip(); read by readLoop() ... broken bool // an error has happened on this connection; marked broken so it's not reused. } ``` 链接池通过map结构维护,而key的计算方式是: ```go func (ck *connectMethod) String() string { proxyStr := "" if ck.proxyURL != nil { proxyStr = ck.proxyURL.String() } return strings.Join([]string{proxyStr, ck.targetScheme, ck.targetAddr}, "|") } ``` 也就是同样的url是一组,共用一组链接池。 再把视线切回到导图里来,在这个版本中,会用getConn获取到当前链接的一个persistConn(如果是有空闲的persistConn就直接获取,否则就新创建一个persistConn),然后会调用persistConn的roundTrip方法,而其实际是往channel中写入请求,请求服务是在persistConn.readLoop中去做的。这时候的模型其实是这样的:  **一个persistConn有一个goroutine去等待着获取请求,然后向服务发起这个请求,并且把响应通过chan回送到调用方**。可以说**这个过程是异步的**,充分利用了golang的goroutine特性。 到了1.2版本,调用流程变得复杂一些了,还是先看导图:  前面调用过程是一样的,主要区别还是从原来的一个persistConn一个worker变成了**一个persistConn两个worker,这两个worker一个用来写request,一个用来读response**。这样效率肯定会高一点,它的工作模型变成如下图所示:  从1.2之后,模型大体上没有变化,只是对细节上做了一些更进一步的优化和修复问题。 #### 总结 第一个点,从一个client库中可以看出golang基础库的高效,也充分利用了golang的特性-goroutine。第二点,从后面版本开始,读写其实是分离的,这个点我们可以借鉴到我们的设计中去,特别是网络程序。 #### 附件 文章写的很匆忙,细节我总结在导图中了,有兴趣的可以下载观看。 [httpclient导图](http://chenxf.org/files/http_client.emmx "httpclient导图")
OAuth2协议学习 作者: nbboy 时间: 2021-06-12 分类: Golang 评论 > OAuth2专注为Web应用程序,桌面程序,移动设备,或者其他设备,提供认证流程,是一种第三方认证的业界标准。 ### OAuth2简介 OAuth2在平时接入第三方API经常用到,比如Github用户授权获取仓库信息,Google API用户授权获取其资源,一般在接入第三方开放平台时,都会涉及到OAuth2的开发。不过接入第三方平台时我们不需要了解Server侧的实现原理,只需要专注Client端的请求参数和响应参数。本着知识应该完整性的想法,总结了OAuth2的一些流程和实现原理,并使用Golang搭建一个简单的OAuth2 Auth Server,配合Resource Server进行用户授权信息的限制访问需求。 ### OAuth2支持的4个流程 OAuth2支持4个流程,分别是Authorization Code,Implicit,Resource Owner Password Credentials,Client Credentials,其分别对应Web应用,SPA应用,电子设备,移动端几个常用场景。我比较关注的是Authorization Code模式,所以只对这种模式进行说明,其他模式也雷同,具体可以看RFC文档。 ###### Authorization Code 这种模式适合用在Web网站上,在用户侧有一个游览器,RFC的图非常好,借用来说明下: ```c +----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token) ``` Resource Owner就是用户,User Agent其实就是游览器,Client就是后端的程序。 A)第一步,用户发起OAuth请求之后,前台页面会重定向到Auth Server,如果用户没有登录过,则显示登录Auth Server的界面,然后显示用户授权的信息和确认按钮。 B)用户看到授权信息后,授权Client获取Auth Server的资源 C)Auth Server授权成功后,就会带着AuthCode重定向到A步骤设置的页面地址 D)携带AuthCode和回调地址参数,向Auth Server发起获取Access Token请求 E)Auth Server认证后,重定向到D设置的回调函数页面,并且携带AccessToken 经过这几步后,AccessToken已经获取,后面资源的获取都是通过携带AccessToken去通过认证的,也可以给AccessToken设置有效时间。当时间过期,可以通过RefreshToken去再次获取AccessToken,而RefreshToken就是在E步骤同时返回的。 ### Authorization Code 3大组件 ###### Client Manager 客户端的管理,比如登记,注销,在配置Client的时候需要提供Identifier和Secret信息,在请求Auth Server时,也需要把Client Identifier发给它。如果对于需要接入很多Client的Auth Server则需要一个统一地方管理这些Client信息,这就是Client Manager的职责。 ###### Token Manager 这个组件值得是Token的生命周期管理和存储管理,比如Token多少时间失效?或者Token存储在Redis还是Mysql? ###### Scope Auth Server生成的Token可以有Scope,它的意义都是由Auth Server定义,其实可以理解为一种权限,比如如果获得的Access Token只有Access 权限,则不能对资源进行写入,需要Write权限才可以。 ### 搭建OAuth2 Auth Server 我这里使用的是社区提供的[OAuth2](https://github.com/go-oauth2/oauth2),这个库对Client Manager和Token Manager做了抽象,使用也非常简单。可以看其提供的examples,但是这个库文档不是很全面,可以说没有文档。根据RFC说的流程,顺着examples来看下关键流程。 ```go var ( config = oauth2.Config{ ClientID: "888888", ClientSecret: "666666", Scopes: []string{"all"}, RedirectURL: "http://localhost:8888/oauth2", Endpoint: oauth2.Endpoint{ AuthURL: "http://localhost:9999/authorize", TokenURL: "http://localhost:9999/token", }, } ) //Step(A):redirect to authorize url func homePage(w http.ResponseWriter, r *http.Request) { log.Println("Step(A): redirect handle") u := config.AuthCodeURL("xyz") log.Println(u) http.Redirect(w, r, u, http.StatusFound) } //Step(C): get access token from auth server func authorize(w http.ResponseWriter, r *http.Request) { log.Println("Step(C): get access token from auth server") r.ParseForm() state := r.Form.Get("state") if state != "xyz" { http.Error(w, "State invalid", http.StatusBadRequest) return } code := r.Form.Get("code") if code == "" { http.Error(w, "Code not found", http.StatusBadRequest) return } token, err := config.Exchange(context.Background(), code) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } e := json.NewEncoder(w) e.SetIndent("", " ") e.Encode(*token) } ``` 设置了oauth2一些配置,比如客户端ID和密钥,scope,回调URL等等信息。其中authorize是回调执行函数,homePage页面函数直接跳转到Auth Server。 Auth Server最重要的方法是如下两个,路径/authorize用来生成authorize code,并且跳转到Step(C),而路径/token用来生成token,它是又客户端调用config.Exchange方法发起访问的。在例子中,我都打印出了跳转的方法名,具体可以跟踪一下,跳来跳去不是太明显,需要根据RFC那张图来理解。 ```go //HandleAuthorizeRequest //Step(B): generate authorize code, and redirect to client's callback url http.HandleFunc("/authorize", func(resp http.ResponseWriter, req *http.Request) { log.Println("Step(B): authorize handle") s, _ := sessionStore.Get(req, "LoginUser") if _, ok := s.Values["LoginId"]; !ok { if req.Form == nil { req.ParseForm() } log.Println("QueryString :", req.Form) encodedForm, _ := json.Marshal(req.Form) s.Values["ReturnUri"] = encodedForm err := s.Save(req, resp) if err != nil { log.Printf("sessionStore save error: %v", err) } resp.Header().Set("Location", "/login") resp.WriteHeader(http.StatusFound) return } if form, ok := s.Values["ReturnUri"]; ok { json.Unmarshal(form.([]byte), &req.Form) log.Println("Get Form String:", req.Form) } err := srv.HandleAuthorizeRequest(resp, req) if err != nil { log.Println("authHandler error") http.Error(resp, err.Error(), http.StatusBadRequest) } }) //HandleTokenRequest //Step(D): auth server generate token by auth code, and redirect to client's callback url http.HandleFunc("/token", func(resp http.ResponseWriter, req *http.Request) { log.Println("Step(D): auth server generate access token") err := srv.HandleTokenRequest(resp, req) if err != nil { http.Error(resp, err.Error(), http.StatusInternalServerError) } }) ``` ### 总结 OAuth2是一个获取第三方资源的认证协议,他解决了想共享资源给第三方,但是又不想造成安全问题的两个问题。这篇文章只了解了Authorization Code这种模式,在这种模式下,需要客户端获取AuthorizeCode后,再去换取AccessToken。而且在获取AccessToken后,可以给其设置一个有效时间,在失效后,需要根据RefreshToken来获取新的AccessToken。 ### 参考文档 [Examples](https://github.com/x-debug/go-examples/tree/master/oauth2-example) [OAuth2 RFC](https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1) [OAuth2的简单介绍,可以作为RFC的补充,里面的图不错](https://itnext.io/an-oauth-2-0-introduction-for-beginners-6e386b19f7a9) [OAuth2一些资源](https://oauth.net/2/) [Golang Client Implement](https://github.com/golang/oauth2)