DesignAssembler

備忘録に近い

Rubyの時間の扱い

時間を表すクラスにはDateとTimeとDateTimeクラスがあって、Dateクラスは日付、Timeクラスは時間、DateTimeクラスは日付と時間をそれぞれ扱います。

時間を表示する

最初1つずつフォーマットごとに切り出して連結していたのですが、strftimeという便利なメソッドがありました。

date_time = Time.now
=> 2016-05-02 06:58:51 +0900
date_time.year.to_s + "/" + date_time.month.to_s + '/' + date_time.day.to_s + '/' + " " + date_time.hour.to_s + ":" + date_time.min.to_s
=> "2016/5/2/ 6:58"
date_time.strftime("%Y/%m/%d %H:%M")
=> "2016/05/02 06:58"

文字列を時間に変換する

文字列を時間に変換するにはどうすればいいかと思っていたらparseメソッドが用意されていました。流石です。

date_time = "2016/05/01 00:00"
DateTime.parse(date_time.gsub("/","-"))
=> #<DateTime: 2016-05-01T00:00:00+00:00 ((2457510j,0s,0n),+0s,2299161j)>

参考

http://docs.ruby-lang.org/ja/1.9.3/class/DateTime.html

sizeとcountの違い

activerecordで取得したレコードの数を調べるときにcountとsizeを使ったら速度にかなりの差が出たのでその違いを調べました。

countとsizeの差はそのレコードがロードされているか否かで変わります。

レコードがロードされている時、countはsqlを生成し、sizeはsqlを生成しません。つまり、sizeの方が高速です。

a = Article.first.tags

a.count
   (0.4ms)  SELECT COUNT(*) FROM `tags` INNER JOIN `articles_tags` ON `tags`.`id` = `articles_tags`.`tag_id` WHERE `articles_tags`.`article_id` = 1
=> 1

a.size
=> 1

レコードがロードされていない時はどちらもcountのsqlを生成します。

Article.first.tags.count
  Article Load (0.3ms)  SELECT  `articles`.* FROM `articles`  ORDER BY `articles`.`id` ASC LIMIT 1
   (0.5ms)  SELECT COUNT(*) FROM `tags` INNER JOIN `articles_tags` ON `tags`.`id` = `articles_tags`.`tag_id` WHERE `articles_tags`.`article_id` = 1
=> 1

Article.first.tags.size
  Article Load (0.3ms)  SELECT  `articles`.* FROM `articles`  ORDER BY `articles`.`id` ASC LIMIT 1
   (0.3ms)  SELECT COUNT(*) FROM `tags` INNER JOIN `articles_tags` ON `tags`.`id` = `articles_tags`.`tag_id` WHERE `articles_tags`.`article_id` = 1
=> 1

条件を付けたい時はcountを使い、他はだいたいsizeで事足りるようです。

参考

http://apidock.com/rails/ActiveRecord/Associations/AssociationCollection/size

httpレスポンスに自作ヘッダーを付与する

小ネタです。

httpレスポンスに自作ヘッダーを付与します。

phpならただ

<?php
header('Name: asmsuechan');
?>

とすればレスポンスのヘッダーにName: asmsuechanを追加できます。ここで注意すべきは、<?php ~ ?>より以前に何か文字を配置しないことです。

herokuでサクっと作って試してみました。https://my-http-response.herokuapp.com/

% ruby http_client.rb
HTTP/1.1 200 OK
Connection: close
Date: Sat, 30 Apr 2016 14:35:19 GMT
Server: Apache
Name: asmsuechan
Content-Length: 0
Content-Type: text/html;charset=UTF-8
Via: 1.1 vegur

Name: asmsuechanが返ってきています。

参考

PHP: header - Manual

mod_headers - Apache HTTP サーバ バージョン 2.2

httpクライアントの実装(2)

続きです。

hyottokoaloha.hatenablog.com

コードをいじりました。

レスポンスをただputsするのではなくてResponseクラスのインスタンスを返すようにしました。

#response.rb
class Response
  attr_accessor :request, :headers, :response_except_body, :status, :body
  def initialize(response)
    @response = response
    @body = @response.slice!(/(\n\r\n).+/m)
    @response_except_body = @response.slice(/.+/m)
    #なぜsliceしないと動かないのだろうか・・・・
    @request = @response.slice!(/.+/)
    @status = @request.slice!(/\d{3}.+/)

    #ヘッダーの切り出し
    @headers = []
    while @response != ""
      header = @response.slice!(/.+/)
      @headers.push(header) unless header.nil?
      @response.sub!(/(\r\n|\r|\n)/, '')
    end

    #切り出したヘッダーからインスタンス変数、
    #アクセサメソッドを作成
    @headers.each do |header|
      header_name_raw = header.slice!(/^.+: /)
      unless header_name_raw.nil?
        header_name = header_name_raw.downcase
                      .gsub("-", "_")
                      .gsub(":", "")
                      .gsub(" ", "")
        self.instance_variable_set("@#{header_name}",header)
        add_accessor header_name
      end
    end
  end

  def all_response
    self.response_except_body
  end

  def disp_body
    self.body
  end

  private

    def add_accessor instance_variable_name
      self.class.send(:attr_accessor, instance_variable_name)
    end

end
#http_client.rb
require 'socket'
require './response'

class HttpClient
  attr_accessor :uri, :port, :path

  def initialize(options = {})
    @uri = options[:uri]
    @path = options[:path]
    @port = options[:port]
  end

  [:get, :post, :put, :delete, :head, :options].each do |method|
    define_method(method) do |content = nil, type = "text/plain"|
      @socket = TCPSocket.open(uri, port)
      @socket << "#{method.to_s.swapcase} #{self.path} HTTP/1.1\r\n"
      @socket << "Host: #{self.uri}\r\n"
      @socket << "Content-Type: #{type}; charset=utf-8\r\n"

      unless content.nil?
        @socket << "Content-Length: #{content.length}\r\n"
        @socket << "\r\n#{content}"
      end

      @socket << "Connection: close\r\n"
      @socket << "\r\n"
      response = Response.new(@socket.read)
      return response
      @socket.close
    end
  end
end

request = HttpClient.new(uri:"example.com", path:"/", port:80)
puts "===================="
puts request.get.all_response
puts "===================="
puts HttpClient.new(uri:"example.com", path:"/", port:80).post("こんにちは").all_response
puts "===================="
puts HttpClient.new(uri:"www.google.com", path:"/", port:80).post("こんにちは").all_response
puts "===================="
puts HttpClient.new(uri:"example.com", path:"/", port:80).options.all_response
puts "===================="

実行すると、

% ruby http_client.rb
====================
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html
Date: Sat, 30 Apr 2016 03:37:44 GMT
Etag: "359670651"
Expires: Sat, 07 May 2016 03:37:44 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (rhv/818F)
Vary: Accept-Encoding
X-Cache: HIT
x-ec-custom-error: 1
Content-Length: 1270
Connection: close
====================
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html
Date: Sat, 30 Apr 2016 03:37:44 GMT
Etag: "359670651"
Expires: Sat, 07 May 2016 03:37:44 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: EOS (lax004/2812)
Content-Length: 1270
====================
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD
Date: Sat, 30 Apr 2016 03:37:44 GMT
Content-Type: text/html; charset=UTF-8
Server: gws
Content-Length: 1589
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
====================
HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD, POST
Cache-Control: max-age=604800
Date: Sat, 30 Apr 2016 03:37:45 GMT
Expires: Sat, 07 May 2016 03:37:45 GMT
Server: EOS (lax004/28A4)
x-ec-custom-error: 1
Content-Length: 0
Connection: close

====================

簡単にResponseクラスの説明をすると、正規表現でガリガリ削ってレスポンスのヘッダーを動的にインスタンス変数に入れて動的にアクセサメソッド付与しています。これだけです。

参考

HTTPクライアントの実装

Webを支える技術を読んでいます。

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webエンジニアなら知ってて当然の基礎の基礎が載っている本です。

この前基礎の基礎を聞かれた時ハッキリと答えられなくて悔しかったので読んでいます。

今回はhttpリクエスト周りです。

httpリクエスト投げるコード書いた

オブジェクト指向っぽく書きました。(書いたつもりです)

optionsメソッド投げて返ってきたAllowed Methodから動的にメソッドを生成すると面白そうですね。

optionsメソッドをそもそも受け付けないサーバーがあるからキツそうですけど・・・

require 'socket'

class HttpClient
  attr_accessor :uri, :port, :path

  def initialize(uri, path,  port)
    @uri = uri
    @path = path
    @port = port
    #@socket = TCPSocket.open(uri, port)
  end

  [:get, :post, :put, :delete, :head, :options].each do |method|
    define_method(method) do |content = nil, type = "text/plain"|
      @socket = TCPSocket.open(uri, port)
      #修正 : ソケットのopenを各メソッドに任せる。
      @socket << "#{method.to_s.swapcase} #{self.path} HTTP/1.1\r\n"
      @socket << "Host: #{self.uri}\r\n"
      @socket << "Content-Type: #{type}; charset=utf-8\r\n"
      unless content.nil?
        @socket << "Content-Length: #{content.length}\r\n"
        @socket << "\r\n#{content}"
      end
      @socket << "Connection: close\r\n"
      @socket << "\r\n"
      puts @socket.read
      @socket.close
    end
  end
end

HttpClient.new("example.com", "/", 80).get
HttpClient.new("example.com", "/", 80).post("こんにちは")
HttpClient.new("example.com", "/", 80).options

各ヘッダーの意味

  • Host: #{ホスト名}
    このホストにリクエスト投げるぞという意味です。

  • Content-Type: #{タイプ}; charset=utf-8
    文字コードとタイプの指定です。ここからはMIMEコード(メディアタイプ)の指定です。

  • Content-Length:

その名の通りメッセージのサイズです。

最初Content-Lengthヘッダーを付けずにexample.comにpost投げつけたら以下のメッセージが返ってきました。

HTTP/1.0 411 Length Required
Content-Type: text/html
Content-Length: 357
Connection: close
Date: Fri, 29 Apr 2016 09:52:50 GMT
Server: ECSF (pae/3788)

Content-Length付けてリクエスト送信すると正常にレスポンスが返ってきました

HTTP/1.0 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html
Date: Fri, 29 Apr 2016 09:55:32 GMT
Etag: "359670651"
Expires: Fri, 06 May 2016 09:55:32 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: EOS (lax004/2816)
Content-Length: 1270
Connection: close

405 Not Allowedが返ってくると思ったんですがちゃんとpostされてますね。空を切っている感。

HTTP/1.0とHTTP/1.1の違い

最初HTTP/1.1でレスポンスが返ってくるのが遅くて困っていました。

調べると以下のヘッダーが足りなかったみたいです。

Connection: close

Connectionヘッダーについて

HTTP/1.0とHTTP/1.1で変わった点に接続の継続性があります。これを表すのがConnectionヘッダーになります。具体的には、

Connection: keep-aliveにすると繋ぎっぱなしになり、 Connection: closeにするとTCPコネクションが閉じます。

HTTP/1.0では要求ごとに接続されていたのですが、HTTP/1.1ではコネクションを閉じない限り接続が続きます。つまりデフォルトでkeep-aliveになりました。

つまり、タイムアウトになるまでTCPコネクション繋ぎっぱなしにしていたことがレスポンスが遅かった原因です。

Connection: closeを書いていないリクエストのレスポンスにはConnection: closeが含まれていません。コネクションはサーバーアプリケーションが切ったようです。

HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD, POST
Cache-Control: max-age=604800
Date: Fri, 29 Apr 2016 11:43:16 GMT
Expires: Fri, 06 May 2016 11:43:16 GMT
Server: EOS (lax004/280C)
x-ec-custom-error: 1
Content-Length: 0

チャンクやキャッシュは次回にします。

参考

http://www.cresc.co.jp/tech/java/Servlet_Tutorial/Lesson_36.htm

rakeタスクを書く場所

この記事のjnchitoさんのコメントを見て確かに、と思いました。

rakeタスクのロジックはモデルに書くべき

何を当たり前の事をって感じですが、ビジネスロジックはモデルに書くべきです。

ですのでrake作りたいときはモデルにクラスメソッド書いてそのメソッドをrakeファイルから呼び出すようにすべきです。

モデルにメソッドを置くとテストも楽に出来るようになります。(上記事参照)

例えば、以下のような感じにします。

#lib/tasks/counter.rake
namespace :count
  desc 'counter'
  task export: :environment do |t|
    Count.count_updadate
  end
end

#app/models/count.rb
class Count
  def self.count_update
  end
end

参考

サーバーでアクセス制御

apache

apachehttps://のみを使いたい、つまりhttp://にアクセスさせたくない時の設定です。

#.htaccess
RewriteCond %{SERVER_PORT} 80
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

最初の.*$は正規表現で、すべての文字列を表します。

%{HTTP_HOST}はアクセスがあったアドレスのドメイン部分、 %{REQUEST_URI}にはアクセスがあったアドレスのドメイン以下部分が入ります。

[R,L]は、それぞれRがリダイレクト、Lが変換終了を表します。

nginx

nginxでは/etc/nginx/nginx.confにアクセス制御文を書きます。

.htaccessをnginx.confに変換できるとっても素敵なサイトがあります。
htaccessファイルはnginxのに変換

お前ら言ってること分かるよな?って感じのサイトです。

参考