Skip to content

Commit

Permalink
fix 5.3
Browse files Browse the repository at this point in the history
  • Loading branch information
cch123 committed Dec 19, 2018
1 parent 188c1b4 commit 2e22cc5
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 28 deletions.
8 changes: 4 additions & 4 deletions ch5-web/ch5-03-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ func main() {
}
```

这是一个典型的 Web 服务,挂载了一个简单的路由。我们的线上服务一般也是从这样简单的服务开始逐渐拓展开去的。
这是一个典型的Web服务,挂载了一个简单的路由。我们的线上服务一般也是从这样简单的服务开始逐渐拓展开去的。

现在突然来了一个新的需求,我们想要统计之前写的 hello 服务的处理耗时,需求很简单,我们对上面的程序进行少量修改:
现在突然来了一个新的需求,我们想要统计之前写的hello服务的处理耗时,需求很简单,我们对上面的程序进行少量修改:

```go
// middleware/hello_with_time_elapse.go
Expand All @@ -37,9 +37,9 @@ func hello(wr http.ResponseWriter, r *http.Request) {
}
```

这样便可以在每次接收到 http 请求时,打印出当前请求所消耗的时间。
这样便可以在每次接收到http请求时,打印出当前请求所消耗的时间。

完成了这个需求之后,我们继续进行业务开发,提供的 api 逐渐增加,现在我们的路由看起来是这个样子:
完成了这个需求之后,我们继续进行业务开发,提供的API逐渐增加,现在我们的路由看起来是这个样子:

```go
// middleware/hello_with_more_routes.go
Expand Down
40 changes: 20 additions & 20 deletions ch5-web/ch5-07-layout-of-web-project.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# 5.7 layout 常见大型 Web 项目分层

流行的 Web 框架大多数是 MVC 框架,MVC 这个概念最早由 Trygve Reenskaug 在 1978 年提出,为了能够对 GUI 类型的应用进行方便扩展,将程序划分为:
流行的Web框架大多数是MVC框架,MVC这个概念最早由Trygve Reenskaug在1978年提出,为了能够对GUI类型的应用进行方便扩展,将程序划分为:

1. 控制器(Controller)- 负责转发请求,对请求进行处理。
2. 视图(View) - 界面设计人员进行图形界面设计。
3. 模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。

随着时代的发展,前端也变成了越来越复杂的工程,为了更好地工程化,现在更为流行的一般是前后分离的架构。可以认为前后分离是把 V 层从 MVC 中抽离单独成为项目。这样一个后端项目一般就只剩下 M 和 C 层了。前后端之间通过 ajax 来交互,有时候要解决跨域的问题,但也已经有了较为成熟的方案。下面是一个前后分离的系统的简易交互图。
随着时代的发展,前端也变成了越来越复杂的工程,为了更好地工程化,现在更为流行的一般是前后分离的架构。可以认为前后分离是把V层从MVC中抽离单独成为项目。这样一个后端项目一般就只剩下 M和C层了。前后端之间通过ajax来交互,有时候要解决跨域的问题,但也已经有了较为成熟的方案。下面是一个前后分离的系统的简易交互图。

![前后分离](../images/ch6-08-frontend-backend.png)

图里的 vue 和 react 是现在前端界比较流行的两个框架,因为我们的重点不在这里,所以前端项目内的组织我们就不强调了。事实上,即使是简单的项目,业界也并没有完全遵守 MVC 框架提出者对于 M 和 C 所定义的分工。有很多公司的项目会在 controller 层塞入大量的逻辑,在 model 层就只管理数据的存储。这往往来源于对于 model 层字面含义的某种擅自引申理解。认为字面意思,这一层就是处理某种建模,而模型是什么?就是数据呗!
图里的Vue和React是现在前端界比较流行的两个框架,因为我们的重点不在这里,所以前端项目内的组织我们就不强调了。事实上,即使是简单的项目,业界也并没有完全遵守MVC框架提出者对于M和C所定义的分工。有很多公司的项目会在controller层塞入大量的逻辑,在model层就只管理数据的存储。这往往来源于对于model层字面含义的某种擅自引申理解。认为字面意思,这一层就是处理某种建模,而模型是什么?就是数据呗!

这种理解显然是有问题的,业务流程也算是一种“模型”,是对真实世界用户行为或者既有流程的一种建模,并非只有按格式组织的数据才能叫模型。不过按照 MVC 的创始人的想法,我们如果把和数据打交道的代码还有业务流程全部塞进 MVC 里的 M 层的话,这个 M 层又会显得有些过于臃肿。对于复杂的项目,一个 C 和一个 M 层显然是不够用的,现在比较流行的纯后端 api 模块一般采用下述划分方法
这种理解显然是有问题的,业务流程也算是一种“模型”,是对真实世界用户行为或者既有流程的一种建模,并非只有按格式组织的数据才能叫模型。不过按照MVC的创始人的想法,我们如果把和数据打交道的代码还有业务流程全部塞进MVC里的M层的话,这个M层又会显得有些过于臃肿。对于复杂的项目,一个C和一个M层显然是不够用的,现在比较流行的纯后端API模块一般采用下述划分方法

1. Controller,与上述类似,服务入口,负责处理路由,参数校验,请求转发。
2. Logic/Service,逻辑(服务)层,一般是业务逻辑的入口,可以认为从这里开始,所有的请求参数一定是合法的。业务逻辑和业务流程也都在这一层中。常见的设计中会将该层称为 Business Rules。
Expand All @@ -22,11 +22,11 @@

![controller-logic-dao](../images/ch6-08-controller-logic-dao.png)

划分为 CLD 三层之后,在 C 层之前我们可能还需要同时支持多种协议。本章前面讲到的 thrift、gRPC 和 http 并不是一定只选择其中一种,有时我们需要支持其中的两种,比如同一个接口,我们既需要效率较高的 thrift,也需要方便 debug 的 http 入口。即除了 CLD 之外,还需要一个单独的 protocol 层,负责处理各种交互协议的细节。这样请求的流程会变成下面这样:
划分为CLD三层之后,在C层之前我们可能还需要同时支持多种协议。本章前面讲到的thrift、gRPC和http并不是一定只选择其中一种,有时我们需要支持其中的两种,比如同一个接口,我们既需要效率较高的thrift,也需要方便debug的http入口。即除了CLD之外,还需要一个单独的protocol层,负责处理各种交互协议的细节。这样请求的流程会变成下面这样:

![control-flow](../images/ch6-08-control-flow.png)

这样我们 controller 中的入口函数就变成了下面这样
这样我们controller中的入口函数就变成了下面这样

```go
func CreateOrder(ctx context.Context, req *CreateOrderStruct) (
Expand All @@ -36,9 +36,9 @@ func CreateOrder(ctx context.Context, req *CreateOrderStruct) (
}
```

CreateOrder 有两个参数,ctx 用来传入 trace_id 一类的需要串联请求的全局参数,req 里存储了我们创建订单所需要的所有输入信息。返回结果是一个响应结构体和错误。可以认为,我们的代码运行到 controller 层之后,就没有任何与“协议”相关的代码了。在这里你找不到 http.Request,也找不到 http.ResponseWriter,也找不到任何与 thrift 或者 gRPC 相关的字眼
CreateOrder有两个参数,ctx用来传入trace_id一类的需要串联请求的全局参数,req里存储了我们创建订单所需要的所有输入信息。返回结果是一个响应结构体和错误。可以认为,我们的代码运行到controller层之后,就没有任何与“协议”相关的代码了。在这里你找不到`http.Request`,也找不到`http.ResponseWriter`,也找不到任何与thrift或者gRPC相关的字眼

protocol 层,处理 http 协议的大概代码如下
在协议(protocol)层,处理http协议的大概代码如下

```go
// defined in protocol layer
Expand Down Expand Up @@ -66,11 +66,11 @@ func HTTPCreateOrderHandler(wr http.ResponseWriter, r *http.Request) {
}
```

理论上我们可以用同一个 request struct 组合上不同的 tag,来达到一个 struct 来给不同的协议复用的目的。不过遗憾的是在 thrift 中,request struct 也是通过 IDL 生成的,其内容在自动生成的 ttypes.go 文件中,我们还是需要在 thrift 的入口将这个自动生成的 struct 映射到我们 logic 入口所需要的 struct 上。gRPC 也是类似。这部分代码还是需要的。
理论上我们可以用同一个request struct组合上不同的tag,来达到一个struct来给不同的协议复用的目的。不过遗憾的是在thrift中,request struct也是通过IDL生成的,其内容在自动生成的ttypes.go文件中,我们还是需要在thrift的入口将这个自动生成的struct映射到我们logic入口所需要的struct上。gRPC也是类似。这部分代码还是需要的。

聪明的读者可能已经可以看出来了,协议细节处理这一层实际上有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的 struct(例如 http.Request,thrift 的被包装过了) 读出来,再绑定到我们协议无关的 struct 上,再把这个 struct 映射到 controller 入口的 struct 上,这些代码实际上长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用 codegen 来把繁复的协议处理代码从工作内容中抽离出去
聪明的读者可能已经可以看出来了,协议细节处理这一层实际上有大量重复劳动,每一个接口在协议这一层的处理,无非是把数据从协议特定的struct(例如`http.Request`,thrift的被包装过了) 读出来,再绑定到我们协议无关的struct上,再把这个struct映射到controller入口的struct上,这些代码实际上长得都差不多。差不多的代码都遵循着某种模式,那么我们可以对这些模式进行简单的抽象,用代码生成的方式,把繁复的协议处理代码从工作内容中抽离出去

先来看看 http 对应的 struct、thrift 对应的 struct 和我们协议无关的 struct 分别长什么样子
先来看看http对应的struct、thrift对应的struct和我们协议无关的struct分别长什么样子

```go
// http request struct
Expand Down Expand Up @@ -100,7 +100,7 @@ type CreateOrderParams struct {

```

我们需要通过一个源 struct 来生成我们需要的 http 和 thrift 入口代码。再观察一下上面定义的三种 struct,实际上我们只要能用一个 struct 生成 thrift 的 IDL,以及 http 服务的 “IDL(实际上就是带 json/form 相关 tag 的 struct 定义)” 就可以了。这个初始的 struct 我们可以把 struct 上的 http 的 tag 和 thrift 的 tag 揉在一起
我们需要通过一个源struct来生成我们需要的http和thrift入口代码。再观察一下上面定义的三种struct,实际上我们只要能用一个struct生成thrift的IDL,以及http服务的“IDL(实际上就是带 json/form相关tag的struct定义)” 就可以了。这个初始的struct我们可以把struct上的http的tag和thrift的tag揉在一起

```go
type FeatureSetParams struct {
Expand All @@ -112,26 +112,26 @@ type FeatureSetParams struct {
}
```

然后通过代码生成把 thrift 的 IDL 和 http 的 request struct 都生成出来
然后通过代码生成把thrift的IDL和http的request struct都生成出来

![code gen](../images/ch6-08-code-gen.png)

至于用什么手段来生成,你可以通过 go 语言内置的 parser 读取文本文件中的 Go 源代码,然后根据 ast 来生成目标代码,也可以简单地把这个源 struct 和 generator 的代码放在一起编译,让 struct 作为 generator 的输入参数(这样会更简单一些),都是可以的。
至于用什么手段来生成,你可以通过Go语言内置的Parser读取文本文件中的Go源代码,然后根据AST来生成目标代码,也可以简单地把这个源struct和generator的代码放在一起编译,让struct作为generator的输入参数(这样会更简单一些),都是可以的。

当然这种思路并不是唯一选择,我们还可以通过解析 thrift 的 IDL,生成一套 http 接口的 struct。如果你选择这么做,那整个流程就变成了这样:
当然这种思路并不是唯一选择,我们还可以通过解析thrift的IDL,生成一套http接口的struct。如果你选择这么做,那整个流程就变成了这样:

![code gen](../images/ch6-08-code-gen-2.png)

看起来比之前的图顺畅一点,不过如果你选择了这么做,你需要自行对 thrift 的 IDL 进行解析,也就是相当于可能要手写一个 thrift 的 IDL 的 parser,虽然现在有 antlr 或者 peg 能帮你简化这些 parser 的书写工作,但在“解析”的这一步我们不希望引入太多的工作量,所以量力而行即可。
看起来比之前的图顺畅一点,不过如果你选择了这么做,你需要自行对thrift的IDL进行解析,也就是相当于可能要手写一个thrift的IDL的Parser,虽然现在有Antlr或者peg能帮你简化这些Parser的书写工作,但在“解析”的这一步我们不希望引入太多的工作量,所以量力而行即可。

既然工作流已经成型,我们可以琢磨一下怎么让整个流程对用户更加友好。

比如在前面的生成环境引入 GUI 或者 Web 页面,只要让用户点点鼠标就能生成 SDK,这些就靠读者自己去探索了。
比如在前面的生成环境引入Web页面,只要让用户点点鼠标就能生成SDK,这些就靠读者自己去探索了。

虽然我们成功地使自己的项目在入口支持了多种交互协议,但是还有一些问题没有解决。本节中所叙述的分层没有将 middleware 作为项目的分层考虑进去。如果我们考虑 middleware 的话,请求的流程是什么样的?
虽然我们成功地使自己的项目在入口支持了多种交互协议,但是还有一些问题没有解决。本节中所叙述的分层没有将middleware作为项目的分层考虑进去。如果我们考虑middleware的话,请求的流程是什么样的?

![control flow 2](../images/ch6-08-control-flow-2.png)

之前我们学习的 middleware 是和 http 协议强相关的,遗憾的是在 thrift 中看起来没有和 http 中对等的解决这些非功能性逻辑代码重复问题的 middleware。所以我们在图上写 `thrift stuff`。这些 `stuff` 可能需要你手写去实现,然后每次增加一个新的 thrift 接口,就需要去写一遍这些非功能性代码。
之前我们学习的middleware是和http协议强相关的,遗憾的是在thrift中看起来没有和http中对等的解决这些非功能性逻辑代码重复问题的middleware。所以我们在图上写`thrift stuff`。这些`stuff`可能需要你手写去实现,然后每次增加一个新的thrift接口,就需要去写一遍这些非功能性代码。

这也是很多企业项目所面临的真实问题,遗憾的是开源界并没有这样方便的多协议 middleware 解决方案。当然了,前面我们也说过,很多时候我们给自己保留的 http 接口只是用来做 debug,并不会暴露给外人用。这种情况下,这些非功能性的代码只要在 thrift 的代码中完成即可
这也是很多企业项目所面临的真实问题,遗憾的是开源界并没有这样方便的多协议middleware解决方案。当然了,前面我们也说过,很多时候我们给自己保留的http接口只是用来做debug,并不会暴露给外人用。这种情况下,这些非功能性的代码只要在thrift的代码中完成即可
2 changes: 1 addition & 1 deletion ch6-cloud/ch6-01-dist-id.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Twitter 的 snowflake 算法是这种场景下的一个典型解法。先来看

timestamp,datacenter_id,worker_id 和 sequence_id 这四个字段中,timestamp 和 sequence_id 是由程序在运行期生成的。但 datacenter_id 和 worker_id 需要我们在部署阶段就能够获取得到,并且一旦程序启动之后,就是不可更改的了(想想,如果可以随意更改,可能被不慎修改,造成最终生成的 id 有冲突)。

一般不同数据中心的机器,会提供对应的获取数据中心 id 的 api,所以 datacenter_id 我们可以在部署阶段轻松地获取到。而 worker_id 是我们逻辑上给机器分配的一个 id,这个要怎么办呢?比较简单的想法是由能够提供这种自增 id 功能的工具来支持,比如 MySQL:
一般不同数据中心的机器,会提供对应的获取数据中心id的API,所以 datacenter_id 我们可以在部署阶段轻松地获取到。而 worker_id 是我们逻辑上给机器分配的一个 id,这个要怎么办呢?比较简单的想法是由能够提供这种自增 id 功能的工具来支持,比如 MySQL:

```shell
mysql> insert into a (ip) values("10.1.2.101");
Expand Down
6 changes: 3 additions & 3 deletions ch6-cloud/ch6-07-crawler.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ nc.Flush()

#### 基本消息消费

直接使用 nats 的 subscribe api 并不能达到任务分发的目的,因为 pub sub 本身是广播性质的。所有消费者都会收到完全一样的所有消息。
直接使用nats的subscribe API并不能达到任务分发的目的,因为pub sub本身是广播性质的。所有消费者都会收到完全一样的所有消息。

除了普通的 subscribe 之外,nats 还提供了 queue subscribe 的功能。只要提供一个 queue group 名字(类似 kafka 中的 consumer group),即可均衡地将任务分发给消费者。
除了普通的subscribe之外,nats还提供了queue subscribe的功能。只要提供一个queue group名字(类似kafka中的 consumer group),即可均衡地将任务分发给消费者。

```go
nc, err := nats.Connect(nats.DefaultURL)
Expand Down Expand Up @@ -153,7 +153,7 @@ for {

#### 结合 colly 的消息生产

我们为每一个网站定制一个对应的 collector,并设置相应的规则,比如 v2ex,v2fx(虚构的),再用简单的工厂方法来将该 collector 和其 host 对应起来
我们为每一个网站定制一个对应的collector,并设置相应的规则,比如v2ex,v2fx(虚构的),再用简单的工厂方法来将该collector和其host对应起来

```go
package main
Expand Down

0 comments on commit 2e22cc5

Please sign in to comment.