main.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. package main
  2. import (
  3. "crypto/md5"
  4. "encoding/base64"
  5. "encoding/hex"
  6. "flag"
  7. "fmt"
  8. "log"
  9. "math/rand"
  10. "net/http"
  11. "os"
  12. "path"
  13. "time"
  14. "github.com/gin-gonic/gin"
  15. "github.com/gomodule/redigo/redis"
  16. "github.com/sirupsen/logrus"
  17. )
  18. // Response is the response structure
  19. type Response struct {
  20. Code int
  21. Message string
  22. LongUrl string
  23. ShortUrl string
  24. }
  25. // redisPoolConf is the Redis pool configuration.
  26. type redisPoolConf struct {
  27. maxIdle int
  28. maxActive int
  29. maxIdleTimeout int
  30. host string
  31. password string
  32. db int
  33. handleTimeout int
  34. }
  35. // letterBytes is a string containing all the characters used in the short URL generation.
  36. const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  37. // shortUrlLen is the length of the generated short URL.
  38. const shortUrlLen = 7
  39. // defaultPort is the default port number.
  40. const defaultPort int = 8002
  41. // defaultExpire is the redis ttl in days for a short URL.
  42. const defaultExpire = 180
  43. // defaultRedisConfig is the default Redis configuration.
  44. const defaultRedisConfig = "127.0.0.1:6379"
  45. // defaultLockPrefix is the default prefix for Redis locks.
  46. const defaultLockPrefix = "myurls:lock:"
  47. // defaultRenewal is the default renewal time for Redis locks.
  48. const defaultRenewal = 1
  49. // secondsPerDay is the number of seconds in a day.
  50. const secondsPerDay = 24 * 3600
  51. // redisPool is a connection pool for Redis.
  52. var redisPool *redis.Pool
  53. // redisPoolConfig is the Redis pool configuration.
  54. var redisPoolConfig *redisPoolConf
  55. // redisClient is a Redis client.
  56. var redisClient redis.Conn
  57. func main() {
  58. gin.SetMode(gin.ReleaseMode)
  59. router := gin.Default()
  60. // Log 收集中间件
  61. router.Use(LoggerToFile())
  62. router.LoadHTMLGlob("public/*.html")
  63. port := flag.Int("port", defaultPort, "服务端口")
  64. domain := flag.String("domain", "", "短链接域名,必填项")
  65. ttl := flag.Int("ttl", defaultExpire, "短链接有效期,单位(天),默认180天。")
  66. conn := flag.String("conn", defaultRedisConfig, "Redis连接,格式: host:port")
  67. passwd := flag.String("passwd", "", "Redis连接密码")
  68. https := flag.Int("https", 1, "是否返回 https 短链接")
  69. flag.Parse()
  70. if *domain == "" {
  71. flag.Usage()
  72. log.Fatalln("缺少关键参数")
  73. }
  74. redisPoolConfig = &redisPoolConf{
  75. maxIdle: 1024,
  76. maxActive: 1024,
  77. maxIdleTimeout: 30,
  78. host: *conn,
  79. password: *passwd,
  80. db: 0,
  81. handleTimeout: 30,
  82. }
  83. initRedisPool()
  84. router.GET("/", func(context *gin.Context) {
  85. context.HTML(http.StatusOK, "index.html", gin.H{
  86. "title": "MyUrls",
  87. })
  88. })
  89. // 短链接生成
  90. router.POST("/short", func(context *gin.Context) {
  91. res := &Response{
  92. Code: 1,
  93. Message: "",
  94. LongUrl: "",
  95. ShortUrl: "",
  96. }
  97. longUrl := context.PostForm("longUrl")
  98. shortKey := context.PostForm("shortKey")
  99. if longUrl == "" {
  100. res.Code = 0
  101. res.Message = "longUrl为空"
  102. context.JSON(200, *res)
  103. return
  104. }
  105. _longUrl, _ := base64.StdEncoding.DecodeString(longUrl)
  106. longUrl = string(_longUrl)
  107. res.LongUrl = longUrl
  108. // 根据有没有填写 short key,分别执行
  109. if shortKey != "" {
  110. redisClient := redisPool.Get()
  111. // 检测短链是否已存在
  112. _exists, _ := redis.String(redisClient.Do("get", shortKey))
  113. if _exists != "" && _exists != longUrl {
  114. res.Code = 0
  115. res.Message = "短链接已存在,请更换key"
  116. context.JSON(200, *res)
  117. return
  118. }
  119. // 存储
  120. _, _ = redisClient.Do("set", shortKey, longUrl)
  121. } else {
  122. shortKey = longToShort(longUrl, *ttl*secondsPerDay)
  123. }
  124. protocol := "http://"
  125. if *https != 0 {
  126. protocol = "https://"
  127. }
  128. res.ShortUrl = protocol + *domain + "/" + shortKey
  129. // context.Header("Access-Control-Allow-Origin", "*")
  130. context.JSON(200, *res)
  131. })
  132. // 短链接跳转
  133. router.GET("/:shortKey", func(context *gin.Context) {
  134. shortKey := context.Param("shortKey")
  135. longUrl := shortToLong(shortKey)
  136. if longUrl == "" {
  137. context.String(http.StatusNotFound, "短链接不存在或已过期")
  138. } else {
  139. context.Redirect(http.StatusMovedPermanently, longUrl)
  140. }
  141. })
  142. // GC 优化
  143. ballast := make([]byte, 1<<30) // 分配 1G 内存,不会实际占用物理内存,不可读写该变量
  144. defer func() {
  145. log.Println("ballast len %v", len(ballast))
  146. }()
  147. router.Run(fmt.Sprintf(":%d", *port))
  148. }
  149. // 短链接转长链接
  150. func shortToLong(shortKey string) string {
  151. redisClient = redisPool.Get()
  152. defer redisClient.Close()
  153. longUrl, _ := redis.String(redisClient.Do("get", shortKey))
  154. // 获取到长链接后,续命1天。每天仅允许续命1次。
  155. if longUrl != "" {
  156. renew(shortKey)
  157. }
  158. return longUrl
  159. }
  160. // 长链接转短链接
  161. func longToShort(longUrl string, ttl int) string {
  162. redisClient = redisPool.Get()
  163. defer redisClient.Close()
  164. // 是否生成过该长链接对应短链接
  165. longUrlMD5Bytes := md5.Sum([]byte(longUrl))
  166. longUrlMD5 := hex.EncodeToString(longUrlMD5Bytes[:])
  167. _existsKey, _ := redis.String(redisClient.Do("get", longUrlMD5))
  168. if _existsKey != "" {
  169. _, _ = redisClient.Do("expire", _existsKey, ttl)
  170. log.Println("Hit cache: " + _existsKey)
  171. return _existsKey
  172. }
  173. // 重试三次
  174. var shortKey string
  175. for i := 0; i < 3; i++ {
  176. shortKey = generate(shortUrlLen)
  177. _existsLongUrl, _ := redis.String(redisClient.Do("get", shortKey))
  178. if _existsLongUrl == "" {
  179. break
  180. }
  181. }
  182. if shortKey != "" {
  183. _, _ = redisClient.Do("mset", shortKey, longUrl, longUrlMD5, shortKey)
  184. _, _ = redisClient.Do("expire", shortKey, ttl)
  185. _, _ = redisClient.Do("expire", longUrlMD5, secondsPerDay)
  186. }
  187. return shortKey
  188. }
  189. // 续命
  190. func renew(shortKey string) {
  191. redisClient = redisPool.Get()
  192. defer redisClient.Close()
  193. // 加锁
  194. lockKey := defaultLockPrefix + shortKey
  195. lock, _ := redis.Int(redisClient.Do("setnx", lockKey, 1))
  196. if lock == 1 {
  197. // 设置锁过期时间
  198. _, _ = redisClient.Do("expire", lockKey, defaultRenewal*secondsPerDay)
  199. // 续命
  200. ttl, err := redis.Int(redisClient.Do("ttl", shortKey))
  201. if err == nil && ttl != -1 {
  202. _, _ = redisClient.Do("expire", shortKey, ttl+defaultRenewal*secondsPerDay)
  203. }
  204. }
  205. }
  206. // generate is a function that takes an integer bits and returns a string.
  207. // The function generates a random string of length equal to bits using the letterBytes slice.
  208. // The letterBytes slice contains characters that can be used to generate a random string.
  209. // The generation of the random string is based on the current time using the UnixNano() function.
  210. func generate(bits int) string {
  211. // Create a byte slice b of length bits.
  212. b := make([]byte, bits)
  213. // Create a new random number generator with the current time as the seed.
  214. r := rand.New(rand.NewSource(time.Now().UnixNano()))
  215. // Generate a random byte for each element in the byte slice b using the letterBytes slice.
  216. for i := range b {
  217. b[i] = letterBytes[r.Intn(len(letterBytes))]
  218. }
  219. // Convert the byte slice to a string and return it.
  220. return string(b)
  221. }
  222. // 定义 logger
  223. func Logger() *logrus.Logger {
  224. logFilePath := ""
  225. if dir, err := os.Getwd(); err == nil {
  226. logFilePath = dir + "/logs/"
  227. }
  228. if err := os.MkdirAll(logFilePath, 0777); err != nil {
  229. fmt.Println(err.Error())
  230. }
  231. logFileName := "access.log"
  232. //日志文件
  233. fileName := path.Join(logFilePath, logFileName)
  234. if _, err := os.Stat(fileName); err != nil {
  235. if _, err := os.Create(fileName); err != nil {
  236. fmt.Println(err.Error())
  237. }
  238. }
  239. //写入文件
  240. src, err := os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
  241. if err != nil {
  242. fmt.Println("err", err)
  243. }
  244. //实例化
  245. logger := logrus.New()
  246. //设置输出
  247. logger.SetOutput(src)
  248. // logger.Out = src
  249. //设置日志级别
  250. logger.SetLevel(logrus.DebugLevel)
  251. //设置日志格式
  252. logger.Formatter = &logrus.JSONFormatter{}
  253. return logger
  254. }
  255. // 文件日志
  256. func LoggerToFile() gin.HandlerFunc {
  257. logger := Logger()
  258. return func(c *gin.Context) {
  259. logMap := make(map[string]interface{})
  260. // 开始时间
  261. startTime := time.Now()
  262. logMap["startTime"] = startTime.Format("2006-01-02 15:04:05")
  263. // 处理请求
  264. c.Next()
  265. // 结束时间
  266. endTime := time.Now()
  267. logMap["endTime"] = endTime.Format("2006-01-02 15:04:05")
  268. // 执行时间
  269. logMap["latencyTime"] = endTime.Sub(startTime).Microseconds()
  270. // 请求方式
  271. logMap["reqMethod"] = c.Request.Method
  272. // 请求路由
  273. logMap["reqUri"] = c.Request.RequestURI
  274. // 状态码
  275. logMap["statusCode"] = c.Writer.Status()
  276. // 请求IP
  277. logMap["clientIP"] = c.ClientIP()
  278. // 请求 UA
  279. logMap["clientUA"] = c.Request.UserAgent()
  280. //日志格式
  281. // logJson, _ := json.Marshal(logMap)
  282. // logger.Info(string(logJson))
  283. logger.WithFields(logrus.Fields{
  284. "startTime": logMap["startTime"],
  285. "endTime": logMap["endTime"],
  286. "latencyTime": logMap["latencyTime"],
  287. "reqMethod": logMap["reqMethod"],
  288. "reqUri": logMap["reqUri"],
  289. "statusCode": logMap["statusCode"],
  290. "clientIP": logMap["clientIP"],
  291. "clientUA": logMap["clientUA"],
  292. }).Info()
  293. }
  294. }
  295. // redis 连接池
  296. func initRedisPool() {
  297. // 建立连接池
  298. redisPool = &redis.Pool{
  299. MaxIdle: redisPoolConfig.maxIdle,
  300. MaxActive: redisPoolConfig.maxActive,
  301. IdleTimeout: time.Duration(redisPoolConfig.maxIdleTimeout) * time.Second,
  302. Wait: true,
  303. Dial: func() (redis.Conn, error) {
  304. con, err := redis.Dial("tcp", redisPoolConfig.host,
  305. redis.DialPassword(redisPoolConfig.password),
  306. redis.DialDatabase(redisPoolConfig.db),
  307. redis.DialConnectTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second),
  308. redis.DialReadTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second),
  309. redis.DialWriteTimeout(time.Duration(redisPoolConfig.handleTimeout)*time.Second))
  310. if err != nil {
  311. return nil, err
  312. }
  313. return con, nil
  314. },
  315. }
  316. }