短域名
为啥叫匕首?是因为本节我们来构建一个短域名工具。短域名,顾名思义就是会将一个很长的域名压缩成一个非常短的域名。短域名主要是容易分发,但说实话真不容易记。但这个缺点却无关紧要,因为让你背下一个长域名也是不可能的事情。
因此短域名就好比一个短匕首,短小精悍易于携带。一个典型的短域名工具,大致来说会有这么几个组件:
- 长短翻译组件
- 查询组件
- 持久化组件
- 其它组件
先声明一下,这些组件名称不是标准名称,而是我自创的。长短翻译组件,是将一个长域名(譬如:https://xxxxxx/xxxxx?zzzzzz 这样的域名翻译成短域名(https://xxx/xxxxx )。在翻译过程当中,需要做两件事:一、确定当前的长域名是否存在相对应的短域名,如果不吝惜存储,其实这部可以忽略。二、保证不会生成重复的短域名,这一点必须保证。
而查询组件,就是用来确定当前的长域名是否存在相对应的短域名。上面说过如果不吝惜存储,那么这步可以省略。此话怎么理解呢?对于一个短域名工具来说,有一个数据指标很重要:跳转速度!用户可以容忍工具生成域名的时候慢一些,但不能容忍这个短域名跳转的时候慢。所以毋庸置疑,所有的域名配对数据必须保存在内存当中,还必须使用一种高效的数据结构来保存这些配对结构。对于搜索的算法复杂度最好做到O(1)(常数时间)。
那么此时此刻问题就来了,内存相对于磁盘来说是一种稀缺资源,应该如何尽可能的有效利用内存呢?一个思路是去重。另外一个思路是LRU方案。查询组件就是用来去重的,我们的短域名工具可以在翻译之前,先查询一下当前域名是否处理过。如果处理过,直接返回相对应的短域名,如果没有处理过,再继续翻译。而LRU方案,可以自己实现也可以通过第三方工具来实现。我的方案是借助于Redis来实现,虽然增加了架构复杂度和降低了一些处理性能,但保证了LRU的准确性和稳定性,利大于弊。
持久化组件,则是用来容灾的。因为所有数据都在内存当中,所以必须要有持久化方案,否则一旦出现数据丢失,用户体验就会直线下降。
其它组件,就包括数据统计,流量管控,自定义域名格式等等辅助组件了。所以这个短域名工具大致的架构图如下:
好,让我们撸起袖子开始写吧。
首先是数据加载模块。也就是从持久化数据里面加载数据,为了简便,我们用文件来替代Redis。
// SO 保存短URL和原始URL对应关系
type SO struct {
Surl string `json:"surl"`
Ourl string `json:"ourl"`
}
func datainit() (map[string]*SO, map[string]*SO, error) {
dir := os.Getenv("FLAK_CONF_DIR")
if dir == "" {
golog.Error("PLEASE SETTING FLAK_CONF_DIR! ")
return nil, nil, errors.New("PLEASE SETTING FLAK_CONF_DIR")
}
urlFile := dir + PERSISTENCE
u := make([]SO)
data, err := ioutil.ReadFile(urlFile)
if err != nil {
golog.Error(err.Error())
return nil, nil, err
}
err = json.Unmarshal(data, &u)
if err != nil {
golog.Error(err.Error())
return nil, nil, err
}
urlMap := make(map[string]*SO)
destMap := make(map[string]*SO)
for _, su := range u.S {
urlMap[su.Surl] = &su
destMap[su.Ourl] = &su
}
return urlMap, destMap, nil
}
FLAK_CONF_DIR 是持久化文件所在目录
通过os.Getenv("FLAK_CONF_DIR")就可以读取到当前的环境变量,但需要注意如果变量为空,则返回的dir则有可能为空。所以需要判断一下。然后就可以通过ioutil.ReadFile(urlFile)来读取文件里面的数据,目前我们的数据不多,一个文件就能存下了。当数据量大的时候,就不能使用文件做持久化了,上一个DB就是很自然的事情了。
前面几式中,我们也没有提过如何使用Golang来处理Json数据,正好这里就补上。如果要将一个文本里的数据转成Json结构,那就先通过ioutil.ReadFile来读取文本里的数据,而后通过json.Unmarshal来反序列化成Json结构。例如上面的:
err = json.Unmarshal(data, &u)
通过上面的datainit函数,我们就加载了持久化之后的数据。
现在让我们开始编写翻译组件吧
func createSUrl(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
surl := vars["url"]
if d[surl] != nil {
Sandstorm.HTTPSuccess(w, DHOST+"/"+d[surl].Surl)
return
}
le := 10
length := os.Getenv("FLAK_URL_LENGTH")
if length == "" {
golog.Error("PLEASE SETTING FLAK_URL_LENGTH! GIVE IT DEFAULT VALUE")
}
le, err := strconv.Atoi(length)
if err != nil {
golog.Error("FLAK_URL_LENGTH WRONG!", length)
le = 10
}
shortUlr := string(Krand(le, KC_RAND_KIND_ALL))
so := &SO{
Surl: shortUlr,
Ourl: surl,
}
u[shortUlr] = so
d[surl] = so
Sandstorm.HTTPSuccess(w, DHOST+"/"+shortUlr)
}
surl是source url的缩写,表示的是准备翻译的长域名。d[surl]用来判断当前这个长域名是否被处理过,d此时此刻就是destMap。这一步充当了查询组件的角色,还是要说明一下,当前没有使用Redis,如果使用了Redis,这一步应该是在Redis中查询surl是否存在。
如果d[surl]!=nil说明surl被处理过,就直接返回相对应的短域名。如果等于nil,则继续处理。
我们用Krand函数来生成随机字符串,
// 随机字符串
func Krand(size int, kind int) []byte {
ikind, kinds, result := kind, [][]int{[]int{10, 48}, []int{26, 97}, []int{26, 65}}, make([]byte, size)
is_all := kind > 2 || kind < 0
rand.Seed(time.Now().UnixNano())
for i := 0; i < size; i++ {
if is_all { // random ikind
ikind = rand.Intn(3)
}
scope, base := kinds[ikind][0], kinds[ikind][1]
result[i] = uint8(base + rand.Intn(scope))
}
return result
}
在Krand函数里面,使用当前时间戳做随机种子,这样尽可能的保证唯一性。但还没有考虑分布式的环境,如果在分布式环境中,应该再添加每台机器的机器ID,这样就能保证唯一性。
通过Krand就生成了一个混合大小写和数字的10位字符串,也就生成了一个短域名。当用户发起一个短域名访问请求时,我们就可以反向查询短域名所对应的长域名:
func getOUrl(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
surl := vars["url"]
golog.Debug("NEW FORWARD", surl)
if u[surl] == nil {
Sandstorm.HTTPError(w, "", http.StatusNotFound)
return
}
newURL := HOST + "/" + u[surl].Ourl
golog.Debug("NEWURL:", newURL)
Sandstorm.DisDebug()
re := make(map[string]string)
re["Location"] = newURL
Sandstorm.HTTPReDirect(w, r, re)
}
然后返回一个302状态码,在Localtion放入相对应的长域名。这样用户端的浏览器(假设是浏览器发起的访问)就可以跳转到相对应的长域名了。
基本工作就这些了,还有一些收尾的动作。比如持久化,再开头的时候,我们只加载了数据,这里需要定时将内存数据写入到文本当中
// SaveData 定时备份数据
func SaveData() {
c := time.Tick(time.Duration(1) * time.Minute)
for _ = range c {
persistence()
}
}
// persistence 将缓存中的数据持久化到文件中
func persistence() {
url := new(URL)
sarray := make([]SO, len(u))
i := 0
for k := range u {
sarray[i] = *u[k]
i++
}
url.S = sarray
data, err := json.Marshal(url)
if err != nil {
golog.Error(err.Error())
return
}
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
golog.Error(err.Error())
return
}
urlFile := dir + PERSISTENCE
err = ioutil.WriteFile(urlFile, data, 0644)
if err != nil {
golog.Error(err.Error())
return
}
}
按照加载的逆向顺序来做就OK了。
此时各个零件都写完了,我们来写个main函数把零件都串起来:
package main
import (
"log"
"net/http"
"os"
"github.com/andy-zhangtao/golog"
"github.com/gorilla/mux"
)
var (
// u 以短url为key保存对应关系
u = make(map[string]*SO)
// d 以目的url为key保存对应关系
d = make(map[string]*SO)
)
func main() {
var err error
if len(os.Args) != 2 {
golog.Error("FLAK NEED A PORT!!")
os.Exit(-1)
}
golog.Debug("FLAK START ON ", os.Args[1])
u, d, err = datainit()
if err != nil {
golog.Error("INIT FAILED ", err.Error())
os.Exit(-1)
}
go SaveData()
r := mux.NewRouter()
r.HandleFunc("/{url}", getOUrl).Methods(http.MethodGet)
r.HandleFunc(CREATE+"{url}", createSUrl).Methods(http.MethodGet)
log.Println(http.ListenAndServe(":"+os.Args[1], r))
}
在main当中,我们暴露了两个API:/create/{url}和/{url},分别是用来创建短域名和使用短域名的。上面所有代码可以在https://github.com/andy-zhangtao/Flak 中看到源码。
通过上面的编码,一个最简单的短域名工具就完成了。