An open API service indexing awesome lists of open source software.

https://github.com/andyron/ron

Ron is a very small HTTP web framework written in Golang.
https://github.com/andyron/ron

Last synced: 2 months ago
JSON representation

Ron is a very small HTTP web framework written in Golang.

Awesome Lists containing this project

README

        

Go语言从零实现
---

参考:https://geektutu.com/post/gee.html

## 1 Web框架ron-web

在设计一个框架之前,需要回答框架核心解决了什么问题。

`net/http`提供了基础的Web功能,即监听端口,映射静态路由,解析HTTP报文。一些Web开发中简单的需求并不支持,需要手工实现。

- 动态路由:例如`hello/:name`,`hello/*`这类的规则。
- 鉴权:没有分组/统一鉴权的能力,需要在每个路由映射的handler中实现。
- 模板:没有统一简化的HTML机制。
- …

当我们离开框架,**使用基础库时,需要频繁手工处理的地方**,就是框架的价值所在。

- 路由(Routing):将请求映射到函数,支持动态路由。例如`'/hello/:name`。
- 模板(Templates):使用内置模板引擎提供模板渲染机制。
- 工具集(Utilites):提供对 cookies,headers 等处理机制。
- 插件(Plugin):Bottle本身功能有限,但提供了插件机制。可以选择安装到全局,也可以只针对某几个路由生效。
- …

### 前置知识

#### 标准库启动Web服务

#### 实现http.Handler接口

### 上下文

将路由(router)独立

- 设计`上下文(Context)`,封装 Request 和 Response ,提供对 JSON、HTML 等返回类型的支持。

### 前缀树路由

动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。

动态路由有很多种实现方式,支持的规则、性能等有很大的差异。例如开源的路由实现`gorouter`支持在路由规则中嵌入正则表达式,例如`/p/[0-9A-Za-z]+`,即路径中的参数仅匹配数字和字母;另一个开源实现`httprouter`就不支持正则表达式。著名的Web开源框架`gin` 在早期的版本,并没有实现自己的路由,而是直接使用了`httprouter`,后来不知道什么原因,放弃了`httprouter`,自己实现了一个版本。

实现动态路由最常用的数据结构,被称为**==前缀树(Trie树)==**:每一个节点的所有的子节点都拥有相同的前缀。

```
curl "http://localhost:9999/login" -X POST -d 'username=geektutu&password=1234'
```

### 分组控制

#### 分组的意义

分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:

- 以`/post`开头的路由匿名可访问。
- 以`/admin`开头的路由需要鉴权。
- 以`/api`开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。

#### 分组嵌套

### 中间件

==中间件(middlewares)==就是**非业务的技术类组件**。Web框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个**插口**,允许**用户自己定义功能**,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2个比较关键的点:

- **插入点**在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。
- 中间件的**输入**是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。

### 模板(HTML Template)

#### 服务端渲染

前后端分离的开发模式,即 Web 后端提供 RESTful 接口,返回结构化的数据(通常为 JSON 或者 XML)。前端使用 AJAX 技术请求到所需的数据,利用 JavaScript 进行渲染。Vue/React 等前端框架持续火热,这种开发模式前后端解耦,优势非常突出。

- 后端专心解决资源利用,并发,数据库等问题,只需要考虑数据如何生成;前端童鞋专注于界面设计实现,只需要考虑拿到数据后如何渲染即可。

- 前后端分离另外一个优势。因为后端只关注于数据,接口返回值是结构化的,与前端解耦。同一套后端服务能够同时支撑小程序、移动APP、PC端 Web 页面,以及对外提供的接口。随着前端工程化的不断地发展,Webpack,gulp 等工具层出不穷,前端技术越来越自成体系了。

前后分离的一大问题,页面是在客户端渲染的,比如浏览器,这**对于爬虫并不友好**。Google 爬虫已经能够爬取渲染后的网页,但是短期内爬取服务端直接渲染的 HTML 页面仍是主流。

#### 静态文件(Serve Static Files)

#### HTML 模板渲染

### 错误恢复

## 2 分布式缓存ron-cache

> 商业世界里,现金为王;架构世界里,缓存为王。

### 为什么

直接使用键值对(`map`)缓存有什么问题呢?

1. 内存不够了怎么办?

那就随机删掉几条数据好了。随机删掉好呢?还是按照时间顺序好呢?或者是有没有其他更好的淘汰策略呢?不同数据的访问频率是不一样的,优先删除访问频率低的数据是不是更好呢?数据的访问频率可能随着时间变化,那优先删除最近最少访问的数据可能是一个更好的选择。

需要实现一个**==合理的淘汰策略==**。

2. 并发写入冲突了怎么办?

对缓存的访问,一般不可能是串行的。map 是没有并发保护的,应对并发的场景,修改操作(包括新增,更新和删除)需要加锁。

3. 单机性能不够怎么办?

单台计算机的资源是有限的,计算、存储等都是有限的。随着业务量和访问量的增加,单台机器很容易遇到瓶颈。如果利用多台计算机的资源,并行处理提高性能就要缓存应用能够支持分布式,这称为**水平扩展(scale horizontally)**。与水平扩展相对应的是**垂直扩展(scale vertically)**,即通过增加单个节点的计算、存储、带宽等,来提高系统的性能,硬件的成本和性能并非呈线性关系,大部分情况下,**分布式系统**是一个更优的选择。

### 是什么

设计一个分布式缓存系统,需要考虑**资源控制、淘汰策略、并发、分布式节点通信**等各个方面的问题。而且,针对不同的应用场景,还需要在不同的特性之间权衡,例如,是否需要支持缓存更新?还是假定缓存在淘汰之前是不允许改变的。不同的权衡对应着不同的实现。

### LRU缓存淘汰策略

### 单机并发缓存

### HTTP服务端

分布式缓存需要实现节点间通信,建立基于 HTTP 的通信机制是比较常见和简单的做法。如果一个节点启动了 HTTP 服务,那么这个节点就可以被其他节点访问。

### 一致性哈希

一致性哈希算法是ron-cache从单节点走向分布式节点的一个重要的环节。

### 分布式节点

- 注册节点(Register Peers),借助一致性哈希算法选择节点。
- 实现 HTTP 客户端,与远程节点的服务端通信。

### 防止缓存击穿

#### 缓存雪崩、缓存击穿与缓存穿透

- ==缓存击穿==:一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到 DB ,造成瞬时DB请求量大、压力骤增。【热点数据】

- ==缓存穿透==:查询一个不存在的数据,因为不存在则不会写到缓存中,所以每次都会去请求 DB,如果瞬间流量过大,穿透到 DB,导致宕机。

- ==缓存雪崩==:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。缓存雪崩通常因为缓存服务器宕机、缓存的 key 设置了相同的过期时间等引起。

### 使用Protobuf通信

#### 为什么要使用 protobuf

protobuf 即 Protocol Buffers,Google 开发的一种数据描述语言,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化。protobuf 以二进制方式存储,占用空间小。

🔖

## 3 ORM框架ron-orm

### 关于ORM框架

对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。

对象和数据库之间映射关系:

| 数据库 | 面向对象的编程语言 |
| :------------------ | :------------------ |
| 表(table) | 类(class/struct) |
| 记录(record, row) | 对象 (object) |
| 字段(field, column) | 对象属性(attribute) |

```sql
CREATE TABLE `User` (`Name` text, `Age` integer);
INSERT INTO `User` (`Name`, `Age`) VALUES ("Tom", 18);
SELECT * FROM `User`;
```

```go
type User struct {
Name string
Age int
}

orm.CreateTable(&User{})
orm.Save(&User{"Tom", 18})
var users []User
orm.Find(&users)
```

ORM 框架相当于对象和数据库中间的一个桥梁,借助 ORM 可以避免写繁琐的 SQL 语言,仅仅通过操作具体的对象,就能够完成对关系型数据库的操作。

> 如何实现一个 ORM 框架呢?

- `CreateTable` 方法需要从参数 `&User{}` 得到对应的结构体的名称 User 作为表名,成员变量 Name, Age 作为列名,同时还需要知道成员变量对应的类型。
- `Save` 方法则需要知道每个成员变量的值。
- `Find` 方法仅从传入的空切片 `&[]User`,得到对应的结构体名也就是表名 User,并从数据库中取到所有的记录,将其转换成 User 对象,添加到切片中。

如果这些方法只接受 User 类型的参数,那是很容易实现的。但是 ORM 框架是通用的,也就是说可以将任意合法的对象转换成数据库中的表和记录。例如:

```go
type Account struct {
Username string
Password string
}

orm.CreateTable(&Account{})
```

> 问题:如何根据任意类型的指针,得到其对应的结构体的信息。

通过反射机制(reflect),可以获取到对象对应的结构体名称,成员变量、方法等信息,例如:

```go
typ := reflect.Indirect(reflect.ValueOf(&Account{})).Type()
fmt.Println(typ.Name()) // Account

for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fmt.Println(field.Name) // Username Password
}
```

- `reflect.ValueOf()` 获取指针对应的反射值。
- `reflect.Indirect()` 获取指针指向的对象的反射值。
- `(reflect.Type).Name()` 返回类名(字符串)。
- `(reflect.Type).Field(i)` 获取第 i 个成员变量。

> 除了对象和表结构/记录的映射以外,设计 ORM 框架还需要关注什么问题呢?

1. MySQL,PostgreSQL,SQLite 等数据库的 SQL 语句是有区别的,ORM 框架如何在开发者不感知的情况下适配多种数据库?
2. 如果对象的字段发生改变,数据库表结构能够自动更新,即是否支持数据库自动迁移(migrate)?
3. 数据库支持的功能很多,例如事务(transaction),ORM框架能实现哪些?
4. ...

#### 关于ron-orm

数据库的特性非常多,简单的增删查改使用 ORM 替代 SQL 语句是没有问题的,但是也有很多特性难以用 ORM 替代,比如复杂的多表关联查询,ORM 也可能支持,但是基于性能的考虑,开发者自己写 SQL 语句很可能更高效。

因此,设计实现一个 ORM 框架,就需要给功能特性排优先级了。

https://github.com/go-gorm/gorm

### database/sql基础

#### database/sql 标准库

- `Exec()` 用于执行 SQL 语句,如果是查询语句,不会返回相关的记录。所以查询语句通常使用 `Query()` 和 `QueryRow()`,前者可以返回多条记录,后者只返回一条记录。
- `Exec()`、`Query()`、`QueryRow()` 接受1或多个入参,第一个入参是 SQL 语句,后面的入参是 SQL 语句中的占位符 `?` 对应的值,占位符一般用来防 SQL 注入。

#### 实现一个简单的 log 库

开发一个框架/库并不容易,详细的日志能够帮助我们快速地定位问题。

log 标准库没有日志分级,不打印文件和行号。

### 对象表结构映射

- 使用 dialect 隔离不同数据库之间的差异,便于扩展。
- 使用反射(reflect)获取任意 struct 对象的名称和字段,映射为数据中的表。
- 数据库表的创建(create)、删除(drop)。

#### 1️⃣Dialect

SQL 语句中的类型和 Go 语言中的类型是不同的,例如Go 语言中的 `int`、`int8`、`int16` 等类型均对应 SQLite 中的 `integer` 类型。因此实现 ORM 映射的第一步,需要思考如何将 Go 语言的类型映射为数据库中的类型。

同时,不同数据库支持的数据类型也是有差异的,即使功能相同,在 SQL 语句的表达上也可能有差异。ORM 框架往往需要兼容多种数据库,因此我们需要将差异的这一部分提取出来,每一种数据库分别实现,实现最大程度的复用和解耦。这部分代码称之为 `dialect`。

#### 2️⃣Schema

对象(object)和表(table)的转换

#### 3️⃣Session

Session 的核心功能是与数据库进行交互。因此,我们将数据库表的增/删操作实现在子包 session 中。

### 记录新增和查询

- 实现新增(insert)记录的功能。
- 使用反射(reflect)将数据库的记录转换为对应的结构体实例,实现查询(select)功能。

#### 1️⃣Clause构造SQL语句

#### 2️⃣Insert功能

后续所有构造 SQL 语句的方式都将与 `Insert` 中构造 SQL 语句的方式一致。分两步:

1. 多次调用 `clause.Set()` 构造好每一个子句。
2. 调用一次 `clause.Build()` 按照传入的顺序构造出最终的 SQL 语句。

构造完成后,调用 `Raw().Exec()` 方法执行。

#### 3️⃣Find功能

### 链式操作与更新删除

- 通过链式(chain)操作,支持查询条件(where, order by, limit 等)的叠加。
- 实现记录的更新(update)、删除(delete)和统计(count)功能。

### 实现钩子

- 通过反射(reflect)获取结构体绑定的钩子(hooks),并调用。
- 支持增删查改(CRUD)前后调用钩子。

#### Hook 机制

Hook,翻译为钩子,其主要思想是提前在可能增加功能的地方埋好(预设)一个钩子,当我们需要重新修改或者增加这个地方的逻辑的时候,把扩展的类或者方法挂载到这个点即可。钩子的应用非常广泛,例如 Github 支持的 travis 持续集成服务,当有 `git push` 事件发生时,会触发 travis 拉取新的代码进行构建。IDE 中钩子也非常常见,比如,当按下 `Ctrl + s` 后,自动格式化代码。再比如前端常用的 `hot reload` 机制,前端代码发生变更时,自动编译打包,通知浏览器自动刷新页面,实现所写即所得。

钩子机制设计的好坏,取决于扩展点选择的是否合适。例如对于持续集成来说,代码如果不发生变更,反复构建是没有意义的,因此钩子应设计在代码可能发生变更的地方,比如 MR、PR 合并前后。

那对于 ORM 框架来说,合适的扩展点在哪里呢?很显然,记录的增删查改前后都是非常合适的。

比如,我们设计一个 `Account` 类,`Account` 包含有一个隐私字段 `Password`,那么每次查询后都需要做脱敏处理,才能继续使用。如果提供了 `AfterQuery` 的钩子,查询后,自动地将 `Password` 字段的值脱敏,是不是能省去很多冗余的代码呢?

### 持事务

#### SQLite和Go标准库中的事务

### 数据库迁移

- 结构体(struct)变更时,数据库表的字段(field)自动迁移(migrate)。
- 仅支持字段新增与删除,不支持字段类型变更。

## 4 RPC框架ron-rpc

> RPC是什么

RPC(Remote Procedure Call,远程过程调用)是一种计算机通信协议,允许调用不同进程空间的程序。RPC的客户端和服务器可以在一台机器上,也可以在不同的机器上。程序员使用时,就像调用本地程序一样,无需关注内部的实现细节。

不同的应用程序之间的通信方式有很多,比如浏览器和服务器之间广泛使用的基于 HTTP 协议的 Restful API。与 RPC 相比,Restful API 有相对统一的标准,因而更**通用,兼容性更好,支持不同的语言**。HTTP 协议是基于**文本**的,一般具备更好的可读性。但是缺点也很明显,RPC和 Restful API对比:

- Restful 接口需要额外的定义,无论是客户端还是服务端,都需要**额外的代码**来处理,而 RPC 调用则更接近于直接调用。
- 基于 HTTP 协议的 Restful 报文冗余,承载了过多的无效信息,而 RPC 通常使用**自定义的协议格式,减少冗余报文**。
- RPC 可以采用更高效的序列化协议,将==文本==转为**==二进制==**传输,获得更高的性能。
- 因为 RPC 的灵活性,所以更容易扩展和集成诸如注册中心、负载均衡等功能。

> RPC框架需要解决什么问题?或者说,为什么需要RPC框架?

两个应用程序之间通信采用的传输协议:

- 位于不同的机器,一般会选择 TCP 协议或者 HTTP 协议;
- 位于相同的机器,选择 Unix Socket 协议

报文的编码格式:

- 最常用的 JSON 或者 XML;
- 报文比较大,可能会选择 protobuf 等;
- 发送端编码之后,再进行压缩;接收端获取报文,先解压再解码。

解决一系列的可用性问题,例如,**连接超时了怎么办?是否支持异步请求和并发?**

如果服务端的实例很多,客户端并不关心这些实例的**地址和部署位置**,只关心自己能否获取到期待的**结果**,那就引出了==注册中心(registry)==和==负载均衡(load balance)==的问题。简单地说,即客户端和服务端互相不感知对方的存在,服务端启动时将自己注册到注册中心,客户端调用时,从注册中心获取到所有可用的实例,选择一个来调用。这样服务端和客户端只需要感知注册中心的存在就够了。注册中心通常还需要实现**服务动态添加、删除**,使用**心跳**确保服务处于可用状态等功能。

再进一步,假设服务端是不同的团队提供的,如果没有统一的 RPC 框架,各个团队的服务提供方就需要各自实现一套**消息编解码、连接池、收发线程、超时处理**等“业务之外”的重复技术劳动,造成整体的低效。因此,“业务之外”的这部分公共的能力,即是 RPC 框架所需要具备的能力。

> ron-rpc

成熟的RPC框架和微服务框架很多:`grpc`、`rpcx`、`go-micro` 等

以Go语言官方的标准库`net/rpc`为基础,新增协议交换(protocol exchange)、注册中心(registry)、服务发现(service discovery)、负载均衡(load balance)、超时处理(timeout processing)等特性。

### 服务端与消息编码

#### 消息的序列化与反序列化

#### 通信过程

```
| Option{MagicNumber: xxx, CodecType: xxx} | Header{ServiceMethod ...} | Body interface{} |
| <------ 固定 JSON 编码 ------> | <------- 编码方式由 CodeType 决定 ------->|
```

```
| Option | Header1 | Body1 | Header2 | Body2 | ...
```

### 支持并发与异步的高性能客户端

### 服务注册

#### 结构体映射为服务

#### 通过反射实现service

#### service 的测试用例

#### 集成到服务端

### 超时处理

#### 为什么需要超时处理机制

超时处理是RPC框架一个比较基本的能力,如果缺少超时处理机制,无论是服务端还是客户端都容易因为网络或其他错误导致挂死,资源耗尽,这些问题的出现大大地降低了服务的可用性。

纵观整个远程调用的过程,需要客户端处理超时的地方有:

- 与服务端建立连接,导致的超时
- 发送请求到服务端,写报文导致的超时
- 等待服务端处理时,等待处理导致的超时(比如服务端已挂死,迟迟不响应)
- 从服务端接收响应时,读报文导致的超时

需要服务端处理超时的地方有:

- 读取客户端请求报文时,读报文导致的超时
- 发送响应报文时,写报文导致的超时
- 调用映射服务的方法时,处理报文导致的超时

#### 创建连接超时

#### Client.Call 超时

#### 服务端处理超时

🔖

#### 测试用例