私は昨年の10月に還暦と定年を迎え、11月からは同じ職場にて「シニアパートナー」という立場で1年契約で働いているエンジニアです。子供は息子が1人。アパートで1人暮らしをしています。
サイトの維持を行うこともそろそろきびしいので、レジストラの契約が切れる今年の10月にドメインを放棄しようと考えています。円安でレジストラへの支払いも割高となり、プロバイダーの固定IPオプションも値上がって、料金もバカにならなくなった事も要因です。よって10月末頃本ウェブサイトは閉鎖となる予定です。節目の年でもあるので少し長くなりますが、自己紹介含め記事にしておきます。
学生の頃は理論物理学を学び、メーカーに就職してからは、通信モジュール、ハイブリッドIC、デジタル無線機器、自動機など多岐にわたる製品の開発をしてきました。そんな中、日本でも急速にインターネットの個人利用が増え、私の興味を引き付けていました。webkoza.comは、最初「ウェブプログラミング講座」という名前で2003年頃に始めた技術系情報発信サイトです。当時Microsoftが初めてWindowsにWebserverを標準搭載し、それを使ってWebsiteを開設する個人が爆発的に増加した時期でした。
私も最初は、自宅でWindows NT Workstation+PWSという構成で始めましたが、ダイナミックドメインとはいえ、Fire Wallも立てずノーガード状態で開設したWeb Siteは、開設から10日ぐらいで、IIS(またはPWS)のセキュリティホールを利用したChinese Hackerに書き換えられ、あえなく閉鎖。でもこれは返って私の興味を強くしました。その後当時格安だったアメリカのレジストラに「webkoza.com」を申請してDomainの維持を開始しました。
ドメインを取得して、最初はWindows OSでtomcatを動作させてServer Side Javaでコンテンツ提供していた時期が有りましたが、2-3年でLinuxに変更。その後は一貫してLinuxサーバーを自宅で運用しています。最初は仕事で知ったRedHat7だったと思います。その後もRedHat系で運用していて、現在はFedora29です。途中、OpenBlocks266に無理やりFedora8を入れて、メールサーバーはこちらで運用しています。
webkoza.comでは、掲示板、DB連携サーチエンジン、ゲストブック、アクセスカウンターなど、perlで自作した様々なコンテンツを動かしていました。とにかくperlでのプログラミングが楽しかったことを覚えています。自由な書き方ができて、しかもオブジェクト指向。そして多くのハッカー達が残した資産を利用させていただきました。7年ぐらい前からは、Movable Type(※1)というブログソフトをインストールしてそのプラグインを開発したり、また3年ぐらい前には、誰でも無料で使えるネット対局型将棋システムを作りました。
プログラミングは趣味だったはずなのに、いつのまにか仕事でやるようになってからは、急速に自宅サーバーでのプログラミングをやる気が起きなくなってしまいました。そんな中、今から約3年7か月前に、妻が亡くなりました。ステージ4で癌が見つかり1年半大変頑張って闘病しました。最後は自宅で看取りました。身近な人が亡くなると49日までは何かと忙しく、落ち込む暇もあまりありません。しかしその後の数か月は酷いものでした。仕事や趣味をしている時だけがなんとか自分を保っていられるような状態だったと思います。そして最初の1年(今も時々。。)は、なぜあの時ああ言ったのか、なぜあの時こうしてあげなかったのか。。と自責の念ばかりでした。
こんな誰かの言葉が有ります。
親を亡くす事は自分の過去を無くすこと
子供を亡くすことは自分の未来を無くすこと
伴侶を亡くすことは自分の現在を亡くすこと
ほんとうにその通りだと思います。さらに言うならば、妻を亡くし自分も半分死んだと思っています。今も残りの半分で生きている感じです。でも時間が経過すると悲しみは薄れていくのは本当の事のようです。
趣味に没頭する事で、無理やり新たな「現在」を作り、半分の命でも何とかやっています。同じような悲しみを味わった方は世の中には多いと思います。でもみなさん「何とかやっている」のだと、なぜかわかりました。
このブログでは当初、趣味の釣りの事も多く書いてきましたが、最近一番はまっているのは天体写真です。高校生~大学1年生ぐらいの銀塩フィルム時代に少々やっていたのですが、友人の影響で再開したのが2022年の12月。再開後まだ1年ぐらいですが、かなりのめり込んでいます。これについては、最近始めた下記「はてブロ」にぜひお越しください。
結局現在の趣味は、天体写真撮影・野鳥撮影・キャンプ・釣り・読書です。釣り仲間では、私しか船舶免許を持っていない事もあり、今後もボート釣りをやめることは無いでしょう。もちろんキャンプや読書も。
最近ようやく妻の遺品整理をやれる気がしています。お恥ずかしい話、今までは、妻の持ち物や妻を思い出す映像などを見ていると正常に自分を保てなくなるので出来ませんでした。ゆっくりゆっくりやるつもりです。年金受給開始まであと5年。多分それまでは仕事をやめられないので、その間に老後について考えないといけないと思っています。
]]>※1
100% perlで書かれたOSS系ブログソフトウェア。アメリカではかなりメジャーだったが日本ではphpで書かれたword pressにシェアを奪われたが、企業向けのCMS(Contents Management System)として今も健在。下記はMTを使ったビジネスを展開するシックアパートのサイト
https://www.sixapart.jp/movabletype/
DEN将棋EXでは、shogi-serverとソケット通信するプログラムを、ウェブソケットサーバーdaemonの子プロセスとして製作しましたが、この子プロセス側でウェブソケットサーバー(親プロセス)と通信するための手段として、perl伝統的な4引数select関数を使いました。
そこでpythonで同じようなものが存在しないか調べたところやはり有ったわけです。今回作った子プロセス用プログラムは、DBのテーブルを監視して条件がそろったら親プロセスにデータを通知することが主な仕事ですので、DEN将棋EXよりずいぶん単純な仕様です。その単純なプログラムにとって手軽で軽量な最適な手段だと思います。ただ注意点として、このselectモジュールはWindows上ではソケットに対してしか動作しないそうです。
(1)selectモジュール
selectモジュールは標準ライブラリに同梱されているモジュールなので、あらためてインストールする必要はありません。まずselect.pollメソッドで、ポーリングオブジェクトを生成します。そしてregisterメソッドでファイルディスクリプタをポーリングオブジェクトに登録します。
ここでは、親プロセスにパイプでオープンされるので、標準入力が親プロセスのファイルディスクリプタになります。ゆえにこのファイルディスクリプタparent_fd をポーリングオブジェクトに登録しています。
これで親プロセスからの受信データをポーリングする準備が整いました。
import sys,select
# ポーリングオブジェクト生成
poll = select.poll()
parent_fd = sys.stdin #親プロセスにパイプでオープンされる前提
poll.register(parent_fd, select.POLLIN)
(2)pollメソッド
実際の無限ループの中で、ポーリングオブジェクトのpollメソッドを呼びます。このメソッドはperlの4引数select関数と同様にタイムアウト時間(ミリ秒単位)を引数で設定する事ができます。タイムアウト時間を超えて受信データを発見できない場合は、次のステップに移ります。ここではタイムアウト値を50msとしています。
pollメソッドの返り値は、ファイルディスクリプタの対応するファイル番号と、結果(イベントマスク値)データです。これらは2 要素のタプル (fd, event)
からなるリストとして返されます。返り値は空の場合も有ります。詳細はココを参照。
無限ループの中では、pollメソッドの返り値を判定することが仕事になります。下記が今回実装したコードです。POLLIN
は親プロセスからの読み出し待ちデータがある事を示します。実際の読み出しは、標準入力データを文字列として受け取るinput()関数を使用しています。
親プロセスとの接続が切れたかエラー(POLLIN以外のイベント発生を全てエラーとする)を捉えるとfinをtrueにして、後処理の部分で無限ループを脱出するようにしています。
ここでは記述していませんが、無限ループの「親プロセスからの受信データ別処理」の中では、適宜親プロセスにデータを送信します。双方向パイプでつながっているので、使っているのはprint()関数です。せいぜい数10バイトのテキストデータしか送信しないので、ブロッキングな関数で十分です。
fin = false
while True:
# イベント発生まで待つ
rdy = poll.poll(50)
for fno, ev in rdy:
if fno == parent_fd.fileno():
if ev == select.POLLIN:
data = input()
# 親プロセスからの受信データ別処理
# ・・・・・・・・
else:
# 接続が切れたか、エラーの時
fin = True
# 受信データに無関係の処理:
# DBの監視テーブルの読み出しと読み出したデータ別処理
# ・・・・・・・・
if fin:
# whileループから抜ける
break
]]>
サブプロセスとの通信方法は色々あるのですが、DEN将棋EX同様双方向パイプで実現しました。
(1)subprocessオブジェクトの生成
前回紹介した全体コードのうち下記がsubprocessコルーチンオブジェクト生成部分です。初期化用コルーチン「Init()」の中で、生成する子プロセス数分のループ中に記述されています。codeは、子プロセス用実行プログラムのフルパスです。子プロセス用実行プログラムもpythonで記述しましたので、sys.executable(pythonインタプリタのフルパス)とcodeを要素としたタプルを定義してcreate_subprocess_execの第一引数としています。第2,3,4引数では、stdout,stdin,stderr各々に「asyncio.subprocess.PIPE」を指定しています。ここのstdoutは子プロセスの標準出力、stdinは子プロセスの標準入力です。つまり子プロセスに出力する時はstdinにデータを渡し、子プロセスから受信する時はstdoutから取り出します。生成したオブジェクトはグローバル配列変数に代入しています。最後の行では、子プロセスに初期化データを送信するために、send2childコルーチンをコールしています。
cmd = (sys.executable, code) proc = await asyncio.create_subprocess_exec( *cmd, stdin=asyncio.subprocess.PIPE ,stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE) Children.append(proc)#子プロセスをMaxChidrenNo 個生成
await send2child(proc,'Seat:' + str(num) + '\n') #子プロセスに自分のプロセス番号を通知する
(2)子プロセスへの送信部分
send2childコルーチンは下記です。ここではwriteメソッドの後に、drainメソッドを使用しています。このメソッドはコルーチンを返しますのでawaitします。予想通り、送信バッファが空になるまで、ノンブロッキングにここで待つことができます。これは大きなデータを送信する場合に特に有効です。
async def send2child(proc,da): proc.stdin.write(da.encode()) await proc.stdin.drain()
(3)子プロセスからの受信イベントハンドラ
子プロセスからの受信イベントハンドラは下記です。このコルーチンは、子プロセスが終了するまでの無限ループです。
proc.stdout.at_eof()とproc.stderr.at_eof()がともに真になる事で子プロセスが終了したと判定しています。終了を捉えるとループを脱出して、念のためawait proc.communicate()で残っている受信バッファデータを吐き出させます。
async for line in proc.stdout:、async for line in proc.stderr:と記述すると、それぞれ子プロセスの標準出力バッファ、エラー出力バッファから1行づつ読み出す事ができます。もちろん非ブロッキング受信なので、受信バッファにデータが無い時でもそこでブロック停止してしまうことは有りません。つまり「無限ループ」といってもイベントループに制御が戻り、他のタスクが処理されます。途中エラー出力データを受信した場合も無限ループを脱出するようにしました。
紹介はしませんがread_actionコルーチンは、子プロセス番号(seat)と受信データを受け取って、受信データ毎の処理を受け持ちます。この中では、必要に応じて前述のsend2childコルーチンを呼び出して子プロセスにデータを送信します。
# procは子プロセスオブジェクト seatは子プロセスNo async def rec_from_child(proc,seat):]]>
#受信専用ループ,子プロセス終了(=EOF)までの無限ループ while True: if proc.stdout.at_eof() and proc.stderr.at_eof(): break async for line in proc.stdout: #これだと非ブロッキング受信となる if line: await read_action(seat,str(line.decode()).replace('\n', '')) async for erdata in proc.stderr: if erdata: print('[sdterr] ' + str(erdata.decode()), end='',flush=True) break print('子プロセスからの受信エラー発生!強制終了') await proc.communicate() print(' proc return code = ' + str(proc.returncode))
asyncioモジュールはpipで簡単にインストールできます。わからない場合は適当にググってください。
asyncioはいわゆる非同期プログラミングを行うことで、リソース由来のブロッキングや時間がかかる処理による無駄な待ち時間を減らすためのモジュールです。perlのイベントドリブンに特化したAnyEventとコルーチンを定義できるCoroという2つのモジュールをドッキングしたようなモジュールです。AnyEvent配下に色々リソース毎のノンブロッキングIOを提供するモジュールが有るように、pythonのasyncioにもその配下にリソース毎の同様なモジュール/メソッド群が有ります。
注意したいのは非同期プログラミングとは、マルチプロセス・マルチスレッドとは異なるという事です。マルチプロセス・マルチスレッドでは、再開するタスクはOSのスケジューラが決定しますが、非同期プログラミングでは、再開するタスクはアプリケーションが決定します。
asyncioでは、通常の関数定義の前に「asyc」キーワードを付加するとその関数は、コルーチンオブジェクトになります。直接この関数を実行しようとしても関数リファレンス値が帰って来るだけです。このオブジェクトを実行するには、イベントループにタスクとして登録する必要が有ります。登録したら、イベントループを実行すれば登録したタスクの順番に関数が実行されます。
下方に、今回作ったプログラムの抜粋を掲載します。実現方法は他にも色々あるはずなのであくまで一例です。main部分の説明は以下の通りです。
(****はポート番号)
紛らわしくてすいませんが、ここでfuturesという集合は、コルーチンの集合です。futureオブジェクトではありません。asyncio.waitにコルーチンの集合を渡すと要素各々がタスクに変換されます。タスクはfutureクラスのサブクラスだそうです。これをイベントループに渡しています。run_until_completeは、全タスクが完了すると終了してしまうので、終了してはいけないウェブソケットイベントハンドラと子プロセスからの受信イベントハンドラは各々関数内で無限ループとなっています。
ウェブソケットイベントハンドラmessage_receivedは、websockets.serveに渡しているコールバックなわけですが、クライアントからの接続が確立されるたびに実行され、受信データ待ちの無限ループに入ります。ここではゾンビソケットが増殖することを防ぐために、例外を捉えてこれをウェブソケット切断と判断しています。これはtryの中のrecvを呼んだ時にクライアントが存在しないと例外が発生するからです。切断を検知すると後処理を行いこのイベントハンドラ(タスク)は終了します。別のクライアントが接続されるとまた、新たなイベントハンドラ(タスク)が実行開始するわけです。
子プロセスからの受信イベントハンドラも無限ループとなっており、設定した子プロセス数のイベントハンドラ(タスク)は終了しませんので、結局run_until_complete()は終了しないということになります。
各コルーチン内では、awaitキーワードを使って、実際の仕事が完了するまでのブロックが発生せずイベントループに戻るようになっています。タスクが完了するとまたその場所から次の処理が行われます。
<ウェブソケットイベントハンドラ内で受信データを取得する部分>
message = await websocket.recv()
上記コードでブラウザからのデータを受信しています。send_imagefile2wstcli というコルーチンの中では、ブラウザにイメージファイルを送信しています。websocketsは、sendメソッドにバイナリデータを渡せば自動でウェブソケットのヘッダー部を「opcode=0x2(バイナリデータ)」にして送信してくれます。ファイルを読み込んでバイナリデータに変換する部分は、PILライブラリのImageモジュールを使用しています。このコルーチンの中で下記がブラウザにバイナリデータを送信する部分です。引数にテキストデータを指定すればテキストデータとして送信してくれます。
<Init()コルーチン内でブラウザにイメージファイルを送信する部分>
await wst.send(img.getvalue())
子プロセスとの通信のために、asyncio.create_subprocess_execを使用して双方向パイプを生成しています。通常の双方向パイプだと送受信でブロックが発生してしまうのですが、このメソッドを使えばノンブロッキングな受信イベントハンドラを書くことができます。asycioの双方向パイプによるサブプロセス通信については次回(rec_from_childの説明など)記事にします。
最後に1つ。「await (croutine)」を多用していますが、これは前述の通りコルーチンが実行を返すのを待ち、実行が終わるまでイベントループに制御を戻すということを行います。よって、recvとかsendとかというメソッドはコルーチンを返すので、awaitで実行させるわけなのですが、awaitの行を含む関数は必ずコルーチンでなければなりません。つまりasync defで定義する必要があります。defだけだとエラーになります。そうするとそのコルーチンを実行させるのにもまたawaitを使う必要があるので、結局awaitとasync defが大量に発生する事になるのです。これを防ぐ試みもあるようですが、python初心者には敷居が高そうです。
<コード全体(抜粋ですが。。)>
import logging , sys ,re ,io, datetime, os, time, urllib.parse import websockets, asyncio from PIL import Image ・・・・・・・・・・・・・・ Children = []#子プロセスオブジェクト配列。インデックスは子プロセスNo ・・・・・・・・・・・・・・ #--初期化 async def Init(): for num in range(MaxChidrenNo): SeatUsing.append(0) code = ChildCode cmd = (sys.executable, code) proc = await asyncio.create_subprocess_exec( *cmd, stdin=asyncio.subprocess.PIPE ,stdout=asyncio.subprocess.PIPE,]]>
stderr=asyncio.subprocess.PIPE) Children.append(proc)#子プロセスをMaxChidrenNo 個生成 await send2child(proc,'Seat:' + str(num) + '\n')
#最初に子プロセスに自分の席番を通知する #--子プロセスからの受信イベントハンドラ # procは子プロセスオブジェクト seatは子プロセスNo async def rec_from_child(proc,seat): #受信専用ループ,子プロセス終了(=EOF)までの無限ループ while True: if proc.stdout.at_eof() and proc.stderr.at_eof(): break async for line in proc.stdout: #これだと非ブロッキング受信となる if line: await read_action(seat,str(line.decode()).replace('\n', '')) async for erdata in proc.stderr: if erdata: print('[sdterr] ' + str(erdata.decode()), end='',flush=True) break print('子プロセスからの受信エラー発生!強制終了') await proc.communicate() print(' proc return code = ' + str(proc.returncode)) #--子プロセスにデータを送信 async def send2child(proc,da): proc.stdin.write(da.encode()) await proc.stdin.drain() #--イメージファイルをウェブソケットクライアントへ送信 async def send_imagefile2wstcli(wst,fname): im = Image.open(Appdir + '/' + fname) img = io.BytesIO() #空のインスタンスを作る im.save(img,"png") #空のインスタンスに保存する await wst.send(img.getvalue()) #--ウェブソケットクライアントからの初期化コマンド処理 async def init_act(websocket,mobj): StatusByWSID[str(websocket.id)] = 'wait_tran' #トップイメージをブラウザへ await send_imagefile2wstcli(websocket,TopImgFile) #トップイメージ送信後の子プロセスへの指示 await send2child(Children[SeatByWSID[str(websocket.id)]],'command2child:1\n') #--ウェブソケットクライアントからの受信イベントハンドラ async def message_received(websocket, path): #・・・・新しいクライアントの認証処理、子プロセスNoとの紐付け、・・・・ #・・・・グローバル変数へのセット・・・・ while True: try: TempWSK = websocket message = await websocket.recv() sda_ar = message.split('=') sda = sda_ar[1] urllib.parse.unquote(sda)#URIアンエスケープ pat_ar = ['^init:','^A','^B','^C'] act_func = {'^init:':init_act,'^A':A_act,'^B':B_Act,'^C':C_Act} if StatusByWSID[str(websocket.id)] == 'wait_reset': mobj = re.match(pat_ar[0],sda)#初期化指示 if mobj:await act_func[pat_ar[0]](websocket,mobj) #・・・・その他の状態の時の処理・・・・ except: #websocketオブジェクトは消滅しているため、 #ここに飛ぶ前にtryでストアしたテンポラリオブジェクトを使って
#ステータス配列から削除 StatusByWSID.pop(str(TempWSK.id)) #・・・・その他グローバル変数からの削除・・・・ break #-- Main loop = asyncio.get_event_loop() #まずInit()では全子プロセスを生成し各プロセスに初期化データ(席番)通知実施 loop.run_until_complete(Init()) futures = set() for k in range(0, MaxChidrenNo): futures.add(rec_from_child(Children[k],k)) start_server = websockets.serve(message_received, "localhost", ****) futures.add(start_server) loop.run_until_complete(asyncio.wait(futures))
調査に1か月、コーディングに半月といった感じの仕事でした。プログラム自体はブラウザを相手にするWebSocketサーバー機能とDBを監視する子プロセスとの通信機能の両方を備えたいわゆる「ゲートウェイdaemon」です。perlで作ったDEN将棋EXよりはずいぶん単純なものです。今回は、WebSocketサーバー機能の実装についての備忘禄です。
DBのあるテンポラリーテーブルの監視を子プロセスにやらせるので、ゲートウェイは上位のブラウザと下位の子プロセス双方からのデータを非同期受信(イベントドリブン)できる仕組みが必要です。
まずは、クライアントであるブラウザとの通信を行うウェブソケットサーバー機能の実装についてです。簡単に実現できそうなモジュールに「websocket-server」というのがあります。pipで簡単にインストールできます。やり方は適当にググってください。下記はこれを使ったサンプルコードです。クライアントから受信したメッセージにHello!!!を付加して送り返すだけのコードです。(****は伏せ字。待ち受けポート番号)
「websocket-server」モジュールを使った例
import logging , asyncio, sys from websocket_server import WebsocketServer logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) handler = logging.StreamHandler() handler.setFormatter(logging.Formatter(' %(module)s - %(asctime)s - %(levelname)s - %(message)s')) logger.addHandler(handler) # Callback functions def new_client(client, server): logger.info('New client {}:{} has joined.'.format(client['address'][0], client['address'][1])) def client_left(client, server): logger.info('Client {}:{} has left.'.format(client['address'][0], client['address'][1])) def message_received(client, server, message): logger.info('Message "{}" has been received from {}:{}'.format(message, client['address'][0], client['address'][1])) reply_message = 'Hello!!! : ' + message server.send_message(client, reply_message) logger.info('Message "{}" has been sent to {}:{}'.format(reply_message, client['address'][0], client['address'][1])) # Main if __name__ == "__main__": server = WebsocketServer(port=****, host='127.0.0.1', loglevel=logging.INFO) server.set_fn_new_client(new_client) server.set_fn_client_left(client_left) server.set_fn_message_received(message_received) server.run_forever()
このコードでは、serverオブジェクトを作ったら、set_fn_message_receivedメソッドに受信イベントハンドラ(コールバック)を渡します。そして、run_forever()。これで受信イベントループを回しているわけです。あとはこのループの中で、受信データを捉えると、渡してあるコールバック関数(受信イベントハンドラ)が非同期実行されます。
perlのAny::Eventで、同様のゲートウェイdaemonを実現している私は、ここで当然ある疑問にたどり着きます。そうです。上位(ブラウザ)からの受信はこれで良いが、下位(子プロセス)からの非同期受信どうすればいいの?
私の答えは、「このモジュールを使いながら、下位からの非同期受信も実現するには大変な工夫と苦労が必要となるはずだからあきらめよう。」というものでした。
そこでさらに調べていくと、紛らわしいのですが「websockets」というモジュールが有る事がわかりました。下記はこのモジュールの公式ドキュメントです。
このモジュールの特徴は、ayncio由来のイベントループを使用するという事。asyncioというのはperlのAnyEventとCoroをドッキングしたようなモジュールだと思います。このasyncioは、coroutineを定義できるので、下位からの受信イベントハンドラと上位からの受信イベントハンドラを両方とも同じイベントループに組み入れることが出来そうです。
これって、perlでもAny::Event由来のwebsocketサーバーモジュールとそうでないwebsocketサーバーモジュールが有ったのと良く似ています。
AnyEventなウェブソケットサーバーのバイナリデータ送信
下記はwebsocketsモジュールによるサーバーの起動例です。(****は伏せ字。待ち受けポート番号)
「websockets」モジュールによるサーバーの起動例
import websockets, asyncio
・・・・・・・・・・・・・・
start_server = websockets.serve(message_received, "localhost", ****)
message_receivedは受信イベントハンドラ(コールバック)です。この関数は、async def によって、coroutineとして定義してあります。そして、下位からの受信イベントハンドラも同様にcouroutineとして定義し、両方のイベントハンドラを、asyncioが提供するイベントループに組み入れることができるのです。
WebSocketクライアントからの非同期受信と子プロセスからの非同期受信を共存させるイベントドリブンなコードは、asyncioモジュールによります。次回はこの説明をしたいと思います。
]]>pthonの一般的な情報に関して備忘録を何回かに分けて記事にしようと思います。
まずは違和感を覚えたperlとの仕様の違いについて思い付く限り書いておきます。
(1){}が無い!
慣れている人には当たり前なんだと思いますが、pythonには{}によるブロック定義が無いんです。ブロックは、最初の行末に「:」を置いて、その下方に半角4文字分のインデントで記述された行がひと固まりのブロックということになります。forループもif文も全てこのルールに従います。この制約は私のコーディング速度に著しく影響しました。見栄えは良くなりますけどね。
(2)main()を使いたくなるわけ
pythonでは、関数は定義した後でしか使うことができません。別の関数内で使用する関数は、後で定義されていても実行することができます。
print(hello())
def hello():
print('Hello!!')
このコードはエラーになりますが、下記であればエラーになりません。別にmain()内のコードを関数定義が終わった最後に記述すれば良いだけなんですが。
def main():
print(hello())
def hello():
print('Hello!!')
main()
(3)if __name__ == "__main__": って何?
pythonでは様々な便利なモジュールを使用するために、「import」文を使用します。perlにもimport文は存在するのですが、私はほとんど、use (Module名)しか使いません。これは、BEGIN { require (Module名); import (Module名; }と等価です。
もし上記のような普通のpythonコードをimportすると、importした時にmain()が実行されてしまいます。「if __name__ == "__main__":」は、「__name__」変数の値が「'__main__'」であるとき(=importでは無く直接実行されたとき)のみmain()を実行することができるということです。上記の場合下記のようになります。
def main():
print(hello())
def hello():
print('Hello!!')
if __name__ == "__main__":
main()
perlでは、「if ($0 eq __FILE__) {}」とすれば良いようです。スクリプト名($0)が実行ファイル名(__FILE__関数が返す値)に等しい場合は、importによる実行では無くて直接実行ということになります。しかし、perlではpackage宣言されているモジュールしか使わないので、使う必要性が正直わかりません。
(4)集合型
pythonには標準で「集合型(set型)」という変数型が有ります。リスト(配列)型の重複要素が除かれたような型です。今回の仕事では、futureオブジェクト(別記事にて登場)の集合を定義しましたが、あまり、型の恩恵を受けた使用方法ではありませんでした。この型同士は、集合演算が標準でできるようです。perlでは、モジュール「Set::Object」を使うと結構便利だそうです。
(5)pythonの関数への引数は全て参照渡し(?)
これが結構な違和感でした。pythonの変数には、イミュータブル(宣言後値を変更不可能)な型とミュータブルな型(宣言後値を変更可能)があり、前者には文字列型、数値型があり、後者にはリスト型などがあります。
結論を言うと、pythonの引数は全て参照渡しですが、その引数がイミュータブルの場合は値渡しの挙動となり、ミュータブルの場合は参照渡しの挙動となります。
イミュータブルの場合、関数内で引数の値を変更すると、その参照を採番し直すため呼び出し元の値に影響を与えないのです。ミュータブルの場合は採番し直さないため、その値が呼び出し元でも変更されます。下記ではこの事を大変わかりやすく説明しています。
ちなみに、pythonではid()関数を使って様々な型の変数の参照値を得ることはできますが、あからさまな「リファレンス(参照)」という変数型がありません。したがってperlの様にリファレンス変数を使った様々な便利なコードを書くことができません。
]]>perlでウェブソケットサーバーを作るには、色々な方法があります。下記はPlack::App::WebSocket作者による記事ですが、色々なウェブソケット関連モジュールを紹介されています。
https://debug-ito.hatenablog.com/entry/20131110/1384067233
DEN将棋EXでは、Plack::App::WebSocketを使用して、PSGIとAnyEventによる完全なイベントドリブンプログラミングを実現しました。つまり、ブラウザからのウェブソケットデータ受信とChildプロセスからのデータ受信両方ともに、AnyEvent由来のイベントハンドラーを記述できるようになりました。このおかげでコードはすっきりし私にとっては手放せないモジュールの一つとなりました。
そして、これを使用すればサーバーの画像データをブラウザに簡単に送信できると思っていたのですが、そうではありませんでした。調べながら3日間ぐらい悩みましたが、モジュールの簡単な改造で解決できました。
DEN将棋EXで使用しているように、Plack::App::WebSocket::Connectionオブジェクトのsendメソッドで、引数$dataがテキストデータの時は、問題無く送信されブラウザ側に表示されるのですが、読み込んだpng画像のバイナリデータは表示されずに、すぐにウェブソケットがクローズされてしまいました。
$conn->send($data);
念のため
Net::WebSocket::Server
も試してみました。下記はNet::WebSocket::Serverによるアプリです。これだけで無限ループとなりウェブソケットサーバーとして機能します。
my $origin = 'http://**.**.**.**';
Net::WebSocket::Server->new(
listen => ****,
on_connect => sub {
my ($serv, $conn) = @_;
$conn->on(
handshake => sub {
my ($conn, $handshake) = @_;
if($handshake->req->origin eq $origin){
$Websockets{"$conn"} = $conn;#確立したソケットを保存
## ゲートウェイオブジェクトを生成
## 接続確立後の処理
}else{
$conn->disconnect();
}
},
utf8 => sub {
my ($conn, $msg) = @_;
## テキストデータ受信時の処理
# 受信データにより、ゲートウェイオブジェクトメソッドの中で
# テキストデータ送信の「$conn->send_utf8($da)」か
# バイナリデータ送信の「$conn->send_binary($da)
# を使い分ける
},
binary => sub {
my ($conn, $msg) = @_;
## バイナリデータ受信時の処理
},
);
},
)->start;
ブラウザへのテキスト送信,バイナリデータ(イメージデータ)送信は当然問題有りませんでした。ところがこのモジュールは独自のイベントループを回すので、ChildプロセスのハンドルについてのAnyEvent::Handleイベントループを定義しても機能しません。
Plack::App::WebSocketではバイナリデータは送信できないのか?まずはこれを調査しました。
このオブジェクトを生成するnewの第二引数で、on_establishをキーとするコールバック関数はPlack::App::WebSocket::Connectionオブジェクトをその第一引数で受け取ります。
ウェブソケットサーバー側からブラウザへの送信は、このオブジェクト$connのsendメソッドを使用します。このsendメソッドの中では、
$self->{connection}->send($message);
となっており、このオブジェクトが生成された時に渡されたコネクションオブジェクトの
sendメソッドを実行するようになっています。ではこの「コネクションオブジェクト」は何か?
Plack::App::WebSocket::Connection
オブジェクトを生成するのは、
Plack::App::WebSocketオブジェクトのcallメソッドの中であり、
Plack::App::WebSocket::Connectionのnewに渡すコネクションオブジェクトは
どうやら、AnyEvent::WebSocket::Connectionオブジェクトです。
ところが、Plack::App::WebSocket::Connectionパッケージのperldocには
$connection->send($message)
Send a message via $connection.
$message should be a UTF-8 encoded string.
となっているのです。つまりテキストしか送信できないということです。
では、AnyEvent::WebSocket::Connectionもバイナリ送信に対応していないのかを調査しました。
AnyEvent::WebSocket::Connectionのsendメソッドの中を見てみると
Protocol::WebSocket::Frameオブジェクトを生成するようになっています。
この時の生成コードは
if(ref $message) {
$frame = Protocol::WebSocket::Frame->new(opcode => $message->opcode,
buffer => $message->body, masked => $self->masked, max_payload_size => 0);
} else {
$frame = Protocol::WebSocket::Frame->new(buffer => $message,
masked => $self->masked, max_payload_size => 0);
}
つまり
引数の$messageはスカラー指定と、リファレンス指定のどちらかが選択できる。
リファレンスの場合、無名ハッシュリファレンスとし下記を指定します。
$message->opcode, $message->body
それぞれ、
Protocol::WebSocket::Frame->newの引数である無名ハッシュとして、opcodeとbufferをキーとする値になります。opcodeというのは、ウェブソケットフレームのヘッダー部に配置するもので、フレームの種類を表します。
これを使えばopcode=0x2(バイナリデータ)を指定でき、バイナリイメージデータを送信できそうです。但し、同上newに渡す引数で、
max_payload_size => 0
固定なので、Protocol::WebSocket::Frameでは、
ウェブソケットのmax_payload_sizeはデフォルトの65536バイトとなってしまうようです。
これではバイナリデータ(opcode=0x2)サイズとして足りないので、
上記AnyEvent::WebSocket::Connectionのsendメソッドの if 文の ref の場合のコードを下記に変更しました。
$frame = Protocol::WebSocket::Frame->new(opcode => $message->opcode,
buffer => $message->body, masked => $self->masked, max_payload_size => 10000000);
これで10MBまで送信可能になりました。
また、上記
AnyEvent::WebSocket::Connection
のperldocには
send
$connection->send($message);
Send a message to the other side. $message may either be a string (in
which case a text message will be sent), or an instance of
AnyEvent::WebSocket::Message.
とあり、sendメソッドの引数は、
string か AnyEvent::WebSocket::Messageオブジェクトと書いてあります。
そこでこの仕組みを利用するために、
Plack::App::WebSocket::Connectionのsendメソッドを下記の通り改造しました。
$self->{connection}->send($message);
↓
if(ref($message)){
$self->{connection}->send(AnyEvent::WebSocket::Message->new(
opcode => ($message->{opcode}), body => ($message->{body})));
}else{
$self->{connection}->send($message);
}
これで、psgi側で下記のようにAnyEvent::WebSocket::Connectionオブジェクトのsendメソッドを呼ぶ時に、$messageがスカラー値の時はテキスト送信(opcode=1)、リファレンスにした時は
そのハッシュキーopcodeを指定した値でフレームを変更でき、bufferキーの値を送信データとすることができます。
##テキスト送信
$conn->send($text_data);
##バイナリ送信
my %message;
$messgae{opcode}=2;
$message{body}=$bin_data;
$conn->send(\%message);
という具合です。これで無事、AnyEventなウェブソケットサーバーをバイナリ送信対応とすることができました。
]]>
現在はwebkoza.comのMT5.2を動かすためにLinux上でMySQL 8.0.13を使用しています。最近の仕事で、社内業務システムをMySQL8.0.23とStrawberry Perlを使用して開発したので、ここではこの時初めて使用した「ウィンドウ関数」について記事にしておきます。DBを使っていてまだウィンドウ関数を使ったことの無い人向けの記事です。MySQLがウィンドウ関数に対応したのは8.0からだそうです。まあまあ最近の技術なんです。
色々言い方は有ると思いますが、私なりに定義してみれば、
抽出したレコード間の集計を行いたい時に抽出したレコード集合に対してウィンドウを開いた上で集計を行う関数で、通常の集計関数は1つの結果行にグループ化しますが、ウィンドウ関数は抽出したレコード毎に結果を生成します。
となります。こう書くと使ったことの無い人は何が何だかわからないかもしれません。実例を挙げながら説明することにします。
下記の担当者マスターテーブルが有るとします。
select * from testdb.person_mas as pm;
このテーブルには10人が登録されており、連番、名前、所属コード、役職コード、登録日時が入力されています。
下記のSQL文で所属部署毎の登録人数をcount関数を使って抽出した結果が次の図です。group by 句を使ってグループ化した人数を表示しています。
select pm.divison_cd,count(*) from testdb.person_mas as pm group by pm.divison_cd;
ここでいきなりウィンドウ関数としてのcount(*)を導入します。下記のSQL文は所属コードをグループ化する事なくレコード数をcount関数で数えた結果ですが、over句を使用しています。over句を使うとこの関数はウィンドウ関数となるのです。over句の役割は、windowを開くことと抽出結果表の範囲指定を行うことです。
select pm.divison_cd,count(*) over() from testdb.person_mas as pm;
結果表は全員分の10行となっていますが、数えた値は全て10となっています。これは各行毎に全員の人数分10人を数えたということです。これが「ウィンドウ関数は抽出したレコード毎に結果を生成」という意味です。但しこの抽出結果自体あまり使い道はなさそうです。
では、ウィンドウ関数を使って且つ最初のSQL文と同様に所属コードでグループ化したらどうなるでしょうか。結果は下記です。
select pm.divison_cd,count(*) over() from testdb.person_mas as pm group by pm.divison_cd;
グループ化はされているのですが、ウィンドウ関数の働きによって各行毎に全行数4が全ての行に出力されています。これも使い道は無さそうです。ではどういう時にウィンドウ関数は使うのか?その話に入る前に、over句について少し説明します。いままでover句に引数を指定していませんでした。引数には集計範囲を指定できます。指定が無ければ全レコードが対象となります。引数の種類としては、
の3種類です。ここではorder by 指定のみ説明します。そのほかに付いては下記あたりを参考にしてください。
上記のsql文でover句にorder by指定をしてみると下記の通りになります。
select pm.divison_cd,count(*) over(order by pm.divison_cd) from testdb.person_mas as pm group by pm.divison_cd;
こんどは各行が4ではありませんね。これは、「over句のoder by 指定は、行を順番に並べた上で、最初の行から現在行までのみを集計の対象にする」という仕様だからです。これにより1行目は1行のみが集計対象、2行目は2行が集計対象・・4行目は4行が集計対象となるためにこのような結果となっているわけです。このことは累積集計を行うような用途では大変便利な機能となります。
あまり高度な例は紹介できませんが、私が最近の仕事で使用したlag,lead関数を使用した例を説明用に単純化して紹介します。
まず、本稿で使用しているテーブルから下記の<命題>を考えます。
<命題>
JさんはAさんが登録された日時から何日後に登録されたか
これをウィンドウ関数を使って計算するためにlagおよびlead関数を使います。lag関数は「指定行数前のレコードを対象として集計」、lead関数は「指定行数後のレコードを対象として集計」という仕様です。
下記はlag関数を使用して、1人前の人の登録日時を併記させたものです。
select pm.person_no,pm.person_name,pm.reg_date,lag(pm.reg_date,1) over(order by pm.person_no) as after_reg_date from testdb.person_mas as pm;
下記はlead関数を使用して、1人後の人の登録日時を併記させたものです。
select pm.person_no,pm.person_name,pm.reg_date,lead(pm.reg_date,1) over(order by pm.person_no) as after_reg_date from testdb.person_mas as pm;
1人前や後のレコードが存在しない場合はNULLとなっていることがわかります。ここで慣れている人は気づいたかもしれませんが、これら結果表を見ると、同じテーブルを1行ずらして外部結合したような結果になっていることがわかります。後で説明しますが、一般的にlag,leadを使って抽出する結果は、ウィンドウ関数を使わずに外部結合を使って実現する事も可能となる場合が有ります。
それでは命題を実現しするにはどうすれば良いでしょうか。上記のsql文でJさんとAさんのみを指定して、lagかlead関数を使ってAさんとJさんの登録日を同一レコードに生成しその差を取れば良いのです。下記はlag関数を使って実現したものです。unix_timeatampは'1970-01-01 00:00:00'から引数までの秒値を返します。2行抽出されますが2行目のdiff_from_AはNULL値となる意味のない行なのでlimit 1で最初の1行のみにしています。
<命題>をウィンドウ関数で実現したSQL文
select pm.person_name,(unix_timestamp(pm.reg_date) - unix_timestamp(lag(pm.reg_date,1) over(order by pm.person_no)))/3600/24 as diff_from_A
from testdb.person_mas as pm
where pm.person_no = 1 or pm.person_no = 10 order by pm.person_no desc limit 1;
前述で示唆したようにこれは、下記のような外部結合を使っても実現可能です。person_noを9だけずらすという結合条件です。
<命題>を外部結合で実現したSQL文
select pm1.person_name,(unix_timestamp(pm1.reg_date) - unix_timestamp(pm0.reg_date))/3600/24 as diff_from_A
from (select pm.person_no,pm.person_name,pm.reg_date from testdb.person_mas as pm where pm.person_no = 10) as pm1
right join (select pm.person_no,pm.person_name,pm.reg_date from testdb.person_mas as pm where pm.person_no = 1) as pm0
on pm1.person_no = pm0.person_no + 9;
どちらも同じ下記の結果を得ることができます。つまりJさんはAさんの9日後に登録されたということです。ウィンドウ関数を使用した方がすっきりしていますね。
]]>息子の友達の家でたくさん生まれたネコのうち1匹をもらう約束をしたのだ。その日家族3人は、千円のお土産(。。なんだったか思い出せない)を手に、待ちに待った息子の友達の家に向かった。うちで猫を飼うのは初めてだったのだが、乳離れするまで5か月ぐらいその友達が育ててくれたので出産直後の苦労は無かった。
息子の友達が住んでるアパートに行くと「猫がいる部屋はこっち」と案内された別の部屋に入ってみると、5-6匹のネコ(もっといたかも)。妻と息子は嬉々としてどのネコにするか相談をはじめた。そして、最も元気そうだという理由で、尾の長い顔のスラっとした美人のサビネコを選択した。こうしてモモはうちにやってきた。病院で先生の「この子はシャムが入ってますねえ」の一言で妻も私もなんかすごく嬉しかった事を覚えている。
最初は、いつも狭くて暗い所にいたモモも次第に我が家に慣れて、特に妻にはかなり懐いた。家に来てからすぐに病院で予防接種を受け、9か月目ぐらいに避妊手術をしてもらった。
モモがうちに来た時は、サビネコという名前を知らなかった。会社の同僚と話していて「サビネコ」と判明。ネットで調べて、遺伝学上全てメス猫であり、あまり人気は無いが性格は総じておっとりとしており、大変飼いやすいネコだとわかった。モモも大変おっとりしていて、しかも大変頭が良かった。
最初の事件は、家に来てから1年目ぐらいのある時、雨戸を閉めようと窓を開けた時に起きた。ダッシュで庭に出た焦げ茶色の何か。。モモが脱走したのだ。「あっ!」というまの出来事であった。妻は早く探さなきゃ!っと狼狽。一応庭周辺を探したが見つけられず私は昔実家で買っていたネコを思い出し、必ずネコは自分の家に帰って来るからと妻をなだめた。次の日の午前中にモモは帰ってきた。庭で鳴き声がして見るとモモがいた。窓を開けると何事もなかったように家の中に入ってきたそうだ。その後はたびたび脱走するようになった。そのうち庭に出せとせがむように鳴くようになり、面倒なので結構外に出してしまうようになった。
ある夏の日、いつものように庭にモモが返ってきたので窓を開けると、セミをくわえていた。またある日はスズメ。またある日は何とウグイス!彼女はその他にも色々な狩りを楽しんだようである。獲物を見せて褒めてもらいたいという行動らしい。そして家の中では何とも愛くるしい目で妻や私を見るのである。モモは完全に家族だった。
うちに来てから5年目、いつの間にか食欲が無く、ぐったりするようになったため、病院に通うように。何回目かに、先生からお腹に大きなしこりが有ると知らされた。おそらく膵臓癌だろうと。手術はできないんですか?と聞くと、開腹手術をしてみて可能ならば取り除けますが、手遅れならそのまま閉じますと言われた。開腹結果はやはり手遅れだった。そのまま閉じて1泊して家に戻ってきた。その後は、スポイトによる投薬と栄養補給。妻は献身的に看病した。平日の病院通いは妻が原チャリに乗せて連れて行った。そしてある朝、ソファーの上の妻の腕の中で息を引き取った。2015年11月12日だった。これで我が家で妻が看取った動物は、ハムスターのタマゴとウズラ達、インコのピン、サビネコのモモとなった。ちなみにモモがうちに来た時は、オスのインコのピンはまだ元気だった。ピンのケージをモモが届かない所に吊るしたが、何度もジャンプして脅かしたのが良くなかったのか、2-3か月でピンは亡くなってしまった。この時も妻の落ち込みはひどかった。でも、モモの時はもっとひどかった。
妻と話し合ってモモの亡骸は、白峯時の動物愛護協会に持ち込んで焼いてもらい、お骨はペット専用共同墓地に眠っている。
]]>下図はDEN将棋EXの画面遷移図です。紺色の画面をMojolicious::Liteで作りました。緑色の画面は、静的htmlとjqueryなどで作っており、PSGIによるwebsocketサーバーと通信します。
まずは Mojolicious::Lite
cpanm install Mojolicious::Lite
続いて Mojolicious::Plugin::PlackMiddleware
cpanm Mojolicious::Plugin::PlackMiddleware
MojoliciousはMVC(Model-View-Controller)モデルのうち、MVを実装しているperlのWAFの一つです。MVCに対応しているcatarystを意識して色々変遷が有ったようですが、入門としてのMojolicious::Liteは、誰でも手軽に使えるMV(Model,View)に対応したWAFで大変重宝しています。
Mojolicious::Lite:本当に簡単なウェブアプリがあればいいときは
基本的なgetやpostメソッドの使い方や、htmlテンプレートの使い方、テンプレートへのperl文法の埋め込み方法などは、下記を参照してください。初心者向けの大変わかりやすい内容です。
Controllerはユーザーが好きなように実装できるような仕組みが実装されています。DEN将棋EXではPlack MiddlewareであるPlack::Sessionを使ったり、オリジナルのモジュールメソッドをヘルパーメソッドとして定義したりしています。
理由は省きますがセッションについてはMojolicious標準では無くてPlack::Sessionを使用しました。
Mojolicious::Plugin::PlackMiddlewareをインストールしておいて、Mojolicious::Liteのpluginメソッドを使うと、Plack Middlewareをプラグインとして登録して使用する事ができます。下記コードがその定義部分です。(****は伏せ字)
クラスメソッドであるpluginメソッドの引数に、「plack_middleware」という文字をキーとしたセッション情報を渡します。$SesAr_refがそのセッション情報で、配列リファレンスです。配列要素として、StateとStoreの情報を定義しています。Plack::Session::State、Plack::Session::Storeについては下記を参照してください。
DEN将棋サービスについて
https://metacpan.org/pod/release/MIYAGAWA/Plack-Middleware-Session-0.30/lib/Plack/Session/State/Cookie.pm
https://metacpan.org/pod/release/MIYAGAWA/Plack-Middleware-Session-0.30/lib/Plack/Session/Store/File.pm
my $SesAr_ref = [
'Session' => {state => Plack::Session::State::Cookie->new(
session_key => '****',
expires => 1200,
path => '/',
httponly => 1
)},
'Session' => {store => Plack::Session::Store::File->new(
dir => $SesDir
)},
];
app->plugin(plack_middleware => $SesAr_ref);#Mojolicious Plugin の書式
DEN将棋EXではログインリクエスト処理(postメソッドの引数で指定するコールバック)でセッション登録用サブルーチンを呼びます。サブルーチンは、
app->start;
でアプリをスタートさせるメソッド呼び出しの後に、記述します。
このセッション登録用サブルーチンを下記に示します。ここではユーザー名とかパスワードをセッションに保存しています。また、第一引数にコントローラーオブジェクトリファレンス(getやpostメソッドで受け取る第一引数)を受け取れるようにしているのは、reqメソッドによりリクエストオブジェクトを取得して、環境変数を得るためです。このリクエスト情報から取得した環境変数を渡してPlack::Sessionオブジェクトを生成します。
sub set_plack_session{ my($c,$un,$pas)=@_; my $request = $c->req; my $sess = Plack::Session->new( $request->env ); $sess->set('uname',$un); $sess->set('pass',$pas); $sess->set('loginok',1); $sess->set('logintime',time); my $sfilename = $SesDir . '/' . $sess->id; undef $sess; eval{ chmod(0666, $sfilename); }; return(1); }
下記のコードは、色々なリクエストを受信するたびに呼ばれ、リクエスト環境変数をもとにセッション判定するサブルーチンです。$ExpreSecondはあらかじめ入力してあるセッション有効期間(秒値)で、セッションから取り出したログイン時刻が有効期間内か判定しています。時刻を扱うためTime::Pieceを使用しています。判定OKならセッションのログイン時刻を更新します。実際にはDBと連携しているのでもう少し複雑なコードになっています。
sub load_plack_session{ my($c,$un_ref,$pas_ref)=@_; my $request = $c->req; my $sess = Plack::Session->new( $request->env ); my $t = localtime($sess->get('logintime')); my $timestampda = $t->year.'/'.$t->mon.'/'.$t->mday.' '.$t->hour.':'
.$t->minute.':'.$t->sec; my $t_now = localtime; my $timestampda_now = $t_now->year.'/'.$t_now->mon.'/'.$t_now->mday.' '
.$t_now->hour.':'.$t_now->minute.':'.$t_now->sec; if($sess->get('loginok') and ($sess->get('logintime') + $ExpreSecond > time)){
#有効期限チェック $$un_ref = $sess->get('uname'); $$pas_ref = $sess->get('pass'); $sess->set('logintime',time);#logintime更新
return(1); }else{ return(0); } }
getやpost処理を記述する前に、独自のヘルパーメソッドを定義しておくと、htmlテンプレート内で使用する事ができます。下記はその定義部分です。ShogiEX::DENLibは独自のライブラリーモジュールで、このモジュールで定義してあるメソッドをヘルパーメソッドとして登録しています。
my $DenLib = ShogiEX::DENLib->new();#DEN将棋EXライブラリオブジェクト helper denlib_get_profile => sub { shift; my($d1,$d2)=@_; return $DenLib->get_profile($d1,$d2); };
下記はプロフィール変更画面を提供するためのgetリクエスト受信後の処理です。getメソッドのハッシュ引数のキー「/ch_profile」がリクエストURL、バリューの部分が処理本体です。セッションOKならch_progileテンプレートをレンダリングしています。
get '/ch_profile' => sub { #プロフィール変更画面 my $self = shift; my($un,$pas); my $wret = ''; if(&load_plack_session($self,\$un,\$pas)){ if($un){ $self->render('ch_profile','myurl' => \$MyURL ,
'uname' => \$un , 'pass' => \$pas); }else{ my $title = 'エラー'; my $h1 = '不正リクエスト'; $self->render('error','myurl' => \$MyURL,'title' => \$title,
'h1' => \$h1,'comment' => \$wret); } }else{ $self->render('login','myurl' => \$MyURL ,'comment' => \$wret); } };
下記コードは上記処理の中で、コントローラーオブジェクト(上記の$self)のrenderメソッドに渡されるhtmlテンプレートです。テンプレートの中でヘルパーメソッドを呼んでいます。スカラー変数の値はstash関数を使ってテンプレートに渡して使用し、独自のサブルーチンはヘルパーメソッドとして登録する事によりテンプレート内で使用できるという事になります。
@@ ch_profile.html.ep <% my $myurl_ref = stash('myurl'); my $un_ref = stash('uname'); my $ps_ref = stash('pass'); my($wret,$txt,@rec,$email,$profile); my $un = $$un_ref; $wret = denlib_get_profile($un,\@rec); if($wret ne 'ok'){ ### プロフィール取得成功時のhtml出力を記述 }else{ ### プロフィール取得失敗時のhtml出力を記述 }]]>
%>
shogi-serverをwebkoza.comで稼働させ、100%PerlでDEN将棋とDEN将棋Xを作りましたが、頻繁な定期XMLhttpRequestにより手データをブラウザに反映させるため、パフォーマンスが今一で、相手に手が伝わるのに数秒かかることがあるという課題がありました。またサーバー負荷を考慮して席数も最大20と少ないものでした。それでも個人的に遊ぶためには問題無いのですが、かねてからやってみたかったPerlによるウェブソケットとAnyEventを使用して改造することにしました。結構苦労したのですが、完成してみると満足のいくレベルに達したと思っています。
最大席数は100にしました。これだけあれば当面問題無いと思っています。ウェブソケットにしたことにより、パフォーマンスは大幅に改善され、モバイル電波経由のスマートフォン同士の対局でも、電波状況にもよりますが、同時対局者数が数人であれば手が相手に伝わる時間は、だいたい1秒程度です。
ここでは、主に、[DEN将棋EX]のゲートウェイdaemon用PSGIで採用したPlack::App::WebSocketとAnyEventをどのように使用したかを記載しておきます。
ウェブソケットって何?な人は、下記のリンクを参考にしてください。
ウェブソケットにすることにより最初のhttpリクエストが成功した後、対局中は80番ポートを経由したTCPSocket接続が維持されます。これにより必要な時だけデータが通信路に流れることになります。具体的には、手を指した時はブラウザからゲートウェイdaemon経由でshogi-serverにデータが送信され、shogi-serverが手データを配信した時はゲートウェイdaemon経由で手データがサーバーからブラウザに届きます。これにより無駄なデータ交換が無くなり、大幅にパフォーマンスが改善するわけです。
さらに、席毎にForkしたワーカープロセス(直接shogi-serverと通信する)経由のshogi-serverからのデータを、AnyEvent::Handleのイベントハンドラーで受信するようにしました。
こうして、ゲートウェイdaemonとしては、Plack::App::WebSocketによる上位(ブラウザ)からと、下位(ワーカープロセス)からの2種類のデータ受信イベントによる、完全なイベントドリブン型のプログラミングが可能になりました。
なお、ゲートウェイdaemon用PSGIの起動は、AnyEventを使用する場合Plackは未対応という事なので、はじめてTwiggyを使用しました。これも宮川さんが開発したツールです。
Twiggy - AnyEvent HTTP server for PSGI (like Thin)
下図は以前も掲載したソフトウェア連関図ですがhttpの部分がWeb Socketに変更になっています。
DEN将棋EXは、連関図のPSGI Programs(ゲートウェイdaemon)とTCP Client(ワーカープロセス)および、ブラウザサイドプログラム(JavaScript群)とhtml、CSSファイルから構成されています。
(1)ウェブソケットアプリケーション本体部
他にも使えそうなモジュールは有るのですが、作者が丁寧な記事を残してくれているPlack::App::WebSocketを使用する事にしました。開発者の下記記事を参考に実装しました。
https://debug-ito.hatenablog.com/entry/20131110/1384067233
https://debug-ito.hatenablog.com/entry/20131110/1384067449
https://qiita.com/debug-ito/items/e347dd1e08891f8dd612
DEN将棋EXのゲートウェイdaemonとしては、最初のリクエストでログイン処理&席番決定まで行い、後はウェブソケット通信によりワーカープロセス(経由shogi-server)とブラウザが継続的にやり取りします。下記のコードは、PSGIアプリケーション本体(かなり省略して記載しています)です。
Plack::App::WebSocketオブジェクトを生成(new)して、このオブジェクトのcallメソッド(PSGI環境変数が引数)を呼ぶ事が仕事です。このnewメソッドの引数には、必要なコールバックを設置できる仕組みが用意されており、on_error 、on_establish をキーとしたコールバックを設置しています。前者はウェブソケット接続時のエラーハンドラーで後者はウェブソケット確立後の処理です。
ウェブソケット確立後の処理としては、最初に、引数で受け取れる接続オブジェクト($conn)毎に、DEN将棋EXのログインオブジェクト$GateWay{"$conn"}を生成し、このオブジェクトのto_appメソッドをコールしてログインと席番確定を行っています。次に接続オブジェクト($conn)のonメソッドをコールします。このonメソッドの引数には、コールバックとして、ウェブソケットでメッセージを受信した時のイベントハンドラと、ウェブソケット切断イベントハンドラの2つを、各々message 、finishをキーとして記述することができます。
受信イベントハンドラの内容としては、ウェブソケットで受信したデータを対応するワーカープロセスに送信する処理がメインです。送信サーブルーチンのsend_daには、$GateWay{"$conn"}のto_appでリファレンス引数として生成された席番に対応するワーカープロセスのIOハンドラリファレンスと、メッセージそのものを渡しています。
切断イベントハンドラは、異常切断と通常切断の処理を分けて記述しました。内部的にはどちらのイベントハンドラも接続オブジェクト($conn)毎に生成されることに注意してください。
my $ShogiLoginEX = sub { # 将棋サーバーへのログインアプリケーション # 対局待ち開始/観戦セッション開始 my $env0 = shift; Plack::App::WebSocket->new( on_error => sub { my $env = shift; return [500, ["Content-Type" => "text/plain"], ["Error: " . $env->{"plack.app.websocket.error"}]]; }, on_establish => sub { my $conn = shift; ## Plack::App::WebSocket::Connection object my $env = shift; ## PSGI env my $hs_res = shift; ## extra results from the handshake callback $Websockets{"$conn"} = $conn;#確立したソケットを保存 my $request = Plack::Request->new($env); ### マッチメークシーケンス開始 $GateWay{"$conn"} = ShogiEX::LoginEX->new($request);# PSGI Requestを渡してnew $Status{"$conn"} = 'start'; $GateWay{"$conn"}->to_app($conn,.....); $conn->on( message => sub { my ($conn, $msg) = @_; my($sda); #データをウェブソケット受信 $sda = [split(/=/,$msg)]->[1];# sda=*** の右辺を取り出す &send_da($OUTREF[$SeatBySocket{"$conn"}],$sda); # データを子プロセスへ }, finish => sub { if(....){
#異常終了時の処理
&ng_close_websoket($conn);
}else{
#正常終了時の処理
&close_websoket($conn);
}
},
);
}
)->call($env0);
};
(2)ワーカプロセスからのデータ受信イベントハンドラ
標準入力を監視するためにAnyEvent::Handleという便利なモジュールが有ります。これを使えば、あらかじめdaemon起動後の最初に生成したワーカープロセス(子プロセス)からの入力データを受信するイベントハンドラを記述する事ができます。下記は多少省略していますが、イベントハンドラを含むイベントループサブルーチンです。
このサブルーチンは、daemon起動後の初期化処理で全ワーカープロセスを生成したときの、受信IOハンドラリフェレンスとワーカープロセス番号(=席番)を受け取って、各ワーカープロセス毎の受信イベントハンドラを定義しています。
AnyEvent->timerに渡すコールバック関数($read_cin)の中で、AnyEvent::Handleオブジェクト$fhのon_readメソッドを呼びます。このメソッドの引数で渡すコールバックが、イベントハンドラ実体です。この中で、受信バッファ全体を一気に受け取るために、$fh->{rbuf}を直接参照しているのが味噌です。
イベントハンドラの中でコールしているread_actionは、ワーカープロセスから受信したデータ毎に行うDEN将棋EX独自処理を記述しているサブルーチンです。
AnyEventについての詳細は下記あたりを参照してください。
https://gihyo.jp/dev/serial/01/perl-hackers-hub/000203?page=2
下記は、AnyEvent::Handleドキュメントの日本語訳で、大変参考になりました!
https://abcb2.hatenadiary.org/entry/20101228/1293491312
sub evloop{ my($iref,$i)=@_; my($cv,$fh,$w); $cv = AnyEvent->condvar; $fh = AnyEvent::Handle->new( fh => $iref, # エラー,もしくはEOF時に$cvに終了通知を送る on_eof => sub { $cv->end }, on_error => sub { $cv->end }, ); # 最後の待ちを有効にするために1回beginを呼ぶ $cv->begin(); my $read_cin ; $read_cin = sub { $fh -> on_read ( sub {#読み出し my($fh) = @_; my $rbuf = $fh->{rbuf}; # 処理している間は # $cvの条件を満たさないように # フラグを立てておく $cv->begin(); my @recdata = split(/\n/,$rbuf); $fh->{rbuf} = ''; for(@recdata){ &read_action($i,$_); } $cv->end();#終わったらフラグを落とす $w = AnyEvent->timer(after => 0, cb => $read_cin); } ); }; # もう1回読むために$read_cinをイベントキューにいれる $w = AnyEvent->timer(after => 0, cb => $read_cin); }]]>
同じような経験をした知り合いからは、「1年間は、いろいろ考えてしまい正常な日常とはいかない」と聞いています。最初の1,2か月は妻が亡くなったことにより諸々の手続きやら、何やらでぼーっと考える暇が無く返って元気でしたが、その後はひどい状況でした。
それでも仕事をしているときは仕事に集中しなければいけないし、生きていかなくてはいけない。暇なときは好きなことをしなくてはいけないと思い、趣味の釣りとITプログラミングを再開しています。毎朝毎晩仏壇にお線香をあげ、夕食後はperlをいじり倒す。休みにはたまに釣りに出かけるという生活です。
それにしても、最近思うのは人とのつながりが人間にとってどんなに大事なことかということです。妻の最後の瞬間にいっしょにいてくれた訪問看護師さんからの温かい手紙、釣りのお誘いをすると必ず付き合ってくれる友人、必ず週1回は電話してきてくれる老いた母親、常に気遣ってくれる近くに住む妻の母親、、、こんなにも人の優しさが有りがたいと思ったことは過去にはありません。
このブログはITプログラミングと釣りという2大趣味について主に書いてきました。徐々に投稿を再開していくつもりです。
最近はDEN将棋Xをウェブソケット対応したDEN将棋EXを開発中です。ウェブソケットにすることにより、本格的な「オンライン将棋対局サービス」として生まれ変わります。主機能の実装は完了しています。目玉は新たに「観戦機能」を実装したことです。現在ユーザー・組織登録機能を作っているところです。
]]>DEN将棋Xの記事で、「shogi-server自体も開放しているので、将棋所やK-将棋のようなshogi-server対応の将棋ソフトとの対局もできます。」と書きましたが使っていませんでした。かねてから、スマートフォンで手軽にあの「やねうら王」と対局してみたかったので、試してみました。最初うまくいきませんでしたが、将棋所作者の方のアドバイスもいただき成功したのでその備忘録です。
これにより、家で寝転がりながら手元のスマートフォンでやねうら王などのUSI対応AIエンジンと対局ができます。スマートフォン側はDEX将棋Xなのでアプリのインストールは不要です。
わかりにくいのイメージ図にしてみました。自宅のWindowsマシン上の将棋所と、DEN将棋Xにログインした手元のスマートフォンとの間でインターネット経由の対局になります。
スマートフォンは、DEN将棋Xですから、webkoza.comのウェブサーバーとゲートウェイプログラム経由でshogi-serverにログインし、将棋所は直接webkoza.comのshogi-serverにログインすることになります。
ここからダウンロードできます。ダウンロードした圧縮ファイルを解凍して、Shogidokoro.exeを起動するだけです。.NET Framework 4以上が必要なので注意してください。
やねうら王は、Git-Hubのリリースサイトからダウンロードしました。この時の最新リリース「やねうら王 実行ファイル詰め合わせ V4.89」の「YaneuraOu2019V489all.zip」をダウンロードし、決めたフォルダに解凍します。
将棋所を起動して、対局-エンジン管理 を選択し、下図の様に複数種のやねうら王エンジンを追加します。各々のやねうら王エンジンについてはreadmeを参照してください。
次にやねうら王の定石ファイルをダウンロードしました。ちょっとわかりにくいのですが、上記リリースサイトの何ページ目かの「やねうら王 定跡ファイル詰め合わせ」の3つのzipファイルをダウンロードし、上記やねうら王エンジンフォルダにbookフォルダを作ってその中に解凍した3つのdbファイルをコピーします。
最後に、評価関数ファイルをダウンロードしました。これもわかりにくかったのですが、上記リリースサイトでは無く、トップページの「過去のサブプロジェクト」をクリックして、「やねうら王 KPP_KKPT型用評価関数」の「リゼロ評価関数 KPP_KKPT型 epoch4」をクリックするとダウンロード画面が表示されます。ダウンロードしたファイルを解凍すると3つのbinファイルができますので、それを上記やねうら王エンジンフォルダに作成した「eval」フォルダにコピーして完了です。最初readmeをよく読まず、これを怠ったため対局開始前にフリーズするという現象が起きてしまいました。
どちらが先でも良いのですが、まずDEN将棋X側から説明します。下図の様にUname・Passwd・Gnameを設定して「Login」を押してログインします。
着席番号が表示されたらOKを押すと、マッチメーク(対局者決定)待ちになります。
次に将棋所側は、「対局ーサーバー通信対局」を選び、下図のように「こちら側の対局者」にやねうら王を選択し、接続先にwww.webkoza.com、ログイン名はわかりやすい英数字、パスワードは、相手がDEM将棋XであればGname(****ー****ー**)と同じ文字列を入力します。DEN将棋であればPasswd(****ー****ー**)と同じ文字列を入力します。ハイフンも忘れずに。
ここでOKを押すと、マッチメークしDEN将棋X側は下図のように先手/後手が通知されます。この時点で対局は開始されていますので、OKを押して手を進めてください。
将棋所側は下図の様になります。
この後は手番が回ってきたらDEN将棋X側で駒をタップすると進める場所がピンク色で示されます。(下図)。
駒を進める場所をタップすると、駒がそこに移動した後相手手番になり、しばらくすると相手の駒が動いてこちらの手番になります。(下図)
やねうら王エンジンのレベルは1-20まで選べるようです。自分のレベルに応じて最新AIと対局する事ができます。もちろん研究にもなります。
そういえば、DEN将棋Xには「中断」機能を実装しましたが、将棋所が対応しているかどうか試してません。shogi-serverの拡張モードを全て網羅していればできるはずなのですが、わかりません。試したらまた後日記事にします。
]]>
状況:
曳舟&係留釣りの大森ボートでイナダ狙いの予定が北風強風によりだめそうなので、久しぶりにS.Mと東伊豆に磯釣りに出かけました。向かったのは稲取港の手前にある黒根崎です。ここは6年前に1回釣行したことがあり、この時は25-32cmが4枚釣れてます。今回も前回同様に「長左衛門」というポイントに入りました。
常連っぽい石鯛師の先客が1人先端に陣取っていました。上物を狙うには左のワンドか右のワンド側が良いので左に入ろうとすると、この人が声をかけて来て、「今日はウネリがひどいから少なくとも満潮の7時過ぎまで、そこは波がかぶって危険だぞ。死んでも責任取れんぞ。」と言ってきました。ヘッドライトで照らしてしばらく海況を見ていると、だいたい状況が判明。確かに相当のウネリでたまに波というか飛沫をかぶる状態でした。右のポイントは本格的に波をかぶりそうでした。先端の石鯛師のポイントは直接波が当たらないので何とかできそうです。左のポイントの根本とその上の高座は飛沫が被りそうに無いので、S.Mは根本、私は高座に入って実釣開始しました。
私の場所は去年新調した7.5mのタモでも水面に届きませんでした。25cm程度のメジナであれば楽に抜ける1.75号の竿で開始。基本的に左のワンドの中を流す感じでした。この日はウネリが入っており、ワンドの最奥部は常に洗濯機状態なのでお話にならず、少しワンドの真ん中ぐらいから流す感じでした。釣った潮は、午前中はワンド奥から右斜め方向に払い出し、午後は左斜め方向に払い出す感じの潮になりました。午前中は右方向に行くのでS.M氏と何度かお祭りしてしまいました。
何回かコマセを打ち込むと、サシエサの沖アミは瞬時に無くなる状態になりました。ウキはB、針はグレ5号の銀色の光沢タイプ。2ヒロのハリスにガン玉G4とG6を打っていました。たまに木っ端がかかるようになったので、針をグレ6号の光沢無しタイプに変更し、G6ガン玉を針のチモト付近に付けた仕掛けにしたところ、20-25cmがたまにかかるようになりました。
ウネリはほとんどおさまらず、下げ4-5分の時刻頃にはかなり潮が速くなりました。そこで、向かい風に煽られて手前の岩に引っ掛かってウキをロストしたのをきっかけに、2Bウキに変更しました。この場所では、たまに混じるオナガが仕掛けを一気に持っていくので、結構楽しい釣りとなりました。
午後になり先端の石鯛師が帰ったので、その場所の右の部分に移動しました。ちなみにこの石鯛師、2本竿の投げ仕掛けの方で、午前中良型のイシガキダイを1枚上げてました。ここはエサ取りがあまりいないように感じました。イスズミ(2枚目の写真)とメジナの2枚を釣り、最後に釣ったメジナ28cmが本日の最大サイズとなりました。イスズミを釣ったのは初めてだったんですが、結構な引きでびっくり。S.M氏が言うには東伊豆ではたまに釣れるのだそうです。
メジナ30cm越えはなりませんでしたが、オナガ混じりの楽しい釣りでした。最後の写真の左5枚がクチブト、右4枚がオナガです。
<本日の釣果>
クチブトメジナ:28cm 1枚、25cm 1枚、21-24cm 3枚
オナガメジナ:20cm-24cm 4枚
木っ端:数枚(全てリリース)
<お魚調理:現場で血抜きは必須ですよ>
塩焼き(長男好評)・醤油とミリン漬け(これと甘めの梅干しを添えたお茶漬けが好きなんです)
**********
日 出 6時50分
正 中 11時45分
日 没 16時40分
潮回り 中潮
6時59分 147cm
11時57分 96cm
17時 8分 152cm
市販の猫ドアの穴は最低でも高さ20cm程度でしたがうちのドアでは15cm程度になりました。下記の通り計画。
ま、こんな感じです。
ガラスが嵌っていることと、ドアの最下部を切ってしまうと水平に入っている枠を切断する事になるので、強度上問題だと思い、枠の高さ半分ぐらいは残しました。これにより高さは15cmとなりました。
天井のラワン材の厚みを差し引くとトンネルの高さは13.7cmということになります。
2枚のべニアのみのところは簡単に切れましたが、株の枠部は大変力仕事でした。厚みが32mmあるので。
カットした5mm厚のアクリル板に長さ8mmのネジとナットにより蝶番を取り付け、ゲートの天井に蝶番の片方を木ネジでねじ込みました。蝶番は小型のものを2個使いましたが、少し長い1個でも良かったかもしれません。2個あると微妙に平行がずれると開閉が固くなってしまうからです。ネジの1か所を少し緩めることでほぼ抵抗なく開閉できるように調整しました。
このゲートの片側(蝶番を取り付けてない側)に茶色のモールを木ネジで取り付けて穴にはめ込んで、モール側から見た写真が2枚目、モールの反対側からみたのが3枚目の写真です。
ネジで取り付けたモールの反対側は両面テープで同じモールを貼りました。
4枚目の写真がうちのオス猫のレン、5枚目はメス猫のルリです。レンは頭がいいのでさっそく潜ってくれましたが、ルリはいまだに潜ってくれません。。。
穴の高さが小さいので窮屈だからかなあ。。でもかわいいでしょ!
]]>