読者です 読者をやめる 読者になる 読者になる

Suica対応スマホに変更

Android

f:id:tmtms:20160919235256j:plain

データ通信用のスマホの他に、通話とモバイルSuicaのためにガラケー(F-01E)を持っていたのですが、ガラケーの2年縛りの契約の更新時期になりました。最近SIMロックフリーのスマホでもSuicaが使えるものが出てきたので、1台にまとめました。

arrows M03 の評判が良いみたいだったのでそれにしました。宗教上の理由で iPhone 7 ではありません。

今までも NifMo を使ってたし、キャッシュバックキャンペーン をやっていたので、NifMo で購入。

月々の維持費はこんな感じです。(税抜き)

docomo タイプシンプルバリュー 1483円
docomo ひとりでも割50         -740円
docomo iモード                 300円
NifMo SMS対応3GB              1050円
------------------------------------
合計                          2093円

NifMo 音声通話対応3GB         1600円
スマホセット割                -200円
arrows M03                    1482円 (24回払いの月額)
キャッシュバック              -416円 (1万円を24分割した値)
------------------------------------
                              2466円

2年経過後は 1600円/月 になる予定。

NifMo は「NifMo バリュープログラム」というのがあって、アプリをインストールしたりショッピングしたりすると料金が割り引かれるみたいなんですけど、面倒なのでやってません。

ただ、「勝てばスマホ利用料が安くなる!川崎フロンターレウイニングキャンペーン」は登録しました。何もしなくても川崎フロンターレが勝つ毎に20円割引されます。 サッカーには興味ないんですけど、ちょっとフロンターレを応援しようかという気になりますね :-)

スマホ移行

元々使ってたスマホは ASUS Zenfone 5 でした。

アプリの移行

基本的には Zenfone で Helium を使って SDカードにバックアップして、それを arrows M03 にリストアする感じでやりました。 詳しくはこちら

バックアップが禁止されているアプリがいくつかあったので、それは個別に。

slack

slack はバックアップできませんでした。まあログインし直せばいいだけです。

LINE

事前にメールアドレスを登録しておけば、新しい環境に LINE でログインするだけで使えるようになりますが、トーク履歴は引き継がれません。

移行前に元の環境で「トーク履歴のバックアップ」をしておいて、新しい環境で「トーク履歴をインポート」すればよいです。トーク毎にやらないといけないのでちょっと面倒です。

詳しくはこちら

データの移行

写真や音楽データは SDカードに元から入れてたので特に何もしなくても良かったです。

Zenfone だと文字化けしていた mp3 タグの「Boφwy」がちゃんと表示されました。Android のバージョンも違うのでそのせいかもしれません。

ガラケー移行

電子マネー

モバイルSuica と nanaco を使っていました。どちらもガラケーが使えなくなる前に機種変更手続きをしておく必要がありました。

モバイルSuica 機種変更

nanaco 携帯電話の機種変更について

ちゃんとやっとかないと、かなり面倒なことになりそうな感じです。

メール

実はちょっとトリッキーなことをして、imode メールをケータイじゃなくてIMAPを使ってスマホで読んでました。

iモードメールをスマホで送受信する(iPhone偽装による設定方法)

なので、PCのメールアプリからメールを全部コピーすれば終わりです。

電話帳&写真

docomo のサイトで新旧の機種を入力すると手順が表示されます。

データ移行ナビ

この手順は、SDカード経由でデータを移行するので通信ができなくなった後からでも行うことができます。

MNP切り替え

同梱されてた紙にURLが書かれてたので、そこにアクセスしてポチっとすれば申請は完了です。

夜に申請したのですが、すぐには切り替わらないので(たぶん人手で処理してる)、翌日は今までのガラケー&スマホと新しいスマホの3台持って出かけました。

切り替わるまでは新しいスマホはデータ通信できないので、元のスマホでテザリングしてました。

昼くらいにガラケーのアンテナが圏外になりました。新しいスマホは通信できないままだったのですが、再起動したら使えるようになりました。

移行後

それまで使ってた Zenfone 5 よりもちょっと小さいので違和感ありましたが、すぐに慣れました。

元から入ってた日本語入力の「Super ATOK ULTIAS」はかなりもっさりだったので、今まで使ってた普通の ATOK にしました。 キーボードを切り替えずに手書き入力ができたりして面白そうだったんですけどね…。

Suica は普通に使えました。これが使えないと話にならない…。

全体的に Zenfone 5 よりもちょっと遅い気がします。Android バージョンが異なるのでそのせいかもしれません。

Zenfone 5 には大きな不満が2つあって(「充電中は通知ランプが機能しない」「ナビゲーションバーが液晶外でボタンが絵なので暗いところで使うと見えない」)、それが解消したのはよかったです。

ホーム画面アプリはすぐに Nova Launcher に切り替えてしまったので使い勝手はわかりません。

モバイルSuica

iPhone 7 に Suica がついて、おそらくモバイルSuica が使えるようになるのに、「Suicaなんていらねー」と思っている人が結構いるようです。

個人的にはモバイルSuicaはチョー便利だと思ってるので解せません。

モバイルSuicaがあると、

  • JR東日本の新幹線の予約がスマホからできる。
  • 座席の指定もスマホから可能。
  • モバイルSuicaで指定席を買うと、普通に自由席を買うよりも安い。
  • Suicaのチャージがスマホからできる。

注意事項もあって、

  • モバイルSuicaの乗車券は新幹線下車駅まで。そこから先の乗車券は別料金。
  • たとえば、長野から品川まで行くとしたら、普通に切符を買ったら東京までの乗車券で品川まで行けるけど、モバイルSuicaの場合は、東京-品川間は別料金が掛かる(それでもモバイルSuicaの方が安いけど)。
  • ビューカード以外ではモバイルSuicaは年会費が掛かる。
  • ビューカードだとモバイルSuicaの年会費は無料なんだけど、ビューカード自体に年会費が掛かる。
  • 個人的におすすめなのは、ビックカメラSuicaカードで、前年度にカード利用が1回でもあれば年会費無料。モバイルSuicaで新幹線に乗ったり、Suicaのチャージにカードを使っても実績になるので、実質年会費無料。

書いてて思ったんですけど、JR東日本の新幹線に乗らないような人にはあんまりメリットないかもしれない…。

MySQL で utf8 と utf8mb4 の混在で起きること

MySQL

MySQL を UTF-8 で使おうと思ってハマりがちなのは charset utf8 を指定してしまうことです。

MySQL の UTF-8 には歴史的事情により utf8 と utf8mb4 の二つあります。

UTF-8 は1バイト〜4バイトで1文字が構成される文字コードですが、MySQL の utf8 は4バイト文字を扱うことができません。ハマりたくなければ utf8mb4 を使いましょう。

utf8 を使ってしまった場合に4バイト文字がどのように扱われるか、自分でもうろ覚えだったのでメモしておきます。

登録

接続が utf8mb4 でカラムが utf8mb4

あたりまえですが、そのまま登録されます。

mysql> insert into utf8mb4 (c) values ('美味しい🍣と🍺');
mysql> select * from utf8mb4;
+-------------------------+
| c                       |
+-------------------------+
| 美味しい🍣と🍺              |
+-------------------------+

接続が utf8 でカラムが utf8mb4

4バイト文字が「????」になります。

mysql> insert into utf8mb4 (c) values ('美味しい🍣と🍺');
Warning (Code 1300): Invalid utf8 character string: 'F09F8D'
Warning (Code 1366): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1
mysql> select * from utf8mb4;
+-------------------------+
| c                       |
+-------------------------+
| 美味しい????と????      |
+-------------------------+

utf8 の接続から送られてくるデータでは4バイト文字は不正な4バイトデータなので、4つの「?」に置き換えられます。

なお、sql_mode の設定によってはエラーになります。MySQL 5.7 のデフォルトではエラーになります。安全ですね。

mysql> insert into utf8mb4 (c) values ('美味しい🍣と🍺');
ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1

接続が utf8mb4 でカラムが utf8

4バイト文字が「?」になります。

mysql> insert into utf8 (c) values ('美味しい🍣と🍺');
Warning (Code 1366): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1
mysql> select * from utf8;
+-------------------+
| c                 |
+-------------------+
| 美味しい?と?      |
+-------------------+

utf8mb4 の接続上は4バイト文字は正しい1文字ですが、utf8 に対応する文字がないため、1つの「?」に置き換えられます。

sql_mode の設定によってエラーになるのは同上です。

接続が utf8 でカラムが utf8

4バイト文字が現れるとそこで文字列が切られてしまいます!

mysql> insert into utf8 (c) values ('美味しい🍣と🍺');
Warning (Code 1300): Invalid utf8 character string: 'F09F8D'
Warning (Code 1366): Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'c' at row 1
mysql> select * from utf8;
+--------------+
| c            |
+--------------+
| 美味しい     |
+--------------+

utf8 の接続で4バイト文字は4バイトの不正データなので「????」になっても良さそうなのですが、MySQL の気持ちはよくわかりません。 接続もカラムも utf8 なので文字コードの変換が行われず、そのまま登録しようとしたらおかしな文字があったからそこで打ち切り…ということなのかもしれません。

まあ、これも sql_mode をちゃんと設定しておけばいいのですけど…。

参照

utf8mb4 接続で参照

あたりまえですが、そのまま参照できます。

mysql> select * from utf8mb4;
+-------------------------+
| c                       |
+-------------------------+
| 美味しい🍣と🍺              |
+-------------------------+

utf8 接続で参照

4バイト文字は「?」に置換されます。

mysql> select * from utf8mb4;
+-------------------+
| c                 |
+-------------------+
| 美味しい?と?      |
+-------------------+

文字「?」そのものが入っているのか、参照時に置換されたのかは HEX() 関数で確認できます。

mysql> select right(c,1),hex(right(c,1)) from utf8mb4;
+------------+-----------------+
| right(c,1) | hex(right(c,1)) |
+------------+-----------------+
| ?          | F09F8DBA        |
+------------+-----------------+

右端1文字の文字コードを16進で出力すると「?」の文字コード 3F ではなく「🍺」の F09F8DBA になっていることがわかります。

Ruby の Timeout の仕組み

Ruby

Ruby で長い時間掛かるかも知れない処理のタイムアウトを行うにはこんな感じにします。

require 'timeout'

begin
  Timeout.timeout(3) do # 3秒でタイムアウト
    hoge                # 何かの処理
  end
rescue Timeout::Error
  puts 'なげーよ'       # タイムアウト発生時の処理
end

Timeout.timeout はブロック開始時にスレッドを作成し、そのスレッドで指定された秒数だけ sleep して、sleep から復帰してもまだブロックが終わってなければ作成元のスレッドに対して Timeout::Error 例外を発生させます。

指定時間以内に処理が終わる場合:

timeout(X)
    │
スレッド作成 ─┐
    │         │
ブロック実行  sleep X
    │         │
スレッドkill→ 🕱
    │
timeout復帰

指定時間以内に処理が終わらない場合:

timeout(X)
    │
スレッド作成 ─┐
    │         │
ブロック実行  sleep X
    :          │
    :  ← 元スレッドに Timeout::Error
    :

仕組みはシンプルです。ですが、実際に timeout.rb を読んでみると中では結構複雑なことをしてました。

たとえば、次のスクリプトを実行すると、'hoge' ではなく 'main' が出力されます。

require 'timeout'

def hoge
  sleep 5
rescue Timeout::Error
  p 'hoge'              # こっちは表示されない
end

begin
  Timeout.timeout(1) do
    hoge
  end
rescue Timeout::Error
  p 'main'              # こっちが表示される
end

hoge 内の sleep 中で Timeout::Error が発生したなら、普通は hoge 内の rescue で処理されるはずなのに、実際には main 側の rescue で処理されています。

確かに使う側からしたらこっちの方が便利なんですけど、Timeout は内部的にどのようにしてこのような処理を実現しているのでしょうか。

例外オブジェクトを作成して、それを元スレッドに対して raise してるのは合ってるんですけど、元スレッドで例外が発生しようとしたら無理やり throw に切り替えてました。

timeout.rb のやってることを簡単に書くとこんな感じです(実際の処理とは異なります):

class Timeout::Error < RuntimeError
  def exception(*)    # 例外発生時には Exception#exception が呼ばれるので
    throw self        # それをフックして代わりに throw する
  end
end

def timeout(sec)
  begin
    x = Thread.current  # 元のスレッド(timeout を呼び出したのと同じ)
    err = Timeout::Error.new
    y = Thread.new do
      sleep sec
      x.raise err       # 元のスレッドに対して例外発生(実際には throw される)
    end
    catch(err) do
      return yield      # ブロックが復帰したら return
    end
    raise err           # タイムアウト時はここで改めて例外発生
  ensure
    y.kill              # タイムアウト用スレッド終了
  end
end

raise - rescue と違って、throw - catch は まったく同じオブジェクト(object_id が同じもの)でないと catch しないので、別の timeout メソッドが作った例外オブジェクトはスルーするんですね。若干トリッキー感が否めませんが。

今回の学び

  • Thread#raise で別のスレッドに対して例外を発生させることができる。
  • Exception#exception を上書きすれば例外発生時に任意の処理を実行できる。

Sequelの罠

Ruby

Sequel の罠っぽい挙動にハマったのでメモ。

次のようなテーブル a, b, c, d がありまして、

mysql> select * from a;
+------+
| id   |
+------+
|    1 |
+------+
mysql> select * from b;
+------+
| id   |
+------+
|    2 |
+------+
mysql> select * from c;
+------+
| id   |
+------+
|    3 |
+------+
mysql> select * from d;
+------+
| id   |
+------+
|    4 |
+------+

このテーブルを結合して、こういう結果を取り出そうと思って、

mysql> select a.id,b.id,c.id,d.id from a join b join c join d;
+------+------+------+------+
| id   | id   | id   | id   |
+------+------+------+------+
|    1 |    2 |    3 |    4 |
+------+------+------+------+

Sequel で書いてみたんですよ。ところがこんな結果に。

DB[:a].join(:b).join(:c).join(:d).get([:a__id, :b__id, :c__id, :d__id])
#=> [4, 4, 4, 4]

発行されているクエリはまともでした。

SELECT `a`.`id`, `b`.`id`, `c`.`id`, `d`.`id` FROM `a` INNER JOIN `b` INNER JOIN `c` INNER JOIN `d` LIMIT 1

どうやら、配列としてレコードを取り出す場合でも、内部的にはカラム名をキーとした Hash が使われてるのが原因のようでした。

なので、カラムにちゃんと別名をつけてやればうまくいきます。

DB[:a].join(:b).join(:c).join(:d).get([:a__id___a, :b__id___b, :c__id___c, :d__id___d])
#=> [1, 2, 3, 4]

Hash として取り出すときには気をつけてたんですけど、配列として取り出す時にもカラム名が影響するとは…。

で、過去に書いたコードを見てみたらちゃんと別名つけてたりしたんで知ってたはずなんだけど、しばらくかかないと忘れちゃうなぁ。

要素数ができるだけ均等になるように配列を分割する

Ruby

例えば10個の要素を持つ配列があって、これを3つに分割したい時に、

a = [1,2,3,4,5,6,7,8,9,10]
n = 3
m = Rational(a.size, n).ceil
a.each_slice(m).to_a            #=> [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10]]

みたいにすると、3つには分割できるんですが、要素数が 4, 4, 2 と偏ってしまいます。これを例えば 4, 3, 3 のようにしたい。

a = [1,2,3,4,5,6,7,8,9,10]
n = 3
n.times.map{|i| a[a.size*i/n ... a.size*(i+1)/n]}
  #=> [[1, 2, 3], [4, 5, 6], [7, 8, 9, 10]]

…のようにやればできました。

なお、a.size よりも n の方が大きいと、結果に空配列を含みます。

a = [1,2,3]
n = 6
n.times.map{|i| a[a.size*i/n ... a.size*(i+1)/n]}
  #=> [[], [1], [], [2], [], [3]]

これが嫌な場合は最後に空配列を除外する感じで。

a = [1,2,3]
n = 6
n.times.map{|i| a[a.size*i/n ... a.size*(i+1)/n]}.reject(&:empty?)
  #=> [[1], [2], [3]]

追記

これは私が考えたわけではなくて、数年前に若い人に教えてもらったものです。

はじめ見た時、なんでこれでいいのかわからなかったんですが、単純にn分割した時の境界の小数点以下を切り捨てて整数にしてるだけです。

f:id:tmtms:20160728215136p:plain

図は10個を6分割した時の例です。

頭いいなー。

Rubyで指定バイト数を超えない文字列の取得

Ruby

文字エンコーディングにUTF-8を使用した場合、1文字は1バイト〜4バイトです。

ある文字列の先頭からn文字の文字列を取り出すには次のようにできます。

str = "本日は晴天なり"
n = 3
str[0, n]    #=> "本日は"

先頭からnバイトを超えない最大の文字列を取り出す方法を考えてみました。

愚直に数える

str = "本日は晴天なり"
n = 10
out = ""
str.each_char do |c|
  break if out.bytesize + c.bytesize > n
  out.concat c
end
out   #=> "本日は"

inject を使うと少しかっこ良くなるかもしれない

str = "本日は晴天なり"
n = 10
str.each_char.inject(''){|a,b| break a if (a+b).bytesize > n; a+b}  #=> "本日は"

バイナリデータとしてnバイト抜き出した後に不正なバイト列を消す

str = "本日は晴天なり"
n = 10
str.b[0, n].force_encoding("utf-8").scrub("")   #=> "本日は"

ちなみに最後の scrub がないと "本日は\xE6" になってしまいます。 最後だけでなく途中に文字として不正なバイトがあった場合はそれも消えます。

[追記]

Twitter で byteslice というメソッドがあるのを教えてもらいました。考え方は同じですが、こっちの方がすっきり書けますね。

StringIO#gets を使う

str = "本日は晴天なり"
n = 10

require 'stringio'
StringIO.new(str).gets(nil, 10)  #=> "本日は晴"
s.chop! if s.bytesize > n
s                                #=> "本日は"

gets は指定バイト数が文字境界にない場合は文字境界まで読むので1文字多くなることがあるので、最後に調整してます。


なんかどれもいまいちな気がします。もっといい方法ありますかねー。

MySQL X Protocol を解析してみる

MySQL

前回 MySQL X Protocol で使用している Protobuf について書きましたが、それだけでは MySQL のプロトコルは解析できません。

TCP を流れるデータは区切りがないので、書き込み側が Protobuf データをただ垂れ流しても、読み込む側がどう読んで良いのかわかりません。

書き込むデータの大きさと、書き込む Protobuf データの型を相手に伝える必要があります。

MySQL X Protocol のパケットは次のようになっているようです。

┌────┬───────────────
│size(4) │type(1) + Protobuf(size-1)
└────┴───────────────

最初の4バイト(リトルエンディアン)で続くデータ部のサイズを示します。 データ部の先頭1バイトは Protobuf データの型を示します。

Protobuf データの型は、クライアントから送るデータは ClientMessages::Type で、サーバーから送るデータは ServerMessages::Type に enum で定義されています。

TCP 上を流れるデータの形式がわかったので、あとは、どの型のデータがどのタイミングでサーバー/クライアントのどちらから送られるかがわかればいいです。

ドキュメント https://dev.mysql.com/doc/internals/en/x-protocol.html もありますが、実際に mysqlsh の通信を見てみるのが手っ取り早いかもしれません。

次のような MySQL X Protocol を中継するプログラムを作って動かしてみました。

require 'mysqlx.pb'
require 'socket'

ClientMessage = {}
Mysqlx::ClientMessages::Type.constants.each do |c|
  v = Mysqlx::ClientMessages::Type.const_get(c)
  if v.is_a? Protobuf::Enum
    ClientMessage[v.to_i] = c
  end
end

ServerMessage = {}
Mysqlx::ServerMessages::Type.constants.each do |c|
  v = Mysqlx::ServerMessages::Type.const_get(c)
  if v.is_a? Protobuf::Enum
    ServerMessage[v.to_i] = c
  end
end

localport, host, port = ARGV

def relay(r, w, from)
  while true
    head = r.read(5)
    break unless head && head.length == 5
    size, type = head.unpack('VC')
    if from == :client
      puts "C: #{ClientMessage[type] || type}"
    else
      puts "S: #{ServerMessage[type] || type}"
    end
    data = r.read(size-1)
    break unless data && data.length == size-1
    w.write(head + data)
  end
rescue => e
  p e
end

Socket.tcp_server_loop(localport) do |client, _addrinfo|
  server = TCPSocket.new(host, port)
  Thread.new(client) do |_client|
    relay(_client, server, :client)
  end
  Thread.new(client) do |_client|
    relay(server, _client, :server)
  end
end

33061 ポートで待ち受けて 127.0.0.1 の 33060 に中継するように動かします。

% ruby -I. ./mysqlx-relay.rb 33061 127.0.0.1 33060

別の端末から mysqlsh を次のように起動します。

% mysqlsh --uri mysql://hoge@127.0.0.1:33061/test --sql
Creating a Node Session to hoge@127.0.0.1:33061/test
Enter password: 
Default schema `test` accessible through db.

mysql-sql> プロンプトが出るまでのパケット。結構多い…。

C: CON_CAPABILITIES_GET
S: CONN_CAPABILITIES
C: SESS_AUTHENTICATE_START
S: SESS_AUTHENTICATE_CONTINUE
C: SESS_AUTHENTICATE_CONTINUE
S: NOTICE
S: SESS_AUTHENTICATE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: SQL_STMT_EXECUTE_OK

SELECT したり、

mysql-sql> SELECT * FROM t;
C: SQL_STMT_EXECUTE
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_COLUMN_META_DATA
S: RESULTSET_ROW
S: RESULTSET_ROW
S: RESULTSET_ROW
S: RESULTSET_FETCH_DONE
S: NOTICE
S: SQL_STMT_EXECUTE_OK

INSERT したり、

mysql-sql> INSERT INTO t (id, value) VALUES (1, 'abc'),(2,'def');
C: SQL_STMT_EXECUTE
S: NOTICE
S: NOTICE
S: SQL_STMT_EXECUTE_OK

いい感じに動いてるようなので、あとは色々試してみます。