Golang の websocket サンプルを書き直してみた

Golang の勉強がてらちょっとしたツールを作ろうと思って、まず websocket サーバーを書いてみた。 書いてみたと言っても go の websocket chat は golang-samples/websocket · GitHub ここにサンプルがあって、これをそのまま使っても良かったのだけれどいくつか気になったのでよりシンプルに書き直してみた。

気になったのは二つあって、一個は client id が競合しそうに見えた点。 https://github.com/golang-samples/websocket/blob/master/websocket-chat/src/chat/client.go#L35 ここで maxId をインクリメントしている. この NewClient 関数は https://github.com/golang-samples/websocket/blob/master/websocket-chat/src/chat/client.go#L35 ここで呼ばれていて、この onConnected はハンドラとして渡されているので GOMAXPROCS が 1 じゃなければ同時に実行されることがありそうとおもった。インクリメント処理はアトミックじゃない(と思ってる)ので、ここでインクリメントすると maxId を破壊することがありそう?? こういう時例えば Java の Thread とかだと maxId を synchronized で排他制御するのが考えられるけど、go では channel を使って制御するのがオシャレなようだ。

func (server *Server) WebsocketHandler() websocket.Handler {
    return websocket.Handler(func (ws *websocket.Conn) {
        client := NewClient(ws, server.removeClientCh, server.messageCh)
        server.addClientCh <- client
        client.Start()
    })
}

競合を避けるために、server に client 追加用の channel(addClientCh) を持たせ、websocket で接続されたらサーバーオブジェクトに client を渡してその先で client id を入れてもらうようにした。この addClientCh を liesten してるのは単一の goroutine なので競合することはない(が、詰まる可能性はある??)

気になったもう一点は、クライアントをハンドルする goroutine が複数あること。client (クライアント、ブラウザとかをハンドリングするサーバー側のオブジェクト)はクライアントからのメッセージを listen しなくちゃいけないのだけれど、チャットなので他のクライアントからのメッセージを受け取って自身が担当するクライアントに投げないといけない。そのため client はクライアント(=ブラウザ)だけでなく server も listen している。たぶん下の図ような感じになっている

f:id:hatz48:20141221192131p:plain

黒線は生成線で、グレーの線が listen 関係を表している。白い箱が goroutine で色ついてるのがオブジェクトとか。request handler が点線になっているのは揮発性(すぐ終了する)goroutine を表現してみた。 これはパフォーマンス的にはいいのかもしれない(?)けどちょっと複雑になって読みづらかった。クライアント切断時に goroutine を殺し忘れないように終了処理もちゃんとしないといけない。以下のように、client を担当する goroutine が一つになっている方がわかりやすかった。

f:id:hatz48:20141221192136p:plain

client から server を listen する goroutine がいなくなったので、message を送る際に server が client のメソッドを直接呼び出している。そのまま呼び出すと server の goroutine がブロックするのでメソッド呼び出すだけの goroutine を生成してる。これだと client の終了処理も goroutine 一つ潰すだけで済む。

まとめ

Golang の勉強がてら websocket server 書き直してみた。 websocket は状態を持つので常駐する goroutine が複数存在することになるのだけど、なるべく簡素化したい。 とくに双方向に listen し合うと複雑になるなーと感じたのでそうならないように書いてみた。

とここまで書いてはみたものの Golang 初心者なので理解が間違ってるのかもしれない。できれば有益なマサカリをゲットしたい。

chrome 拡張をシークレットモードで有効にする

chrome 拡張は普通にインストールすると、シークレットモードで有効になっていない。 有効にするには拡張機能(chrome://extensions)から、「シークレットモードでの実行を許可する」にチェックを入れればよいのだけど、自分で書いた拡張をこれで有効にして実行したら以下のようなエラーが出た

Unchecked runtime.lastError while running tabs.executeScript: Cannot access a chrome:// URL at ...

試しにシークレットウィンドウから chrome://... の URL(拡張機能の js ファイル) にアクセスしても、禁止されていてみることができない。 シークレットモードからは executeScript することが出来ないのかなーと思って調べてみた

Overview - Google Chrome

ここに、シークレットモード (incognito mode) でのデータ保存に関するポリシーが書いてある。要約すると「ユーザーがどこで何をしたか、シークレットモードからは」保存するなよーと書いてあって、それは確かにという感じ。特に executeScript 出来ないとかは書いてない

いろいろ巡った結果、 manifest.json に一行追加すればいいことがわかった

Manifest - Incognito - Google Chrome

manifest の incognito はデフォルトが spanning になっている。 chrome 拡張は一つのプロセスとして動くのだけど、 spanning だとシークレットモードとそうでない普通のウィンドウに対して、1つのプロセスで処理する。対して split にすると、シークレットウィンドウの拡張は別のプロセスで動く(その拡張のプロセスが二つできる)。 どうやらシークレットモードに対して executeScript を実行するには split でないといけないようだ。プロセスが一緒で executeScript が実行出来ると、シークレットモードじゃない拡張からシークレットウィンドウにアクセス出来てしまいそう?

{
  ...
  "incognito": "split",
  ...
}

で解決した

YAPC::Asia 2014 に参加してきた

ブログを書くまでが YAPC らしいのでブログを書こうと思う(ブログを書かなければいつまでも YAPC ということか)

個人的には最近 Golang に興味があるので Go for perl mongers がすごく勉強になったし、ずっと Web アプリケーションばかり書いていたので ウェッブエンジニアのローレベルプログラミング このセッションもすごく面白かった。 こうして振り返ってみるとほとんど Perl の話は聞いていない気がする。そもそも Perl の話そんんなに多くなかったというと身も蓋もないかもしれない。YA'P'C とは?という感じでもあるけど、 Perl がどうこうということではなく、Perl に親しいエンジニアにはベテランエンジニアが多いのでそういう人たちの話を聞ける貴重なイベントという感じだった。

あと、自分より年下のエンジニアがスピーカーになっているのを見るといろんな意味でいい刺激になる。普段それなりに仕事しているつもりだけどアウトプットはあんまり出来ていなくて、なるべくアウトプットしていこうと思った。来年スピーカーとして話せるよう、がんばろうと思う。

ビール投げ売りしてた

1ケース3000円

f:id:hatz48:20140614145217j:plain

やまや に行ったらビール投げ売りしてたので思わずケースで買ってしまった。やまやのサイトを見ると1ケース4800円と書いてあるが、過剰入荷してしまったようで京都のいくつかの店舗では賞味期限が近いものに限り1缶125〜130円という発泡酒以下の値段で購入できる。

ピルスナー2種と白ビール(ヴァイス)のものがあって投げ売りする前から200円で売ってた。日本で白ビールを買おうと思うと少し値段が張るので、白ビール飲みたいと思ったらこれ買って飲んでた。 最近よく見かける白ビールだとヒューガルデン・ホワイトとか水曜日のネコとかがあるけど、Amazonだとどちらも1ケース6〜7000円強する

水曜日のネコ 350ml 24缶 1ケース

水曜日のネコ 350ml 24缶 1ケース

エッティンガーのヴァイスは今だと1ケースで3000円なのでめちゃくちゃお買い得。 味は白ビールにしては割とあっさり目、後味すっきりという感じ。ドイツで一番売れているらしいけど、やっぱりさらりと飲めるのが人気なのだろうか。コク派の人には物足りないかもしれないけどその分ご飯と一緒に飲んでもいける。

賞味期限は7/1くらいまでらしい?(箱には EXP: 30.10.2014って書いてある)けど、君たちそのくらい飲むだろうしやまやを助けると思って買いに行くべき。

画像をまとめて zip にしてダウンロードする拡張を作った

前回 xhr で画像をダウンロードして zip するコードを書いた。せっかくなので拡張にしてみた。

使い方

上の拡張をインストールすると、URLバーの横に

f:id:hatz48:20140204010831p:plain

こういうボタンがでるので、画像をダウンロードしたいページでそれを押す。 押すと、マウスオーバーした html 要素がふわっとハイライトされるようになる。

f:id:hatz48:20140204135358p:plain

これだと選択した要素内に画像がないので空の zip になってしまう

f:id:hatz48:20140204135356p:plain

こんな感じに選択されればオッケー。 クリックすると、その要素内の画像が zip してダウンロードされる。

実装

chrome 拡張内で実行されるコードはページをブロックしないので、重い処理は拡張内のコードで実行してあげればよい。

  1. ページ内からダウンロードしたい画像のURLを抽出する
  2. 拡張のコードで、画像を取得・zip (ここは前回と同じ)
  3. zip をローカルにダウンロードする

けど拡張からページの document オブジェクトを直接操作することはできない(たぶん)

要素をハイライトしたり、クリックした要素内から画像 URL を抽出するといったコードは、そのページのコンテキストで実行する必要があるので chrome.tabs APIexecuteScript メソッドを利用する。 executeScript はスクリプトの最後で評価した値を callback に渡してくれるが、ここで渡せる値は jsonable な値のみなので、promise オブジェクトとかを渡すことはできない。クリックで選んだ要素内の画像を・・・ということをしたい場合、スクリプト終了時にはまだ画像URLをとることはできないので、ページのスクリプトと拡張のコードで通信する必要がある

これを使う。クリックで要素を選択したら、その要素内の画像URLを抽出して sendMessage するようにした。

通知された URL を元に画像を取得して zip した後は、chrome.downloads API を用いてユーザーのダウンロードフォルダにダウンロードさせる。 downloads APIchrome 31 くらいから入ったようなので、それ以前の chrome では使えない。


やっつけ感があるので、zip ファイルの名前がよくわからない 16 進文字列だったり、zip 処理中のフィードバックがなかったりとアラはいろいろとあるけどそれなりに便利と思う。

適当にご利用ください。