実装方針について考えてみた

いつにもなく抽象的なタイトルですが、久々の投稿。

webシステムを構築する際に実装方針はある程度あると思うけど、

  • 開発初期と後期で色々変わる
  • 変更したいけどテストが不十分で治すのが怖い
  • 本当はもっといい書き方があるけここだけ治すのもちょっと

などの理由で一貫性のあるコードを書くのは実は難しかったりするのではないかと。

実装方針が明文化されてないために迷いながらコードを書くのも生産性に悪影響ですよね。

前提

  • ここでいう実装方針とは開発する上で何をどこに書くかみたいなやつ
  • ある特定の言語やフレームワークに依存する話でもない
  • 品質も定義が難しいけど、ここではお客さんが求める品質ではなく開発者的にとりあえず実装方針に準拠してるか否かみたいなので定義

目標

新規メンバーのオンボーディングや、引き継ぎの際に実装方針をちゃんと話せるようにできるところを目指してる

実装方針は極力言語化してwikiに残す

実装をしていく過程でよい書き方が見つかったとか、実は今まで書いてたコードが書き方は世の中的に良くないとか、
変数名の付け方が人によってはよって揺らぎがあるとか、レビュワーがいればあれですが、
レビュワーも人間なので設計方針が変わったりすることもあるのではないかと。

ので、テックリードなりアーキテクトな人は常に自分が思う設計方針をwikiなりに言語化しておくのがよいかと。

このwikiは開発初期に書いてもいいですし、開発中もどんどん更新して良いかと思います。
これは悪手だったとか、筋悪だったとかそういったものも見つかるだろうし、開発中期でようやくドメインが表層化してくることもあるかと思います。

このときテストがちゃんとかけてることが前提になりますが、方針が更新されたら過去のプロダクションコードも見直していく、
みたいなフィードバックループが回せればなんでこうなったみたいな現象も減らせるのではないかなと。

ワタシはgithub wikiに思ったことや気づいたこと、良かったことを逐一更新するようにしてます

自分の設計に自信が持てない時は誰かがに多様なことやってるリンクや記事を引用して自信を持つ。。。!

そもそも書かなければ方針とか考えなくてよい

自分で頑張って書いたコードは実はFWでそれ簡単に実装できるよとか、ライブラリで解決できるよとか結構あるかと思います。

これ実はFWで簡単に解決できるんじゃね?とかメソッドオーバーライドすれば済む話だったりとかとか、実は実はコールバックで振る舞い変えられるとか、常に自分のコードに疑いをもつことは大事かなと。

テストコードも書かなくてよくなったり、負債が減るのでそういったところで生産性を高めていきましょう。

結果、そもそも実装しないという方針になり、生産性が向上する。。。はず。

テストからのフィードバック

開発後半に差し掛かると既に開発したプロダクションコードやDBのテーブル名を変更したい(しっくりくる用語が見つかった)とか出てくると思います。

主にプロジェクトの序盤で要件定義が不十分だったり、設計フェーズに時間がかけられなかったり、ドメインエキスパートとのやり取りもそんなに時間かけられないなど、
正しい設計を完璧に行うのは難しい気がしてます。

ので開発中に設計が変更になってもテストコードがあれば躊躇なくリファクタリングできるので、よりよい実装方針に追随していくためにはテストは欠かせないかなと思います。

特に知見のないフレームワークとかだと想定と振る舞いが違うとか、これはテストしにくいコードだから設計を変えようとか(時にはmockで頑張る)、実装方針へとフィードバックを行えます。

テストが正しい実装へと導いてくれる。。。はず。

CIは早めに導入してレビューをしやすく

昨今CIの導入の敷居はものすごく下がったので、早めに導入しちゃいましょう

テスト、テストカバレッジ、静的解析、コーディング規約チェックなど、運用時入った場合にも心置きなくCDできるようにしておきましょう

筆者はCIがこけるとちゃんとテストが機能してるぜい!ってなるので逆に喜びます
あと、この書き方はヤベーとか機械的にチェックしてくれてたり、コードカバレッジもテストがたりてないところが定量的に見れるので、早期に導入しておくとチーム内で書き方が統一されたり、レビューポイントが設計周りにとかに注力できるので楽です。

中盤に導入すると面倒なので序盤にさくっといれちゃうのが良いと思います。

まとめ

フリーランスや個人事業主でも小さいチームを作って開発をすすめることは多々あるかと思いますが、
事前に決めた方針通りにすすめるのではなく、常にアップデートをかけてよいフィードバックサイクルを回していくことが大事。。なはず。

Please share  

NATを越えろ

自宅のデスク周りを充実させまくったら、部屋から完全に出不精になってしまいました。

ビデオ会議をする機会が大分増えてきて、今後もビデオチャットで仕事をすることや、プライベートでも活用の幅がふえてくるのでは?ということでせっかくなので※1 WebRTCでビデオチャットを実装した際にNATについて色々学びがあったのでここでアウトプット。

※1. Web RealTime Communicationの略でブラウザで気軽にリP2P通信ができるようにしたもの(オープン標準技術)

以下WebRTCのプロトコルスタック
主に赤枠の部分についての話

図:オライリー

そもそもNATってなんやねん

NAT(Network Address Translation)とは、コンピュータネットワークにおいて、主にグローバルIPアドレスをプライベートIPアドレスに、またはその逆の変換を行う技術のことである。

by wikipedia

つまりプライベートアドレスをグローバルアドレスに変換してインターネット上のアドレスとやりとり出来るようになる

ローカルネットワークとグローバルネットワークを行き来するための仕組み

静的・動的?

静的NAT

グローバルアドレスとプライベートアドレスを1対1の固定ペアとして変換を行う

動的NAT

外部への通信が発生したときに、プール内のグローバルアドレスを使用して変換を行う

NAPT(動的)

NAPT(Network Address Port Translation)とは、アドレスに加えてポート番号も変換する技術。
グローバルアドレスがひとつの場合でも、動的に割り当てられた送り元Portと送り先Portを元に関連付けを行うことができる。

家や会社で一つのグローバルIPで同時にインターネットができるのはこの仕組みのおかげ

送り元IP:送り元ポート.送り先IP:送り先ポート内容で接続ごとに管理している

NATタイプ

Full cone NAT(フルコーンNAT)

送信したことがないアドレスでも受信可能
※UPnPのポートマッピングはほぼこれに該当する

Address-Restricted cone NAT(制限付きコーンNAT)

一度送信した端末(NATデバイス上にWAN側に送信先アドレスのエントリが存在する場合)であれば受信可能

Port-Restricted cone NAT(ポート制限付きコーンNAT)

一度送信した端末(NATデバイス上にWAN側に送信先アドレスと送り元Portのエントリが存在する)であれば受信可能

Symmetric NAT(対象型NAT)

対象型NATは最も制限の厳しいもので、宛先アドレスとポートを用いて接続先を区別し、一つの接続先に対し一つのアドレス変換を用いる

Nat traversal(NAT越えの手法)

以上をふまえると(ふまえなくても)NATを越えるのがいかに難しく、不確定要素が多いのかが分かります。。。

人様のPCに外部から接続なんで冷静に考えたら厳しいのは当たり前ではあるのですが(; ・`д・´)

NATを越えるためにいくつか手法として、ルーターで対応してるもの(UPnP)や、connection reversalなどがありますが、
WebRTCで利用されるSTUNという仕様があり、その中で利用されているUDP hole punchingについてちょっと触れてみた。

UDP hole punching

上記Cone型による接続を行う際にファイヤーウォールとかどうやって突破すんねんって疑問が沸くかと思いますが、
それをうまいことするのがこのUDP hole punching。

上記の仕組みを使うことで、NAT外サーバー(STUN SERVER、SIGNALING SERVER)を経由してアドレス交換を行い、NAT内ホスト同士をNAT外から通信の開始を可能にしている。

まとめ

今回は触れませんでしたが、Peer間で最適な通信方法(STUN, TURN)の交渉にICEという仕組みがあったりとか、
STUNで越えられない場合の最終手段TURNなども今回ビデオチャットを実装する上で軽く触れましたが、割愛させていただきます(*´з`)

最後に、本技術を駆使して以下プロダクトを作りましたのでよろしければ!

DNSのデバックのおともにdigコマンド

DNSサーバーの動作確認をする際のコマンドとして、nslookupとか使って対話的に確認したりすると思うけど、
どうやらUNIX, Linux的に非推奨だということが(今更)分かり、digコマンドをようやく使ってみたメモです。

また、nslooupより高機能らしいので高機能好きの僕には今が覚える好機。(いみふ)

ちなみにwindowsには標準ではいってないので使う方は頑張ってインストールしてください。

まずはシンプルに実行


実行コマンド
1
$ dig google.com
実行結果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[admin@localhost ~]$ dig google.com

; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17627
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;google.com. IN A

;; ANSWER SECTION:
google.com. 299 IN A 216.58.197.142

;; Query time: 54 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: 10 22 17:45:30 JST 2019
;; MSG SIZE rcvd: 55

確かにnslookupより情報量が多い( ゚Д゚)

status 応答コード
NOERROR 正常
REFUSED 拒否
flags 応答コード
qr サーバーからの回答が正常に行われた
rd 再帰的問い合わせを要求
ra サーバが再帰的問い合わせをサポート

※ここでいう再起はプログラミング的な再起処理ではなくて、「再び帰ってくる」という意味でちゃんとした結果を返してくることをサポートしますよ!ということ。

1
2
3
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17627
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

Question

オプションに何も指定せずに実行するとクエリタイプが「a」になるっぽい

1
2
;; QUESTION SECTION:
;google.com. IN A

Answer

Aレコードの内容がきちんと見れました

1
2
;; ANSWER SECTION:
google.com. 299 IN A 216.58.197.142

リソースを指定したい場合


■ ns(ネームサーバー)

実行コマンド

1
$ dig ns google.com

実行結果

Answerセクション部分のみ抜粋

1
2
3
4
5
;; ANSWER SECTION:
google.com. 19288 IN NS ns3.google.com.
google.com. 19288 IN NS ns1.google.com.
google.com. 19288 IN NS ns4.google.com.
google.com. 19288 IN NS ns2.google.com.

■ mx(メール交換情報)

実行コマンド

1
$ dig ms google.com

実行結果

Answerセクション部分のみ抜粋

1
2
;; ANSWER SECTION:
google.com. 234 IN A 172.217.161.46

■ any

実行コマンド

1
$ dig any google.com

実行結果

全部入り的なやつ

Answerセクション部分のみ抜粋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;; ANSWER SECTION:
google.com. 299 IN A 216.58.197.142
google.com. 299 IN AAAA 2404:6800:4004:800::200e
google.com. 21599 IN NS ns1.google.com.
google.com. 599 IN MX 30 alt2.aspmx.l.google.com.
google.com. 599 IN MX 10 aspmx.l.google.com.
google.com. 599 IN MX 20 alt1.aspmx.l.google.com.
google.com. 299 IN TXT "docusign=1b0a6754-49b1-4db5-8540-d2c12664b289"
google.com. 21599 IN NS ns2.google.com.
google.com. 599 IN MX 50 alt4.aspmx.l.google.com.
google.com. 21599 IN NS ns3.google.com.
google.com. 599 IN MX 40 alt3.aspmx.l.google.com.
google.com. 3599 IN TXT "globalsign-smime-dv=CDYX+XFHUw2wml6/Gb8+59BsH31KzUr6c1l2BPvqKX8="
google.com. 59 IN SOA ns1.google.com. dns-admin.google.com. 275806207 900 900 1800 60
google.com. 3599 IN TXT "v=spf1 include:_spf.google.com ~all"
google.com. 21599 IN NS ns4.google.com.
google.com. 3599 IN TXT "facebook-domain-verification=22rm551cu4k0ab0bxsw536tlds4h95"
google.com. 21599 IN CAA 0 issue "pki.goog"
google.com. 299 IN TXT "docusign=05958488-4752-4ef2-95eb-aa7ba8a3bd0e"

ネームサーバーを指定する場合

ネームサーバー切り替え前にデバックするときに重宝する

実行コマンド

1
$ dig a google.com 8.8.8.8

必要最低限の情報だけほしい場合

実行コマンド

+shortをつけるだけ!

1
$ dig ns google.com +short

実行結果

大分、目に優しい感じで表示されます

1
2
3
4
ns3.google.com.
ns2.google.com.
ns4.google.com.
ns1.google.com.

Please share  

メールが送信されるまでをパケットを見ながら解剖!

以前、コマンドだけでメールを送信する記事を書きましたが、
メール送信の一連の流れをパケットで追いかけていきたいと思います!

人物相関図


下記のソースを使えば、上記の図の構成でDocker環境を構築することができ、実際に動作を検証することができます。
https://github.com/kuniiskywalker/mail-tester

登場人物


送信君(送り手のメールサーバー)

IP:10.5.0.6
ドメイン:fuga.local


受信君(受け手のメールサーバー)

役割:メールサーバー
IP:10.5.0.5
ドメイン:hoge.local


案内マン(ネームサーバー)

IP:10.5.0.4


あらすじ(送信完了までの送信サーバーのパケットの流れ)

この図を元に解説していきます。

① 送信先メールアドレスのMXレコード確認


※MXレコード:メールの送信先IPアドレスが設定されたDNSレコード。

 送信君:

root@hoge.localに送信したいので、hoge.localのMXレコード教えてください

 案内マン: MXレコードは`mail.hoge.local`です。
 送信君:

ではmail.hoge.localのIPアドレスを教えてください。

 案内マン:

10.5.0.5です。

② TCPハンドシェイク


 送信君:

はじめまして受信君!
案内マンからIPアドレスきいてご連絡しました!
今からメールを送るためのやり取りしたいのですがいいですか?

 受信君:

はい大丈夫です!

 送信君:

ありがとうございます!

③ EHLO


※EHLOとはSMTP拡張機能を使うために送信者と受信者でお互いにサポートしてる機能をあわせて使用しましょうというやり取り。

 受信君:

メール受信を行う上での開始準備できてます!

 送信君:

それではまず、受信君はどういった機能をサポートしてるか知りたいので、教えてもらっていいですか?

 受信君:

↓こういうやつに対応してます!

SIZE:受信可能サイズ 10240KB

PIPELINE:SMTPで送信を行うための準備に使うコマンドを一括実行できる

他は割愛。。。

 送信君:

ありがとうございます!

PIPELINE対応してるんですねー、ではまとめて送信メールアドレスとか、送り元メールアドレスを送りますので、送信可能かどうか確認お願いします!

※MAIL FROM, RCTP TO, DATAコマンドを使って上記の確認を行う。

※1個ずつ実行していく場合は通信の利用効率が悪くなるため、こういった方式が導入されている。

⑤ 送信可否結果


 受信君: 結果をまとめてお返しします!
 受信君: ありがとうございます! 全部OKそうですね!

⑥ メール送信

 送信君: 結果うけとりました! メールを送信を許可いただきありがとうございます!

それではroot@hoge.local宛てに、差出人root@fuga.localとして、件名:test本文:This is test mail.でお送りします!

まとめ

登場人物も含めてやり取りをまとめてみました!

ちょっと駆け足で書いたので、ちょっとずつ修正します。。。

ローカル環境でメールの送受信の確認ができる環境をDockerでつくったのでご活用ください!
https://github.com/kuniiskywalker/mail-tester

Please share  

SNIのメモ

SNIのパケット覗き見

SNI(Server Name Indication)とは?

apacheのTLS通信用バーチャルホストをネームベース(ホスト名)で設定できる技術。
通常TLS通信の場合は、ホスト名も含めて暗号化されるからapacheは対応するバーチャルホストを見つけることができず、IPで見分けるしかなかった。

つまりIPひとつに対して443ポートが一つしか関連付けできないため、
事実上、IPに対してひとつしかTLSのバーチャルホストを設定できなかった。

そこで、ホスト名を暗号せずにブラウザから送信して、ネームベースでバーチャルホストの設定をできるようにする技術をSNI(Server Name Indication)と呼ぶらしい。

TLS拡張(RFC4366)
http://www.ipa.go.jp/security/rfc/RFC4366JA.html

検証したこと

  1. 俺俺TLS証明書
  2. httpd-ssl.confに以下バーチャルホストを設定
  3. chromeで https://test1.localhost にアクセス
  4. 実際パケット上でサーバー名がどうやって送信されるかwiresharkで確認

httpd-ssl.confの設定例

1
2
3
4
5
6
7
<VirtualHost *:443>
ServerName test1.localhost
DocumentRoot "/usr/local/apache2/htdocs"
SSLEngine on
SSLCertificateFile "/usr/local/apache2/conf/server.crt"
SSLCertificateKeyFile "/usr/local/apache2/conf/server.key"
</VirtualHost>

パケット

TLSハンドシェイクのClient Hello時(Client Helloについてはこちら)にTLS拡張で暗号化せずにserver_nameを送信してる。
おそらくapacheはこの情報をみてバーチャルホストを判定してる。

まとめ

パケットを見て分かるように、クライアントからservernameを暗号せずに送信してるため、当然のごとくクライアント側の実装依存になってしまい、旧ブラウザとか使えない(今はどうか知らない。。。)

apacheの設定をすることもめっきり減りましたが、ちょっとレンタルサーバーいじってて気になったのでメモしてみました(´・ω・`)

tlsハンドシェイクのメモ

tlsハンドシェイクとかわけわかめだったのでメモ。

やったこと

1. TLSの俺俺証明書作成

1
2
3
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

2. dokcerでapacheたてて、tcpdumpでパケットキャプチャー

apache用dockerコンテナー

https://github.com/kuniiskywalker/docker-test-capture-https

apache設定内容

DH鍵交換(DHE)無効

クライアントとサーバーで一時的な秘密鍵を生成して共通鍵を交換する安全な方法ですが、wiresharkで復号できないためRSAを使った鍵交換のみに設定。
但しクライアント側でもDHEを無効にしないと意味ない。

1
2
# SSLCipherSuite HIGH:MEDIUM:!MD5:!RC4:!3DES
SSLCipherSuite kRSA
SSLのキャッシュ無効

キャッシュを利用されると二回目以降とかの通信でhttpsの通信の一部を省略されてしまうため無効

1
2
3
4
#SSLSessionCache         "dbm:/usr/local/apache2/logs/ssl_scache"
#SSLSessionCache "shmcb:/usr/local/apache2/logs/ssl_scache(512000)"
SSLSessionCache none
SSLSessionCacheTimeout 300

3. クライアントからcurlでアクセス

ブラウザの設定でDHEを無効にするのが面倒だったので、curlで検証しました。。。

1
2
# dokcerが動いてるvmによって以下アクセス先は要変更
curl --cacert server.crt https://localhost

4. キャプチャしたパケットをwiresharkで眺める

Wiresharkを開き、キャプチャしたファイルを開く。

通信が開始されるまでの大まかな流れ

Hello交換

事前にこういう方式で暗号化のやりとりしますよという取り決め

鍵交換

暗号化に使う鍵の交換
(実際はパラメーターを送信しあってお互いに共通の鍵を作る)

データ通信

利用開始!
実際は暗号化されていて中身は暗号化されていて、よくわからない文字列で埋め尽くされている

1.Hello交換

ざっくり言うと私(クライアント)が暗号化方式とか諸々提示しますので、あなた(サーバー)がどうやって暗号化するか決めてください。

というやりとりっぽい。

◯client hello

プロトコルバージョン

TLS1.0, 1.1, 1.2などの接続に用いるプロトコルを提示

(クライアント:TLS1.2で接続でお願いします!)

疑似乱数

クライアント側で生成した乱数をサーバーに送信

(クライアント:後で鍵交換に使う文字列なので控えといてください!)

暗号化方式

候補値(複数可)を提示

(クライアント:暗号化方式をこの中からお選びください!)

圧縮方式

候補値(複数可)を提示

(クライアント:圧縮方式は無圧縮でお願いします!)

※過去に色々問題があったため無圧縮一択らしい

拡張

候補値(複数可)を提示

(クライアント:その他もろもろの拡張に使用するものはこの中からお選びください)

※拡張に関しては割愛します

◯server hello

プロトコルバージョン

クライアントから提示された接続に用いるプロトコルに合意

(サーバー:TLS1.2で接続了解しました!)

疑似乱数

サーバー側で生成した乱数をクライアントに送信

(サーバー:後で鍵交換に使う文字列なので控えといて!)

暗号方式(暗号スイート CipherSuite)

提示された暗号化方式を選択

(サーバー:TLS_RSA_WITH_AES_256_GCM_SHA384 (0x009d)でOKです)

  • TLS_RSA:サーバーのRSA鍵を使った鍵交換アルゴリズム
    今回は強制的にRSAを使う設定にしたのでこれが選択された模様

  • AES_256_GCM:共通鍵暗号のアルゴリズム
    TODO:後で調べる

  • SHA384:MAC(メッセージ認証)のアルゴリズム
    TODO:後で調べる

圧縮方式

提示された圧縮方式を選択

(サーバー:無圧縮でOKです)

◯Certificate

サーバーの証明書情報をクライアントへ。

◯Server Hello Done

Server Hello 終了

2.鍵交換

暗号化する際に使用する鍵を生成する。

◯Client Key Exchange

クライアントで生成したキーをサーバーに送信する。

鍵交換アルゴリズム

1. RSA

クライアント側で生成した共通鍵生成に必要な情報を、サーバー側の公開鍵を使って暗号化して、
サーバーに送信する。
悪い人に暗号化したデータをずっと保存されて、ある日サーバーの秘密鍵が漏洩してすべて解読されてしまうというリスクがある。

ClientKeyExchangeのみ行われる。

上記Certificateでサーバーから取得した認証情報から公開鍵を取り出し、「ランダムな文字列」を暗号化してサーバーに送信する。

以下の部分が暗号化された文字列

このPreMaster Secretの値を使って後述するクライアントとサーバーで使用する共通鍵を作成するっぽい。

2. DHE

サーバーの公開鍵ではなく、お互いに一時的に生成した秘密鍵/公開鍵のペアで公開鍵を送り合って、
共通鍵生成に必要な情報を暗号化して送り合う。
ServerKeyExchangeとClientKeyExchangeの2回の通信で鍵交換を行います

一時的に作成される鍵なのでサーバーの秘密鍵が漏洩しても安心!

数学苦手だけど、以下みたいな感じです。

これでお互いに安全に秘密値(PreMaster Secret)を送り合えます。


DHEだと一時的に作成した鍵を使用するので復号が面倒くさい。
今回は簡単に復号できるRSAを使用します。

(上記server helloの暗号化方式でTLS_RSAに合意をしている)

共有秘密鍵の算出

これまでのやりとには、鍵を生成するための前準備でした。
これでクライアントとサーバー側で通信を暗号化するために使う鍵を生成するための情報が出揃いました。

ECDHEというアルゴリズムで算出してるみたいですが、割愛します!
ざっくりいうと以下の情報でいい感じに鍵を作るってこと見たいです。

・Hello Clientでクライアントが生成した乱数
・Server Helloでサーバーが生成した乱数
・PreMaster Secret

◯Change Cipher Spec

新しい暗号化仕様の利用開始を通知

これでTLSハンドシェイクは終了

2.データ通信

これで晴れて暗号化通信が出来るわけです。

暗号化されたデータ通信

リクエストデータ

レスポンスデータ

通信内容の復号化

上記のままだと暗号化されててわけわかめだから、以下の設定で事前に作成した証明書をインポートすることでTLS通信内容が復号化でき中身が確認できる。

Preference->Protocols->SSL->RSA Key List->Editを選択し、以下を設定。

IP Address-> 172.17.0.2
Port->443
Protocol->http
Key File->俺俺証明書key(server.key)

リクエストデータ

レスポンスデータ

雑感

ちょーざっくりメモでした

Please share  

場所が分かりにくいAmazonプライムビデオの視聴履歴リンクをメニューに表示させるchrome拡張機能

夏も早いものでもう終わろうとしている今日この頃、皆さんはどのような動画をみてお過ごしでしょうか?
僕はAmazonプライムビデオのラインナップの豊富さをいいことに動画三昧の日々を送っています。
そんなAmazonプライムビデオのありそうでなかったchrome拡張機能を作ってみました。

場所が分かりにくい視聴履歴へのリンク

Amazonプライムビデオを使っていて以下のように思ったことはありませんでしょうか?

  • ちょっと前に見てた動画のタイトルを思い出せない!
  • スマホで途中まで見てたやつをPCでみたい!
  • 過去に視聴した動画を見たい、

そんなとき視聴履歴を見たいという思いはは至極当たり前の心理だと思いますが、なぜか視聴履歴までたどり着くには以下のステップを踏まなければなりません。

  1. ヘッダーの「マイストアへ」をクリック

  2. 「おすすめ商品を正確にする」をクリック

  3. 画面左部の「視聴済みのビデオ」をクリック

そんな頻繁に見ることがないとはいえ、いざ見たいと思ったときにこのステップを思い出すのは至難の業です。。。

視聴履歴をみるための方法をgoogleで検索なんてしてたら時間が勿体無いですね。

たった一つの解決方法。。。

この視聴履歴の分かりずらさはサイトの回遊率を上げるためのものなのか、プライバシー的なものなのかはさておき、視聴履歴を見る方法だけで検索するとそれだけで記事になってしまいそうなニーズ感がある以上、この問題を解決するためのソリューションを提供することが僕の使命であることは自明である。

この問題をシンプルに解く方法、それは。。。。

そう、ヘッダーに「視聴履歴」のリンクを追加することです!

使い方

chromeをお使いの方限定なのですが、以下URLから拡張機能の追加を行ってください。

https://chrome.google.com/webstore/detail/amazon-prime-video-fast-h/cfifaldajbjpfiafieifemojpfdiccbm

インストール後、再度Amazonプライムビデオのサイトに訪れるとヘッダーのメニューに「視聴履歴」へのリンクが追加されているはずです!

それではよいAmazonプライムビデオ生活を!!

機能に関するお問い合わせは以下からお願いします。
https://github.com/kuniiskywalker/AmazonPrimeVideoFastHistory/issues

Please share  

MySQLのwait_timeout実験

秘伝のバッチが突如悲鳴を上げるということは少なくないと思いますが、自分の知識不足から復旧に時間を要してしまった。

自戒もかねてメモを残す。

やばいクエリによるtimeoutでgone away

“MySQL server has gone away” が発生する原因としては以下の要因が考えられる。

  • クエリのサイズがでかすぎ
  • 接続のアイドルタイムアウト

前者はmax_packet_sizeの値を調整して解決できる。
が、今回の原因は後者のタイムアウトの秒数だった。

接続が開始されてから、最後にデータベースに問い合わせをした時間から、指定した秒数たってしまうと、データベースはクライアントとの接続を勝手に切ってしまう。
接続が切れた状態でクエリを送信すると、「おまえの接続はもう切れてるぜ!」となりgone awayとなる。

この指定した秒数の謎についておいかけていきたいと思います(^O^)/

なぜ原因判明まで時間がかかってしまったのか

そもそも時間がかかりすぎるクエリがよくないのだが、明らかに設定された時間内より早いタイミングで切断されていた。

デフォルトだとおそらく2880秒で設定されている

1
2
3
4
5
6
7
8
9
% mysql -u root

mysql> SHOW VARIABLES LIKE 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

2880秒もたってないのになぜgone awayになるんだよ。。。

マニュアルに書いてあった。。。

以下公式からの引用

Interactive(対話型)で接続したやつはinteractive_timeoutの秒数をwait_timeoutとして設定しちゃうぜ!
ってことみたいです。

interactive_timeoutの秒数

1
2
3
4
5
6
7
8
9
% mysql -u root

mysql> SHOW VARIABLES LIKE 'interactive_timeout';
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| interactive_timeout | 28800 |
+---------------------+-------+
1 row in set (0.00 sec)

じゃあ対話型じゃない場合はどうなるかっていうと

1
2
3
4
5
6
7
% mysql -u root -e "show variables like 'wait_timeout'"

+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout | 60 |
+---------------+-------+

my.iniで設定されている数字がやっとでてきた。

アプリケーションから接続する際は非対話モード(Interactiveじゃない)になるので、
60秒が設定されていたため、早いタイミングで落ちていたみたいです。

28800秒で設定されるから他に原因があるのかと思い余計な時間をとられてしまった。。。

実験

前提

  • mysql 5.6
  • php7.1
  • pdo

実験1. wait_timeoutを5秒に設定して、DBに接続してからsleepを6秒いれた後、クエリを実行するとgone awayになるか確認。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
define('WAIT_TIME', 6);

try {
$pdo = new PDO('mysql:host=db;dbname=test;charset=utf8','root','pass',
array(PDO::ATTR_EMULATE_PREPARES => false));

sleep(WAIT_TIME);

$sql = "SELECT version();";
$pdo->query($sql);

} catch (PDOException $e) {
exit($e->getMessage());
}

見事gone away!

1
Warning: PDO::query(): MySQL server has gone away in /src/connect.php on line 11

実験2. interactive_timeoutを5秒に設定して、対話接続でクエリ実行後に5秒まち、再度クエリを実行するとgone awayになるか確認。

1
2
3
4
5
6
7
8
9
10
% mysql -u root

mysql> SHOW VARIABLES LIKE 'interactive_timeout';
+---------------------+-------+
| Variable_name | Value |
+---------------------+-------+
| interactive_timeout | 5 |
+---------------------+-------+
1 row in set (0.00 sec)

5秒待って再度クエリ実行

見事gone away!

1
2
mysql> SHOW VARIABLES LIKE 'interactive_timeout';
ERROR 2013 (HY000): Lost connection to MySQL server during query

テストに使ったコードはこちら

https://github.com/kuniiskywalker/test-mysql-goneaway

まとめ

対話型の場合だけ秒数が別指定が多めにとられてるのは、一旦接続していろいろいじることが前提なので当たり前といえば当たり前。。。

JSでバイナリを扱うことへの理解とメモ

ちょいちょい使うことがあるバイナリ操作処理だけど、イマイチ用語と用途が整理できていないのでメモ

背景

HTML5からバイナリが使えて便利最高!みたいな情報多いけど何が便利になってるのかよくわからない。
昔と比べてどう便利になったのかもふまえて整理してみる。

ゴール

JSでバイナリを使うことの利便性を享受できるようになる

まずバイナリが扱えるようになると何が嬉しいのか?

つーかバイナリって何?!

以下wikipedia引用

バイナリ(またはバイナリー:binary)とは、2進数のこと。 テキスト形式(文字データ)以外のデータ形式全般を指したり、コンピュータが処理・記憶するために2進化されたファイル(バイナリファイル)、またはその内部表現の形式(バイナリデータ、バイナリ形式)のことを指すのに用いられる。

つまり人間が読めないコンピューターだけが扱えるデータ形式

JSでバイナリが扱えなかった時代

  • バイナリを扱うためのAPIがなかったので、formタグでinput type=”file”を選択して送信するぐらいしかできなかった。
  • CSVやXML、画像ファイルなどのバイナリファイルはサーバー側に送って何かしらの処理を行っていた。

バイナリが扱えるようになった時代

  • HTML5以降にはいったFILE apiを使うことにより、バイナリの読み書きが行えるようになり、クライアント側だけで完結する処理が作れるようになった。
  • 画像をアップロードする前の画像をリサイズして送信したりとか、非同期で送信したりとか、アップロードするまえの画像をbase64エンコードしてimgタグに表示したりとかできて便利になった。

APIの種類と用途

Blob

  • バイナリデータを保持( immutable な生データ )
  • 値を取り出し不可( FileReaderやxhrなどを使用して中身をとりだせる)
  • サイズの確認や切り出しは可能(Blob.prototype.slice や Blob.prototype.size)
1
2
3
4
5
6
7
8
var req = new XMLHttpRequest();
req.open("GET", "https://i.imgur.com/rjI42fW.png", true);

req.onload = function(e) {
var blob = e.target.response; // これがBlob
};

req.send();

File

  • input type=”file”のファイル選択時にラップされるオブジェクト
  • Blobを継承している
  • Blobにファイル名や最終更新日の情報が追加されたもの( { name:String, lastModifiedDate:Date } )
    • ユーザーのシステム上のファイルをサポートするための機能を拡張
1
2
3
4
5
6
7
8
9
10
11
<input type="file" id="local_file">

input.onchange = function (event){
var input = event.target;
var file_list = input.files; // これはFileList
var file = file_list[0]; // これがFile

console.log(file.name);
console.log(file.lastModifiedDate);
};
</script>

ArrayBuffer

  • 物理メモリの領域(バッファ)を確保するためのクラス
  • バッファに対する操作はできない
    • バイト数取得と既存のバッファからあらたなバッファを作ったりしかできない
  • エンディアン方式がCPU依存
    • リトルエンディアンかビックエンディアンかを意識する必要がある
1
2
3
var buffer = new ArrayBuffer(12);

console.log(buffer.byteLength); // 12

Typed Array

  • 型付き配列
  • 型を指定してバッファから配列を生成
  • 「高速に」読み書きできる
  • 種類
    • Uint8Array
    • Uint16Array
    • Uint32Array
1
2
3
4
5
new Uint8Array([ (2 ** 8) -1 , 2, 3])
> Uint8Array(3) [255, 2, 3]

new Uint16Array([( 2 **16) - 1, 2, 3])
> Uint16Array (3) [65535, 2, 3]

DataView

  • TypedArrayよりも高機能なバッファ操作用クラス
  • ArrayBufferのエンディアン方式の差を吸収してくれる
    • リトルエンディアンかビックエンディアンかを意識する必要がない
1
2
3
4
var buffer = new ArrayBuffer(12);
var dataview = new DataView( buffer );

console.log(dataview);

BinaryString と DataURI と BlobURL

1
2
こういう形式のやつ
/9j/4AA~~~~~

DataURI

  • 画像のリソースをURIで表現できる
  • BinaryStringなのでサイズが1.3倍になる。。。
  • テキスト情報にバイナリデータをうめこめる(imgタグにうめこめる)
  • data: スキームが先頭についている URL
1
2
こういう形式のやつ
data:image/jpeg;base64,/9j/4AA~~~~~

FileReader使うと取得できる

1
2
3
4
5
6
7
8
9
10
11
12
<input type="file" id="local_file">

<script>
document.getElementById("local_file").onchange = function(event) {
var input = event.target;
var reader = new FileReader();
reader.onload = function(){
var dataURL = reader.result; // これがDataURI
   };
  reader.readAsDataURL(input.files[0]);
};
</script>

↓imgタグに埋め込めば画像も表示できる

BlobURL

  • 画像のリソースをURIで表現できる
  • セッション内でユニークなURIを生成する
  • DataURIとちがってリソースへのポインター的な扱いになるからメモリにやさしい
1
2
こういう形式のやつ
blob:http://kuniiskywalker.github.io/f6ea8265-610f-4fbe-abfa-335f13cfe34c

createObjectURLでURIを取得

1
2
3
var blob = new Blob( ["文字列テスト"] , {type: "text/plain"} );
var blob_url = URL.createObjectURL(blob); // これがBlobURL
console.log(blob_url); // blob:http://kuniiskywalker.github.io/f6ea8265-610f-4fbe-abfa-335f13cfe34c

まとめ

クライアントサイドでのオフライン対応が充実していく背景には、じつはこういった便利APIを知らず知らずのうちに使っていたことを改めて認識できた。
サーバーで仕事をさせるかクライアントで仕事をさせるかは、通信時のデータ転送量をとるか端末のCPUコストをとるか、要件次第な感じ。

jQueryプラグインを理解するためのjQuery関数とjQueryインスタンス

jQueryでプラグイン開発をする場合、jQuery.fnへの拡張を行ってするのはもはやテンプレなのですが、なぜfnへの拡張なのかをメモって見た。

jQueryインスタンス?

よく使う以下はjQueryインスタンスです

1
2
3
// セレクターを指定してjQueryにラップされたDOMオブジェクトが返る
$('#page');

もうちょっと細かく見ていきましょう。

この2つの実行形式は同じで、どちらもjQuery関数を実行している

1
2
3
$('#page');

jQuery('#page');

jQuery関数の中身はというと以下

1
2
3
4
var jQuery = function( selector, context ) {
// こいつが実行されている
return new jQuery.fn.init( selector, context, rootjQuery );
}

いつも、$(‘#page’)とかで取得してたやつの正体はこのnew jQuery.fn.initだった。

このjQueryインスタンスにはよく目にするメソッドが定義されている。

つまり、jqueryインスタンス自体に何かしらの拡張を行うには、このjQuery.fnへの拡張が必要になってくる。


jQueryオブジェクトへの拡張

jQuery自体もオブジェクトなのでメソッドを追加できる。
ここではインスタンスとの違いを見ていただきたい。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// jQueryオブジェクトへの拡張したメソッドを定義
jQuery.output_message = function (msg) {
console.log(msg);
};

// jQueryオブジェクトを参照してメソッドを実行した結果
$.output_message("aaa");

> aaa

// jQueryインスタンスを参照してメソッドを実行した結果
$().output_message("aaa");

// インスタンス自体への拡張ではないのでエラーになる
> Uncaught TypeError: $(...).output_message is not a function
at <anonymous>:1:5

jQueryインスタンスへの拡張

こちらはpluginを作る時によく目にするやりかたで、jQuery.fnへの拡張を行う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// jQueryインスタンスへの拡張を行ったメソッドを定義
jQuery.fn.output_message = function (msg) {
console.log(msg);
};

// jQueryオブジェクトを参照してメソッドを実行した結果
$.output_message("aaa");

// jQuery自体への拡張ではないのでエラーになる
> Uncaught TypeError: $.output_message is not a function
at <anonymous>:1:3

// jQueryインスタンスを参照してメソッドを実行した結果
$().output_message("aaa");

> aaa


jQueryプラグイン実装

上記を踏まえて、jQueryプラグインの実装例を見ていく。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 実行時にパラメーターを渡せる仕様になっていると便利
$.fn.hiddenItem = function (config) {
// デフォルトパラメーター
var defaults = {
speed: "slow"
};
var options = $.extend(defaults, config);

// thisはjQueryインスタンス
return this.click(function() {
// ここのthisはjQueryインスタンスではないので$(this)を実行してjQueryインスタンス化する必要がある
$(this).fadeOut(options.speed);
})
};

$("#header").hiddenItem({speed: "normal"})

プラグイン内メソッドの外部呼出し

DOM依存じゃないものや、プラグインのなかで使用しているメソッドを公開メソッドとして使いたいなどの場合は以下のような形で呼び出せる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var F = $.myPlugin = function () {
F.open.apply( this, arguments );
};
$.extend(F, {
// 公開用メソッド
testMethod: function () {
console.log("test");
}
});
$.fn.myPlugin = function (config) {
var defaults = {
message: "yes"
};
var options = $.extend(defaults, config);

return this.click(function() {
alert(options.message)
})
};

$.myPlugin.testMethod();

> test

まとめ

pluginの機能提供範囲を意識しながらつくるとよいのではという感じです。