mrubyでプログラミングの楽しさを再認識した話
https://3.bp.blogspot.com/-E4A4Qo6Qwcg/Wf1kbWw2jcI/AAAAAAABH8c/Skz6Qd3urX4NGVqzYQLL4TH1FO5vSoltwCLcBGAs/s800/dance_yorokobi_mai_man.png
【結論】
2021年はmrubyでなにか作ったり、mrubyに関連するプロダクトにコミットしたりして、久しぶりにプログラミングの楽しさに気付かされました、というお話です。
【作ったもの】
MQTTのクライアントをmrubyで1から作ってみました。。MQTT 3.1.1に準拠しており、mruby2.1.2で動作確認済みです。
Subscriberとしての機能は作成中ですが、最低限Publisherとしての機能は実装しています。
こんな感じブローカー(publisherとsubscriberの関係を担保するサーバー)にトピックとメッセージを送ることが出来ます。
code:publish.rb
s = Kwrb::Client.connect(host: 'host')
s.publish(topic: 'a/b', message: 'hello')
【難しかったこと】
さて、このmgemではmruby-socketのTCPSocketクラスを使用してブローカーとのソケット通信を実現していますが、1点長くハマってしまったポイントがありました。該当コードはこちら。 code:ruby
socket = TCPSocket.open(host, port)
connect_packet = Kwrb::Packet::Connect.new(@username, @password,@client_id, @qos, @clean_session)
socket.syswrite connect_packet.data
response = socket.sysread MAXSIZE
ブローカーとの接続に必要な情報を送信にsyswrite、ブローカーからのレスポンスにsysreadを使っていますが、最初はwriteとreadで実装していました。ここで、以下のようなコードを実行したとします。
code:ruby
s = Kwrb::Client.connect(host: 'host', username: 'username', password: 'password')
s.publish(topic: 'a/b', message: 'Hello')
s.disconnect
過去にはpublish(メッセージの送信)でもsyswriteではなくwriteを利用してメッセージの送信を実装してしていましたが
sysread failed: End of File (EOFError)
が発生してしまい、ブローカーからのレスポンス取得に失敗しました。この原因が一体何なのか簡単に調査してみました。
【調査】
mruby-socketでは依存するmgemとしてmruby-ioを使用しています。どのような形で依存しているか以下のようなコードを書いてみます。 code:ruby
p TCPSocket.superclass
p TCPSocket.superclass.superclass
p TCPSocket.superclass.superclass.superclass
p TCPSocket.superclass.superclass.superclass.superclass
p TCPSocket.superclass.superclass.superclass.superclass.superclass
実行結果は以下の通りです。
code:shell
IPSocket
BasicSocket
IO
Object
BasicObject
こちらから確認できる通り、TCPSocketはIOクラスを親クラスに持ち、IOクラスはmruby-ioで宣言されています。 よってwriteとreadに関してはmruby-ioの実装を見てみたほうが良さそうです。
【writeメソッド】
IOクラスのwriteメソッドの実装を見るとseekメソッドが呼ばれていました。更に遡ってみると、sysseekメソッドが呼び出されており、lseekが使われているのがわかります。lseekはIOクラスのインスタンス変数のbufが空ではない場合実行されます。 lseekはファイルオフセットを検索するために使用するメソッドであり、今回のようにTCPソケットにおいてはファイルオフセットを必要とはしません。writeとsyswriteの違いは、lseekを実行するかそうではないかであり、どちらも共通してCで定義されたsyswrite(mrb_io_syswrite)を実行します。 【readメソッド】
IOクラスのreadメソッドにはループ中に_read_bufが呼び出されており、sysreadが実行されます。sysreadにより取得したバイト列はIOクラスのインスタンス変数のbufに格納されます。readメソッドは読み取りの最大バイト数を引数(length)に持っていますが、デフォルト値はnilです。またreadメソッドのbreakの条件は です。
つまり、バイト列をすべて読み込むか、length分のバイト列を読み込むまでループし続けます。今回のようにreadメソッドに読み取るバイト列のサイズを設定しない場合はストリームから読み取ったバイト列をすべて読み取るまでループすることになります。今回作成したプログラムにおいて、ブローカーとの接続の場合は接続のEOFによる例外処理が実行され、接続を確保することができましたが、publishの場合にsysread failed: End of File (EOFError)が発生したと同時にEOFによる例外処理が走らず、実行停止状態になりました。 【MQTTの特性】
MQTTではキープアライブタイマーが存在し、設定された時間内にPublisher側からのリクエストがない場合にブローカーから接続を切られてしまうので定期的にpingを送る必要性があります。そのためreadを利用した場合はSubscruberからのレスポンスを受け取れない場合はループが終了せずに、sysread failed: End of File (EOFError)が発生したと考えられます。そのためループをクライアント側で実施し毎回sysreadするようにし、ストリームからバイト列を取得した場合は正常にループを中断し、取得できなかった場合は、ブローカーにpingを投げて、更にsysreadすることでエラーを回避しました。
【とはいえ】
read/writeの問題は解決したものの、Subscribeの際にストリームからバイト列を全て読み込んだ上で、更に連続してバイト列が送られてくることを待つ場合どのように実装すべきか明確な答えがまだ出ていないので、Subscribeの機能実装はもう少し先になりそうです。現状はマルチスレッドによるread用スレッドを複数建てることを考えていますが、難しそう…(頑張るぞ!)
【コミットしたもの】
mruby-mrbgem-templateにてGitHub Actionsが利用できるようにプルリクエストを送りました。現在マージされており、こちらのテンプレートを利用して作成したmgemでもGithub Actionsが利用可能になっています。 久しぶりのOSSへのコミットでしたが、取り込んでもらえてよかったです。
【最後に(ただの思い出話です)】
私が最初にRubyおよびmrubyに出会って7~8年が経ちますが、当時は自分がプログラミングをやっていくことになるだなんて、全く想像していませんでした。むしろプログラミングというものに興味が持てず、どこか敬遠しているところさえありました。そこからなぜだかwebアプリケーションを作ったりCLIを作ったりするきっかけがあったのですが、その際には全てRubyを使っていました。そのうちに「Rubyを使ってなにかを作る」ことに興味を持ち始め、自分なりに勉強するうちにプログラミングが好きになったという経緯があります。そして職業としてプログラムを書くようになり、今でもちょいちょいRubyを書いています。なぜだかわからないけど、Rubyは私の人生をどこかで支えてくれています。今回紹介したプロダクトの作成やOSSへのコミットをしていた最中も初めてプログラミング出会ったときの楽しさと変わらない気持ちを彷彿させてくれました。ありがとう、Ruby!
そんなお話でしたー。
次の方にお繋ぎします。