短域名

为啥叫匕首?是因为本节我们来构建一个短域名工具。短域名,顾名思义就是会将一个很长的域名压缩成一个非常短的域名。短域名主要是容易分发,但说实话真不容易记。但这个缺点却无关紧要,因为让你背下一个长域名也是不可能的事情。

因此短域名就好比一个短匕首,短小精悍易于携带。一个典型的短域名工具,大致来说会有这么几个组件:

  1. 长短翻译组件
  2. 查询组件
  3. 持久化组件
  4. 其它组件

先声明一下,这些组件名称不是标准名称,而是我自创的。长短翻译组件,是将一个长域名(譬如: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 中看到源码。

通过上面的编码,一个最简单的短域名工具就完成了。

results matching ""

    No results matching ""