博客:https://www.mintimate.cn Mintimate’s Blog,只为与你分享
短链接
短链接,相信大家再熟悉不过了。比如之前写过的文章:
- 搭建短链接平台详细分析及具体代码实现: https://cloud.tencent.com/developer/article/1860739
原理就是:
- 生成唯一ID,用于存储当作查询的唯一键;
- 存储的唯一键,映射到完整的URL地址上;
- 使用302/301进行重定向跳转,建议需要统计访问量使用302,不需要统计访问量或者完成映射后不再更改,使用301。
用短链接替换较长的原始 URL,使得用户在访问网页或资源时可以使用更短、更便于记忆和分享的链接,也方便隐藏Get请求。
但是,这样的短链接,还是缺少一些乐趣。从算法和乐趣触发,长链接,了解一下?
长链接
其实并没有公认的长链接定义,我之所以称本次内容为长链接生成,是因为本次介绍的算法效果,和短链接最后达成的效果相反。
这一切源自于我看到这个网站:
- https://ooooooooooooooooooooooo.ooo
这个网站看起来全都是o
,但是你细看再细看,就会发现,实际上包含4四种不同的字符:
o
是英语小写字母oο
是希腊字母omicronо
是西里尔字母оᴏ
是小号形式的字母o
当然,更有趣的是,这个网站所做的内容:
举个例子:你输入网址:https://cloud.tencent.com/developer,那么可以得到:
访问得到的字符串,发现实际上重定向到了:https://cloud.tencent.com/developer。
也就是把一个URL链接,变长和风格化了。
那么,是怎么做到的呢? 又是如何复现呢?
原理解析
嘿嘿,本来想F12看看是不是前端实现的,结果发现作者直接开源了这个好玩的网站:
- https://github.com/lucaceriani/ooo
核心代码就是这块:
这个时候,我们就知道原理了:
简单地说,访问访问这个网站,如果存在二级目录,那么:
- 截取二级目录内容,尝试映射为UTF-8字符数组;
- 成功映射的情况,还原UTF-8字符串数组为原始URL并跳转;
- 映射失败或者不存在二级目录,直接进入主页。
为什么使用UTF-8数组进行字段的映射呢?
UTF-8数组
首先,我们要知道UTF-8是Unicode的一种字节序列表示形式(编码方案),UTF-8将一个Unicode字符根据其码点转化为1-4个字节的序列来存储和传输。
基础的Unicode定义了从0到1114111之间的码位空间,用于表示世界上主流文字系统中的字符。
例如:
- 字母
A
的Unicode码点是0x0041
,数字0
的码点是0x0030
。 - 汉字也都有自己的Unicode码点,如"人"字的码点是0x4EBA。
回到UTF-8,因为UTF-8为1-4个字节的序列,所以可以用UTF-8数组来表示,比如你好世界
:
- "你"字符的Unicode码点是0x4F60,0x4F60在UTF-8编码为3个字节数字序列:
[228, 189, 160]
- "好"字符的Unicode码点是0x597D,0x597D在UTF-8编码为3个字节数字序列:
[229, 165, 189]
所以,"你好世界"每个字符的UTF-8编码数组是:
[228, 189, 160, 229, 165, 189, 224, 168, 104, 227, 174, 164]
根据用例,转换过程就是:
- 查找每个字符在Unicode标准中的码点编号
- 根据UTF-8编码规则,将码点转化为1-4个字节的数字序列
- 把各个字节序列整合成一个数字数组
这样就完成了从字符串到UTF-8编码数组的转换。并且,新的数组一定是1~4个数字序列,每个字符序列,由高到低排序。
数组映射
综合上述的UTF-8数组,我们可以把任意的字符全部转为UTF-8的数组,并且数据内部全部是数组。
这个时候,是怎么映射为o
呢?
关键代码:
enc = ["o", "ο", "о", "ᴏ"]
encodeUrl(url) {
// get utf8 array
let unversioned = this.toUTF8Array(url)
// convert to string with base 4
// padstart very important! otherwise missing leading 0s
.map(n => n.toString(4).padStart(4, "0"))
// convert to array of characters
.join("").split("")
// map to the o's
.map(x => this.enc[parseInt(x)])
// join into single string
.join("")return this.addVersion(unversioned)
}
核心逻辑:
- 数组中的元素转4进制字符串,前位补0;
- 连接成的长字符串,再切割成单字符数组;
- 每个字符映射成字母表字符(四个不同的
o
); - 字符数组连接成新的字符串。
这样,原本的你好世界
,就被映射为:ᴏоοoоᴏᴏοооooᴏоοοооοοоᴏᴏο
。
解码
恢复就很简单了,上一节的操作反着进行就可以了:
decodeUrl(ooo) {
ooo = this.removeAndCheckVersion(ooo) if (ooo === null) return // get the base 4 string representation of the url let b4str = ooo.split("").map(x => this.dec[x]).join("") let utf8arr = [] // parse 4 characters at a time (255 in b10 = 3333 in b4) // remember adding leading 0s padding for (let i = 0; i < b4str.length; i += 4) utf8arr.push(parseInt(b4str.substring(i, i + 4), 4)) return this.Utf8ArrayToStr(utf8arr)
}
可以看到,还是很简单的。就是思路,非常新颖。
复刻为乐谱
掌握了原理,我们就可以复刻为音符的版本了,既然原版使用四个不同的o
,那么我们可以使用特殊符号:"♫", "♪", "♬", "¶","♩"
。
首先是定义一个env,用于映射:
const enc = ["♫", "♪", "♬", "¶","♩"]
既然使用了五个音符,那么就应该用5进制了:
// 获取utf8数组
let UTF8Array = toUTF8Array(originUrl)
// 转换为base 4字符串
// padstart非常重要!否则会丢失前导0
.map(n => n.toString(5).padStart(5, "0"))
// 转换为字符数组
.join("").split("")
// 映射到o的不同形式
.map(x => enc[parseInt(x)])
// 连接成单个字符串
.join("");
解码也需要更改一下:
decodeUrl(ooo) {
ooo = this.removeAndCheckVersion(ooo) if (ooo === null) return // 每次解析5个字符 let b5str = ooo.split("").map(x => this.dec[x]).join("") let utf8arr = [] for (let i = 0; i < b5str.length; i += 5) utf8arr.push(parseInt(b5str.substring(i, i + 5), 5)) return this.Utf8ArrayToStr(utf8arr) }</code></pre></div></div><p>最后,看看效果,输入:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>text</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-text"><code class="language-text" style="margin-left:0">https://cloud.tencent.com/developer</code></pre></div></div><p>输出:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>text</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-text"><code class="language-text" style="margin-left:0">♫♫♩♫♩♫♫♩¶♪♫♫♩¶♪♫♫♩♬♬♫♫♩¶♫♫♫♬♪¶♫♫♪♩♬♫♫♪♩♬♫♫¶♩♩♫♫♩♪¶♫♫♩♬♪♫♫♩¶♬♫♫♩♫♫♫♫♪♩♪♫♫♩¶♪♫♫♩♫♪♫♫♩♬♫♫♫¶♩♩♫♫♩♫♪♫♫♩♬♫♫♫♩¶♪♫♫♪♩♪♫♫¶♩♩♫♫♩♬♪♫♫♩♪♩♫♫♪♩♬♫♫♩♫♫♫♫♩♫♪♫♫♩¶¶♫♫♩♫♪♫♫♩♪¶♫♫♩♬♪♫♫♩♬♬♫♫♩♫♪♫♫♩♬♩</code></pre></div></div><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:100%"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1723068132927595776.png" /></div><div class="figure-desc">编码成功</div></div></div></figure><p>接下来看看如何网站上实现。</p><h2 id="8usgj" name="Nuxt3%E4%B8%8A%E5%AE%9E%E7%8E%B0">Nuxt3上实现</h2><p>我们需要达成一个302的重定向跳转。在SpringBoot中,你可以使用<code>RedirectView</code>进行跳转:</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>java</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-java"><code class="language-java" style="margin-left:0">@GetMapping("/path")
public View handle() {
return new RedirectView("/new/path", true);
}
如果使用Nginx,可以直接激活Nginx的Lua脚本,使用Lua脚本对字符串进行编码转字符串解析后:
location /old-path {
rewrite ^ /new-path? permanent last;
}
我最近用Nuxt3比较多,就说一下Nuxt3上如何操作。
在Nuxt3上,编码部分就不再多说了,跳转解码,可以使用服务端路由进行实现:
import { Utf8ArrayToStr } from '~/untils/longUrlMake';
export default defineEventHandler((event) => {
const ooo = decodeURIComponent(event.context.params.encoderUrl);
console.log(ooo)
const dec = {
'♫': '0',
'♪': '1',
'♬': '2',
'¶': '3',
'♩': '4'
};
// 获取url的base 5字符串表示
let b5str = ooo
.split('')
.map((x) => dec[x])
.join('');
console.log("b5str: "+b5str)if (b5str === undefined || b5str.length ===0){ return sendRedirect(event, "/404", 302); } let utf8arr = []; // 每次解析5个字符 // 记住添加前导0的填充 for (let i = 0; i < b5str.length; i += 5) utf8arr.push(parseInt(b5str.substring(i, i + 5), 5)); // 返回解码后的字符串 let originUrl = Utf8ArrayToStr(utf8arr); return sendRedirect(event, originUrl, 302);
});
这样,就可以完成映射,并且直接使用302进行跳转。如果存在解析失败,直接跳转到404的界面。
END
好啦,本次的演示就到这里。或许有小伙伴问,这样把URL变长,有什么用呢?
实际上,确实用处不大,最多也就是隐藏地址内容、隐藏Get请求参数;并且乐趣十足。
不过呢,使用UTF-8数组,确实是一个很精巧的方法,后续其他的算法,也可以进行考虑。
我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表