WebPushについて簡単整理

WebPushについて私なりに理解したことを最大限簡単に書いてみます。

WebPushとは

ブラウザに対してサーバからプッシュ通知ができる技術です。
他のウェブページを見ていても通知が来ます。ブラウザによっては、ブラウザを立ち上げなくても通知が来ます。
専用アプリは不要です。ウェブページ(html, jsp...)とjsだけでプッシュ通知できます。
通知がたくさん来ると、ユーザに迷惑でしょうが、うまく使えば色々な使い方ができそうですね。

プッシュ通知の流れ

プッシュ通知は以下の流れで動作します。
標準のWebPush APIを利用すると仮定します。
AP ServerはApplication ServerでTomcatなどのコンテナーを表します。

ユーザ > Browser

  • ユーザがBrowserでウェブページにアクセスします。
  • Browserからプッシュ通知を許可するかどうかの選択を求めるダイアログが表示されます。
  • ユーザが許可を選択します。

Browser > PushServer > Browser

  • ユーザがBrowserでプッシュ通知の購読ボタンを押します。
  • BrowserからPushServerに購読をリクエストします。
  • PushServerはEndPoint(URL)をBrowserに返します。

Browser > AP Server > DB

  • BrowserはPushServerから受け取ったEndPointとユーザキーなどをAP Serverに送ります。
  • AP ServerはEndPointとユーザキーなどをDBに保存します。

AP Server > PushServer > Browser

  • AP Serverの使用者はEndPoint宛にメッセージを送り、メッセージはPushServerに届きます。
  • PushServerはBrowserにメッセージ送り、Browserはメッセージを通知ダイアログで表示します。

PushServer

ブラウザの利用者にプッシュ通知を送るためにはEndPointというURLにメッセージを送る必要があります。
EndPointはPushServerのURLを表し、ユーザごとにユニークでドメインもブラウザごとに違います。
WebPushを標準的な方法で利用すると、特にEndPointが何者か意識する必要はありません。
Push APIの購読メソッドを呼び出すと、EndPointが返ってきますが、そのEndPoint宛にメッセージを送れば、
該当ブラウザにプッシュ通知が表示されます。

サポートするブラウザ

ChromeFirefoxSafariがWebPushをサポートします。
ブラウザのバージョンは大体2016年7月以降のものなら、WebPushの標準的な使い方ができます。
ChromeFirefoxは検証できましたが、SafariMacがないので、分かりません。
またSafariは少々サポート(アイコン未サポート..)が足りないようです。
MicrosoftのEdgeは開発中だそうです。
従いまして、今のところChromeFirefoxが確実にサポートすると考えればいいと思います。

実装方法の違い

Firefoxでは特に意識する必要はありません。
Chromeの場合、バージョン52未満ではChrome専用で行わなければならないもの(例 : ID取得、manifest.jsonファイル)があります。
今はChrome 52以降なら、Web Pushプロトコル標準をサポートしているため、ブラウザ依存なしで実装できます。

より多くのブラウザをサポートするためにChromeの旧バージョンもサポートすべきか?と悩むところですが、
私は必要ないのでは?と思います。
なぜかというと、IEならまだしも、ChromeFirefoxは知らぬ間に自動更新してくれますし、
ブラウザシェアを見ると、ほとんどのChromeユーザは52以降のバージョンを使用中だからです。
(WebブラウザシェアランキングTOP10(日本国内・世界) – ソフトウェアテスト・第三者検証ならウェブレッジ)

ですので、近々なくなるChrome依存のコードは要らないのでは?と思います。

サンプル

以下のgithubのサンプルが役立ちました。
Javaベースですが、標準的なAPIを使用しているので、他の言語への適用は問題ないと思います。
github.com
WebPushの標準的な実装だけでなく、古いバージョンのChromeにも対応しています。
暗号化する部分が複雑ですが、ただWebPushを利用するだけなら、スキップし、実際通信する部分だけを参照しても問題ないと思います。

実装ポイント

  • プッシュ通知の許可や購読を行うjsは該当ウェブサイトと別のドメインにあってもいい。
  • 通知を行うjsは該当ウェブサイトに配置する必要がある。
  • 通知を行うjsはブラウザでService Workerとして常駐することになる。つまり、該当ウェブサイトを去ってもずっとブラウザのメモリに残って通知が来ることを待つ。
  • Service Workerとして登録された通知を行うjsの更新に注意する必要がある。期限を設定し、うまく更新されるか確認しよう。

SPFとSenderIDで曖昧な部分を整理

SPFについてブログやドキュメントを読んでも何を言っているのか分からないところを整理しました。
実際に検証したところもあり、英文を探して分かったところもあります。

SPFとSenderIDはどのドメインを確認するか

  • SPFはMAIL FROMコマンド(いわゆるEnvelope From)に指定されるメールアドレスのドメイン部のSPFレコードをチェックします。
  • SenderIDはPRA(Purported Responsible Address)といって、メールヘッダーにある最終的に責任があるメールアドレスのドメイン部のSPFレコードをチェックします。
    • PRAを抽出するアルゴリズムRFC4407の2. Determining the Purported Responsible Addressに書いてあります。

下記はRFC4407でPRAを求めるアルゴリズムのStep 1からStep 6までを疑似コードで書いてみたのです。

def getPRA
  def step2_to_step6
    # step 2 
    if Resent-From.isEmpty
      if Sender.isEmpty
        # step 4
        if From.size == 1
          return From[0]
        else
          throw new Exception
        end
      else
        # step 3
        if Sender.size == 1
          return Sender[0] 
        else
          throw new Exception
        end
      end
    else
      return Resent-From
    end
  end
  # step 1
  if Resent-Sender.isEmpty
    return step2_to_step6()
  else
    if Resent-From.isBefore(Resent-Sender) 
         && (Received.isAfter(Resent-From) && Received.isBefore(Resent-Sender)) 
           || (Return-Path.isAfter(Resent-From) && Return-Path.isBefore(Resent-Sender))
      return step2_to_step6()
    else
      return Resent-Sender
    end
  end
end

step1で条件が複雑ですが、Resent-Sender > Resent-From > Sender > Fromの順にチェックしていて、通常はFromのメールアドレスがPRAになると考えればよさそうです。

includeとredirectの違い

  • includeはincludeの右に指定したメールサーバのSPF設定を参照する上に自分のSPF設定を追加できます
example.org. IN TXT "v=spf1 include:_spf-a.example.com include:_spf-b.example.com include:_spf-c.example.com -all"
  • redirectはredirectの右に指定したメールサーバのSPF設定しか利用できません。自分のSPF設定を追加できません。完全に委任するということです。
example.net. IN TXT "v=spf1 redirect=example.org"
  • まとめると、includeとredirectの違いは自分のSPF設定を追加できるかどうかです。

メールログの時間がずれる時の対処

メール送信と受信のため、PostfixDovecotをインストールしましたが、
メールログ(/var/log/maillog)に出力される時間がサーバ時間と9時間もずれていました。
色々ググって試して失敗したりしましたが、ついに解決できましたので、メモします。

以下のコマンドを実行したら、いけました。rootで行う必要があります。

cd /var/spool/postfix
mkdir etc #既にあれば、スキップ
cd etc
cp /etc/localtime .
service postfix restart
service rsyslog restart

殆どのブログでは/etc/localtimeを/var/spool/postfix/etcにコピーすればいいと言っていましたが、
私が使うサーバではrsyslogの再起動も必要でした。

セッションストアに関してメモ

最近、RoR(Ruby on Rails)を勉強しています。

RoRでセッションデータはデフォルトでブラウザのクッキーに保存されます。
最初はServletのようにセッションIDだけ、ブラウザに送られて、実際のデータはサーバのメモリに保存されると思いました。

しかし、そうではなく、セッションに入れたすべてのデータがブラウザのクッキーに保存されるのでした。
これはまずいんじゃない?と思いました。
なぜなら、たとえ、クッキーのセッションデータが暗号化されていても理論的には復号化できるので、
セキュリティ上、安全ではないからです。

幸い、RoRではセッションストアを変更できます。
セッションデータをDBはもちろんのこと、Redisやmemcachedなどのキャッシュサーバに保存することも可能です。

RoRで本格的にウェブアプリケーションを開発する時にはセッションストアをクッキー以外に変更しましょう。
以下のgemはセッションデータをDBに保存します。
github.com

以下はredisです。
github.com

最後にmemcachedです。
github.com


どのセッションストアもセッションデータを一つのAPサーバのメモリでなく、クッキーやDBなどの共有ストレージに保存するとの共通点がありますね。
こうなると、APサーバの冗長化時にセッションを同期するためにクッキーにサーバのIDを送るとか、セッションレプリケーションなどを行う必要がないから、便利だと思います。

勉強メモ1

Rubyは以前遊びぐらいでしか触ったことがありませんが、
今回勉強したいと思い、学習を始めることになりました。

ただ、以前のように分厚い本を買って、表紙(まえがき)から最後の隅々まで
読んで写経することはしないつもりです。

時間を節約し、効率的に勉強したいと思います。

その方法として、既存のプログラミング言語に共通しそうな部分は除いて
Rubyにしかないものだけを勉強する、差分学習を取ります。

この記事はその学習メモです。

文字列中の変数

"#{変数名}"

コメント

  • 1行 : #
  • ブロック =begin ~ =end

変数

ローカル

_variable

グローバル

$variable

インスタンス

@variable

クラス

@@variable

定数

大文字。変更不可能。

多重代入

q, a, z = 1, 2, 3
q, a, *z = 1, 2, 3, 4, 5 # zには配列として代入。1, 2, [3, 4, 5]

q, a = 0, 1
q, a = b, z

ar = [3,4]
q,a = ar # 3, 4

ar = [1, [2, 3], 4] 
q, (a1, a2), z = ar # 1, 2, 3, 4

JavaでのisXXXのようなメソッドRubyでは末尾に?がつく。例)empty?

分岐

  • else ifはelsifと書く。
  • unlessはifの反対。
  • case文にはJavaと違い、色々な条件(固定値、クラス、正規表現)を指定することができる。条件はwhenに書く。

if修飾子とunless修飾子

文の右にifやunlessを書くことができる。ミニif文、ミニunless文ってことか。

a = 1
"Hi" if a > 1 # nil
"Man" if a == 1 # Man
"Hoi" unless a > 1 # Hoi

===

==に似ているが、左辺がリテラル(クラス、数値、文字列など)の場合、右辺との比較を柔軟に行う。
例えば、String == "a"だと、false。String === "a"だと、"a"がStringクラスのインスタンスという比較を行い、trueがリターンされる。

同一性

オブジェクトの値を比較するなら、==を使えばいい。厳密にオブジェクトの同一性を比較するなら、eql?を使う。
Javaとは反対ですね。

繰り返し

  • untilはwhileの反対。
  • nextはJavaのcontinue。
  • redoは同じことをもう一回繰り返す。

times

10.times do 
  print "Duke"
end

配列

names = ["Duke", "Foo"]
names.each do |n|
  puts n
end

ハッシュ

person = { name: "Duke", job: "Engineer" }
person.each do |key, value|
  puts "#{key}: #{value}"
end

ブロック

1行にまとめるときは{ ... }、複数行に分ける場合、do ... end

メソッド

  • 最後にreturn文は省略してもよい。
  • 可変長引数は引数に*argsと書く。

キーワード引数

引数名:デフォルト値

def m1(q:1, a:2, z:3)
  q + a + z
end
m1(z:5,q:3) # 10

ブロック付きメソッド

def m1
  puts "Hello!"
  yield # これがブロックを実行する。
end
m1 do
  puts "Hey Duke!"
end

# Hello!
# Hey Duke!

引数がハッシュの場合、{}を省略可能

def m(q)
  q
end

m({ "n" => "d", "t" => 5 }) # { "n" => "d", "t" => 5 }
m("n" => "d", "t" => 5) # { "n" => "d", "t" => 5 }
m(n: "d", "t": 5) # { "n" => "d", "t" => 5 }

クラス

アクセスメソッド

getter, setterのようなもの。
一々メソッドを定義してもいいが、getter/setterの数が多いと、面倒。
代わりに以下のものを使えばgetterとsetterを自動生成してくれる。

attr_reader :変数名

getter

attr_setter :変数名

setter

attr_accessor :変数名

getter + setter

クラスメソッド

class << オブジェクト ~ end形式がある。
オブジェクトにクラス名を指定すると、以下の例のようにクラスメソッドになる(Classクラスのインスタンスメソッドを追加)。
しかし、オブジェクトに特定インスタンス変数を指定すると、そのインスタンスのみにメソッドが追加される。
まるでJavascriptでオブジェクトに自由にメソッドを追加できるのと同じ感じだ。

# クラス内で定義
class DukeLab
  class << self
    def classMethod1
      "Hey"
    end
  end
end

DukeLab.classMethod1 # Hey

# 特異クラス形式で定義。既にDukeLabクラスが定義されなければならない。
class << DukeLab
  def classMethod2
    "You!"
  end
end

DukeLab.classMethod2 # You!

# クラスの外から定義
def DukeLab.classMethod3
  "Man!"
end

DukeLab.classMethod3 # Man!

# クラス内で定義
class DukeLab
  def self.classMethod4
    "Woman!"
  end
end

DukeLab.classMethod4 # Man!

定数

クラス名::変数名

アクセス制限

何も指定しなければ、public。
public, private, protectedキーワードを使ってアクセス制限を定義できる。

class DukeTest
  def m1
    "Duke1"
  end
  public :m1 #デフォルト

  def m2
    "Duke2"
  end
  private :m2
end

dt = DukeTest.new
dt.m1
dt.m2 # エラー

まとめてprivateにする

class DukeTest
  def m1
    "Duke1"
  end

  private # これより下はprivate
  def m2
    "Duke2"
  end
end

getだけ可能にする。

class DukeTest
  attr_accessor :q, :a  
  private :q=, :a= # xとyにsetできないようにする。
  def initialize(q, a)
    @q, @a = q, a
  end
end

dt = DukeTest.new
dt.q
dt.q = 8 # エラー

継承

class クラス < スーパークラス

メソッドの別名と削除

alias

別名。

undef

削除。スーパークラスメソッドを削除する時に使用。

モジュール

Scalaのtrait、Javaのdefault interfaceって感じ?
クラスでinclude モジュール名すると、そのクラスにモジュールの全てのメソッドが追加される。
モジュールをクラスに入れることをMixinという。

module Mod1
  def m1
    "variable : #{@q}"
  end
end
class Duke
  include Mod1
  def initialize(q)
    @q = q
  end
end

d = Duke.new(5)
d.m1 # variable : 5

Object.extend

特定オブジェクトにモジュールをMixinする。
module Mod1
  def m1
    "variable : #{@q}"
  end
end
class Duke
  def initialize(q)
    @q = q
  end
end
d1 = Duke.new(8)
d1.extend(Mod1)
d1.m1 # variable : 8
extendでクラスメソッドを追加
module Mod1
  def m1
    "Hi!"
  end
end
module Mod2
  def m2
    "Man!"
  end
end
class Duke
  extend Mod1 #クラスメソッド
  include Mod2 #インスタンスメソッド
end
d1 = Duke.new

d1.m1 # エラー。インスタンスメソッドではない。
Duke.m1 # Hi!

d1.m2 # Man!
Duke.m2 # エラー。クラスメソッドではない。

演算子

範囲演算子

Range.new(x, y)又はx..yを使う。

for i in 1..10
  puts i
end

(1..10).to_a # 1,2,3,4,5,6,7,8,9,10
(1...10).to_a # 1,2,3,4,5,6,7,8,9  yを除く。

演算子定義

class Apple
  attr_reader :n, :p
  def initialize(n, p)
    @n = n
    @p = p
  end
  def +@ # 単項演算子の場合、後尾に@をつける。
    "Plus"
  end
  def +(other)
    self.class.new(n + other.n,p + other.p)
  end
  def inspect
    "n : #{n}, p : #{p}"
  end
end

a = Apple.new(1, 10)
b = Apple.new(2, 20)
+a # "Plus"
a + b # n : 3, p : 30

例外

キーワードが違うだけで殆どJavaと同じ。

begin # try
  ...
rescue => ex # catch
  ...
  retry # beginに戻って再実行。無限ループに注意。
ensure # finally 
  ...
end

メソッド全体をtry-catchしたい場合、beginとendは省略可能。

def duke
  ...
  rescue => ex # catch
    ...
    retry # beginに戻って再実行。無限ループに注意。
  ensure # finally 
    ...

例外のスローはraiseキーワードを使う。

rescue修飾子

A rescue Bで、Aが失敗すると、Bが実行される。ミニtry-catchのようなもの。

File.open("duke.txt") rescue "-_-Error" # -_-Error

ブロック

処理のかたまり。

メソッドに何かを実行するブロックを渡すと、前後処理は自動でやってくれるケースが多い。
例えば、以下の場合、ファイルをオープン(前処理)し、その内容を一行ずつ表示する。
ファイルを閉じる処理(後処理)は自動でやってくれる。

File.open("README.md") do |file|
  file.each_line do |line|
    print line
    end
end

ブロックの実行

ブロックはメソッドでyieldコマンドで実行する。
yieldコマンドに引数を指定すると、ブロックに渡される。

def m1(q)
  v = yield(q + 1)
  puts "value : #{v}"
end

m1(5) { |n| "wow => #{n}" }  # value : wow => 6
m1(7) do |n| # value : haha => 7
  "haha => #{n}" 
end

制御

break, next, redoをブロック内でも使うことができる。

def m1(q)
  puts "Step1"
  yield(q)
  puts "Step2"
  yield(q + 1)
  puts "Step3"
  yield(q + 2)
end

m1(1) do |q|   # Step1 Block Step2 Block Step3 Block => 3!!!
  if (q == 5)
    break
  end
  puts "Block"
  "#{q}!!!" 
end

m1(4) do |q|   # Step1 Block Step2 => nil
  if (q == 5)
    break
  end
  puts "Block"
  "#{q}!!!" 
end

# breakやnextに戻り値を指定できる。
m1(4) do |q|   # Step1 Block Step2 => Break!
  if (q == 5)
    break "Break!"
  end
  puts "Block"
  "#{q}!!!" 
end

m1(4) do |q|   # Step1 Block Step2 Step3 Block => 6!!!
  if (q == 5)
    next
  end
  puts "Block"
  "#{q}!!!" 
end

redoは無限ループの危険がありそうだ。

Proc

ブロックをオブジェクトにしてくれるもの。"ブロックをProcオブジェクトにする"と言い方をするらしい。
Procオブジェクト化したブロックはcallメソッドで呼べる。

def m1(q, &b)
  # &はブロックをProcオブジェクトに自動変換する。
  v = b.call(q)
  "yaho!#{v}"
end

m1(5) do |q| # yaho!haha5
  "haha#{q}"
end 

myb = Proc.new do |q|
  "haha2#{q}"
end
m1(8, &myb) # yaho!haha28
m1(8, myb) # エラー。&をつける必要がある。

def m2(q, b)
  v = b.call(q)
  "kaho!#{v}"
end
myb2 = Proc.new do |q|
  "kaha2#{q}"
end
m2(9, myb2) # kaho!kaha29

#別の書き方 : procメソッド
myb3 = proc do |q|
  "kaha3#{q}"
end
m2(10, myb3) #kaho!kaha310

#別の書き方 : lambda
myb4 = lambda do |q|
  "kaha4#{q}"
end
m2(11, myb4) #kaho!kaha411

#別の書き方 : ->
myb4 = ->(q) {
  "kaha5#{q}"
}
m2(12, myb4) #kaho!kaha512

#procとlamdbaの違い1 : lamdbaの方が引数のチェックが厳密だ。
myb5 = proc do |q,a|
  print [q, a]
end
myb6 = lambda do |q,a|
  print [q, a]
end
myb5.call(2) # [2,nil]
myb6.call(2) # エラー : ArgumentError: wrong number of arguments (1 for 2)

#procとlamdbaの違い2 : procを作ったメソッド外でproc内でreturnすると、エラーになる。
def m1
  proc { return 1 }
end
def m2
  lambda { return 2 }
end
p = m1 
p.call # エラー(procが自分を作ったm1の外で呼びだされ、returnしたため) : LocalJumpError: unexpected return
d = m2 
d.call # 2

今日はここまで。

webapps配下でないディレクトリにリソースを置く

Tomcatで、ウェブアプリで使われるリソースをwebapps配下でないディレクトリに置きたい場合あります。
例えば、リソース(画像やJSPなど)をアップロードしてウェブアプリの画面を動的に構成したい時です。
またお客様がウェブアプリ本体のソースを修正することなく、カスタマイズできるようにしたい時もありますね。

もし、そういうリソースをwebapps配下におくと、
ウェブアプリをデプロイするたびに既存のリソースが消えてしまうので、困りますね。

こういう場合、Tomcat8に追加されたResources Componentを使えばいいです。
以下のようにserver.xmlのContext要素配下にResources要素とその下のPostResources要素を追加します。

<Context docBase="myweb" path="" reloadable="false">
    <Resources allowLinking="false">
        <PostResources
            className="org.apache.catalina.webresources.DirResourceSet"
            base="${other}/myres"
            webAppMount="/myres" />
    </Resources>
</Context>

PostResourcesのbaseにはリソースが位置する絶対パスを指定します。
${other}のようにJVMシステムプロパティを指定することもできます。

webAppMountにはURLのパス(ウェブアプリのルートパスで次の部分)を指定します。
上記の場合、URLの後ろに/myresと入力した場合、
baseディレクトリにあるリソースを読み込んでレスポンスとして返します。

リソースの位置がwebappsでないだけで、既存webapps配下にあるリソースとまったく同じです。
良い機能ですね。

機械学習の種類とR言語でのパッケージ

機械学習の種類とR言語でのパッケージをざっくりと整理してみました。
自分の理解に役に立つ形態で整理したため、書籍に出るようなまともなものとは違うところがあります。
また、間違った部分もありえますので、気付き次第、修正していくつもりです。

予測

分類(Classification)

データを決まったカテゴリにグループ分けすることです。

kNNアルゴリズム

classパッケージのknn関数

ナイーブベイズアルゴリズム

e1071ペッケージのnaiveBayes()関数

決定木

C5.0アルゴリズムの実装では、C50パッケージのC5.0()関数

分類規則

One Ruleアルゴリズムの実装では、RWekaパッケージのOneR()関数。
RIPPERアルゴリズムの実装では、RWekaパッケージのJRip()関数。

回帰(Regression)

数値の予測を行うことです。

線形回帰

statsパッケージのlm(関数)

回帰木

CARTアルゴリズムの実装では、rpartパッケージのrpart()関数。
M5'(prime)アルゴリズムの実装では、RWekaパッケージのM5P関数。

複数用途

神経網

人間の脳を構成するニューロンの仕組みをコンピューターで真似て予測を行います。
Neuralnetパッケージのneuralnet()関数。

SVM(Support Vector Machine)

kernlabパッケージのksvm()関数。

パターン検索

複数のものとの間にどんな関係があるかを識別します。
小売業界ではバスケット分析として使われます(この商品を買った人が一緒に買った商品)。
アプリオリ(aprioi)アルゴリズムの実装では、arulesパッケージのapriori関数。

クラスタリング

分類(Classification)と似ていますが、分類はデータを決まったカテゴリにグループ分けするのに対し、
クラスタリングは決まったカテゴリはなく、ただ単に類似したもの同士でグループ分けします。
クラスタリングで得たグループから特徴を把握し、ビジネスに使えるパターンを捕らえます。

K-Meansアルゴリズムの実装では、statsパッケージのkmeans関数。