Tự tạo dịch vụ thu gọn url với sinatra và redis
Ruby
114
Redis
10
White

huydx viết ngày 23/05/2015

Mở đầu

Chắc hẳn các bạn đã biết về các dịch vụ rút gọn url, điển hình là bit.ly. Mục đích của dịch vụ này là nhằm thu gọn là những url rất dài để tiết kiệm chữ (cho những dịch vụ giới hạn về số kí tự như twitter chẳng hạn) và để cho url nhìn gọn hơn.
Cơ chế của một dịch vụ rút gọn url khá đơn giản, vậy tại sao không tự làm một dịch vụ cho chính mình. Bài này mình sẽ hướng dẫn cách làm một url shorten service đơn giản dựa trên sinatra và redis.

Cài đặt

Để cài đặt sinatra thì bạn phải có ruby đã cài sẵn trên máy, việc cài đặt sinatra khá đơn giản thông qua gem:

  gem install sinatra

Tiếp đến là redis, để cài đặt redis thì tùy thuộc vào hệ điều hành, trên mac-osx các bạn có thể cài đặt rất dễ dàng thông qua brew:

  brew install redis

Trên linux hoặc windows các bạn có thể google để tìm ra hướng dẫn cài tương ứng.
Các bạn khởi động redis thông qua, khi khởi động redis mà không set option với config gì cả thì redis sẽ chạy trên localhost và port là 6789:

  redis-server

Giới thiệu qua về sinatra và redis

Sinatra là một mini webframework based trên Rack(Rack là web server interface rút gọn nhất có thể rất nổi tiếng trên ruby). Sinatra cung cấp cho bạn một DSL(Domain specific language) để có thể build một web app một cách dễ dàng nhất. Các bạn có thể tìm hiểu về sinatra ở trang chủ của sinatra.

Redis là hệ thống lưu trữ key-value với rất nhiều tính năng và được sử dụng rộng rãi. KTMT blog đã có một bài viết về redis cách đây không lâu, các bạn có thể tham khảo lại. Về cách sử dụng redis, các bạn có thể tham khảo tại trang chủ redis

Thiết kế chương trình

Cơ chế của một dịch vụ thu gọn hết sức đơn giản, được thể hiện ở diagram dưới đây:

alt text

Chắc bạn nào đã dùng bit.ly sẽ tưởng tượng ra usage flow một dịch vụ rút gọn url nên có. Cơ chế chúng ta dùng ở bài viết này như sau: đầu tiên user sẽ gửi url cần rút gọn thông qua form input. Web server nhận được request này sẽ hash url lại thành một chuỗi ngắn hơn, thông thường từ 5~10 kí tự, webserver sẽ lưu lại cặp vào redis, và trả lại chuỗi hash đó được ghép vào url của dịch vụ : http://your-service.com/hash.

Khi user click vào http://your-service.com/hash, đầu tiên server sẽ dùng chuỗi hash tìm original url trên redis, sau đó sẽ trả về response 301 (redirect) với location mới là original url, nhờ vậy mà user sẽ được redirect đến original url.

Như vậy là đã định hình ý tưởng nên làm gì, chúng ta sẽ bắt tay vào code

Coding

Tạo khung

Đầu tiên là sườn của một Sinatra app, có get, post. Chúng ta tạo folder cho app của chúng ta và tạo 1 file là app.rb chính là sinatra app:

mkdir url_shorten && cd url_shorten
touch app.rb && vim app.rb

Sau đó chúng ta sẽ tạo cái khung cho sinatra app như dưới đây:

require 'sinatra/base'
require 'redis'

class UrlShort < Sinatra::Base
  set :public_dir, File.dirname(__FILE__) + '/static'

  get '/' do
    erb :index
  end
end

UrlShort.run!

Sinatra có syntax dạng DSL: get 'some info' do 'something' để represent cho 'get' request rất dễ hiểu. Đoạn code trên có nghĩa là khi request đến '/' (root) thì sẽ render file index nằm trong views.
Như vậy chúng ta đã có cái khung đơn giản nhất của một web service, nhận request, trả về view. (erb là template engine có sẵn của ruby, nó sẽ render file có đuôi erb ra html)

Tiếp đến chúng ta sẽ thực hiện xử lý hash url. Về mặt lý thuyết thì gọi là hash không đúng lắm vì hash phải là nhận đầu vào X và trả lại Y là kết quả của việc hash X, bài toán của chúng ta ở đây chỉ đơn thuần là generate ra một chuỗi random để represent cho một cái url, như vậy bài toán của chúng ta sẽ là một hàm random_hash(N) nhận đầu vào N là độ dài của chuỗi input và đầu ra là một chuỗi random (cả chữ cả số) có độ dài N, ví dụ như sau:

  rand_hash(5) #=> "s4xA6"

Để giải quyết bài toán này thì có khá nhiều hướng đi:

  1. Loop từ 1 đến 5 rồi với mỗi lần loop bạn random ra một số hoặc chữ cái.
  2. Tạo ra 1 array có độ dài 64 gồm từ [0..9] [a..z] và [A..Z] và random ra 5 vị trí trong đó (việc này có thể thực hiện rất dễ dàng thông qua Array#sample của ruby).
  3. Sử dụng base64. base64 có một đặc điểm là sẽ biến 1 số M thành 1 chuỗi cả chữ cả số có độ dài max là log(64)(M). Do đó để tạo ra một chuỗi random có length N thì chúng ta chỉ cần random một số M nằm trong khoảng 64^(N-1) đến 64^(N) và chuyển nó về base 64.
  4. Sử dụng một số kĩ thuật generate 64bit (mà đã được giới thiệu ở bài viết về generate 64bit uid trên KTMT gần đây.

Ở bài viết này chúng ta sẽ sử dụng kĩ thuật số (3). Chúng ta đưa hàm generate hash vào trong helper của sinatra thông qua hàm helpers như sau:

class UrlShort < Sinatra::Base
  ..
  helpers do
    def rand_hash(length)
      gap = 64**(length) - 64**(length-1)
      (rand(gap) + 64**(length-1)).to_s(64) 
    end
  end 
  ...

Tiếp theo chúng ta sẽ làm nhiệm vụ gắn hash thu được với url thành một cặp key-value và ghi vào redis. Để làm nhiệm vụ này thì đầu tiên chúng ta cần khởi tạo redis, mình khởi tạo redis bằng cách overwrite constructor của app và đưa redis instance vào thành 1 instance property. Ngoài ra, chúng ta cũng không muốn 1 url mà mỗi lần request lại tạo một hash khác nhau , rất tốn tài nguyên, do đó mỗi lần gen hash mình sẽ lưu duplicate thành 2 cặp key-value. Một cặp chứa url làm key và hash làm value, và một cặp chứ hash làm key và url là value, điều này đảm bảo mối liên hệ giữa url/hash là 1/1.

post '/' do 
  @error = nil
  @error = 'please enter url' if URI.regexp.match(params[:url]).nil?
  @success = false

  unless @error
    if params[:url] and not params[:url].empty?
      @url = params[:url]
      @hash = rand_hash(5)
      exist = @redis.setnx "url:#{@url}", @hash
      if exist #key not set
        @redis.setnx "hash:#{@hash}", @url
      else
        @hash = @redis.get "url:#{@url}"
      end
      @success = true
    end
  end

  erb :index
end

Như vậy chúng ta đã có @hash để trả về cho user, chúng ta sẽ ghep hash vào trong views để hiển thị cho user (views/index.rb)

<form id="form" method="post">
  <input type="text" value="" name="url" id="url"/>
  <input type="submit" value="shorten" id="submit" class="submit"/>
</form>
<hr/>
<div class="mes">Result</div>
<% if @error %>
  <div id='error'>
    <%= @error %>
  </div>
<% else %>
  <% if @success %>
    <a id="result" href='<%= "#{escape_html(url)}#{@hash}" %>'><%= "#{escape_html(url)}#{@hash}" %></a>
    <input data-clipboard-text='<%="#{escape_html(url)}#{@hash}" %>' type="button" id="yank" class="submit" value="yank"/>
  <% end %>  
<% end %>

Phần việc còn lại chúng ta phải giải quyết là khi user click vào link. Link của chúng ta có dạng là www.my-application.com/#{hash}, với sinatra để lọc phần hash hết sức đon giản vì sinatra đã tự động lọc hộ chúng ta và đưa vào biến global params, do đó chúng ta chỉ cần lấy hash từ params, tìm trong redis, và redirect user là ok:

  get '/:hash' do
    url = @redis.get "hash:#{params[:hash]}" 
    redirect url
  end

Như vậy chúng ta đã có một flow hoàn chỉnh rồi, thêm tí css và sử dụng ZeroClipboard để có nút yank để copy url vào clipboard, chúng ta đã có một dịch vụ rút gọn url cho riêng mình!

alt text

Toàn bộ source code cho tutorial này mình đang để ở đây, mọi người có thể sử dụng tùy ý :).
https://github.com/ktmt/link_shorttener

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

huydx

116 bài viết.
944 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
148 14
Introduction (Link) là một cuộc thi ở Nhật, và cũng chỉ có riêng ở Nhật. Đây là một cuộc thi khá đặc trưng bởi sự thú vị của cách thi của nó, những...
huydx viết gần 2 năm trước
148 14
White
118 15
Happy programmer là gì nhỉ, chắc ai đọc xong title của bài post này cũng không hiểu ý mình định nói đến là gì :D. Đầu tiên với cá nhân mình thì hap...
huydx viết hơn 3 năm trước
118 15
White
95 10
(Ảnh) Mở đầu Chắc nhiều bạn đã nghe đến khái niệm oauth. Về cơ bản thì oauth là một phương thức chứng thực, mà nhờ đó một web service hay một ap...
huydx viết 3 năm trước
95 10
Bài viết liên quan
White
8 6
Chưa xem phần 2? Xem (Link) Trong bài viết này tôi giới thiệu cho các bạn về khái niệm function arity, một cách gọi mĩ miều của số lượng argument ...
Lơi Rệ viết gần 3 năm trước
8 6
White
8 1
Tiếp theo (Link) Mình sẽ hướng dẫn cách test căn bản cho API mình tạo. Thật ra mà nói thì mình phải viết test trước khi làm nhưng mà để tránh việc...
My Mai viết 3 năm trước
8 1
White
4 2
__Chú thích__: Đây là bản dịch tiếng Việt của bài viết gốc của tôi. Nếu bạn muốn xem bản tiếng Anh, xin hãy trỏ tới URL (Link) Lời mở (Link) là ...
Lơi Rệ viết hơn 3 năm trước
4 2
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
116 bài viết.
944 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!