短域名

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

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

  • 长短翻译组件
  • 查询组件
  • 持久化组件
  • 其它组件
    先声明一下,这些组件名称不是标准名称,而是我自创的。长短翻译组件,是将一个长域名(譬如:https://xxxxxx/xxxxx?zzzzzz 这样的域名翻译成短域名(https://xxx/xxxxx )。在翻译过程当中,需要做两件事:一、确定当前的长域名是否存在相对应的短域名,如果不吝惜存储,其实这部可以忽略。二、保证不会生成重复的短域名,这一点必须保证。

而查询组件,就是用来确定当前的长域名是否存在相对应的短域名。上面说过如果不吝惜存储,那么这步可以省略。此话怎么理解呢?对于一个短域名工具来说,有一个数据指标很重要:跳转速度!用户可以容忍工具生成域名的时候慢一些,但不能容忍这个短域名跳转的时候慢。所以毋庸置疑,所有的域名配对数据必须保存在内存当中,还必须使用一种高效的数据结构来保存这些配对结构。对于搜索的算法复杂度最好做到O(1)(常数时间)。

那么此时此刻问题就来了,内存相对于磁盘来说是一种稀缺资源,应该如何尽可能的有效利用内存呢?一个思路是去重。另外一个思路是LRU方案。查询组件就是用来去重的,我们的短域名工具可以在翻译之前,先查询一下当前域名是否处理过。如果处理过,直接返回相对应的短域名,如果没有处理过,再继续翻译。而LRU方案,可以自己实现也可以通过第三方工具来实现。我的方案是借助于Redis来实现,虽然增加了架构复杂度和降低了一些处理性能,但保证了LRU的准确性和稳定性,利大于弊。

持久化组件,则是用来容灾的。因为所有数据都在内存当中,所以必须要有持久化方案,否则一旦出现数据丢失,用户体验就会直线下降。

其它组件,就包括数据统计,流量管控,自定义域名格式等等辅助组件了。所以这个短域名工具大致的架构图如下:

短域名 - 图1

好,让我们撸起袖子开始写吧。

首先是数据加载模块。也就是从持久化数据里面加载数据,为了简便,我们用文件来替代Redis。

  1. // SO 保存短URL和原始URL对应关系
  2. type SO struct {
  3. Surl string `json:"surl"`
  4. Ourl string `json:"ourl"`
  5. }
  6. func datainit() (map[string]*SO, map[string]*SO, error) {
  7. dir := os.Getenv("FLAK_CONF_DIR")
  8. if dir == "" {
  9. golog.Error("PLEASE SETTING FLAK_CONF_DIR! ")
  10. return nil, nil, errors.New("PLEASE SETTING FLAK_CONF_DIR")
  11. }
  12. urlFile := dir + PERSISTENCE
  13. u := make([]SO)
  14. data, err := ioutil.ReadFile(urlFile)
  15. if err != nil {
  16. golog.Error(err.Error())
  17. return nil, nil, err
  18. }
  19. err = json.Unmarshal(data, &u)
  20. if err != nil {
  21. golog.Error(err.Error())
  22. return nil, nil, err
  23. }
  24. urlMap := make(map[string]*SO)
  25. destMap := make(map[string]*SO)
  26. for _, su := range u.S {
  27. urlMap[su.Surl] = &su
  28. destMap[su.Ourl] = &su
  29. }
  30. return urlMap, destMap, nil
  31. }
  1. FLAK_CONF_DIR 是持久化文件所在目录

通过os.Getenv("FLAK_CONF_DIR")就可以读取到当前的环境变量,但需要注意如果变量为空,则返回的dir则有可能为空。所以需要判断一下。然后就可以通过ioutil.ReadFile(urlFile)来读取文件里面的数据,目前我们的数据不多,一个文件就能存下了。当数据量大的时候,就不能使用文件做持久化了,上一个DB就是很自然的事情了。

前面几式中,我们也没有提过如何使用Golang来处理Json数据,正好这里就补上。如果要将一个文本里的数据转成Json结构,那就先通过ioutil.ReadFile来读取文本里的数据,而后通过json.Unmarshal来反序列化成Json结构。例如上面的:

  1. err = json.Unmarshal(data, &u)

通过上面的datainit函数,我们就加载了持久化之后的数据。

现在让我们开始编写翻译组件吧

  1. func createSUrl(w http.ResponseWriter, r *http.Request) {
  2. vars := mux.Vars(r)
  3. surl := vars["url"]
  4. if d[surl] != nil {
  5. Sandstorm.HTTPSuccess(w, DHOST+"/"+d[surl].Surl)
  6. return
  7. }
  8. le := 10
  9. length := os.Getenv("FLAK_URL_LENGTH")
  10. if length == "" {
  11. golog.Error("PLEASE SETTING FLAK_URL_LENGTH! GIVE IT DEFAULT VALUE")
  12. }
  13. le, err := strconv.Atoi(length)
  14. if err != nil {
  15. golog.Error("FLAK_URL_LENGTH WRONG!", length)
  16. le = 10
  17. }
  18. shortUlr := string(Krand(le, KC_RAND_KIND_ALL))
  19. so := &SO{
  20. Surl: shortUlr,
  21. Ourl: surl,
  22. }
  23. u[shortUlr] = so
  24. d[surl] = so
  25. Sandstorm.HTTPSuccess(w, DHOST+"/"+shortUlr)
  26. }

surl是source url的缩写,表示的是准备翻译的长域名。d[surl]用来判断当前这个长域名是否被处理过,d此时此刻就是destMap。这一步充当了查询组件的角色,还是要说明一下,当前没有使用Redis,如果使用了Redis,这一步应该是在Redis中查询surl是否存在。

如果d[surl]!=nil说明surl被处理过,就直接返回相对应的短域名。如果等于nil,则继续处理。

我们用Krand函数来生成随机字符串,

  1. // 随机字符串
  2. func Krand(size int, kind int) []byte {
  3. ikind, kinds, result := kind, [][]int{[]int{10, 48}, []int{26, 97}, []int{26, 65}}, make([]byte, size)
  4. is_all := kind > 2 || kind < 0
  5. rand.Seed(time.Now().UnixNano())
  6. for i := 0; i < size; i++ {
  7. if is_all { // random ikind
  8. ikind = rand.Intn(3)
  9. }
  10. scope, base := kinds[ikind][0], kinds[ikind][1]
  11. result[i] = uint8(base + rand.Intn(scope))
  12. }
  13. return result
  14. }

在Krand函数里面,使用当前时间戳做随机种子,这样尽可能的保证唯一性。但还没有考虑分布式的环境,如果在分布式环境中,应该再添加每台机器的机器ID,这样就能保证唯一性。

通过Krand就生成了一个混合大小写和数字的10位字符串,也就生成了一个短域名。当用户发起一个短域名访问请求时,我们就可以反向查询短域名所对应的长域名:

  1. func getOUrl(w http.ResponseWriter, r *http.Request) {
  2. vars := mux.Vars(r)
  3. surl := vars["url"]
  4. golog.Debug("NEW FORWARD", surl)
  5. if u[surl] == nil {
  6. Sandstorm.HTTPError(w, "", http.StatusNotFound)
  7. return
  8. }
  9. newURL := HOST + "/" + u[surl].Ourl
  10. golog.Debug("NEWURL:", newURL)
  11. Sandstorm.DisDebug()
  12. re := make(map[string]string)
  13. re["Location"] = newURL
  14. Sandstorm.HTTPReDirect(w, r, re)
  15. }

然后返回一个302状态码,在Localtion放入相对应的长域名。这样用户端的浏览器(假设是浏览器发起的访问)就可以跳转到相对应的长域名了。

基本工作就这些了,还有一些收尾的动作。比如持久化,再开头的时候,我们只加载了数据,这里需要定时将内存数据写入到文本当中

  1. // SaveData 定时备份数据
  2. func SaveData() {
  3. c := time.Tick(time.Duration(1) * time.Minute)
  4. for _ = range c {
  5. persistence()
  6. }
  7. }
  8. // persistence 将缓存中的数据持久化到文件中
  9. func persistence() {
  10. url := new(URL)
  11. sarray := make([]SO, len(u))
  12. i := 0
  13. for k := range u {
  14. sarray[i] = *u[k]
  15. i++
  16. }
  17. url.S = sarray
  18. data, err := json.Marshal(url)
  19. if err != nil {
  20. golog.Error(err.Error())
  21. return
  22. }
  23. dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
  24. if err != nil {
  25. golog.Error(err.Error())
  26. return
  27. }
  28. urlFile := dir + PERSISTENCE
  29. err = ioutil.WriteFile(urlFile, data, 0644)
  30. if err != nil {
  31. golog.Error(err.Error())
  32. return
  33. }
  34. }

按照加载的逆向顺序来做就OK了。

此时各个零件都写完了,我们来写个main函数把零件都串起来:

  1. package main
  2. import (
  3. "log"
  4. "net/http"
  5. "os"
  6. "github.com/andy-zhangtao/golog"
  7. "github.com/gorilla/mux"
  8. )
  9. var (
  10. // u 以短url为key保存对应关系
  11. u = make(map[string]*SO)
  12. // d 以目的url为key保存对应关系
  13. d = make(map[string]*SO)
  14. )
  15. func main() {
  16. var err error
  17. if len(os.Args) != 2 {
  18. golog.Error("FLAK NEED A PORT!!")
  19. os.Exit(-1)
  20. }
  21. golog.Debug("FLAK START ON ", os.Args[1])
  22. u, d, err = datainit()
  23. if err != nil {
  24. golog.Error("INIT FAILED ", err.Error())
  25. os.Exit(-1)
  26. }
  27. go SaveData()
  28. r := mux.NewRouter()
  29. r.HandleFunc("/{url}", getOUrl).Methods(http.MethodGet)
  30. r.HandleFunc(CREATE+"{url}", createSUrl).Methods(http.MethodGet)
  31. log.Println(http.ListenAndServe(":"+os.Args[1], r))
  32. }

在main当中,我们暴露了两个API:/create/{url}和/{url},分别是用来创建短域名和使用短域名的。上面所有代码可以在https://github.com/andy-zhangtao/Flak 中看到源码。

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