本文最后更新于 2023年9月17日 上午
作者:Mintimate
博客:https://www.mintimate.cn
Mintimate’s Blog,只为与你分享
短链接 什么是短链接 短链接,又称缩略网址服务、缩址、短址、短网址、缩略网址、网址缩短、缩短网址、URL缩短等,指的是一种互联网上的技术与服务。此服务可以提供短URL以代替原来可能较长的URL,将长的URL地址缩短。
用户访问缩短后的URL时,通常将会重定向到原来的URL。
为什么用短链接 使用短链接,主要的场景有:
Twitter、微博等平台,消息字数限制,使用短链接对原有链接缩短。
隐藏Get、PATH参数。
……
实例演示 有些小伙伴可能还是没有概念,这里举个腾讯云自带的短链接。比如腾讯云服务器限时秒杀活动的链接是:
1 https://cloud.tencent.com/act/cps/redirect ?redirect =1077&cps_key =&from =console
而腾讯云给的短链接:
可以看到,链接有效地缩短了。同时,已经看不到PATH
和Get
参数。用户访问短链接,会自动301/302跳转到原链接:
实现思路 其实实现的思路很简单,我们生成一个短链接,大概的思路是传入原链接,在后台进行处理后,得到一个唯一识别码,一同存入数据库,最后再把这个唯一识别码回显给用户 。 得到短链接后,用户发给其他用户进行访问时,后台根据这个识别码,再进行数据库查询,最后重定向到原链接即可 :
所以,其实实现很简单,要点:
生成唯一识别码,对应链接,且识别码要短。
后台301/302重定向跳转。
使用Java作为后台API服务,处理上面两点很简单:
雪花ID转换为六十二进制,得到短的识别码。
使用RedirectView
设置响应头,并重定向链接。
本文以Java(Springboot)为例,其他编程语言可以按图索骥。
唯一识别码 每次后台接收前台的响应,则生成一个识别码存储到数据库,已备后续调取重定向。
这个识别码最好与时间戳有关,同时,如果有多个服务器同时组网,这个识别码最好还要加上机械识别码。
综上,我们可以使用雪花ID,但是雪花ID最为一个Long类型,转换为int类型有19位,肯定是太长了,所以,我们还需要转码为六十二进制。
雪花ID 雪花算法(Snowflake) 是一种生成分布式全局唯一ID的算法,生成的ID称为Snowflake IDs或snowflakes。这种算法由Twitter创建,并用于推文的ID。Discord和Instagram等其他公司采用了修改后的版本。 一个雪花ID:
前41位是时间戳
之后10位代表计算机ID
其余12位代表每台机器上生成ID的序列号
参考代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 public class SnowFlakeShortUrl { private final static long START_TIMESTAMP = 1480166465631L ; private final static long SEQUENCE_BIT = 12 ; private final static long MACHINE_BIT = 5 ; private final static long DATA_CENTER_BIT = 5 ; private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT); private final static long MACHINE_LEFT = SEQUENCE_BIT; private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT; private long dataCenterId; private long machineId; private long sequence = 0L ; private long lastTimeStamp = -1L ; private long getNextMill () { long mill = getNewTimeStamp(); while (mill <= lastTimeStamp) { mill = getNewTimeStamp(); } return mill; } private long getNewTimeStamp () { return System.currentTimeMillis(); } public SnowFlakeShortUrl (long dataCenterId, long machineId) { if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0 ) { throw new IllegalArgumentException ("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!" ); } if (machineId > MAX_MACHINE_NUM || machineId < 0 ) { throw new IllegalArgumentException ("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!" ); } this .dataCenterId = dataCenterId; this .machineId = machineId; } public synchronized long nextId () { long currTimeStamp = getNewTimeStamp(); if (currTimeStamp < lastTimeStamp) { throw new RuntimeException ("Clock moved backwards. Refusing to generate id" ); } if (currTimeStamp == lastTimeStamp) { sequence = (sequence + 1 ) & MAX_SEQUENCE; if (sequence == 0L ) { currTimeStamp = getNextMill(); } } else { sequence = 0L ; } lastTimeStamp = currTimeStamp; return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | dataCenterId << DATA_CENTER_LEFT | machineId << MACHINE_LEFT | sequence; } public static void main (String[] args) { SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl (2 , 3 ); for (int i = 0 ; i < (1 << 4 ); i++) { System.out.println(snowFlake.nextId()); } } }
当然,如果你用使用Mybatis Plus,可以引用Mybatis Plus的IdWorker.getId
方法,生成雪花ID。生成后的Long类型,我们使用十进制展开,应该是一个17-19位的数字。
六十二进制 因为雪花ID通过十进制展开是一个17-19位的数字,如果直接用来当作短链接,太长了点,我们需要对其缩短。
为了保证唯一,且可对照。我们转换为六十二进制。原因很简单:六十二进制使用A-Z、a-z和0-9组成。 把十进制,转换为六十二进制,能有效减短长度。 根据Wiki-Base62 ,六十二进制中0-61对应0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
。所以,我们编写编码和解码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 private static String CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ; private static int SCALE = 62 ; private static String REGEX = "^[0-9a-zA-Z]+$" ; public static String encode10To62 (long val) { if (val < 0 ) { throw new IllegalArgumentException ("this is an Invalid parameter:" + val); } StringBuilder sb = new StringBuilder (); int remainder; while (Math.abs(val) > SCALE - 1 ) { remainder = Long.valueOf(val % SCALE).intValue(); sb.append(CHARS.charAt(remainder)); val = val / SCALE; } sb.append(CHARS.charAt(Long.valueOf(val).intValue())); return sb.reverse().toString(); } public static long decode62To10 (String val) { if (val == null ) { throw new NumberFormatException ("null" ); } if (!val.matches(REGEX)) { throw new IllegalArgumentException ("this is an Invalid parameter:" + val); } String tmp = val.replace("^0*" , "" ); long result = 0 ; int index = 0 ; int length = tmp.length(); for (int i = 0 ; i < length; i++) { index = CHARS.indexOf(tmp.charAt(i)); result += (long )(index * Math.pow(SCALE, length - i - 1 )); } return result; }
再测试一下:
1 2 3 4 5 6 7 8 9 public static void main (String[] args) { Long snow = IdWorker.getId(); System.out.println(snow); String str = encode10To62(snow); System.out.println(str); Long g = decode62To10(str); System.out.println(g); }
输出:
1 2 3 1425664925648310274 1hJYkVByV0M 1425664925648310274
响应头 重定向链接,响应头很重要。Nginx内可以使用配置直接跳转301/302,比如强制HTTPS:
1 2 3 if ($server_port !~ 443) { rewrite ^(/.*)$ https://$host $1 permanent ; }
而我们搭建短链接平台,也利用301或者302进行重定向:
301/302 301和302都是重定向,那它们的区别是什么呢?
301:永久重定向,在请求的URL已被移除时使用,响应的location首部中应包含资源现在所处的URL
302:临时重定向,和永久重定向类似,客户端应用location给出URL临时定位资源,将来的请求仍为原来的URL。
实际场景里,301在跳转后,浏览器会记住这个跳转,后续请求,不再请求原地址,而是直接请求新地址;所以301一般用于网站域名的迁移,强制网站https等,而302一般是网站维护,需要临时跳转到非维护页面等情况。
那我们搭建短链接平台,需要什么重定向呢?我认为是都可以。使用301重定向,可以减少服务器负载,而使用302重定向,可以方便我们统计链接实际调取次数。
Java内,进行301/302的跳转,其实很简单,使用类RedirectView
,其中的HttpStatus
即可:
1 2 3 4 5 6 # RedirectView类RedirectView redirectView = new RedirectView (fullURL); # 301 跳转 redirectView.setStatusCode(HttpStatus.MOVED_PERMANENTLY); # 302 跳转 redirectView.setStatusCode(HttpStatus.MOVED_TEMPORARILY);
实际上,看HttpStatus
的源码,可以看到这里枚举了很多HTTP的响应头:
Maven部署(代码实现) 最后,我们看看实际部署和代码实现。只是随便提供思路,代码可能有逻辑不严谨地方嗷。
本次使用MariaDB作为数据库,使用Mybatis Plus对数据库进行操作,Springboot提供框架并方便打包。
依赖包 首先,我们创建一个工程,其中Lombok是为了方便实体类生成Set/Get方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.mariadb.jdbc</groupId > <artifactId > mariadb-java-client</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.0</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > </dependencies >
实体类 我们看看短链接实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Data @NoArgsConstructor public class ShortUrl { @TableId private Long id; private String baseUrl; private String suffixUrl; private String fullUrl; private String shortCode; @TableField(fill = FieldFill.INSERT) private Integer totalClickCount; @TableField(fill = FieldFill.INSERT) private Date expirationDate; public ShortUrl (String baseUrl, String suffixUrl, String fullUrl) { this .baseUrl = baseUrl; this .suffixUrl = suffixUrl; this .fullUrl = fullUrl; } }
其中:
baseUrl:用户提供的原链接域名,如:tool.mintimate.cn
。
suffixUrl:用户提供链接的参数,如:/user?login=yes
。
fullUrl:用户提供的原链接,如:https://tool.mintimate.cn/curl
。
shortCode:生成的短链接。
totalClickCount:统计点击次数(Hander
自动设置默认值)
expirationDate:失效时间(Hander
自动设置默认值)
短链接处理 首先,做一个控制器,用来接收用户请求:
1 2 3 4 5 6 7 8 9 10 @ResponseBody @PostMapping(value = "/add") public ShortUrl encodeURL (@RequestParam(value = "UserURL") String UserURL) { String Domain = DomainUntil.getDomain(UserURL); if (Domain==null ){ return null ; } return shortUrlService.saveURL(UserURL); }
之后,看看业务层,我们需要对域名进行加工,先得到一个雪花ID,再对其转至六十二进制,并回显:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Resource ShortUrlMapper shortUrlMapper;@Override public ShortUrl saveURL (String UserURL) { ShortUrl shortUrl=new ShortUrl (DomainUntil.getTopDomain(UserURL),DomainUntil.getFile(UserURL),UserURL); Long ID= IdWorker.getId(shortUrl); String Short_URL_CODE = DecimalAndSixtyBinary.encode10To62(ID); shortUrl.setShortCode(Short_URL_CODE); int code=shortUrlMapper.insert(shortUrl); return shortUrl; }
这个时候,我们使用Postman来测试一下: 可以看到,测试成功。
短链接重定向 短链接重定向,就很简单了。我们写一个请求即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ResponseBody @RequestMapping(value = "/{UrlID}") public RedirectView reverseURL (@PathVariable(value = "UrlID") String UrlID) { String fullURL=shortUrlService.findURL(UrlID); if (fullURL==null ){ RedirectView redirectView = new RedirectView ("/error/404" ); redirectView.setStatusCode(HttpStatus.NOT_FOUND); return redirectView; } else { RedirectView redirectView = new RedirectView (fullURL); redirectView.setStatusCode(HttpStatus.MOVED_PERMANENTLY); return redirectView; } }
其中,shortUrlService
的 findURL
就是简单的JDBC查询,不具体实现。
Demo 我根据上述思路,初略搭建一个Demo,并部署在了腾讯云轻量应用服务器(不得不说,个人开发者,使用轻量应用服务器实在是太方便了,而且性价比极高):
前端:基于Vue,使用element ui和Bootstrap
后端:Springboot
我们可以在Linux/macOS上使用curl
测试一下,比如直接用腾讯云轻量应用服务器的Linux远程终端:
1 curl - I "https://curl.mintimate.ml/1Hjsg8wDe8i"
完善思路 可以看到,文章实现的有些粗糙,提供以下完善思路:
限制单IP一段时间的请求频率:目前我是使用前端Vue进行控制,但是最好后端也进行控制。
数据库优化:目前使用的是MariaDB,如果要更好的体验,或者响应数据量大,使用Redis会更好。
Cron定时任务:使用雪花ID转六十二进制,在链接长度上,还是有点长,但是安全性应该是很高的;如果降低安全性,并进一步缩短长度,可以创建Cron定时线程,无效旧的短链接。