來整一個本身的短連接服務吧!

什麼是短連接?

所謂短連接,就是把普通網址轉換成一個比較短的網址,而訪問獲得的內容不變。html

好比說對於一個這樣的連接 juejin.im/post/5dece9… ,使用短連接服務的話就能夠將它轉換成相似這種 http://xxx/abc (因爲某不可描述的緣由與某不可抗力的影響 這個URL實際上是我編的)。是否是感受簡潔了許多 (。-`ω´-)前端

短連接是如何實現的?

基本原理

簡單來講,當咱們輸入 http://xxx/abc 後,會通過以下過程 :java

  1. DNS 首先解析得到 http://xxx 的 IP 地址
  2. 當 DNS 得到 IP 地址後,會向這個地址發送 GET 請求,查詢短碼 abc
  3. http://xxx 服務器上運行的服務會經過短碼 abc 獲取其本來的 URL
  4. 請求經過 Http 重定向跳轉到對應的長 URL,便可以正常訪問啦

其中,重定向又分 301(永久重定向)和 302(臨時重定向)。因爲短地址一經生成就不會變化的性質,使用永久重定向能夠對服務器壓力又必定減小,但也所以沒法統計到經由短地址來訪問該頁面的次數。因此,當對這種數據存在分析需求的時候,可使用 302 進行跳轉,以增長一點服務器壓力的代價來實現對更多數據的收集。mysql

經常使用的算法實現

內容壓縮算法(MD5壓縮算法)git

使用算法直接對長鏈內容進行壓縮,例如獲取 hash 算法,或是採用 MD5 算法,將長連接生成一個 128 位的特徵碼,而後將特徵碼截取成 4 到 8 位用做短鏈碼。github

優勢 :生成簡單,不須要創建對應關係就能夠支持長連接重複查詢。算法

缺點 :因爲 MD5 爲有損壓縮算法,不可避免會出現重複的問題。spring

發號器算法(id自增算法)sql

給每一個請求過來的長連接分配一個 id 號,對該索引進行加密,並用其做爲短碼生成須要的短連接。因爲 id 是自增的,因此理論上生成的短連接永遠不會重複,所以也叫永不重複算法。數據庫

優勢 :避免了短連接重複問題。

缺點 :自增 id 暴露在外,會有很大的安全風險,形成連接信息泄露;須要創建對應關係才能夠支持長連接重複查詢。

整一個本身的短連接服務

這裏咱們選用id自增算法進行實現。

首先建立一個 springboot 項目,技術棧 spring boot + spring-data-jpa + mysql,項目結構以下 :

而後就要開始理需求啦 :咱們實際須要的其實只有兩個功能

  1. 實現根據 URL 獲取對應的短 URL
  2. 實現經過短 URL 訪問原 URL,即跳轉到原 URL

實現URL的映射算法

首先要解決的是 長連接重複查詢問題。上文提到須要創建對應關係,這裏咱們就簡單用一張表來存儲。與其對應的實體類 TranslationEntity 以下 :

package com.demo.demosurl.demosurl.model.entity;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/** * @author yuanyiwen * @create 2019-12-06 21:04 * @description */
@Entity
@Data
public class TranslationEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    /** * 真實url(長鏈) */
    private String url;

    /** * 轉換url(短鏈) */
    private String shortUrl;

}
複製代碼

而後是 直接暴露自增主鍵形成的連接信息泄露問題。簡單來想的話,若是 id 變化的規律爲 一、二、三、4,...,就很好推測下一個;但若是 id 變化的規律是 一、3二、1八、97,...,呢?是否是就猜不到下一個是啥啦!

隱藏遞增規律的核心就是使用 Feistel 密碼結構,引用大佬的話 :

Feistel 加密算法的輸入是長爲 2w 的明文和一個密鑰 K=(K1,K2...,Kn)。將明文分組分紅左右兩半 L 和 R ,而後進行 n 輪迭代,迭代完成後,再將左右兩半合併到一塊兒以產生密文分組。

Feistel 加密算法可以產生一個很是好用的特色,那就是,在必定的數字範圍內(2 的 n 次方),每個數字都能根據密鑰找到惟一的一個匹配對象,這就達到了咱們隱藏遞增規律的目的。

例如,咱們給定數字範圍爲 64(2 的 6 次方),其中,每一個數字都能找到惟一一個隨機對應的數字對。這裏的隨機經過密鑰來產生。

那麼咱們將 1 和 2 使用算法進行計算,會發現對應到的數字就是 17 和 25。這就完美解決了咱們的數字遞增問題,外部用戶沒法從數字表面看出是遞增的。並且每一個數字的匹配模式都不同。

藉助這種思想,咱們添加一個 id 混淆工具類,來將有序的 id 映射爲無序 :

package com.demo.demosurl.demosurl.util.encrypt;

/** * @author yuanyiwen * @create 2019-12-07 22:02 * @description id混淆工具類 */
public abstract class NumberEncodeUtil {

    /** * 對id進行混淆 * @param id * @return */
    public static Long encryptId(Long id) {
        Long sid = (id & 0xff000000);
        sid += (id & 0x0000ff00) << 8;
        sid += (id & 0x00ff0000) >> 8;
        sid += (id & 0x0000000f) << 4;
        sid += (id & 0x000000f0) >> 4;
        sid ^= 11184810;
        return sid;
    }

    /** * 對混淆的id進行還原 * @param sid * @return */
    public static Long decodeId(Long sid) {
        sid ^= 11184810;
        Long id = (sid & 0xff000000);
        id += (sid & 0x00ff0000) >> 8;
        id += (sid & 0x0000ff00) << 8;
        id += (sid & 0x000000f0) >> 4;
        id += (sid & 0x0000000f) << 4;
        return id;
    }
}
複製代碼

解決了數字遞增問題後,接下來要作的是 數字壓縮與加密。對一個數字長度進行壓縮而不改變其含義,最簡單的方式就是將它變爲更高的進制。對於一個符號位來講,若是咱們採用不帶符號的簡單字符(0-9a-zA-Z),加起來正好 62 個經常使用字符。因此咱們能夠選擇將混淆後的十進制 id 轉換爲 62 進制 :

package com.demo.demosurl.demosurl.util.encrypt;

import java.util.Stack;

/** * @author yuanyiwen * @create 2019-12-07 22:00 * @description 進制轉換工具類 */
public abstract class ScaleConvertUtil {

    /** * 將10進制數字轉換爲62進制 * @param number 數字 * @param length 轉換成的62進制長度,不足length長度的高位補0 * @return */
    public static String convert(long number, int length){

        char[] charSet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();

        Long rest=number;
        Stack<Character> stack=new Stack<Character>();
        StringBuilder result=new StringBuilder(0);
        while(rest!=0){
            stack.add(charSet[new Long((rest-(rest/62)*62)).intValue()]);
            rest=rest/62;
        }
        for(;!stack.isEmpty();){
            result.append(stack.pop());
        }
        int result_length = result.length();
        StringBuilder temp0 = new StringBuilder();
        for(int i = 0; i < length - result_length; i++){
            temp0.append('0');
        }

        return temp0.toString() + result.toString();
    }
}
複製代碼

而後就能夠開始進行具體的實現了!ServiceImpl 包含兩個方法 :

  • 根據真實url獲取短url時,返回轉換實體
  • 根據短url獲取真實url時,返回轉換實體

具體的實現以下,關鍵思路標在註釋裏了 :

package com.demo.demosurl.demosurl.service.impl;

import com.demo.demosurl.demosurl.common.CommonConstant;
import com.demo.demosurl.demosurl.dao.TranslationDto;
import com.demo.demosurl.demosurl.model.entity.TranslationEntity;
import com.demo.demosurl.demosurl.model.vo.TranlationVo;
import com.demo.demosurl.demosurl.service.TranslationService;
import com.demo.demosurl.demosurl.util.convertion.EntityVoUtil;
import com.demo.demosurl.demosurl.util.encrypt.NumberEncodeUtil;
import com.demo.demosurl.demosurl.util.encrypt.ScaleConvertUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/** * @author yuanyiwen * @create 2019-12-06 21:12 * @description */
@Service
public class TranslationServiceImpl implements TranslationService {

    @Autowired
    private TranslationDto translationDto;

    @Override
    public TranlationVo getShortUrlByUrl(String url) {

        // 若不是URL格式,直接返回空
        if(!isHttpUrl(url)) {
            return null;
        }

        TranslationEntity translationEntity = translationDto.findByUrl(url);

        // 若是該實體不爲空,直接返回對應的短url
        if(translationEntity != null) {
            return EntityVoUtil.convertEntityToVo(translationEntity);
        }

        // 不然,從新生成轉換實體並存入數據庫 todo 存入緩存
        translationEntity = new TranslationEntity();

        // 獲取當前id並生成短url尾綴
        Long currentId = translationDto.count()+1;
        String shortUrlSuffix = ScaleConvertUtil.convert(NumberEncodeUtil.encryptId(currentId), CommonConstant.LENGTH_OF_SHORT_URL);

        // 短連接拼接
        String shortUrl = CommonConstant.PRIFIX_OF_SHORT_URL + shortUrlSuffix;

        translationEntity.setUrl(url);
        translationEntity.setShortUrl(shortUrl);
        translationDto.save(translationEntity);

        return EntityVoUtil.convertEntityToVo(translationEntity);
    }

    @Override
    public TranlationVo getUrlByShortUrl(String shortUrl) {
        TranslationEntity translationEntity = translationDto.findByShortUrl(shortUrl);
        if(translationEntity != null) {
            return EntityVoUtil.convertEntityToVo(translationEntity);
        }
        return null;
    }

    /** * 判斷一個字符串是否爲URL格式 * @param url * @return */
    private boolean isHttpUrl(String url) {
        boolean isUrl = false;
        if(url.startsWith("http://") || url.startsWith("https://")) {
            isUrl = true;
        }
        return isUrl;
    }
}
複製代碼

對外暴露一個「根據URL獲取對應短連接」的接口方法 :

@PostMapping("/surl")
public ResponseVo<TranlationVo> getShortUrlByUrl(String url) {

    TranlationVo tranlationVo = translationService.getShortUrlByUrl(url);

    if(tranlationVo == null) {
        return ResponseUtil.toFailResponseVo("請檢查上傳的url格式");
    }

    return ResponseUtil.toSuccessResponseVo(tranlationVo);
}
複製代碼

最後是一些能夠集中配置的常量參數,這裏單獨抽出來方便進行一些自定義調配 :

package com.demo.demosurl.demosurl.common;

/** * @author yuanyiwen * @create 2019-12-07 22:13 * @description 保存項目中用到的常量 */
public interface CommonConstant {

    /** * 默認生成的短連接後綴長度 */
    Integer LENGTH_OF_SHORT_URL = 4;


    /** * 默認生成的短連接前綴 */
    String PRIFIX_OF_SHORT_URL = "http://localhost:8090/";


    /** * 若輸入的短連接不存在,默認跳轉的頁面 */
    String DEFAULT_URL = "http://localhost:8090/default";
}
複製代碼

實現URL的跳轉

這個就很是簡單了,只須要經過短鏈獲取到長鏈,而後使用 ModelAndView 的重定向進行跳轉 :

@GetMapping("{shortUrl}")
public ModelAndView redirect(@PathVariable String shortUrl, ModelAndView mav){

    // 獲取對應的長連接(若短連接不存在,則跳轉到默認頁面)
    TranlationVo tranlationVo = translationService.getUrlByShortUrl(CommonConstant.PRIFIX_OF_SHORT_URL + shortUrl);
    String url = (tranlationVo == null) ? CommonConstant.DEFAULT_URL : tranlationVo.getUrl();

    // 跳轉
    mav.setViewName("redirect:" + url);

    return mav;
}
複製代碼

最後來檢測一下成果

啓動項目,打開 postman ,輸入咱們的URL :

能夠看到,服務已經將長連接 juejin.im/post/5dece9… 轉成對應的短連接 http://localhost:8090/kvgQ 返了回來。讓咱們訪問一下試試 :

跳轉成功!這樣一個簡單的短連接服務就完成啦。

項目改進與源代碼

由於是一個自定義的很是簡單的短連接服務,因此仍是有很是多地方能夠繼續改進的,好比說

  • 使用緩存創建連接的對應關係,及時清理再也不須要的短連接,以免數據的無限膨脹
  • 作一些數據統計,好比訪問量、IP地址段、用戶使用的設備等等
  • 使用多個發號器,段性錯開進行號碼的發放,以解決改造爲分佈式後的同步問題
  • 給人家加一個前端頁面 TuT

最後悄悄放一個github地址 _(:3 」∠)_ : github.com/yuanLier/su…


參考文章 :
www.cnblogs.com/yuanjiangw/… my.oschina.net/u/2485991/b…

相關文章
相關標籤/搜索