短信短链接项目中的实践

编程入门 行业动态 更新时间:2024-10-16 20:29:12

<a href=https://www.elefans.com/category/jswz/34/1769965.html style=短信短链接项目中的实践"/>

短信短链接项目中的实践

1、什么是短 URL?

短网址(Short URL),顾名思义就是比较短的 URL 网络地址, 在如今 Web 2.0 的时代,短网址十分得流行,在业界已经有许多短网址生成的服务,使我们可以用各位简短的网址来替代原来十分冗长的网址。让分享的网页链接不会因为太长而引起用户反感,影响体验,使使用者更容易分享哈。

事实上,短网址,也就是短链接在我们生活中随处可见,如微博分享、外卖订单信息、或者如上面的快递短信,短信中就含有一条短网址 。

2、短网址的意义

  • 短信和许多平台(微博)有字数限制:用户每次能接收和发送短信的字符数,是160个英文或数字字符,或者70个中文字符。
  • 简介美观(用户友好)。节省网址长度,便于社交化传播。短链接URL更短小,传播更方便。尤其是URL中有中文和特殊字符时,短网址解决了长链接URL难以记忆、不利于传播的问题。
  • 统计需要(网页流量统计、点击统计等)
  • 安全。 不暴露访问参数。规避关键词、域名屏蔽手段、隐藏真实地址,适合做付费推广链接。

3、使用短链接的一些典型场景

3.1 字数限制场景

1) 微博内容

我们在新浪微博上发布的内容包含长链接网址的时候,微博服务会自动判别出长链接网址,并将其转换为短链接网址。

这是因为新浪微博限制字数为140字一条。如果我们需要发布的内容包含一些链接,但是这些链接非常长,以至于要占用我们内容的一半篇幅、甚至更多,这肯定是不能被允许的,或者说用户体验很差的。此时,就需要将内容里的长链接地址替换为短链接地址。

2)用户短信

一般短信发文有长度限度。如果使用长链接地址,那么一条短信很可能要拆分成两三条发,本来一条一毛的短信费变成了两三毛,直接提升了几倍的花费。另外,使用短链接在内容排版上也更为美观。

3.2 短链接二维码

二维码核心解决的是跨平台、跨现实的数据传输问题,我们经常需要将链接转成二维码的形式分享给他人。使用长链接网址生成的二维码,码点密集复杂,难以识别。使用短链接生成的二维码,码点稀疏清晰,就不存在这个问题了。

3.3 无法识别长链接场景

在有些平台上,长链接地址无法自动识别为完整的超链接,只能识别一部分url地址,甚至根本无法识别。譬如,在钉钉、企微上,长链接地址通常只能被识别一部分,而不是完整的链接地址。

4、短网址的原理

4.1 短网址是如何生成的呢?

短网址服务是如何将那么多的长网址对应到相应的短网址呢?

短网址通常结构如下:域名/短网址id。

短网址 id 其通常由 26 个大写字母 + 26 小写字母 +10 个数字 即 62 种字符组成,随机生成 6 到 7 个,然后组成对应一个 短网址 id,并存入相应的数据存储服务中。

当短网址被访问的时候,短网址的解析服务,会根据 id 查询到对应页面从而实现相应的跳转。

原理:打开短链接的时候,通过 302 的方式,即临时重定向的方式进行跳转

为什么要用62进制转换?

  • 62进制转换是因为62进制转换后只含数字+小写+大写字母。而64进制转换会含有/,+这样的符号(不符合正常URL的字符)
  • 10进制转62进制可以缩短字符,如果我们要6位字符的话,已经有560亿个组合了。
    示例:转换如下的url为对应的短链接

http://localhost:8080/rabbitmq/delayMsg?msg=sendLongMsg&delayTime=10000

地址对应的主键id:假设为100000

通过十进制转换为62进制:q0U

转换地址:.html

ID自增后,转成62进制,在DB保存映射关系,生成短链接。

http://localhost:8080/q0U,通过q0U 找到对应的长链接地址,重定向跳转。

4.2 如何保证短网址 id 不重复

事实上,假如短网址 id 为 6 位,那就是共有 2^62 个短网址。超过这个数目的网页可能性并不大。但在生成即发放短网址的时候,需要保证能够发送不重复的短网址 id。

为了保证不冲突和重复,大多数短网址服务都会采用自增的方式来分发 id,如第一个使用这个服务的人得到的短地址是 http://xxx/0 ,第11个是 http://xxx/a 等依次生成。

对于大多数小型的短网址服务,直接使用 mysql 的自增索引就可以保证不冲突,但这种方式不太适合大型的应用。因为每次操作都需要涉及数据库的增删的资源损耗。因此对于一些大型应用,我们可以通过一些分布式 key-value 系统做短网址的分发。同时不停的自增就可以来。

4.3 如何分布式生成不重复的短网址?

如果生成短网址的服务是分布式的(用户量很大,只有一台生成一台不够用,如天猫、新浪微博),那么每个服务节点要保持同步自增,而不起冲突。是怎么做的呢?

事实上我们可以这样做。加入我们要实现有 5 台分布的短网址服务,此时我们让:

服务 1,从 1 开始发放,然后每次自增 5 即 1、6、11、16…

服务 2,从 2 开始发放,然后每次自增 5 即 2、7、12、17…

服务 3,从 3 开始发放,然后每次自增 5 即 3、8、13、18…

服务 4,从 4 开始发放,然后每次自增 5 即 4、9、14、19…

服务 5,从 5 开始发放,然后每次自增 5 即 5、10、15、20…

这样每个分布的服务都能够独立工作,从而互不干扰。从而实现分布式发放。

5、短链接服务实现

5.1 实现步骤

  • 将长链接通过一定的手段生成一个短链接
    • 生成短路径PATH:利用放号器,初始值为0,对于每一个短链接生成请求,都递增放号器的值,再将此值转换为62进制(a-zA-Z0-9),这个62进制值即为短URI。比如第一次请求时放号器的值为0,对应62进制为a,第二次请求时放号器的值为1,对应62进制为b,第10001次请求时放号器的值为10000,对应62进制为sBc。
    • 短链接服务域名与短URL PATH拼接:将短链接服务器域名与短路径PATH进行字符串连接,即为短链接的URL,比如:t/sBc。
  • 保存短链接与长链接的关系到数据库
  • 访问短链接时实际访问的是短链接服务器,然后根据短链接的参数找回对应的长链接
  • 短链接跳转
    • 短链接服务器返回302状态码,将响应头中的Location设置为长链接
    • 浏览器访问长链接
    • 业务服务器响应

5.2 数据库设计

    CREATE TABLE `short_url` (`id` bigint(20) unsigned NOT NULL COMMENT '主键ID',`short_url_code` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '短链接编码',`long_url` varchar(256) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '长链接url',`long_url_md5` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '长链接url md5值',`delete_flag` tinyint(2) NOT NULL DEFAULT '0' COMMENT '删除标记,0未删除  1已删除',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` timestamp NULL DEFAULT NULL COMMENT '更新时间',`create_user_code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人',`update_user_code` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人',`version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',`remark` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',`click_count` bigint(20) NOT NULL DEFAULT '0' COMMENT '链接点击数',PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

5.3 进制转换工具和主键id的生成

进制转换工具:

    package com.sflx.shortmessage.util;/*** 进制转换工具,最大支持十进制和62进制的转换* 1、将十进制的数字转换为指定进制的字符串* 2、将其它进制的数字(字符串形式)转换为十进制的数字*/public class NumericConvertUtils {/*** 在进制表示中的字符集合,0-Z分别用于表示最大为62进制的符号表示*/private static final char[] digits = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm','n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M','N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z','0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};/*** 将十进制的数字转换为指定进制的字符串* @param number 十进制的数字* @param seed   指定的进制* @return 指定进制的字符串*/public static String toOtherNumberSystem(long number, int seed) {if (number < 0) {number = ((long) 2 * 0x7fffffff) + number + 2;}char[] buf = new char[32];int charPos = 32;while ((number / seed) > 0) {buf[--charPos] = digits[(int) (number % seed)];number /= seed;}buf[--charPos] = digits[(int) (number % seed)];return new String(buf, charPos, (32 - charPos));}/*** 将其它进制的数字(字符串形式)转换为十进制的数字* @param number 其它进制的数字(字符串形式)* @param seed   指定的进制,也就是参数str的原始进制* @return 十进制的数字*/public static long toDecimalNumber(String number, int seed) {char[] charBuf = number.toCharArray();if (seed == 10) {return Long.parseLong(number);}long result = 0, base = 1;for (int i = charBuf.length - 1; i >= 0; i--) {int index = 0;for (int j = 0, length = digits.length; j < length; j++) {//找到对应字符的下标,对应的下标才是具体的数值if (digits[j] == charBuf[i]) {index = j;}}result += index * base;base *= seed;}return result;}public static void main(String[] args) {/*** 10进制:100000  62进制:Aa4* 62进制:Aa4  10进制:100000** 10进制:100001  62进制:Aa5* 62进制:Aa5  10进制:100001** 10进制:100002  62进制:Aa6* 62进制:Aa6  10进制:100002** 10进制:100003  62进制:Aa7* 62进制:Aa7  10进制:100003*/for (int i = 100000; i <100010 ; i++) {//10进制//62进制String convertedNumStr = NumericConvertUtils.toOtherNumberSystem(i, 62);//10进制转化为62进制System.out.println("10进制:" + i + "  62进制:" + convertedNumStr);//TODO 执行具体的存储操作,可以存放在Redis等中//62进制转化为10进制System.out.println("62进制:" + convertedNumStr + "  10进制:" + NumericConvertUtils.toDecimalNumber(convertedNumStr, 62));System.out.println();}}}  

雪花算法生成id:

   package com.sflx.shortmessage.util;import org.springframework.stereotype.Component;/*** 使用SnowFlake算法生成一个整数,然后转化为62进制,变成一个短地址URL的PATH*/public class SnowFlake {/*** 起始的时间戳*/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;  //上一次时间戳/*** 根据指定的数据中心ID和机器标志ID生成指定的序列号* @param dataCenterId 数据中心ID* @param machineId    机器标志ID*/public SnowFlake(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;}/*** 产生下一个ID* @return*/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 {//不同毫秒内,序列号置为0sequence = 0L;}lastTimeStamp = currTimeStamp;return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分| dataCenterId << DATA_CENTER_LEFT       //数据中心部分| machineId << MACHINE_LEFT             //机器标识部分| sequence;                             //序列号部分}private long getNextMill() {long mill = getNewTimeStamp();while (mill <= lastTimeStamp) {mill = getNewTimeStamp();}return mill;}private long getNewTimeStamp() {return System.currentTimeMillis();}public static void main(String[] args) {/*** 10进制:771450362589884416  62进制:49pIpAvQfk* 62进制:49pIpAvQfk  10进制:771450362589884416** 10进制:771450362594078720  62进制:49pIpANrno* 62进制:49pIpANrno  10进制:771450362594078720** 10进制:771450362594078721  62进制:49pIpANrnp* 62进制:49pIpANrnp  10进制:771450362594078721** 10进制:771450362594078722  62进制:49pIpANrnq* 62进制:49pIpANrnq  10进制:771450362594078722*/SnowFlake snowFlake = new SnowFlake(2, 3);for (int i = 0; i < (1 << 4); i++) {//10进制Long id = snowFlake.nextId();//62进制String convertedNumStr = NumericConvertUtils.toOtherNumberSystem(id, 62);//10进制转化为62进制System.out.println("10进制:" + id + "  62进制:" + convertedNumStr);//TODO 执行具体的存储操作,可以存放在Redis等中//62进制转化为10进制System.out.println("62进制:" + convertedNumStr + "  10进制:" + NumericConvertUtils.toDecimalNumber(convertedNumStr, 62));System.out.println();}}}

配置:

    @Beanpublic SnowFlake snowFlake() {return new SnowFlake(1, 1);}

5.4 短链接生成

    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.sflx.shortmessage.entity.ShortUrl;import com.sflx.shortmessage.service.ShortUrlService;import com.sflx.shortmessage.util.NumericConvertUtils;import com.sflx.shortmessage.util.SnowFlake;import lombok.extern.slf4j.Slf4j;import org.apache.tomcat.util.security.MD5Encoder;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import java.math.BigInteger;import java.security.MessageDigest;import java.util.List;import java.util.UUID;@RestController@Slf4jpublic class ShortUrlController {@Resourceprivate ShortUrlService shortUrlService;@Resourceprivate SnowFlake snowFlake;/*** 根据ID获取** @param id* @return*/@GetMapping("/shortUrl/getById")public ShortUrl getById(@RequestParam("id") Long id) {ShortUrl shortUrl = shortUrlService.getById(id);return shortUrl;}/*** 模拟的长链接地址** @param shortUrl* @return*/@GetMapping("/shortUrl/list")public List<ShortUrl> list(ShortUrl shortUrl) {List<ShortUrl> list = shortUrlService.list(new LambdaQueryWrapper<ShortUrl>().eq(ShortUrl::getId, shortUrl.getId()));return list;}private static final String url = "localhost:8081/a/";@PostMapping("/shortUrl/save")public String save() {ShortUrl shortUrl = new ShortUrl();//生成对应的主键long id = snowFlake.nextId();//转换为对应的短链接地址 10进制转换为62进制String code = NumericConvertUtils.toOtherNumberSystem(id, 62);shortUrl.setId(id);shortUrl.setShortUrlCode(code);String longUrl = generateLongUrl();shortUrl.setLongUrl(longUrl);MessageDigest md5 = null;try {md5 = MessageDigest.getInstance("md5");byte[] digest = md5.digest(longUrl.getBytes("utf-8"));//16是表示转换为16进制数String md5Str = new BigInteger(1, digest).toString(16);shortUrl.setLongUrlMd5(md5Str);} catch (Exception e) {log.error("md5加密异常:{}", e.getMessage());}shortUrlService.save(shortUrl);//发送短信:包含短链接String linkUrl = url + code;log.info("短信短链接:{}", linkUrl);return linkUrl;}/*** http://localhost:8081/shortUrl/list?id=1&shortUrlCode=1&longUrlMd5=xxxx&remark=111322&createUserCode=xxx** @return*/private static final String listUrl = "http://localhost:8081/shortUrl/list?id=1&";/*** 随机生成对应的长链接地址** @return*/public String generateLongUrl() {StringBuilder builder = new StringBuilder(listUrl);String uuid = UUID.randomUUID().toString().substring(16);builder.append("&shortUrlCode=" + uuid);builder.append("&longUrlMd5=" + uuid);builder.append("&remark=" + uuid);builder.append("&createUserCode=" + uuid);return builder.toString();}}

5.5 短链接访问

@Controller
@Slf4j
public class ShortUrlRedirectController {@Resourceprivate ShortUrlService shortUrlService;/*** 访问短链接重定向到长链接* 接口需要开通白名单** @param shortUrlCode* @return*/@GetMapping("/a/{shortUrlCode}")public void redirectLongURL(@PathVariable("shortUrlCode") String shortUrlCode, HttpServletResponse response) {ShortUrl shortUrl = shortUrlService.getOne(new LambdaQueryWrapper<ShortUrl>().eq(ShortUrl::getShortUrlCode, shortUrlCode).last("limit 1"));if (shortUrl == null) {throw new RuntimeException("链接不存在");}// 增加短链点击次数shortUrl.setClickCount(shortUrl.getClickCount() + 1);shortUrlService.updateById(shortUrl);try {//重定向到长链接response.sendRedirect(shortUrl.getLongUrl());} catch (IOException e) {log.error("重定向异常:{}", e.getMessage());}}
}

5.6 验证测试

save方法:com.sflx.shortmessage.controller.ShortUrlController#save

http://localhost:8081/shortUrl/save

返回:localhost:8081/a/49CZd5BPvU
访问:http://localhost:8081/a/49CZd5BPvU,重定向到长链接地址

6、短链接服务提供平台

目前,国内有很多提供短链接服务的平台,例如:

  • 新浪:/
    • 一定时间内,同样的网址生成的短网址都是一样的。且支持短网址后缀选择。
  • 百度:/
  • 0x3:/
  • MRW:/
    需要注意的是,如果使用某一个平台的短地址服务,一定要保证长期可靠的服务,不然一段时间失效了,我们以前转换得到的短链接地址就没法访问了!

附代码地址:

更多推荐

短信短链接项目中的实践

本文发布于:2024-03-07 01:55:27,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1716541.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:短信   链接   项目

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!