system desing 系统设计(四):网站API和短网址short url的生成
1、(1)逆向APP时,第一个干的事就是抓包了,抓包的结果类似下面:
GET https://aweme.snssdk.com/aweme/v1/commit/item/digg/?aweme_id=6956180208793718055&type=1&channel_id=-1&city=510100&activity=0&os_api=22&device_type=M973Q&ssmix=a&manifest_version_code=150501&dpi=480&uuid=865166023654745&app_name=aweme&version_name=15.5.0&ts=1620296753&cpu_support64=false&app_type=normal&appTheme=light&ac=wifi&host_abi=armeabi-v7a&update_version_code=15509900&channel=xiaomi&_rticket=1620296754623&device_platform=android&iid=2744832902828125&version_code=150500&cdid=7addad62-d097-41da-b852-e0de8b24fe75&openudid=338803367b93a120&device_id=70039083151&resolution=1080*1920&os_version=5.1.1&language=zh&device_brand=Meizu&aid=1128&minor_status=0&mcc_mnc=46000 HTTP/1.1 Accept-Encoding: gzip x-tt-dt: AAA7NE2KX2XRH5ZIBMIIJHUGNWEXQLPT2G5U2VKAPRSBVVGBNOLZGYT36N7VMCWTDFTQBQXBW6FSC2IWBD36QSIH6MSUI2SKDMHF4T27M5MS6TL2XHUCXVFWWDJM2Y3HQZEHXQ42UCB7JUEHURQRH6Y passport-sdk-version: 18 X-Tt-Token: 000e8f473aea647b3c44fd3b9f7482439e02da23d858b1609a77f5edbf3bcc6c774f39175ea27eed09ffb2dee02c173fcc137b53019bb3614db48a84e88879cf2cabc8a810d38b88fb1d9bc7640b02e655a186f8b1e0daf197e60067b35a68adc68ad-1.0.1 sdk-version: 2 X-SS-REQ-TICKET: 1620296754625 Cookie: passport_csrf_token_default=f4442957bc07adf1986f4df139296dd7; install_id=2744832902828125; ttreq=1$184784690dc292ea1ad4e5db65553166a5674c3e; multi_sids=95063141447%3A0e8f473aea647b3c44fd3b9f7482439e; odin_tt=fac32cc300e49d8f22a70f1cd7b92569039d7305b6fdef7f2389cb98e0f9e9493f6c8374ad33dabed24ea41c55e1834e; n_mh=B6WRe0yd-1qIuffF6ZWNO-CSGlW1Q-VhC0E79NrqYTg; sid_guard=0e8f473aea647b3c44fd3b9f7482439e%7C1620296607%7C5184000%7CMon%2C+05-Jul-2021+10%3A23%3A27+GMT; uid_tt=d06498fc7329b7187e209d0e6279e1d7; sid_tt=0e8f473aea647b3c44fd3b9f7482439e; sessionid=0e8f473aea647b3c44fd3b9f7482439e; X-Ladon: gW1IO3ulq0eYymnbYa+7nAu2l116ADdAdmSIA1eB3Cv5BBIo X-Khronos: 1620296754 X-Gorgon: 0404401f40051f42c987ec09aebf8038d629adf2add584933f8f X-Tyhon: s2S+nKZ8jrepKJ2VtCulu7carp/ZLqe2gn24aHA= X-Argus: Uictv+neuH8ZqjOjXzvEIvXCEXYEgUy3dUKWzj08JUtmGeYa4HfNfN8bp7Yga22Jbik2N2dBKezA48YN1E9A1KaBjXi2ixu5cHzAkQU9Tl4f/+a9xWuLYIZ/+PkW/YXUsmRCfszWlcMePPeyNQYEGkb5FssTkIr3EZ87TJ9NLBDJCJGA0i4PICbLnClzdfmBs+57JVi1sU2/MELCr9gO/Nka5eIJsEoGB/CIaL3gPmEbZ0sUl7sxIvKksMwj3f7tYBCriYBKmfeREuiYf1S18c7i Host: aweme.snssdk.com Connection: Keep-Alive User-Agent: okhttp/3.10.0.1
client和server通信靠的全是这类API的方式:要么是GET,要么是POST!后台又是怎么生成这些API接口的了?还是要靠springboot!文章末尾参考1有个工程,把unidbg和springboot结合起来了,用起来很方便,demo如下:
package com.anjia.unidbgserver.web; import com.anjia.unidbgserver.service.TTEncryptServiceWorker; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * 控制类 * * @author AnJia * @since 2021-07-26 18:31 */ @Slf4j @RestController @RequestMapping(path = "/api/tt-encrypt", produces = MediaType.APPLICATION_JSON_VALUE) public class TTEncryptController { @Resource(name = "ttEncryptWorker") private TTEncryptServiceWorker ttEncryptServiceWorker; /** * 获取ttEncrypt * <p> * public byte[] ttEncrypt(@RequestParam(required = false) String key1, @RequestBody String body) * // 这是接收一个url参数,名为key1,接收一个post或者put请求的body参数 * key1是选填参数,不写也不报错,值为,body只有在请求方法是POST时才有,GET没有 * * @return 结果 */ @SneakyThrows @RequestMapping(value = "encrypt", method = {RequestMethod.GET, RequestMethod.POST}) public byte[] ttEncrypt(String key,String body1) { String key1 = key; String body = body1; /*String key1 = "key1"; String body = "body";*/ // 演示传参 http://127.0.0.1:9999/api/tt-encrypt/encrypt?key=aaaa&body1=bbbbb byte[] result = ttEncryptServiceWorker.ttEncrypt(key1, body).get(); log.info("入参:key1:{},body:{},result:{}", key1, body, result); return result; } }
类上面加了一个 @RequestMapping(path = "/api/tt-encrypt", produces = MediaType.APPLICATION_JSON_VALUE) 的注释,表明请求的API中,凡是有“api/tt-encrypt”路径的,都在这个类内部的方法中处理;再看类中有个方法叫ttEncrypt的,上面也带了@RequestMapping(value = "encrypt", method = {RequestMethod.GET, RequestMethod.POST})这样的注释,说明这个方法的访问路径是“/api/tt-encrypt/encrypt”,访问的方法可以是GET,也可以是POST!所以如果client想调用server的ttEncrypt方法,windows下完整的路径如为:http://127.0.0.1:9999/api/tt-encrypt/encrypt?key=aaaa&body1=bbbbb ,这样server就会执行ttEncrypt方法,然后把执行结果反回给client,是不是很方便了? 正式如此,spring相关的“脚手架”一跃成为java应用层开发的“一哥”! 在浏览器调用API接口时,能看到的日志如下:
2022-08-16 16:19:59.054 INFO 9280 --- [nio-9999-exec-8] c.a.u.web.TTEncryptController : 入参:key1:aaaa,body:bbbbb,result:[116, 99, 3, 0, 0, 1, -97, -48, -122, 106, -96, -53, -48, 50, 57, 51, -46, -46, -4, -116, 32, -20]
(2)这种API接口的设计,业界最流行的莫过于rest风格了!这种风格的接口有几个要点:
- 变化的部分做成参数,不变的部分做成路径!比如“POST /api/accounts/1/transfer/500/to/2/”就是错误的示范,正确的应该是这样:“POST /api/transaction/?from=1&to=2&money=500”;其实站在代码角度也容易理解:URL的路径最终会变成函数的路径,这部分代码写死后就固定了,没法变,能变的只能是参数了!这样做成松耦合的形式,也方便后续扩展!
- 常见的method除了GET、POST,还有DELETE、PUT等,建议都在@RequestMapping(value = "encrypt", method = {RequestMethod.GET, RequestMethod.POST})这类注释这里指定,而不是直接写死在URL里面:比如“/api/accounts/1/delete/” 这样就是错的,应该是“DELETE /api? account = 1”才对!
2、(1)大家平时常用的网址都很长,为了方便传输和使用【比如跨设备优化链接、跟踪单个链接以分析受众和活动绩效,以及隐藏关联的原始URL等】,短网址生成服务孕育而生(weibo这类对用户发帖长度有限制的站点尤为常见),其实原理也很简单:用户提供long url长网址,有专门的服务用特定的算法把long url转换short url!当用户访问short url时,转换服务会根据short url把long url找到,然后返回给client,并提示client需要301 redirect!过程图示如下:
设计这种长、短网址转换有两个最核心的点:
- server端的URL table的schema该怎么设计? 换句话说:长、短网址该怎么存储?存储的形式直接决定了读写的效率, 也就是QPS!
- long url怎么转成short url了?转换的算法间接决定了服务的效率!
- short URL的长度多少合适了?short url取值是0-9、a-z、A-Z,一共62个字符;如果short url长度是5个字符,那么可以得到总的url数量就是62^5 = 9亿; 依次类推: 62^6 = 570亿;62^7=35000亿; 很明显:5位太少了,7位又太多了,6位刚好合适!当然,考虑到未来的扩展性,也可以选择7位的short url(毕竟有之前的IPV4经验教训,考虑远点也是对的)!每个url多一个字符,额外也耗费不了多少存储空间,对效率的影响并不明显;
(2)先来看看转换的算法,业界主要有两种:随机生成和进制转换!
(2.1) 随机转换:从逻辑上讲,long url和short url并无直接的关联关系,怎么匹配完全可以开发人员自己确定,所以最直接、简单、粗暴的方法是随机生成一个字符串就能做为short url返回到client!注意:因为short url是随机生成的,返回给client前肯定要去重,所以理论上讲:short url生成的越多,去重的耗时越长,整个系统的效率就越低!
(2.2)进制转换:上面第一种生成随机数的方法越往后效率越低,核心原因就是short url多了以后去重很耗时,怎么改进这个了? 既然生成的随机数不可控,那改成可控的自增数(auto increment sequential ID)的是不是就能解决去重耗时的这个问题了?显然是可行的!唯一的问题就是怎么把自增数转成0-9、a-z、A-Z组成的url了? 本质上是个62进制转换的问题了!计算过程说明如下:
- 先确定映射表,比如这样的:
char
map[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.toCharArray();
- 假如id= 12345,那么:
- 最后一个字符的offset就是12345%62=7,map[7]=h;
- 到数第二个字符:123456/62=199, 199%62=13,map[13]=n
- 到数第三个字符:199/62=3, 3%62=3,map[3]=d
所以12345转成short url的结果就是dnh(当然不足6位的可以用0补齐)!代码如下:
// Java program to generate short url from integer id and // integer id back from short url. import java.util.*; import java.lang.*; import java.io.*; class GFG { // Function to generate a short url from integer ID static String idToShortURL(int n) { // Map to store 62 possible characters char map[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray(); StringBuffer shorturl = new StringBuffer(); // Convert given integer id to a base 62 number while (n > 0) { // use above map to store actual character // in short url shorturl.append(map[n % 62]); n = n / 62; } // Reverse shortURL to complete base conversion return shorturl.reverse().toString(); } // Function to get integer ID back from a short url static int shortURLtoID(String shortURL) { int id = 0; // initialize result // A simple base conversion logic for (int i = 0; i < shortURL.length(); i++) { if ('a' <= shortURL.charAt(i) && shortURL.charAt(i) <= 'z') id = id * 62 + shortURL.charAt(i) - 'a'; if ('A' <= shortURL.charAt(i) && shortURL.charAt(i) <= 'Z') id = id * 62 + shortURL.charAt(i) - 'A' + 26; if ('0' <= shortURL.charAt(i) && shortURL.charAt(i) <= '9') id = id * 62 + shortURL.charAt(i) - '0' + 52; } return id; } // Driver Code public static void main (String[] args) throws IOException { int n = 12345; String shorturl = idToShortURL(n); System.out.println("Generated short url is " + shorturl); System.out.println("Id from url is " + shortURLtoID(shorturl)); } } // This code is contributed by shubham96301
(3)因为要建立long url和short url的映射关系(方便查询),并且要持久化存储在磁盘,所以肯定是要使用数据库的!sql和NoSql怎么选了?挨个分析一下需求:
- 不需要transaction,Nosql+1
- 不需要复杂的sql查询,Nosql+1
- 对于QPS的要求:假设每天有10 milion的long url需要转成short url,那么:
- average write QPS = 10million/86400= 115;考虑到白天可能有峰值,所以peak write QPS = 115*2=230;要求并不高,sql和NoSql都是可以的!
- 理论上讲:read的需求远比write的需求大,这里按照10~20倍估算,也就是average read QPS = 115*10=1150; peak read QPS = 115*20=2300,按照每条206byte计算,peak也才460KB,机械硬盘都能满足;sql和Nosql也是可以的!
- 对于磁盘的容量的要求:还是按照每天10million的需求计算:short url只有6byte,long url平均有200byte,那么存储一条记录需要206byte,每天需要消耗2GB磁盘,每年也才730GB的数据!10年也就消耗不到8TB的磁盘!
综上所述:存放的数据并不多,并且QPS也不高,配置稍微高点的单机服务器(cpu>=24core、内存>=256G、SSD>=12T等)都能满足需求!为了提高QPS,还可以在内存用hashMap缓存mapping关系!假设除去操作系统等必要的开销,还有200G的memory可用,理论上可在memory cache大约970million的mapping关系!由于内存的速度比硬盘快了好多数量级,TPS也能提升好多倍!
如果选用sql型数据库,比如mysql,那么表单schema的设计如下:
当然也可以是short url和long url:
因为sql型数据库可以根据不同的列建索引,所以上面两种schema二选一即可!但是如果用Nosql数据库,由于无法对不同的列建索引,所以可能需要两张表,这里以 Cassandra 为例子:
- 第一张表:根据 Long 查询 Short,row_key=longURL, column_key=ShortURL, value=null or timestamp
- 第二张表:根据 Short 查询 Long,row_key=shortURL, column_key=LongURL, value=null or timestamp
- 还有,由于Nosql数据库没有auto increment sequential ID,代码中需要额外考虑生成全局自增ID。由于自增ID只有在write的时候才需要,并且peak write QPS也才230,通过AtomicLong生成自增ID的压力也不大,完全能满足需求!
参考:
1、https://github.com/anjia0532/unidbg-boot-server unidbg+springboot多线程
2、https://cloud.tencent.com/developer/article/1872330 url短链接设计
3、https://www.geeksforgeeks.org/how-to-design-a-tiny-url-or-url-shortener/ 转换代码