www.mtkn.jp

Manuscripts for my personal webpage.
git clone https://git.mtkn.jp/www.mtkn.jp
Log | Files | Refs | LICENSE

commit 4fd4b8a714791627f4f7838a93a06df75d6c761c
parent a750068a24cd37566376260d42810d47b979d650
Author: Matsuda Kenji <info@mtkn.jp>
Date:   Wed, 18 Dec 2024 20:55:26 +0900

divide 9p article

Diffstat:
Mdata/weblog | 6++++++
Aman/computer/9p.html | 489+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mman/computer/index.html | 7+++++++
Dman/draft/9p.html | 872-------------------------------------------------------------------------------
Aman/draft/9p_go.html | 385+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apub/computer/9p.html | 319+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpub/computer/index.html | 7+++++++
Dpub/draft/9p.html | 618-------------------------------------------------------------------------------
Apub/draft/9p_go.html | 334+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpub/index.html | 7-------
Mpub/rss.xml | 297++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpub/sitemap.xml | 3++-
12 files changed, 1844 insertions(+), 1500 deletions(-)

diff --git a/data/weblog b/data/weblog @@ -218,3 +218,9 @@ 1733842800 /journal/posts/20241211.html 1733842800 /journal/index.html 1733842800 /journal/posts/20241211.html +1733842800 /index.html +1734447600 /computer/index.html +1734447600 /computer/9p.html +1734447600 /computer/9p.html +1734447600 /computer/9p.html +1734447600 /computer/9p.html diff --git a/man/computer/9p.html b/man/computer/9p.html @@ -0,0 +1,489 @@ +<h1>9P</h1> +<time>2024-10-27</time> +<h2>はじめに</h2> +<p> +9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。\ +このプロトコルを使えば、ディスクに記録されたデータだけでなく、\ +マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等も\ +ファイルとして扱える。\ +Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOS\ +のために設計された。 +</p> + +<p> +Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計\ +されているので、\ +この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。\ +</p> + +<p> +便利そうなのでGo言語で実装してみた。 +</p> + +<h2>9Pプロトコル</h2> +<h3>概要</h3> +<p> +9Pの通信ではクライアントがサーバーに対してリクエストを送り、サーバーがそれに\ +対してリプライを返すことを繰り返す。\ +クライアントからのリクエストをTメッセージ、サーバーからのリプライをRメッセージと\ +言う。\ +Tメッセージにはファイルにアクセスするための色々なものが用意されている。\ +例えばファイルを開くためのTopenや、\ +開いたファイルに書き込むためのTwrite等である。\ +それぞれのTメッセージには対応するRメッセージがある。\ +Topenに対してRopen、\ +Twriteに対してRwrite等である。\ +</p> +<p> +通信は必要な情報をプロトコルの定める方法でバイト列に変換して行う。\ +2バイト以上のデータはリトルエンディアンになるように配置する。\ +テキストデータはUTF-8にエンコードする。\ +テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。\ +</p> +<p> +各メッセージは4バイトの整数から始まる。\ +これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。\ +次にメッセージの種類を記述する1バイトのデータが来る。\ +次はメッセージのタグである。クライアントはサーバーからの返事を\ +待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージ\ +に対する返事なのかを識別する必要がある。そのために使うのがタグである。\ +クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに\ +同じタグを付ける。\ +その後ろには、メッセージの種類によって固有の情報が付けられる。\ +</p> +<p> +以下は実際の9P通信のログである。\ +実際はバイナリの通信だが、ログとして見易いように変換してある。\ +また、先頭にくるメッセージサイズは省略してある。\ +この例では、サーバーのルートディレクトリにある<code>hello</code>という\ +ファイルの内容を読みこんでいる。:\ +</p> +<pre><code>\ +&lt;-- Tversion Tag 65535 msize 8192 version '9P2000' +--&gt; Rversion Tag 65535 msize 8192 version '9P2000' +&lt;-- Tattach Tag 0 fid 0 afid -1 uname kenji aname +--&gt; Rattach Tag 0 qid (0000000000000000 0 d) +&lt;-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello +--&gt; Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 ) +&lt;-- Tstat Tag 0 fid 1 +--&gt; Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0 +&lt;-- Twalk Tag 0 fid 1 newfid 2 nwname 0 +--&gt; Rwalk Tag 0 nwqid 0 +&lt;-- Topen Tag 0 fid 2 mode 0x0 +--&gt; Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169 +&lt;-- Tread Tag 0 fid 2 offset 0 count 4096 +--&gt; Rread Tag 0 count 7 ' 776f726c 64210a' +&lt;-- Tread Tag 0 fid 2 offset 7 count 4096 +--&gt; Rread Tag 0 count 0 '' +&lt;-- Tclunk Tag 0 fid 2 +--&gt; Rclunk Tag 0 +</code></pre> +<p> +まず最初にクライアントがサーバーに対してTversionを送り、\ +プロトコルのバージョンと、メッセージの最大サイズを交渉する。\ +Tversionのタグは<code>65535</code>と決められている。\ +<code>msize</code>の8192はメッセージの最大サイズ(バイト)で、\ +<code>version</code>の9P2000がプロトコルのバージョンである。\ +サーバーからRversionで同じ\ +<code>msize</code>と<code>version</code>が返ってきたので\ +このサイズとバージョンで以降の通信を行う。\ +</p> +<p> +次のTattachで、クライアントがサーバーにルートディレクトリの\ +<code>fid</code>を要求する。\ +<code>afid</code>以降は認証用の情報である(後述)。\ +<code>fid</code>はコネクションにおいてファイルと紐付けられる整数で、\ +Unixにおけるファイルディスクリプタのようなものである。\ +ファイルの読み書きはこの<code>fid</code>を使って行われる。\ +サーバーからの<code>Rattach</code>にはルートディレクトリの\ +<code>qid</code>が含まれる。\ +これはサーバー上でファイルを一意に識別するもので、\ +Unixにおけるinodeに相当するものである。\ +以上でサーバーに繋いでセッションを確立できた。\ +</p> +<p> +次にTwalkで目的のファイル(<code>hello</code>)まで\ +ファイルツリーを辿る。\ +ここでは起点として先程得られたルートディレクトリ(<code>fid = 1</code>)\ +を起点として、このディレクトリ内の<code>hello</code>ファイルを要求して\ +<code>fid = 2</code>を割り当てようとしている。\ +サーバーはルートディレクトリ内に要求されたファイルが見付かったので、\ +そのファイルの<code>qid</code>を返す。\ +</p> +<p> +目的のファイルに<code>fid</code>が割り当てられたので、\ +Tstatでこのファイルの属性を要求している。\ +おそらくファイルのパーミッションを調べるためである。\ +</p> +<p> +次にTopenを使ってこのファイルを開き、\ +Treadを使って内容を読む。\ +二回目のTreadに対して0バイトのデータが返ってきたので、\ +ファイルはこれで終りである。\ +</p> +<p> +最後に、必要なくなった<code>fid</code>はTclunkで捨てる。\ +</p> + +<h3>List of all Message Types</h3> + +<table> +<tr> +<th>Tメッセージ</th> +<th>Rメッセージ</th> +<th>説明</th> +</tr> +<tr> +<td>Tversion</td> +<td>Rversion</td> +<td>バージョンの交渉</td> +</tr> +<tr> +<td>Tauth</td> +<td>Rauth</td> +<td>認証ファイルの取得</td> +</tr> +<tr> +<td></td> +<td>Rerror</td> +<td>エラー(Rメッセージのみ)</td> +</tr> +<tr> +<td>Tflush</td> +<td>Rflush</td> +<td>処理中のリクエストをキャンセル</td> +</tr> +<tr> +<td>Tattach</td> +<td>Rattach</td> +<td>ルートディレクトリの取得</td> +</tr> +<tr> +<td>Twalk</td> +<td>Rwalk</td> +<td>ファイルツリーを辿る</td> +</tr> +<tr> +<td>Topen</td> +<td>Ropen</td> +<td>ファイルを開く</td> +</tr> +<tr> +<td>Tcreate</td> +<td>Rcreate</td> +<td>ファイルの作成</td> +</tr> +<tr> +<td>Tread</td> +<td>Rread</td> +<td>ファイルを読む</td> +</tr> +<tr> +<td>Twrite</td> +<td>Rwrite</td> +<td>ファイルへ書き込む</td> +</tr> +<tr> +<td>Tclunk</td> +<td>Rclunk</td> +<td>fidの削除</td> +</tr> +<tr> +<td>Tremove</td> +<td>Rremove</td> +<td>ファイルを削除</td> +</tr> +<tr> +<td>Tstat</td> +<td>Rstat</td> +<td>ファイルの属性を取得</td> +</tr> +<tr> +<td>Twstat</td> +<td>Rwstat</td> +<td>ファイルの属性を変更</td> +</tr> +</table> + +<h2><code>fid</code>と<code>qid</code></h2> +<p> +<code>fid</code>はコネクションにおいてファイルを指定するために\ +使われる整数で、Unixのファイルディスクリプタのようなものである。\ +使用する整数はクライアントが指定できる。\ +サーバーに接続する際にTattachにより、サーバーの\ +ルートディレクトリの<code>fid</code>が設定され、\ +Twalkにより追加され、Tclunkにより削除される。\ +</p> +<p> +<code>qid</code>はサーバーがファイルを一意に識別するためのもので、\ +Unixのinodeに相当するものである。\ +ただしinodeはファイルが削除されると再利用される可能性があるのに対し、\ +<code>qid</code>は再利用できない。\ +<code>qid</code>はこのファイルがディレクトリかどうか等を示す<code>type</code>、\ +ファイルが変更されたときにインクリメントされる<code>vers</code>、\ +そしてファイルを一意に表す<code>path</code>の3つの情報で構成される。\ +</p> +<h2>認証</h2> +<p> +9Pにはクライアントの認証が組込まれていない。\ +もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすれば\ +いちいちコードを修正してコンパイルしなおさなければいけないうえ、\ +9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。\ +認証に必要なやりとりは認証サーバーとクライアントが行い、\ +9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することで\ +クライアントの確認を行う。 +</p> +<p> +9PサーバーはTattachメッセージを受けとると、\ +認証サーバーとのコネクションを確立する。\ +その後<code>Rattach</code>メッセージで<code>afid</code>という\ +特殊なFidをクライアントに返す。\ +クライアントからこの<code>afid</code>に対して読み書きする命令が届くと、\ +9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。\ +つまりクライアントはこの<code>afid</code>を通じて認証サーバーと\ +直接やりとりができる。\ +クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報を\ +<code>afid</code>に書き込んで認証する。\ +このように認証をプロトコル自体から切り離すことで、\ +認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。\ +また、より強力な認証システムを組み込むのも楽である。 +</p> + +<h2>コネクションの共有</h2> +<p> +クライアントはサーバーと<code>version</code>メッセージを交すことで\ +コネクションを確立する。\ +コネクションの確立からの一連のやりとりをセッションという。\ +ひとつのコネクションは複数のクライアントが共有できる。\ +</p> +<p> +クライアントはTauthまたはTattachメッセージにより\ +サーバーに<code>fid</code>を要求できる。\ +このうちTauthにより得られた<code>fid</code>は認証以外には使えない。\ +Tattachで得られた<code>fid</code>は、\ +Twalkメッセージによりファイルツリーを辿るための起点として利用でき、\ +このとき辿った先のファイルを示す<code>fid</code>が生成される。\ +これ以外の方法で<code>fid</code>が増えることはない。\ +つまり、サーバーはひとつのコネクションのなかで、から\ +派生した<code>fid</code>を追跡することで、クライアントを区別できる。\ +</p> + +<h2>メッセージ詳細</h2> +<p> +各メッセージの詳細を書く。\ +それぞれ最初にメッセージの形式を書く。\ +<code><i>field</i>[<i>n</i>]</code>は<code><i>field</i></code>\ +という名前の<i>n</i>バイトのデータを表す(<i>n</i>は整数)。\ +また、括弧の中が整数ではなくsになっている場合、そのフィールドは\ +文字列のデータで、その前に文字列の長さを示す2バイトのデータが先行する。\ +</p> +<h3>version</h3> +<p> +プロトコルのバージョンを交渉すし、コネクションを確立する。 +</p> +<pre><code>\ +size[4] Tversion tag[2] msize[4] version[s] +size[4] Rversion tag[2] msize[4] version[s] +</code></pre> +<p> +クライアントは最初にサーバーにこのメッセージを送ってバージョンと\ +メッセージの最大サイズ(<code>msize</code>)を交渉する。\ +サーバーはクライアントから送られたバージョンとメッセージサイズに\ +対応していれば、同じバージョンとサイズを送り返し、交渉成立である。\ +</p> +<p> +サーバーとクライアントはこのメッセージを以ってコネクションを確立する。\ +</p> + +<h3>attach、auth</h3> +<p> +コネクションを確立する。 +</p> +<pre><code>\ +size[4] Tauth tag[2] afid[4] uname[s] aname[s] +size[4] Rauth tag[2] aqid[13] + +size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] +size[4] Rattach tag[2] qid[13] +</code></pre> +<p> +<code>attach</code>メッセージはサーバーのルートディレクトリを要求し、\ +<code>fid</code>を割り当てる。\ +認証が必要なサーバーであれば、\ +先に<code>auth</code>メッセージで認証を済ませておき、\ +<code>attach</code>メッセージの<code>afid</code>に、\ +先程使用した<code>afid</code>をセットする。\ +サーバーはこの<cdoe>afid</code>に紐付いている認証サーバーに\ +クライアントが認証済みであるかを確認し、\ +認証済みであればルートディレクトリの<code>qid</code>を返す。\ +サーバーが複数のファイルツリーを公開している場合、\ +<code>aname</code>で指定する。\ +</p> +<p> +<code>auth</code>メッセージは認証サーバーとやりとりするための\ +<code>fid</code>である<code>afid</code>を要求する。\ +この<code>afid</code>を読み書きすることで\ +認証サーバーとやりとりをし、自身を身分を証明する。\ +認証が済んだらこの<code>afid</code>をセットした\ +<code>attach</code>メッセージを送る。\ +</p> + +<h3>error</h3> +<p> +エラーを返す。 +</p> +<pre><code>\ +size[4] Rerror tag[2] ename[s] +</code></pre> +<p> +クライアントからの要求に対して、なにかエラーがおきたときに\ +そのエラーを返すために使われる。\ +そのためサーバーからクライアントへの<code>Rerror</code>はあるが\ +逆向きのTメッセージはない。\ +</p> + +<h3>flush</h3> +<p> +リクエストをキャンセルする。 +</p> +<pre><code>\ +size[4] Tflush tag[2] oldtag[2] +size[4] Rflush tag[2] +</code></pre> +<p> +実行中の要求をキャンセルする。\ +キャンセルしたいリクエストの<code>tag</code>を<code>oldtag</code>にセットする。 +</p> + +<h3>walk</h3> +<p> +ファイルツリーを移動する。 +</p> +<pre><code>\ +size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) +size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13]) +</code></pre> +<p> +<code>fid</code>を起点に移動する。\ +移動先のファイルを<code>newfid</code>にセットする。\ +移動するディレクトリの数を<code>nwname</code>に、\ +移動するディレクトリの名前を移動する順に<code>wname</code>に\ +それぞれセットする。\ +例えばカレントディレクトリから<code>./dir1/dir2/</code>に\ +移動する場合、<code>nwname</code>は<code>2</code>、<code>wname</code>は\ +<code>{"dir1", "dir2"}</code>となる。\ +<code>wname</code>の最後の要素はディレクトリでなくファイルでもいい。\ +</p> +<p> +<code>wname</code>の最初の要素への移動が失敗した場合は\ +<code>Rerror</code>が返される。\ +それ以外の場合は<code>Rwalk</code>が返され、\ +<code>qid</code>は移動が成功した順番にそのファイルの<code>qid</code>\ +がセットされる。\ +<code>nwname</code>と<code>nwqid</code>が一致した場合は\ +最後まで移動できたことになる。\ +</p> +<p> +<code>walk</code>は<code>fid</code>を増殖させる唯一の方法である。\ +</p> + +<h3>open、create</h3> +<p> +<code>fid</code>を開いて(作成して)読み書きできる状態にする。 +</p> +<pre><code>\ +size[4] Topen tag[2] fid[4] mode[1] +size[4] Ropen tag[2] qid[13] iounit[4] + +size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] +size[4] Rcreate tag[2] qid[13] iounit[4] +</code></pre> +<p> +<code>open</code>は<code>fid</code>と紐付いたファイルを開く。\ +<code>mode</code>は読み込み専用の<code>OREAD</code>、\ +書き込み専用の<code>OWRITE</code>、\ +読み書きの<code>ORDWR</code>等である。\ +ファイルを開く手順は以下の通り: +<ol> +<li><code>attach</code>でルートディレクトリの<code>fid</code>を取得</li> +<li>ルートディレクトリの<code>fid</code>から\ +<code>walk</code>で開きたいファイルの<code>fid</code>を取得</li> +<li>その<code>fid</code>を<code>open</code>で開く</li> +</ol> +</p> +<p> +<code>create</code>は<code>fid</code>と紐付いたディレクトリに\ +新しいファイルを作成する。\ +<code>creat</code>ではなく<code>create</code>である。\ +作成が成功したら<code>fid</code>は新しく作成されたファイルに紐付く。\ +</p> + +<h3>read、write</h3> +<p> +ファイルを読み書きする。 +</p> +<pre><code>\ +size[4] Tread tag[2] fid[4] offset[8] count[4] +size[4] Rread tag[2] count[4] data[count] + +size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] +size[4] Rwrite tag[2] count[4] +</code></pre> +<p> +<code>read</code>は<code>fid</code>の<code>offset</code>バイト目\ +から<code>count</code>バイト読む。\ +読んだデータと、読めたデータサイズが\ +<code>data</code>、<code>count</code>ととして返される。\ +<code>write</code>は<code>fid</code>の<code>offset</code>バイト目\ +に<code>count</code>バイトのデータ<code>data</code>を書く。\ +書きこめたサイズが<code>count</code>として返される。\ +ファイルを読み書きするためには\ +あらかじめ<code>fid</code>を<code>open</code>する必要がある。\ +</p> + +<h3>clunk</h3> +<p> +<code>fid</code>を忘れる。 +</p> +<pre><code>\ +size[4] Tclunk tag[2] fid[4] +size[4] Rclunk tag[2] +</code></pre> +<p> +いらなくなった<code>fid</code>を忘れる。\ +一度忘れた<code>fid</code>は<code>walk</code>で別のファイルを取得する際に\ +再利用できる。\ +</p> + +<h3>remove</h3> +<p> +ファイルを削除する。 +</p> +<pre><code>\ +size[4] Tremove tag[2] fid[4] +size[4] Rremove tag[2] +</code></pre> +<p> +<code>fid</code>と紐付いたサーバー上のファイルを削除する。\ +<code>fid</code>は<code>clunk</code>したのと同様に忘れられる。\ +</p> + +<h3>stat、wstat</h3> +<p> +ファイルの属性を読み書きする。 +</p> +<pre><code>\ +size[4] Tstat tag[2] fid[4] +size[4] Rstat tag[2] stat[n] + +size[4] Twstat tag[2] fid[4] stat[n] +size[4] Rwstat tag[2] +</code></pre> +<p> +<code>stat</code>はファイルの属性を読む。\ +読めた情報は<code>stat</code>として返される。\ +<code>wstat</code>はファイルの属性を<code>stat</code>に変更する。\ +<code>stat</code>はファイルの名前や<code>qid</code>、サイズ、\ +作成日、更新日等の情報がバイト列になったものである。\ +</p> + diff --git a/man/computer/index.html b/man/computer/index.html @@ -2,11 +2,18 @@ <p> Small is beautiful. </p> + +<h2>9P in Go</h2> +<ul> +<li><a href="9p.html">9P</a></li> +</ul> + <h2>RP2040</h2> <ul> <li><a href="rp2040_1.html">RP2040 SDKなしでLチカ</a></li> <li><a href="rp2040_2.html">RP2040 SDKなし2 Clock、UART</a></li> </ul> + <h2>Xlib</h2> <ul> <li><a href="xlib_playground1.html">Xlibで遊んでみる1</a></li> diff --git a/man/draft/9p.html b/man/draft/9p.html @@ -1,872 +0,0 @@ -<h1>9P</h1> -<time>2024-10-27</time> -<h2>はじめに</h2> -<p> -9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。\ -このプロトコルを使えば、ディスクに記録されたデータだけでなく、\ -マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等も\ -ファイルとして扱える。\ -Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOS\ -のためのプロトコルである。 -</p> - -<p> -Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計\ -されているので、\ -この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。\ -</p> - -<p> -便利そうなのでGo言語で実装してみた。 -</p> - -<h2>9Pプロトコル</h2> -<h3>概要</h3> -<p> -9Pの通信ではクライアントがサーバーに対してリクエストを送り、サーバーがそれに\ -対してリプライを返すことを繰り返す。\ -クライアントからのリクエストをTメッセージ、サーバーからのリプライをRメッセージと\ -言う。\ -Tメッセージにはファイルにアクセスするための色々なものが用意されている。\ -例えばファイルを開くための<code>Topen</code>や、\ -開いたファイルに書き込むための<code>Twrite</code>等である。\ -それぞれのTメッセージには対応するRメッセージがある。\ -<code>Topen</code>に対して<code>Ropen</code>、\ -<code>Twrite</code>に対して<code>Rwrite</code>等である。\ -</p> -<p> -通信は必要な情報をプロトコルの定める方法でバイト列に変換して送受信する。\ -2バイト以上のデータはリトルエンディアンになるように配置する。\ -テキストデータはUTF-8にエンコードする。\ -テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。\ -</p> -<p> -各メッセージは4バイトのサイズから始まる。\ -これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。\ -次にメッセージの種類を記述する1バイトのデータが来る。\ -次はメッセージのタグである。クライアントはサーバーからの返事を\ -待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージ\ -に対する返事なのかを識別する必要がある。そのために使うのがタグである。\ -クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに\ -同じタグを付ける。\ -その後ろには、メッセージの種類によって固有の情報が付けられる。\ -</p> -<p> -以下は実際の9P通信のログである。\ -実際はバイナリの通信だが、ログとして見易いように変換してある。\ -また、先頭にくるメッセージサイズは省略してある。\ -この例では、サーバーのルートディレクトリにある<code>hello</code>という\ -ファイルの内容を読みこんでいる。:\ -</p> -<pre><code>\ -&lt;-- Tversion Tag 65535 msize 8192 version '9P2000' ---&gt; Rversion Tag 65535 msize 8192 version '9P2000' -&lt;-- Tattach Tag 0 fid 0 afid -1 uname kenji aname ---&gt; Rattach Tag 0 qid (0000000000000000 0 d) -&lt;-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello ---&gt; Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 ) -&lt;-- Tstat Tag 0 fid 1 ---&gt; Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0 -&lt;-- Twalk Tag 0 fid 1 newfid 2 nwname 0 ---&gt; Rwalk Tag 0 nwqid 0 -&lt;-- Topen Tag 0 fid 2 mode 0x0 ---&gt; Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169 -&lt;-- Tread Tag 0 fid 2 offset 0 count 4096 ---&gt; Rread Tag 0 count 7 ' 776f726c 64210a' -&lt;-- Tread Tag 0 fid 2 offset 7 count 4096 ---&gt; Rread Tag 0 count 0 '' -&lt;-- Tclunk Tag 0 fid 2 ---&gt; Rclunk Tag 0 -</code></pre> -<p> -まず最初にクライアントがサーバーに対して<code>Tversion</code>を送り、\ -プロトコルのバージョンと、メッセージの最大サイズを交渉する。\ -<code>Tversion</code>のタグは<code>65535</code>と決められている。\ -<code>msize</code>の8192はメッセージの最大サイズ(バイト)で、\ -<code>version</code>の9P2000がプロトコルのバージョンである。\ -サーバーから<code>Rversion</code>で同じ\ -<code>msize</code>と<code>version</code>が返ってきたので\ -このサイズとバージョンで以降の通信を行う。\ -</p> -<p> -次の<code>Tattach</code>で、クライアントがサーバーにルートディレクトリの\ -<code>fid</code>を要求する。\ -<code>afid</code>以降は認証用の情報である(後述)。\ -<code>fid</code>はコネクションにおいてファイルと紐付けられる整数で、\ -Unixにおけるファイルディスクリプタのようなものである。\ -ファイルの読み書きはこの<code>fid</code>を使って行われる。\ -サーバーからの<code>Rattach</code>にはルートディレクトリの\ -<code>qid</code>が含まれる。\ -これはサーバー上でファイルを一意に識別するもので、\ -Unixにおけるinodeに相当するものである。\ -以上でサーバーに繋いでセッションを確立できた。\ -</p> -<p> -次に<code>Twalk</code>で目的のファイル(<code>hello</code>)まで\ -ファイルツリーを辿る。\ -ここでは起点として先程得られたルートディレクトリ(<code>fid = 1</code>)\ -を起点として、このディレクトリ内の<code>hello</code>ファイルを要求して\ -<code>fid = 2</code>を割り当てようとしている。\ -サーバーはルートディレクトリ内に要求されたファイルが見付かったので、\ -そのファイルの<code>qid</code>を返す。\ -</p> -<p> -目的のファイルに<code>fid</code>が割り当てられたので、\ -<code>Tstat</code>でこのファイルの属性を要求している。\ -おそらくファイルのパーミッションを調べるためである。\ -</p> -<p> -次に<code>Topen</code>を使ってこのファイルを開き、\ -<code>Tread</code>を使って内容を読む。\ -二回目の<code>Tread</code>に対して0バイトのデータが返ってきたので、\ -ファイルはこれで終りである。\ -</p> -<p> -最後に、必要なくなった<code>fid</code>は<code>Tclunk</code>で捨てる。\ -</p> - -<h3>List of all Message Types</h3> - -<table> -<tr> -<th>Tメッセージ</th> -<th>Rメッセージ</th> -<th>説明</th> -</tr> -<tr> -<td>Tversion</td> -<td>Rversion</td> -<td>バージョンの交渉</td> -</tr> -<tr> -<td>Tauth</td> -<td>Rauth</td> -<td>認証ファイルの取得</td> -</tr> -<tr> -<td></td> -<td>Rerror</td> -<td>エラー(Rメッセージのみ)</td> -</tr> -<tr> -<td>Tflush</td> -<td>Rflush</td> -<td>処理中のリクエストをキャンセル</td> -</tr> -<tr> -<td>Tattach</td> -<td>Rattach</td> -<td>ルートディレクトリの取得</td> -</tr> -<tr> -<td>Twalk</td> -<td>Rwalk</td> -<td>ファイルツリーを辿る</td> -</tr> -<tr> -<td>Topen</td> -<td>Ropen</td> -<td>ファイルを開く</td> -</tr> -<tr> -<td>Tcreate</td> -<td>Rcreate</td> -<td>ファイルの作成</td> -</tr> -<tr> -<td>Tread</td> -<td>Rread</td> -<td>ファイルを読む</td> -</tr> -<tr> -<td>Twrite</td> -<td>Rwrite</td> -<td>ファイルへ書き込む</td> -</tr> -<tr> -<td>Tclunk</td> -<td>Rclunk</td> -<td>fidの削除</td> -</tr> -<tr> -<td>Tremove</td> -<td>Rremove</td> -<td>ファイルを削除</td> -</tr> -<tr> -<td>Tstat</td> -<td>Rstat</td> -<td>ファイルの属性を取得</td> -</tr> -<tr> -<td>Twstat</td> -<td>Rwstat</td> -<td>ファイルの属性を変更</td> -</tr> -</table> - -<h2><code>fid</code>と<code>qid</code></h2> -<p> -<code>fid</code>はコネクションにおいてファイルを指定するために\ -使われる整数で、Unixのファイルディスクリプタのようなものである。\ -使用する整数はクライアントが指定できる。\ -サーバーに接続する際に<code>Tattach</code>により、サーバーの\ -ルートディレクトリの<code>fid</code>が設定され、\ -<code>Twalk</code>により追加され、<code>Tclunk</code>により削除される。\ -</p> -<p> -<code>qid</code>はサーバーがファイルを一意に識別するためのもので、\ -Unixのinodeに相当するものである。\ -ただしinodeはファイルが削除されると再利用される可能性があるのに対し、\ -<code>qid</code>は再利用できない。\ -<code>qid</code>はこのファイルがディレクトリかどうか等を示す<code>type</code>、\ -ファイルが変更されたときにインクリメントされる<code>vers</code>、\ -そしてファイルを一意に表す<code>path</code>の3つの情報で構成される。\ -</p> -<h2>認証</h2> -<p> -9Pにはクライアントの認証が組込まれていない。\ -もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすれば\ -いちいちコードを修正してコンパイルしなおさなければいけないうえ、\ -9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。\ -認証に必要なやりとりは認証サーバーとクライアントが行い、\ -9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することで\ -クライアントの確認を行う。 -</p> -<p> -9Pサーバーは<code>Tattach</code>メッセージを受けとると、\ -認証サーバーとのコネクションを確立する。\ -その後<code>Rattach</code>メッセージで<code>afid</code>という\ -特殊なFidをクライアントに返す。\ -クライアントからこの<code>afid</code>に対して読み書きする命令が届くと、\ -9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。\ -つまりクライアントはこの<code>afid</code>を通じて認証サーバーと\ -直接やりとりができる。\ -クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報を\ -<code>afid</code>に書き込んで認証する。\ -このように認証をプロトコル自体から切り離すことで、\ -認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。\ -また、より強力な認証システムを組み込むのも楽である。 -</p> - -<h2>コネクションの共有</h2> -<p> -クライアントはサーバーと<code>version</code>メッセージを交すことで\ -コネクションを確立する。\ -コネクションの確立からの一連のやりとりをセッションという。\ -ひとつのコネクションは複数のクライアントが共有できる。\ -</p> -<p> -クライアントは<code>Tauth</code>または<code>Tattach</code>メッセージにより\ -サーバーに<code>fid</code>を要求できる。\ -このうち<code>Tauth</code>により得られた<code>fid</code>は認証以外には使えない。\ -<code>Tattach</code>で得られた<code>fid</code>は、\ -<code>Twalk</code>メッセージによりファイルツリーを辿るための起点として利用でき、\ -このとき辿った先のファイルを示す<code>fid</code>が生成される。\ -これ以外の方法で<code>fid</code>が増えることはない。\ -つまり、サーバーはひとつのコネクションのなかで、<code>Tattach</code>から\ -派生した<code>fid</code>を追跡することで、クライアントを区別できる。\ -</p> - -<h2>メッセージ詳細</h2> -<p> -各メッセージの詳細を書く。\ -それぞれ最初にメッセージの形式を書く。\ -<code><i>field</i>[<i>n</i>]</code>は<code><i>field</i></code>\ -という名前の<i>n</i>バイトのデータを表す(<i>n</i>は整数)。\ -また、括弧の中が整数ではなくsになっている場合、そのフィールドは\ -文字列のデータで、その前に文字列の長さを示す2バイトのデータが先行する。\ -</p> -<h3>version</h3> -<p> -プロトコルのバージョンを交渉すし、コネクションを確立する。 -</p> -<pre><code>\ -size[4] Tversion tag[2] msize[4] version[s] -size[4] Rversion tag[2] msize[4] version[s] -</code></pre> -<p> -クライアントは最初にサーバーにこのメッセージを送ってバージョンと\ -メッセージの最大サイズ(<code>msize</code>)を交渉する。\ -サーバーはクライアントから送られたバージョンとメッセージサイズに\ -対応していれば、同じバージョンとサイズを送り返し、交渉成立である。\ -</p> -<p> -サーバーとクライアントはこのメッセージを以ってコネクションを確立する。\ -</p> - -<h3>attach、auth</h3> -<p> -コネクションを確立する。 -</p> -<pre><code>\ -size[4] Tauth tag[2] afid[4] uname[s] aname[s] -size[4] Rauth tag[2] aqid[13] - -size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] -size[4] Rattach tag[2] qid[13] -</code></pre> -<p> -<code>attach</code>メッセージはサーバーのルートディレクトリを要求し、\ -<code>fid</code>を割り当てる。\ -認証が必要なサーバーであれば、\ -先に<code>auth</code>メッセージで認証を済ませておき、\ -<code>attach</code>メッセージの<code>afid</code>に、\ -先程使用した<code>afid</code>をセットする。\ -サーバーはこの<cdoe>afid</code>に紐付いている認証サーバーに\ -クライアントが認証済みであるかを確認し、\ -認証済みであればルートディレクトリの<code>qid</code>を返す。\ -サーバーが複数のファイルツリーを公開している場合、\ -<code>aname</code>で指定する。\ -</p> -<p> -<code>auth</code>メッセージは認証サーバーとやりとりするための\ -<code>fid</code>である<code>afid</code>を要求する。\ -この<code>afid</code>を読み書きすることで\ -認証サーバーとやりとりをし、自身を身分を証明する。\ -認証が済んだらこの<code>afid</code>をセットした\ -<code>attach</code>メッセージを送る。\ -</p> - -<h3>error</h3> -<p> -エラーを返す。 -</p> -<pre><code>\ -size[4] Rerror tag[2] ename[s] -</code></pre> -<p> -クライアントからの要求に対して、なにかエラーがおきたときに\ -そのエラーを返すために使われる。\ -そのためサーバーからクライアントへの<code>Rerror</code>はあるが\ -逆向きのTメッセージはない。\ -</p> - -<h3>flush</h3> -<p> -リクエストをキャンセルする。 -</p> -<pre><code>\ -size[4] Tflush tag[2] oldtag[2] -size[4] Rflush tag[2] -</code></pre> -<p> -実行中の要求をキャンセルする。\ -キャンセルしたいリクエストの<code>tag</code>を<code>oldtag</code>にセットする。 -</p> - -<h3>walk</h3> -<p> -ファイルツリーを移動する。 -</p> -<pre><code>\ -size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) -size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13]) -</code></pre> -<p> -<code>fid</code>を起点に移動する。\ -移動先のファイルを<code>newfid</code>にセットする。\ -移動するディレクトリの数を<code>nwname</code>に、\ -移動するディレクトリの名前を移動する順に<code>wname</code>に\ -それぞれセットする。\ -例えばカレントディレクトリから<code>./dir1/dir2/</code>に\ -移動する場合、<code>nwname</code>は<code>2</code>、<code>wname</code>は\ -<code>{"dir1", "dir2"}</code>となる。\ -<code>wname</code>の最後の要素はディレクトリでなくファイルでもいい。\ -</p> -<p> -<code>wname</code>の最初の要素への移動が失敗した場合は\ -<code>Rerror</code>が返される。\ -それ以外の場合は<code>Rwalk</code>が返され、\ -<code>qid</code>は移動が成功した順番にそのファイルの<code>qid</code>\ -がセットされる。\ -<code>nwname</code>と<code>nwqid</code>が一致した場合は\ -最後まで移動できたことになる。\ -</p> -<p> -<code>walk</code>は<code>fid</code>を増殖させる唯一の方法である。\ -</p> - -<h3>open、create</h3> -<p> -<code>fid</code>を開いて(作成して)読み書きできる状態にする。 -</p> -<pre><code>\ -size[4] Topen tag[2] fid[4] mode[1] -size[4] Ropen tag[2] qid[13] iounit[4] - -size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] -size[4] Rcreate tag[2] qid[13] iounit[4] -</code></pre> -<p> -<code>open</code>は<code>fid</code>と紐付いたファイルを開く。\ -<code>mode</code>は読み込み専用の<code>OREAD</code>、\ -書き込み専用の<code>OWRITE</code>、\ -読み書きの<code>ORDWR</code>等である。\ -ファイルを開く手順は以下の通り: -<ol> -<li><code>attach</code>でルートディレクトリの<code>fid</code>を取得</li> -<li>ルートディレクトリの<code>fid</code>から\ -<code>walk</code>で開きたいファイルの<code>fid</code>を取得</li> -<li>その<code>fid</code>を<code>open</code>で開く</li> -</ol> -</p> -<p> -<code>create</code>は<code>fid</code>と紐付いたディレクトリに\ -新しいファイルを作成する。\ -<code>creat</code>ではなく<code>create</code>である。\ -作成が成功したら<code>fid</code>は新しく作成されたファイルに紐付く。\ -</p> - -<h3>read、write</h3> -<p> -ファイルを読み書きする。 -</p> -<pre><code>\ -size[4] Tread tag[2] fid[4] offset[8] count[4] -size[4] Rread tag[2] count[4] data[count] - -size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] -size[4] Rwrite tag[2] count[4] -</code></pre> -<p> -<code>read</code>は<code>fid</code>の<code>offset</code>バイト目\ -から<code>count</code>バイト読む。\ -読んだデータと、読めたデータサイズが\ -<code>data</code>、<code>count</code>ととして返される。\ -<code>write</code>は<code>fid</code>の<code>offset</code>バイト目\ -に<code>count</code>バイトのデータ<code>data</code>を書く。\ -書きこめたサイズが<code>count</code>として返される。\ -ファイルを読み書きするためには\ -あらかじめ<code>fid</code>を<code>open</code>する必要がある。\ -</p> - -<h3>clunk</h3> -<p> -<code>fid</code>を忘れる。 -</p> -<pre><code>\ -size[4] Tclunk tag[2] fid[4] -size[4] Rclunk tag[2] -</code></pre> -<p> -いらなくなった<code>fid</code>を忘れる。\ -一度忘れた<code>fid</code>は<code>walk</code>で別のファイルを取得する際に\ -再利用できる。\ -</p> - -<h3>remove</h3> -<p> -ファイルを削除する。 -</p> -<pre><code>\ -size[4] Tremove tag[2] fid[4] -size[4] Rremove tag[2] -</code></pre> -<p> -<code>fid</code>と紐付いたサーバー上のファイルを削除する。\ -<code>fid</code>は<code>clunk</code>したのと同様に忘れられる。\ -</p> - -<h3>stat、wstat</h3> -<p> -ファイルの属性を読み書きする。 -</p> -<pre><code>\ -size[4] Tstat tag[2] fid[4] -size[4] Rstat tag[2] stat[n] - -size[4] Twstat tag[2] fid[4] stat[n] -size[4] Rwstat tag[2] -</code></pre> -<p> -<code>stat</code>はファイルの属性を読む。\ -読めた情報は<code>stat</code>として返される。\ -<code>wstat</code>はファイルの属性を<code>stat</code>に変更する。\ -<code>stat</code>はファイルの名前や<code>qid</code>、サイズ、\ -作成日、更新日等の情報がバイト列になったものである。\ -</p> - -<h2>Goによる実装</h2> -<h3>メッセージの構造体の定義</h3> -<p> -メッセージには<code>size</code>、<code>tag</code>等共通の要素がある。\ -クライアントからメッセージを受けとる際、\ -<code>size</code>を参照して何バイト読むか判断し、\ -メッセージの<code>type</code>によりその後の処理を場合分けする。\ -そのためにメッセージを<code>interface</code>として定義した: -</p> -<pre><code>\ -// A Msg represents any kind of message of 9P. -// It defines methods for common fields. -// For each message type &lt;T&gt;, new&lt;T&gt;([]byte) function parses the byte array -// of 9P message into the corresponding message struct. -// For detailed information on each message, consult the documentation -// of 9P protocol. -type Msg interface { - // Size returns the size field of message. - // Size field holds the size of the message in bytes - // including the 4-byte size field itself. - Size() uint32 - // Type returns the type field of message. - Type() MsgType - // GetTag returns the Tag of message. - // Tag is the identifier of each message. - // The Get prefix is to avoid name confliction with the each - // message's Tag field. - GetTag() uint16 - // SetTag sets the Tag field of the message. - SetTag(uint16) - // Marshal convert Msg to byte array to be transmitted. - marshal() []byte - String() string -} -</code></pre> -<p> -<code>marshal()</code>はサーバーから返信を送る際に\ -メッセージの構造体をバイト列にエンコードするための関数である。\ -<code>String() string</code>はログ出力用。\ -</p> -<p> -メッセージはバイト列として受け取った後、\ -メッセージのタイプによって処理を分ける。\ -その際、扱いやすいようにバイト列を各メッセージの構造体に\ -変換する。\ -変換前のバイト列から、各メッセージに共通のフィールドを\ -取得できると便利なので、\ -バイト列のまま<code>Msg</code>インターフェースを実装する\ -<code>bufMsg</code>を定義した。\ -名前はいまいち。\ -</p> -<pre><code>\ -type bufMsg []byte - -func (msg bufMsg) Size() uint32 { return gbit32(msg[0:4]) } -func (msg bufMsg) Type() MsgType { return MsgType(msg[4]) } -func (msg bufMsg) GetTag() uint16 { return gbit16(msg[5:7]) } -func (msg bufMsg) SetTag(t uint16) { pbit16(msg[5:7], t) } -func (msg bufMsg) marshal() []byte { return []byte(msg)[:msg.Size()] } -func (msg bufMsg) String() string { - switch msg.Type() { - case Tversion: - return newTVersion(msg).String() - /* 省略 */ - } -} -</code></pre> -<p> -<code>gbit32</code>は4バイトのリトルエンディアンの配列を\ -整数に変換するもので、\ -<code>pbit32</code>は逆に整数をリトルエンディアンのスライスに\ -格納するものである。\ -</p> -<p> -各メッセージはそれぞれのフィールドを構造体のフィールドとして\ -定義したものである。\ -例えば<code>tversion</code>に対応する構造体は以下の通り。\ -</p> -<pre><code>\ -type TVersion struct { - Tag uint16 - Msize uint32 - Version string -} -</code></pre> -<p> -バイト列から各メッセージの構造体に変換するために\ -<code>new<i>メッセージタイプ</i></code>\ -を定義した。 -</p> -<pre><code>\ -func newTVersion(buf []byte) *TVersion { - msg := new(TVersion) - msg.Tag = gbit16(buf[5:7]) - msg.Msize = gbit32(buf[7:11]) - vs := gbit16(buf[11:13]) - msg.Version = string(buf[13 : 13+vs]) - return msg -} -</code></pre> - -<h3>メッセージのやりとり</h3> -<p> -まずは9Pメッセージを読む関数を実装する。\ -バイト列が読めればいいので引数は<code>io.Reader</code>にした: -</p> -<pre><code>\ -func readMsg(r io.Reader) ([]byte, error) { -</code></pre> -<p> -最初にメッセージのサイズを読む: -</p> -<pre><code> buf := make([]byte, 4) - read, err := r.Read(buf) - if err != nil { - if err == io.EOF { - return nil, err - } - return nil, fmt.Errorf(&quot;read size: %v&quot;, err) - } - if read != len(buf) { - return buf, fmt.Errorf(&quot;read size: invalid message.&quot;) - } - size := bufMsg(buf).Size() -</code></pre> -<p> -続いて読みこんだサイズに基づいてメッセージの残りの部分を読む。 -一回の<code>Read</code>で最後まで読めないことがあったので、 -全部読めるまで<code>Read</code>を繰り返す: -</p> -<pre><code> mbuf := make([]byte, size-4) - for read = 0; read &lt; int(size)-4; { - n, err := r.Read(mbuf[read:]) - if err != nil { - return buf, fmt.Errorf(&quot;read body: %v&quot;, err) - } - read += n - } - buf = append(buf, mbuf...) - return buf, nil -} -</code></pre> -<p> -読み込んだバイト列は<code>ReadMsg</code>関数で\ -メッセージの構造体に変換する。\ -</p> -<pre><code>\ -RecvMsg(r io.Reader) (Msg, error) { - b, err := readMsg(r) - if err == io.EOF { - return nil, err - } else if err != nil { - return nil, fmt.Errorf(&quot;readMsg: %v&quot;, err) - } - return unmarshal(b) -} -</code></pre> -<p> -返信のメッセージはメッセージの構造体を<code>marshal</code>関数\ -でバイト列に変換して<code>io.Writer</code>に書き込む。\ -</p> -<pre><code>\ -// SendMsg send a 9P message to w -func SendMsg(msg Msg, w io.Writer) error { - if _, err := w.Write(msg.marshal()); err != nil { - return fmt.Errorf(&quot;write: %v&quot;, err) - } - return nil -} -</code></pre> - -<h3>メインループ</h3> -<p> -ライブラリはメッセージを読み込むためのリスナーgoroutinと\ -返信を書き込むためのレスポンダーgoroutine、\ -そして各メッセージを処理するためのgoroutineを立ち上げて、\ -チャネルでそれぞれを繋ぐ。\ -リスナーがメッセージを読み込むと、<code>request</code>構造体に格納し、\ -メッセージのタイプに応じて各goroutineに渡す。\ -渡されたgoroutineはメッセージに応じた処理をし、\ -返信のメッセージをリスポンダーgoroutineに渡す。\ -リスポンダーgoroutineはメッセージをバイト列に変換して返信する。 -</p> -<p> -この設計がいいかどうかよく知らんけど、\ -Go言語を触るからgoroutineとチャネルによるパイプラインを作ってみたかった。\ -</p> - -<h3>各メッセージの処理</h3> -<p> -メッセージは<code>s<i>メッセージタイプ</i></code>goroutineで処理する。\ -関数は<code>for</code>ループのなかでチャンネル<code>rc</code>から\ -<code>request</code>が届くのを待つ。\ -届いたリクエストには<code>ifcall</code>フィールドにクライアントからの\ -<code>Msg</code>が含まれるので、\ -この関数が担当するstructにキャストする。\ -このキャストが失敗するのは明かなサーバーのバグなのでタイプアサーションはしない。\ -</p> -<pre><code>\ -func s<i>メッセージタイプ</i>(ctx context.Context, c *conn, rc &lt;-chan *request) { - for { - select { - case &lt;-ctx.Done(); - return - case r, ok := rc: - if !ok { - return - } - ifcall := r.ifcall.(*T<i>メッセージタイプ</i>) -</code></pre> -<p> -各種処理を完了したら、届いた<code>request</code>の<code>ofcall</code>に\ -返信の<code>Msg</code>を格納して返信を担当するgoroutineに送る。\ -<pre><code> select { - case c.respChan &lt;- r: - case &lt;-ctx.Done(): - return - } -</code></pre> -</p> -<h4>Version</h4> -<p> -クライアントから届いたバージョンの提案を見て、頭に"9P2000"があれば\ -返信のバージョンを"9P2000"にする。\ -現在定義されているバージョンは"9P2000"の他、\ -Unix用に拡張した"9P2000.u"、Linux用に拡張した"9P2000.l"がある。\ -基本的な昨日を実装することが目標なので、"9P2000"のみを受け付ける。\ -</p> -<p> -Versionメッセージではメッセージの最大サイズも決定する。\ -サーバーは予め8Kバイトを最大サイズにしている。\ -クライアントがこれ以上のサイズを提案した場合、8Kにしてもらい、\ -これ以下のサイズを提案した場合、そのサイズでメッセージをやりとりする。\ -8Kバイトという既定値はplan9の実装を参考にした。\ -このサイズである必然性はよく知らない。\ -</p> -<pre><code> version := ifcall.Version - if strings.HasPrefix(version, &quot;9P2000&quot;) { - version = &quot;9P2000&quot; - } else { - version = &quot;unknown&quot; - } - msize := ifcall.Msize - if msize &gt; c.mSize() { - msize = c.mSize() - } - r.ofcall = &amp;RVersion{ - Msize: msize, - Version: version, - } - c.setMSize(r.ofcall.(*RVersion).Msize) -</code></pre> - -<h4>Auth</h4> -<p> -サーバーの認証は<code>Server</code>の<code>Auth</code>フィールドに\ -認証のためのやりとりを待ち受ける関数を登録することで有効化できる。\ -このフィールドが<code>nil</code>の場合、認証が必要ない旨のエラーを返す。\ -</p> -<pre><code> if c.s.Auth == nil { - r.err = fmt.Errorf(&quot;authentication not required&quot;) - goto resp - } -</code></pre> -<p> -認証が必要な場合、クライアントから提案された<code>afid</code>をコネクションに\ -登録して、<code>Server.Auth</code>を呼び出す。 -</p> - -<h4>attach</h4> -<p> -サーバーはまず認証が必要な場合認証が済んでいるか確認する。\ -</p> -<pre><code> switch { - case c.s.Auth == nil &amp;&amp; ifcall.Afid == NOFID: - case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID: - r.err = ErrBotch - goto resp - case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID: - r.err = fmt.Errorf(&quot;authentication required&quot;) - goto resp - case c.s.Auth != nil &amp;&amp; ifcall.Afid != NOFID: - afid, ok := c.fPool.lookup(ifcall.Afid) - if !ok { - r.err = ErrUnknownFid - goto resp - } - af, ok := afid.file.(*AuthFile) - if !ok { - r.err = fmt.Errorf(&quot;not auth file&quot;) - goto resp - } - if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK { - r.err = fmt.Errorf(&quot;not authenticated&quot;) - goto resp - } - } -</code></pre> -<p> -次にクライアントが要求してきたファイルツリーがサーバーにあるかどうか確認し、\ -クライアントが設定した<code>fid</code>にルートディレクトリを紐付ける。\ -</p> -<pre><code> r.fid, err = c.fPool.add(ifcall.Fid) - if err != nil { - r.err = ErrDupFid - goto resp - } - r.fid.path = &quot;.&quot; - r.fid.uid = ifcall.Uname - fsys, ok = c.s.fsmap[ifcall.Aname] - if !ok { - r.err = fmt.Errorf(&quot;no such file system&quot;) - goto resp - } - r.fid.fs = fsys - fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;) - if err != nil { - r.err = fmt.Errorf(&quot;stat root: %v&quot;, err) - goto resp - } - r.fid.qidpath = fi.Sys().(*Stat).Qid.Path - r.ofcall = &amp;RAttach{ - Qid: fi.Sys().(*Stat).Qid, - } -</code></pre> - -<h4>flush</h4> -<p> -指定されたリクエストの<code>flush</code>メソッドを呼び出す。\ -<code>flush</code>メソッドはリクエストの<code>done</code>チャンネルを閉じ、\ -リクエストのタグを削除する。\ -処理に時間のかかるものは、リクエストの<code>done</code>チャネルが閉じられたら、\ -その処理を中止する。\ -</p> -<pre><code>\ -// flush cancels the request by calling r.cancel. -// It also delete the request from its pool. -func (r *request) flush() { - close(r.done) - r.pool.delete(r.tag) -} -</code></pre> - -<h4>walk</h4> -<p> -仕様に書かれている通りの条件を確認した後、\ -ファイルツリーを順番に辿り、得られた<code>Qid</code>を\ -記録してクライアントに返す。\ -</p> -<pre><code> wqids = make([]Qid, 0, len(ifcall.Wnames)) - cwdp = oldFid.path - for _, name := range ifcall.Wnames { - cwdp = path.Clean(path.Join(cwdp, name)) - if cwdp == &quot;..&quot; { - cwdp = &quot;.&quot; // parent of the root is itself. - } - stat, err := fs.Stat(ExportFS{oldFid.fs}, cwdp) - if err != nil { - break - } - wqids = append(wqids, stat.Sys().(*Stat).Qid) - } - if len(wqids) == 0 { - newFid.qidpath = oldFid.qidpath - } else { - newFid.qidpath = wqids[len(wqids)-1].Path - } - newFid.path = cwdp - newFid.uid = oldFid.uid - newFid.fs = oldFid.fs - r.ofcall = &amp;RWalk{ - Qids: wqids, - } -</code></pre> - -<h4>open</h4> -<h4>create</h4> -<h4>read</h4> -<h4>write</h4> -<h4>clunk</h4> -<h4>remove</h4> -<h4>stat</h4> -<h4>wstat</h4> diff --git a/man/draft/9p_go.html b/man/draft/9p_go.html @@ -0,0 +1,385 @@ +<h1>9PのGoによる実装</h1> +<time>2024-12-18</time> + +<h2>メッセージの構造体の定義</h2> +<p> +メッセージには<code>size</code>、<code>tag</code>等共通の要素がある。\ +クライアントからメッセージを受けとる際、\ +<code>size</code>を参照して何バイト読むか判断し、\ +メッセージの<code>type</code>によりその後の処理を場合分けする。\ +そのためにメッセージを<code>interface</code>として定義した: +</p> +<pre><code>\ +// A Msg represents any kind of message of 9P. +// It defines methods for common fields. +// For each message type &lt;T&gt;, new&lt;T&gt;([]byte) function parses the byte array +// of 9P message into the corresponding message struct. +// For detailed information on each message, consult the documentation +// of 9P protocol. +type Msg interface { + // Size returns the size field of message. + // Size field holds the size of the message in bytes + // including the 4-byte size field itself. + Size() uint32 + // Type returns the type field of message. + Type() MsgType + // GetTag returns the Tag of message. + // Tag is the identifier of each message. + // The Get prefix is to avoid name confliction with the each + // message's Tag field. + GetTag() uint16 + // SetTag sets the Tag field of the message. + SetTag(uint16) + // Marshal convert Msg to byte array to be transmitted. + marshal() []byte + String() string +} +</code></pre> +<p> +<code>marshal()</code>はサーバーから返信を送る際に\ +メッセージの構造体をバイト列にエンコードするための関数である。\ +<code>String() string</code>はログ出力用。\ +</p> +<p> +メッセージはバイト列として受け取った後、\ +メッセージのタイプによって処理を分ける。\ +その際、扱いやすいようにバイト列を各メッセージの構造体に\ +変換する。\ +変換前のバイト列から、各メッセージに共通のフィールドを\ +取得できると便利なので、\ +バイト列のまま<code>Msg</code>インターフェースを実装する\ +<code>bufMsg</code>を定義した。\ +名前はいまいち。\ +</p> +<pre><code>\ +type bufMsg []byte + +func (msg bufMsg) Size() uint32 { return gbit32(msg[0:4]) } +func (msg bufMsg) Type() MsgType { return MsgType(msg[4]) } +func (msg bufMsg) GetTag() uint16 { return gbit16(msg[5:7]) } +func (msg bufMsg) SetTag(t uint16) { pbit16(msg[5:7], t) } +func (msg bufMsg) marshal() []byte { return []byte(msg)[:msg.Size()] } +func (msg bufMsg) String() string { + switch msg.Type() { + case Tversion: + return newTVersion(msg).String() + /* 省略 */ + } +} +</code></pre> +<p> +<code>gbit32</code>は4バイトのリトルエンディアンの配列を\ +整数に変換するもので、\ +<code>pbit32</code>は逆に整数をリトルエンディアンのスライスに\ +格納するものである。\ +</p> +<p> +各メッセージはそれぞれのフィールドを構造体のフィールドとして\ +定義したものである。\ +例えば<code>tversion</code>に対応する構造体は以下の通り。\ +</p> +<pre><code>\ +type TVersion struct { + Tag uint16 + Msize uint32 + Version string +} +</code></pre> +<p> +バイト列から各メッセージの構造体に変換するために\ +<code>new<i>メッセージタイプ</i></code>\ +を定義した。 +</p> +<pre><code>\ +func newTVersion(buf []byte) *TVersion { + msg := new(TVersion) + msg.Tag = gbit16(buf[5:7]) + msg.Msize = gbit32(buf[7:11]) + vs := gbit16(buf[11:13]) + msg.Version = string(buf[13 : 13+vs]) + return msg +} +</code></pre> + +<h2>メッセージのやりとり</h2> +<p> +まずは9Pメッセージを読む関数を実装する。\ +バイト列が読めればいいので引数は<code>io.Reader</code>にした: +</p> +<pre><code>\ +func readMsg(r io.Reader) ([]byte, error) { +</code></pre> +<p> +最初にメッセージのサイズを読む: +</p> +<pre><code> buf := make([]byte, 4) + read, err := r.Read(buf) + if err != nil { + if err == io.EOF { + return nil, err + } + return nil, fmt.Errorf(&quot;read size: %v&quot;, err) + } + if read != len(buf) { + return buf, fmt.Errorf(&quot;read size: invalid message.&quot;) + } + size := bufMsg(buf).Size() +</code></pre> +<p> +続いて読みこんだサイズに基づいてメッセージの残りの部分を読む。 +一回の<code>Read</code>で最後まで読めないことがあったので、 +全部読めるまで<code>Read</code>を繰り返す: +</p> +<pre><code> mbuf := make([]byte, size-4) + for read = 0; read &lt; int(size)-4; { + n, err := r.Read(mbuf[read:]) + if err != nil { + return buf, fmt.Errorf(&quot;read body: %v&quot;, err) + } + read += n + } + buf = append(buf, mbuf...) + return buf, nil +} +</code></pre> +<p> +読み込んだバイト列は<code>ReadMsg</code>関数で\ +メッセージの構造体に変換する。\ +</p> +<pre><code>\ +RecvMsg(r io.Reader) (Msg, error) { + b, err := readMsg(r) + if err == io.EOF { + return nil, err + } else if err != nil { + return nil, fmt.Errorf(&quot;readMsg: %v&quot;, err) + } + return unmarshal(b) +} +</code></pre> +<p> +返信のメッセージはメッセージの構造体を<code>marshal</code>関数\ +でバイト列に変換して<code>io.Writer</code>に書き込む。\ +</p> +<pre><code>\ +// SendMsg send a 9P message to w +func SendMsg(msg Msg, w io.Writer) error { + if _, err := w.Write(msg.marshal()); err != nil { + return fmt.Errorf(&quot;write: %v&quot;, err) + } + return nil +} +</code></pre> + +<h2>メインループ</h2> +<p> +ライブラリはメッセージを読み込むためのリスナーgoroutinと\ +返信を書き込むためのレスポンダーgoroutine、\ +そして各メッセージを処理するためのgoroutineを立ち上げて、\ +チャネルでそれぞれを繋ぐ。\ +リスナーがメッセージを読み込むと、<code>request</code>構造体に格納し、\ +メッセージのタイプに応じて各goroutineに渡す。\ +渡されたgoroutineはメッセージに応じた処理をし、\ +返信のメッセージをリスポンダーgoroutineに渡す。\ +リスポンダーgoroutineはメッセージをバイト列に変換して返信する。 +</p> +<p> +この設計がいいかどうかよく知らんけど、\ +Go言語を触るからgoroutineとチャネルによるパイプラインを作ってみたかった。\ +</p> + +<h2>各メッセージの処理</h2> +<p> +メッセージは<code>s<i>メッセージタイプ</i></code>goroutineで処理する。\ +関数は<code>for</code>ループのなかでチャンネル<code>rc</code>から\ +<code>request</code>が届くのを待つ。\ +届いたリクエストには<code>ifcall</code>フィールドにクライアントからの\ +<code>Msg</code>が含まれるので、\ +この関数が担当するstructにキャストする。\ +このキャストが失敗するのは明かなサーバーのバグなのでタイプアサーションはしない。\ +</p> +<pre><code>\ +func s<i>メッセージタイプ</i>(ctx context.Context, c *conn, rc &lt;-chan *request) { + for { + select { + case &lt;-ctx.Done(); + return + case r, ok := rc: + if !ok { + return + } + ifcall := r.ifcall.(*T<i>メッセージタイプ</i>) +</code></pre> +<p> +各種処理を完了したら、届いた<code>request</code>の<code>ofcall</code>に\ +返信の<code>Msg</code>を格納して返信を担当するgoroutineに送る。\ +<pre><code> select { + case c.respChan &lt;- r: + case &lt;-ctx.Done(): + return + } +</code></pre> +</p> +<h3>Version</h3> +<p> +クライアントから届いたバージョンの提案を見て、頭に"9P2000"があれば\ +返信のバージョンを"9P2000"にする。\ +現在定義されているバージョンは"9P2000"の他、\ +Unix用に拡張した"9P2000.u"、Linux用に拡張した"9P2000.l"がある。\ +基本的な昨日を実装することが目標なので、"9P2000"のみを受け付ける。\ +</p> +<p> +Versionメッセージではメッセージの最大サイズも決定する。\ +サーバーは予め8Kバイトを最大サイズにしている。\ +クライアントがこれ以上のサイズを提案した場合、8Kにしてもらい、\ +これ以下のサイズを提案した場合、そのサイズでメッセージをやりとりする。\ +8Kバイトという既定値はplan9の実装を参考にした。\ +このサイズである必然性はよく知らない。\ +</p> +<pre><code> version := ifcall.Version + if strings.HasPrefix(version, &quot;9P2000&quot;) { + version = &quot;9P2000&quot; + } else { + version = &quot;unknown&quot; + } + msize := ifcall.Msize + if msize &gt; c.mSize() { + msize = c.mSize() + } + r.ofcall = &amp;RVersion{ + Msize: msize, + Version: version, + } + c.setMSize(r.ofcall.(*RVersion).Msize) +</code></pre> + +<h3>Auth</h3> +<p> +サーバーの認証は<code>Server</code>の<code>Auth</code>フィールドに\ +認証のためのやりとりを待ち受ける関数を登録することで有効化できる。\ +このフィールドが<code>nil</code>の場合、認証が必要ない旨のエラーを返す。\ +</p> +<pre><code> if c.s.Auth == nil { + r.err = fmt.Errorf(&quot;authentication not required&quot;) + goto resp + } +</code></pre> +<p> +認証が必要な場合、クライアントから提案された<code>afid</code>をコネクションに\ +登録して、<code>Server.Auth</code>を呼び出す。 +</p> + +<h3>attach</h3> +<p> +サーバーはまず認証が必要な場合認証が済んでいるか確認する。\ +</p> +<pre><code> switch { + case c.s.Auth == nil &amp;&amp; ifcall.Afid == NOFID: + case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID: + r.err = ErrBotch + goto resp + case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID: + r.err = fmt.Errorf(&quot;authentication required&quot;) + goto resp + case c.s.Auth != nil &amp;&amp; ifcall.Afid != NOFID: + afid, ok := c.fPool.lookup(ifcall.Afid) + if !ok { + r.err = ErrUnknownFid + goto resp + } + af, ok := afid.file.(*AuthFile) + if !ok { + r.err = fmt.Errorf(&quot;not auth file&quot;) + goto resp + } + if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK { + r.err = fmt.Errorf(&quot;not authenticated&quot;) + goto resp + } + } +</code></pre> +<p> +次にクライアントが要求してきたファイルツリーがサーバーにあるかどうか確認し、\ +クライアントが設定した<code>fid</code>にルートディレクトリを紐付ける。\ +</p> +<pre><code> r.fid, err = c.fPool.add(ifcall.Fid) + if err != nil { + r.err = ErrDupFid + goto resp + } + r.fid.path = &quot;.&quot; + r.fid.uid = ifcall.Uname + fsys, ok = c.s.fsmap[ifcall.Aname] + if !ok { + r.err = fmt.Errorf(&quot;no such file system&quot;) + goto resp + } + r.fid.fs = fsys + fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;) + if err != nil { + r.err = fmt.Errorf(&quot;stat root: %v&quot;, err) + goto resp + } + r.fid.qidpath = fi.Sys().(*Stat).Qid.Path + r.ofcall = &amp;RAttach{ + Qid: fi.Sys().(*Stat).Qid, + } +</code></pre> + +<h3>flush</h3> +<p> +指定されたリクエストの<code>flush</code>メソッドを呼び出す。\ +<code>flush</code>メソッドはリクエストの<code>done</code>チャンネルを閉じ、\ +リクエストのタグを削除する。\ +処理に時間のかかるものは、リクエストの<code>done</code>チャネルが閉じられたら、\ +その処理を中止する。\ +</p> +<pre><code>\ +// flush cancels the request by calling r.cancel. +// It also delete the request from its pool. +func (r *request) flush() { + close(r.done) + r.pool.delete(r.tag) +} +</code></pre> + +<h3>walk</h3> +<p> +仕様に書かれている通りの条件を確認した後、\ +ファイルツリーを順番に辿り、得られた<code>Qid</code>を\ +記録してクライアントに返す。\ +</p> +<pre><code> wqids = make([]Qid, 0, len(ifcall.Wnames)) + cwdp = oldFid.path + for _, name := range ifcall.Wnames { + cwdp = path.Clean(path.Join(cwdp, name)) + if cwdp == &quot;..&quot; { + cwdp = &quot;.&quot; // parent of the root is itself. + } + stat, err := fs.Stat(ExportFS{oldFid.fs}, cwdp) + if err != nil { + break + } + wqids = append(wqids, stat.Sys().(*Stat).Qid) + } + if len(wqids) == 0 { + newFid.qidpath = oldFid.qidpath + } else { + newFid.qidpath = wqids[len(wqids)-1].Path + } + newFid.path = cwdp + newFid.uid = oldFid.uid + newFid.fs = oldFid.fs + r.ofcall = &amp;RWalk{ + Qids: wqids, + } +</code></pre> + +<h3>open</h3> +<h3>create</h3> +<h3>read</h3> +<h3>write</h3> +<h3>clunk</h3> +<h3>remove</h3> +<h3>stat</h3> +<h3>wstat</h3> diff --git a/pub/computer/9p.html b/pub/computer/9p.html @@ -0,0 +1,319 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <link rel="stylesheet" type="text/css" href="/style.css"> + <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> + <title>9P</title> +</head> +<body> + <header> + <a href="/">主頁</a> | + <a href="/about.html">自己紹介</a> | + <a href="/journal">日記</a> | + <a href="/farm">農業</a> | + <a href="/kitchen">台所</a> | + <a href="/computer">電算機</a> | + <a href="/poetry">詩</a> | + <a href="/books">本棚</a> | + <a href="/gallery">絵</a> | + <a href="https://git.mtkn.jp">Git</a> + </header> + <main> + <article> +<h1>9P</h1> +<time>2024-10-27</time> +<h2>はじめに</h2> +<p> +9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。このプロトコルを使えば、ディスクに記録されたデータだけでなく、マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等もファイルとして扱える。Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOSのために設計された。 +</p> + +<p> +Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計されているので、この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。</p> + +<p> +便利そうなのでGo言語で実装してみた。 +</p> + +<h2>9Pプロトコル</h2> +<h3>概要</h3> +<p> +9Pの通信ではクライアントがサーバーに対してリクエストを送り、サーバーがそれに対してリプライを返すことを繰り返す。クライアントからのリクエストをTメッセージ、サーバーからのリプライをRメッセージと言う。Tメッセージにはファイルにアクセスするための色々なものが用意されている。例えばファイルを開くためのTopenや、開いたファイルに書き込むためのTwrite等である。それぞれのTメッセージには対応するRメッセージがある。Topenに対してRopen、Twriteに対してRwrite等である。</p> +<p> +通信は必要な情報をプロトコルの定める方法でバイト列に変換して行う。2バイト以上のデータはリトルエンディアンになるように配置する。テキストデータはUTF-8にエンコードする。テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。</p> +<p> +各メッセージは4バイトの整数から始まる。これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。次にメッセージの種類を記述する1バイトのデータが来る。次はメッセージのタグである。クライアントはサーバーからの返事を待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージに対する返事なのかを識別する必要がある。そのために使うのがタグである。クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに同じタグを付ける。その後ろには、メッセージの種類によって固有の情報が付けられる。</p> +<p> +以下は実際の9P通信のログである。実際はバイナリの通信だが、ログとして見易いように変換してある。また、先頭にくるメッセージサイズは省略してある。この例では、サーバーのルートディレクトリにある<code>hello</code>というファイルの内容を読みこんでいる。:</p> +<pre><code>&lt;-- Tversion Tag 65535 msize 8192 version '9P2000' +--&gt; Rversion Tag 65535 msize 8192 version '9P2000' +&lt;-- Tattach Tag 0 fid 0 afid -1 uname kenji aname +--&gt; Rattach Tag 0 qid (0000000000000000 0 d) +&lt;-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello +--&gt; Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 ) +&lt;-- Tstat Tag 0 fid 1 +--&gt; Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0 +&lt;-- Twalk Tag 0 fid 1 newfid 2 nwname 0 +--&gt; Rwalk Tag 0 nwqid 0 +&lt;-- Topen Tag 0 fid 2 mode 0x0 +--&gt; Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169 +&lt;-- Tread Tag 0 fid 2 offset 0 count 4096 +--&gt; Rread Tag 0 count 7 ' 776f726c 64210a' +&lt;-- Tread Tag 0 fid 2 offset 7 count 4096 +--&gt; Rread Tag 0 count 0 '' +&lt;-- Tclunk Tag 0 fid 2 +--&gt; Rclunk Tag 0 +</code></pre> +<p> +まず最初にクライアントがサーバーに対してTversionを送り、プロトコルのバージョンと、メッセージの最大サイズを交渉する。Tversionのタグは<code>65535</code>と決められている。<code>msize</code>の8192はメッセージの最大サイズ(バイト)で、<code>version</code>の9P2000がプロトコルのバージョンである。サーバーからRversionで同じ<code>msize</code>と<code>version</code>が返ってきたのでこのサイズとバージョンで以降の通信を行う。</p> +<p> +次のTattachで、クライアントがサーバーにルートディレクトリの<code>fid</code>を要求する。<code>afid</code>以降は認証用の情報である(後述)。<code>fid</code>はコネクションにおいてファイルと紐付けられる整数で、Unixにおけるファイルディスクリプタのようなものである。ファイルの読み書きはこの<code>fid</code>を使って行われる。サーバーからの<code>Rattach</code>にはルートディレクトリの<code>qid</code>が含まれる。これはサーバー上でファイルを一意に識別するもので、Unixにおけるinodeに相当するものである。以上でサーバーに繋いでセッションを確立できた。</p> +<p> +次にTwalkで目的のファイル(<code>hello</code>)までファイルツリーを辿る。ここでは起点として先程得られたルートディレクトリ(<code>fid = 1</code>)を起点として、このディレクトリ内の<code>hello</code>ファイルを要求して<code>fid = 2</code>を割り当てようとしている。サーバーはルートディレクトリ内に要求されたファイルが見付かったので、そのファイルの<code>qid</code>を返す。</p> +<p> +目的のファイルに<code>fid</code>が割り当てられたので、Tstatでこのファイルの属性を要求している。おそらくファイルのパーミッションを調べるためである。</p> +<p> +次にTopenを使ってこのファイルを開き、Treadを使って内容を読む。二回目のTreadに対して0バイトのデータが返ってきたので、ファイルはこれで終りである。</p> +<p> +最後に、必要なくなった<code>fid</code>はTclunkで捨てる。</p> + +<h3>List of all Message Types</h3> + +<table> +<tr> +<th>Tメッセージ</th> +<th>Rメッセージ</th> +<th>説明</th> +</tr> +<tr> +<td>Tversion</td> +<td>Rversion</td> +<td>バージョンの交渉</td> +</tr> +<tr> +<td>Tauth</td> +<td>Rauth</td> +<td>認証ファイルの取得</td> +</tr> +<tr> +<td></td> +<td>Rerror</td> +<td>エラー(Rメッセージのみ)</td> +</tr> +<tr> +<td>Tflush</td> +<td>Rflush</td> +<td>処理中のリクエストをキャンセル</td> +</tr> +<tr> +<td>Tattach</td> +<td>Rattach</td> +<td>ルートディレクトリの取得</td> +</tr> +<tr> +<td>Twalk</td> +<td>Rwalk</td> +<td>ファイルツリーを辿る</td> +</tr> +<tr> +<td>Topen</td> +<td>Ropen</td> +<td>ファイルを開く</td> +</tr> +<tr> +<td>Tcreate</td> +<td>Rcreate</td> +<td>ファイルの作成</td> +</tr> +<tr> +<td>Tread</td> +<td>Rread</td> +<td>ファイルを読む</td> +</tr> +<tr> +<td>Twrite</td> +<td>Rwrite</td> +<td>ファイルへ書き込む</td> +</tr> +<tr> +<td>Tclunk</td> +<td>Rclunk</td> +<td>fidの削除</td> +</tr> +<tr> +<td>Tremove</td> +<td>Rremove</td> +<td>ファイルを削除</td> +</tr> +<tr> +<td>Tstat</td> +<td>Rstat</td> +<td>ファイルの属性を取得</td> +</tr> +<tr> +<td>Twstat</td> +<td>Rwstat</td> +<td>ファイルの属性を変更</td> +</tr> +</table> + +<h2><code>fid</code>と<code>qid</code></h2> +<p> +<code>fid</code>はコネクションにおいてファイルを指定するために使われる整数で、Unixのファイルディスクリプタのようなものである。使用する整数はクライアントが指定できる。サーバーに接続する際にTattachにより、サーバーのルートディレクトリの<code>fid</code>が設定され、Twalkにより追加され、Tclunkにより削除される。</p> +<p> +<code>qid</code>はサーバーがファイルを一意に識別するためのもので、Unixのinodeに相当するものである。ただしinodeはファイルが削除されると再利用される可能性があるのに対し、<code>qid</code>は再利用できない。<code>qid</code>はこのファイルがディレクトリかどうか等を示す<code>type</code>、ファイルが変更されたときにインクリメントされる<code>vers</code>、そしてファイルを一意に表す<code>path</code>の3つの情報で構成される。</p> +<h2>認証</h2> +<p> +9Pにはクライアントの認証が組込まれていない。もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすればいちいちコードを修正してコンパイルしなおさなければいけないうえ、9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。認証に必要なやりとりは認証サーバーとクライアントが行い、9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することでクライアントの確認を行う。 +</p> +<p> +9PサーバーはTattachメッセージを受けとると、認証サーバーとのコネクションを確立する。その後<code>Rattach</code>メッセージで<code>afid</code>という特殊なFidをクライアントに返す。クライアントからこの<code>afid</code>に対して読み書きする命令が届くと、9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。つまりクライアントはこの<code>afid</code>を通じて認証サーバーと直接やりとりができる。クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報を<code>afid</code>に書き込んで認証する。このように認証をプロトコル自体から切り離すことで、認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。また、より強力な認証システムを組み込むのも楽である。 +</p> + +<h2>コネクションの共有</h2> +<p> +クライアントはサーバーと<code>version</code>メッセージを交すことでコネクションを確立する。コネクションの確立からの一連のやりとりをセッションという。ひとつのコネクションは複数のクライアントが共有できる。</p> +<p> +クライアントはTauthまたはTattachメッセージによりサーバーに<code>fid</code>を要求できる。このうちTauthにより得られた<code>fid</code>は認証以外には使えない。Tattachで得られた<code>fid</code>は、Twalkメッセージによりファイルツリーを辿るための起点として利用でき、このとき辿った先のファイルを示す<code>fid</code>が生成される。これ以外の方法で<code>fid</code>が増えることはない。つまり、サーバーはひとつのコネクションのなかで、から派生した<code>fid</code>を追跡することで、クライアントを区別できる。</p> + +<h2>メッセージ詳細</h2> +<p> +各メッセージの詳細を書く。それぞれ最初にメッセージの形式を書く。<code><i>field</i>[<i>n</i>]</code>は<code><i>field</i></code>という名前の<i>n</i>バイトのデータを表す(<i>n</i>は整数)。また、括弧の中が整数ではなくsになっている場合、そのフィールドは文字列のデータで、その前に文字列の長さを示す2バイトのデータが先行する。</p> +<h3>version</h3> +<p> +プロトコルのバージョンを交渉すし、コネクションを確立する。 +</p> +<pre><code>size[4] Tversion tag[2] msize[4] version[s] +size[4] Rversion tag[2] msize[4] version[s] +</code></pre> +<p> +クライアントは最初にサーバーにこのメッセージを送ってバージョンとメッセージの最大サイズ(<code>msize</code>)を交渉する。サーバーはクライアントから送られたバージョンとメッセージサイズに対応していれば、同じバージョンとサイズを送り返し、交渉成立である。</p> +<p> +サーバーとクライアントはこのメッセージを以ってコネクションを確立する。</p> + +<h3>attach、auth</h3> +<p> +コネクションを確立する。 +</p> +<pre><code>size[4] Tauth tag[2] afid[4] uname[s] aname[s] +size[4] Rauth tag[2] aqid[13] + +size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] +size[4] Rattach tag[2] qid[13] +</code></pre> +<p> +<code>attach</code>メッセージはサーバーのルートディレクトリを要求し、<code>fid</code>を割り当てる。認証が必要なサーバーであれば、先に<code>auth</code>メッセージで認証を済ませておき、<code>attach</code>メッセージの<code>afid</code>に、先程使用した<code>afid</code>をセットする。サーバーはこの<cdoe>afid</code>に紐付いている認証サーバーにクライアントが認証済みであるかを確認し、認証済みであればルートディレクトリの<code>qid</code>を返す。サーバーが複数のファイルツリーを公開している場合、<code>aname</code>で指定する。</p> +<p> +<code>auth</code>メッセージは認証サーバーとやりとりするための<code>fid</code>である<code>afid</code>を要求する。この<code>afid</code>を読み書きすることで認証サーバーとやりとりをし、自身を身分を証明する。認証が済んだらこの<code>afid</code>をセットした<code>attach</code>メッセージを送る。</p> + +<h3>error</h3> +<p> +エラーを返す。 +</p> +<pre><code>size[4] Rerror tag[2] ename[s] +</code></pre> +<p> +クライアントからの要求に対して、なにかエラーがおきたときにそのエラーを返すために使われる。そのためサーバーからクライアントへの<code>Rerror</code>はあるが逆向きのTメッセージはない。</p> + +<h3>flush</h3> +<p> +リクエストをキャンセルする。 +</p> +<pre><code>size[4] Tflush tag[2] oldtag[2] +size[4] Rflush tag[2] +</code></pre> +<p> +実行中の要求をキャンセルする。キャンセルしたいリクエストの<code>tag</code>を<code>oldtag</code>にセットする。 +</p> + +<h3>walk</h3> +<p> +ファイルツリーを移動する。 +</p> +<pre><code>size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) +size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13]) +</code></pre> +<p> +<code>fid</code>を起点に移動する。移動先のファイルを<code>newfid</code>にセットする。移動するディレクトリの数を<code>nwname</code>に、移動するディレクトリの名前を移動する順に<code>wname</code>にそれぞれセットする。例えばカレントディレクトリから<code>./dir1/dir2/</code>に移動する場合、<code>nwname</code>は<code>2</code>、<code>wname</code>は<code>{"dir1", "dir2"}</code>となる。<code>wname</code>の最後の要素はディレクトリでなくファイルでもいい。</p> +<p> +<code>wname</code>の最初の要素への移動が失敗した場合は<code>Rerror</code>が返される。それ以外の場合は<code>Rwalk</code>が返され、<code>qid</code>は移動が成功した順番にそのファイルの<code>qid</code>がセットされる。<code>nwname</code>と<code>nwqid</code>が一致した場合は最後まで移動できたことになる。</p> +<p> +<code>walk</code>は<code>fid</code>を増殖させる唯一の方法である。</p> + +<h3>open、create</h3> +<p> +<code>fid</code>を開いて(作成して)読み書きできる状態にする。 +</p> +<pre><code>size[4] Topen tag[2] fid[4] mode[1] +size[4] Ropen tag[2] qid[13] iounit[4] + +size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] +size[4] Rcreate tag[2] qid[13] iounit[4] +</code></pre> +<p> +<code>open</code>は<code>fid</code>と紐付いたファイルを開く。<code>mode</code>は読み込み専用の<code>OREAD</code>、書き込み専用の<code>OWRITE</code>、読み書きの<code>ORDWR</code>等である。ファイルを開く手順は以下の通り: +<ol> +<li><code>attach</code>でルートディレクトリの<code>fid</code>を取得</li> +<li>ルートディレクトリの<code>fid</code>から<code>walk</code>で開きたいファイルの<code>fid</code>を取得</li> +<li>その<code>fid</code>を<code>open</code>で開く</li> +</ol> +</p> +<p> +<code>create</code>は<code>fid</code>と紐付いたディレクトリに新しいファイルを作成する。<code>creat</code>ではなく<code>create</code>である。作成が成功したら<code>fid</code>は新しく作成されたファイルに紐付く。</p> + +<h3>read、write</h3> +<p> +ファイルを読み書きする。 +</p> +<pre><code>size[4] Tread tag[2] fid[4] offset[8] count[4] +size[4] Rread tag[2] count[4] data[count] + +size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] +size[4] Rwrite tag[2] count[4] +</code></pre> +<p> +<code>read</code>は<code>fid</code>の<code>offset</code>バイト目から<code>count</code>バイト読む。読んだデータと、読めたデータサイズが<code>data</code>、<code>count</code>ととして返される。<code>write</code>は<code>fid</code>の<code>offset</code>バイト目に<code>count</code>バイトのデータ<code>data</code>を書く。書きこめたサイズが<code>count</code>として返される。ファイルを読み書きするためにはあらかじめ<code>fid</code>を<code>open</code>する必要がある。</p> + +<h3>clunk</h3> +<p> +<code>fid</code>を忘れる。 +</p> +<pre><code>size[4] Tclunk tag[2] fid[4] +size[4] Rclunk tag[2] +</code></pre> +<p> +いらなくなった<code>fid</code>を忘れる。一度忘れた<code>fid</code>は<code>walk</code>で別のファイルを取得する際に再利用できる。</p> + +<h3>remove</h3> +<p> +ファイルを削除する。 +</p> +<pre><code>size[4] Tremove tag[2] fid[4] +size[4] Rremove tag[2] +</code></pre> +<p> +<code>fid</code>と紐付いたサーバー上のファイルを削除する。<code>fid</code>は<code>clunk</code>したのと同様に忘れられる。</p> + +<h3>stat、wstat</h3> +<p> +ファイルの属性を読み書きする。 +</p> +<pre><code>size[4] Tstat tag[2] fid[4] +size[4] Rstat tag[2] stat[n] + +size[4] Twstat tag[2] fid[4] stat[n] +size[4] Rwstat tag[2] +</code></pre> +<p> +<code>stat</code>はファイルの属性を読む。読めた情報は<code>stat</code>として返される。<code>wstat</code>はファイルの属性を<code>stat</code>に変更する。<code>stat</code>はファイルの名前や<code>qid</code>、サイズ、作成日、更新日等の情報がバイト列になったものである。</p> + + </article> + + </main> + <footer> + <address>info(at)mtkn(dot)jp</address> + <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> + </footer> +</body> +</html> diff --git a/pub/computer/index.html b/pub/computer/index.html @@ -26,11 +26,18 @@ <p> Small is beautiful. </p> + +<h2>9P in Go</h2> +<ul> +<li><a href="9p.html">9P</a></li> +</ul> + <h2>RP2040</h2> <ul> <li><a href="rp2040_1.html">RP2040 SDKなしでLチカ</a></li> <li><a href="rp2040_2.html">RP2040 SDKなし2 Clock、UART</a></li> </ul> + <h2>Xlib</h2> <ul> <li><a href="xlib_playground1.html">Xlibで遊んでみる1</a></li> diff --git a/pub/draft/9p.html b/pub/draft/9p.html @@ -1,618 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width,initial-scale=1"> - <link rel="stylesheet" type="text/css" href="/style.css"> - <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> - <title>9P</title> -</head> -<body> - <header> - <a href="/">主頁</a> | - <a href="/about.html">自己紹介</a> | - <a href="/journal">日記</a> | - <a href="/farm">農業</a> | - <a href="/kitchen">台所</a> | - <a href="/computer">電算機</a> | - <a href="/poetry">詩</a> | - <a href="/books">本棚</a> | - <a href="/gallery">絵</a> | - <a href="https://git.mtkn.jp">Git</a> - </header> - <main> - <article> -<h1>9P</h1> -<time>2024-10-27</time> -<h2>はじめに</h2> -<p> -9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。このプロトコルを使えば、ディスクに記録されたデータだけでなく、マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等もファイルとして扱える。Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOSのためのプロトコルである。 -</p> - -<p> -Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計されているので、この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。</p> - -<p> -便利そうなのでGo言語で実装してみた。 -</p> - -<h2>9Pプロトコル</h2> -<h3>概要</h3> -<p> -9Pの通信ではクライアントがサーバーに対してリクエストを送り、サーバーがそれに対してリプライを返すことを繰り返す。クライアントからのリクエストをTメッセージ、サーバーからのリプライをRメッセージと言う。Tメッセージにはファイルにアクセスするための色々なものが用意されている。例えばファイルを開くための<code>Topen</code>や、開いたファイルに書き込むための<code>Twrite</code>等である。それぞれのTメッセージには対応するRメッセージがある。<code>Topen</code>に対して<code>Ropen</code>、<code>Twrite</code>に対して<code>Rwrite</code>等である。</p> -<p> -通信は必要な情報をプロトコルの定める方法でバイト列に変換して送受信する。2バイト以上のデータはリトルエンディアンになるように配置する。テキストデータはUTF-8にエンコードする。テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。</p> -<p> -各メッセージは4バイトのサイズから始まる。これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。次にメッセージの種類を記述する1バイトのデータが来る。次はメッセージのタグである。クライアントはサーバーからの返事を待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージに対する返事なのかを識別する必要がある。そのために使うのがタグである。クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに同じタグを付ける。その後ろには、メッセージの種類によって固有の情報が付けられる。</p> -<p> -以下は実際の9P通信のログである。実際はバイナリの通信だが、ログとして見易いように変換してある。また、先頭にくるメッセージサイズは省略してある。この例では、サーバーのルートディレクトリにある<code>hello</code>というファイルの内容を読みこんでいる。:</p> -<pre><code>&lt;-- Tversion Tag 65535 msize 8192 version '9P2000' ---&gt; Rversion Tag 65535 msize 8192 version '9P2000' -&lt;-- Tattach Tag 0 fid 0 afid -1 uname kenji aname ---&gt; Rattach Tag 0 qid (0000000000000000 0 d) -&lt;-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello ---&gt; Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 ) -&lt;-- Tstat Tag 0 fid 1 ---&gt; Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0 -&lt;-- Twalk Tag 0 fid 1 newfid 2 nwname 0 ---&gt; Rwalk Tag 0 nwqid 0 -&lt;-- Topen Tag 0 fid 2 mode 0x0 ---&gt; Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169 -&lt;-- Tread Tag 0 fid 2 offset 0 count 4096 ---&gt; Rread Tag 0 count 7 ' 776f726c 64210a' -&lt;-- Tread Tag 0 fid 2 offset 7 count 4096 ---&gt; Rread Tag 0 count 0 '' -&lt;-- Tclunk Tag 0 fid 2 ---&gt; Rclunk Tag 0 -</code></pre> -<p> -まず最初にクライアントがサーバーに対して<code>Tversion</code>を送り、プロトコルのバージョンと、メッセージの最大サイズを交渉する。<code>Tversion</code>のタグは<code>65535</code>と決められている。<code>msize</code>の8192はメッセージの最大サイズ(バイト)で、<code>version</code>の9P2000がプロトコルのバージョンである。サーバーから<code>Rversion</code>で同じ<code>msize</code>と<code>version</code>が返ってきたのでこのサイズとバージョンで以降の通信を行う。</p> -<p> -次の<code>Tattach</code>で、クライアントがサーバーにルートディレクトリの<code>fid</code>を要求する。<code>afid</code>以降は認証用の情報である(後述)。<code>fid</code>はコネクションにおいてファイルと紐付けられる整数で、Unixにおけるファイルディスクリプタのようなものである。ファイルの読み書きはこの<code>fid</code>を使って行われる。サーバーからの<code>Rattach</code>にはルートディレクトリの<code>qid</code>が含まれる。これはサーバー上でファイルを一意に識別するもので、Unixにおけるinodeに相当するものである。以上でサーバーに繋いでセッションを確立できた。</p> -<p> -次に<code>Twalk</code>で目的のファイル(<code>hello</code>)までファイルツリーを辿る。ここでは起点として先程得られたルートディレクトリ(<code>fid = 1</code>)を起点として、このディレクトリ内の<code>hello</code>ファイルを要求して<code>fid = 2</code>を割り当てようとしている。サーバーはルートディレクトリ内に要求されたファイルが見付かったので、そのファイルの<code>qid</code>を返す。</p> -<p> -目的のファイルに<code>fid</code>が割り当てられたので、<code>Tstat</code>でこのファイルの属性を要求している。おそらくファイルのパーミッションを調べるためである。</p> -<p> -次に<code>Topen</code>を使ってこのファイルを開き、<code>Tread</code>を使って内容を読む。二回目の<code>Tread</code>に対して0バイトのデータが返ってきたので、ファイルはこれで終りである。</p> -<p> -最後に、必要なくなった<code>fid</code>は<code>Tclunk</code>で捨てる。</p> - -<h3>List of all Message Types</h3> - -<table> -<tr> -<th>Tメッセージ</th> -<th>Rメッセージ</th> -<th>説明</th> -</tr> -<tr> -<td>Tversion</td> -<td>Rversion</td> -<td>バージョンの交渉</td> -</tr> -<tr> -<td>Tauth</td> -<td>Rauth</td> -<td>認証ファイルの取得</td> -</tr> -<tr> -<td></td> -<td>Rerror</td> -<td>エラー(Rメッセージのみ)</td> -</tr> -<tr> -<td>Tflush</td> -<td>Rflush</td> -<td>処理中のリクエストをキャンセル</td> -</tr> -<tr> -<td>Tattach</td> -<td>Rattach</td> -<td>ルートディレクトリの取得</td> -</tr> -<tr> -<td>Twalk</td> -<td>Rwalk</td> -<td>ファイルツリーを辿る</td> -</tr> -<tr> -<td>Topen</td> -<td>Ropen</td> -<td>ファイルを開く</td> -</tr> -<tr> -<td>Tcreate</td> -<td>Rcreate</td> -<td>ファイルの作成</td> -</tr> -<tr> -<td>Tread</td> -<td>Rread</td> -<td>ファイルを読む</td> -</tr> -<tr> -<td>Twrite</td> -<td>Rwrite</td> -<td>ファイルへ書き込む</td> -</tr> -<tr> -<td>Tclunk</td> -<td>Rclunk</td> -<td>fidの削除</td> -</tr> -<tr> -<td>Tremove</td> -<td>Rremove</td> -<td>ファイルを削除</td> -</tr> -<tr> -<td>Tstat</td> -<td>Rstat</td> -<td>ファイルの属性を取得</td> -</tr> -<tr> -<td>Twstat</td> -<td>Rwstat</td> -<td>ファイルの属性を変更</td> -</tr> -</table> - -<h2><code>fid</code>と<code>qid</code></h2> -<p> -<code>fid</code>はコネクションにおいてファイルを指定するために使われる整数で、Unixのファイルディスクリプタのようなものである。使用する整数はクライアントが指定できる。サーバーに接続する際に<code>Tattach</code>により、サーバーのルートディレクトリの<code>fid</code>が設定され、<code>Twalk</code>により追加され、<code>Tclunk</code>により削除される。</p> -<p> -<code>qid</code>はサーバーがファイルを一意に識別するためのもので、Unixのinodeに相当するものである。ただしinodeはファイルが削除されると再利用される可能性があるのに対し、<code>qid</code>は再利用できない。<code>qid</code>はこのファイルがディレクトリかどうか等を示す<code>type</code>、ファイルが変更されたときにインクリメントされる<code>vers</code>、そしてファイルを一意に表す<code>path</code>の3つの情報で構成される。</p> -<h2>認証</h2> -<p> -9Pにはクライアントの認証が組込まれていない。もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすればいちいちコードを修正してコンパイルしなおさなければいけないうえ、9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。認証に必要なやりとりは認証サーバーとクライアントが行い、9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することでクライアントの確認を行う。 -</p> -<p> -9Pサーバーは<code>Tattach</code>メッセージを受けとると、認証サーバーとのコネクションを確立する。その後<code>Rattach</code>メッセージで<code>afid</code>という特殊なFidをクライアントに返す。クライアントからこの<code>afid</code>に対して読み書きする命令が届くと、9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。つまりクライアントはこの<code>afid</code>を通じて認証サーバーと直接やりとりができる。クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報を<code>afid</code>に書き込んで認証する。このように認証をプロトコル自体から切り離すことで、認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。また、より強力な認証システムを組み込むのも楽である。 -</p> - -<h2>コネクションの共有</h2> -<p> -クライアントはサーバーと<code>version</code>メッセージを交すことでコネクションを確立する。コネクションの確立からの一連のやりとりをセッションという。ひとつのコネクションは複数のクライアントが共有できる。</p> -<p> -クライアントは<code>Tauth</code>または<code>Tattach</code>メッセージによりサーバーに<code>fid</code>を要求できる。このうち<code>Tauth</code>により得られた<code>fid</code>は認証以外には使えない。<code>Tattach</code>で得られた<code>fid</code>は、<code>Twalk</code>メッセージによりファイルツリーを辿るための起点として利用でき、このとき辿った先のファイルを示す<code>fid</code>が生成される。これ以外の方法で<code>fid</code>が増えることはない。つまり、サーバーはひとつのコネクションのなかで、<code>Tattach</code>から派生した<code>fid</code>を追跡することで、クライアントを区別できる。</p> - -<h2>メッセージ詳細</h2> -<p> -各メッセージの詳細を書く。それぞれ最初にメッセージの形式を書く。<code><i>field</i>[<i>n</i>]</code>は<code><i>field</i></code>という名前の<i>n</i>バイトのデータを表す(<i>n</i>は整数)。また、括弧の中が整数ではなくsになっている場合、そのフィールドは文字列のデータで、その前に文字列の長さを示す2バイトのデータが先行する。</p> -<h3>version</h3> -<p> -プロトコルのバージョンを交渉すし、コネクションを確立する。 -</p> -<pre><code>size[4] Tversion tag[2] msize[4] version[s] -size[4] Rversion tag[2] msize[4] version[s] -</code></pre> -<p> -クライアントは最初にサーバーにこのメッセージを送ってバージョンとメッセージの最大サイズ(<code>msize</code>)を交渉する。サーバーはクライアントから送られたバージョンとメッセージサイズに対応していれば、同じバージョンとサイズを送り返し、交渉成立である。</p> -<p> -サーバーとクライアントはこのメッセージを以ってコネクションを確立する。</p> - -<h3>attach、auth</h3> -<p> -コネクションを確立する。 -</p> -<pre><code>size[4] Tauth tag[2] afid[4] uname[s] aname[s] -size[4] Rauth tag[2] aqid[13] - -size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] -size[4] Rattach tag[2] qid[13] -</code></pre> -<p> -<code>attach</code>メッセージはサーバーのルートディレクトリを要求し、<code>fid</code>を割り当てる。認証が必要なサーバーであれば、先に<code>auth</code>メッセージで認証を済ませておき、<code>attach</code>メッセージの<code>afid</code>に、先程使用した<code>afid</code>をセットする。サーバーはこの<cdoe>afid</code>に紐付いている認証サーバーにクライアントが認証済みであるかを確認し、認証済みであればルートディレクトリの<code>qid</code>を返す。サーバーが複数のファイルツリーを公開している場合、<code>aname</code>で指定する。</p> -<p> -<code>auth</code>メッセージは認証サーバーとやりとりするための<code>fid</code>である<code>afid</code>を要求する。この<code>afid</code>を読み書きすることで認証サーバーとやりとりをし、自身を身分を証明する。認証が済んだらこの<code>afid</code>をセットした<code>attach</code>メッセージを送る。</p> - -<h3>error</h3> -<p> -エラーを返す。 -</p> -<pre><code>size[4] Rerror tag[2] ename[s] -</code></pre> -<p> -クライアントからの要求に対して、なにかエラーがおきたときにそのエラーを返すために使われる。そのためサーバーからクライアントへの<code>Rerror</code>はあるが逆向きのTメッセージはない。</p> - -<h3>flush</h3> -<p> -リクエストをキャンセルする。 -</p> -<pre><code>size[4] Tflush tag[2] oldtag[2] -size[4] Rflush tag[2] -</code></pre> -<p> -実行中の要求をキャンセルする。キャンセルしたいリクエストの<code>tag</code>を<code>oldtag</code>にセットする。 -</p> - -<h3>walk</h3> -<p> -ファイルツリーを移動する。 -</p> -<pre><code>size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) -size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13]) -</code></pre> -<p> -<code>fid</code>を起点に移動する。移動先のファイルを<code>newfid</code>にセットする。移動するディレクトリの数を<code>nwname</code>に、移動するディレクトリの名前を移動する順に<code>wname</code>にそれぞれセットする。例えばカレントディレクトリから<code>./dir1/dir2/</code>に移動する場合、<code>nwname</code>は<code>2</code>、<code>wname</code>は<code>{"dir1", "dir2"}</code>となる。<code>wname</code>の最後の要素はディレクトリでなくファイルでもいい。</p> -<p> -<code>wname</code>の最初の要素への移動が失敗した場合は<code>Rerror</code>が返される。それ以外の場合は<code>Rwalk</code>が返され、<code>qid</code>は移動が成功した順番にそのファイルの<code>qid</code>がセットされる。<code>nwname</code>と<code>nwqid</code>が一致した場合は最後まで移動できたことになる。</p> -<p> -<code>walk</code>は<code>fid</code>を増殖させる唯一の方法である。</p> - -<h3>open、create</h3> -<p> -<code>fid</code>を開いて(作成して)読み書きできる状態にする。 -</p> -<pre><code>size[4] Topen tag[2] fid[4] mode[1] -size[4] Ropen tag[2] qid[13] iounit[4] - -size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] -size[4] Rcreate tag[2] qid[13] iounit[4] -</code></pre> -<p> -<code>open</code>は<code>fid</code>と紐付いたファイルを開く。<code>mode</code>は読み込み専用の<code>OREAD</code>、書き込み専用の<code>OWRITE</code>、読み書きの<code>ORDWR</code>等である。ファイルを開く手順は以下の通り: -<ol> -<li><code>attach</code>でルートディレクトリの<code>fid</code>を取得</li> -<li>ルートディレクトリの<code>fid</code>から<code>walk</code>で開きたいファイルの<code>fid</code>を取得</li> -<li>その<code>fid</code>を<code>open</code>で開く</li> -</ol> -</p> -<p> -<code>create</code>は<code>fid</code>と紐付いたディレクトリに新しいファイルを作成する。<code>creat</code>ではなく<code>create</code>である。作成が成功したら<code>fid</code>は新しく作成されたファイルに紐付く。</p> - -<h3>read、write</h3> -<p> -ファイルを読み書きする。 -</p> -<pre><code>size[4] Tread tag[2] fid[4] offset[8] count[4] -size[4] Rread tag[2] count[4] data[count] - -size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] -size[4] Rwrite tag[2] count[4] -</code></pre> -<p> -<code>read</code>は<code>fid</code>の<code>offset</code>バイト目から<code>count</code>バイト読む。読んだデータと、読めたデータサイズが<code>data</code>、<code>count</code>ととして返される。<code>write</code>は<code>fid</code>の<code>offset</code>バイト目に<code>count</code>バイトのデータ<code>data</code>を書く。書きこめたサイズが<code>count</code>として返される。ファイルを読み書きするためにはあらかじめ<code>fid</code>を<code>open</code>する必要がある。</p> - -<h3>clunk</h3> -<p> -<code>fid</code>を忘れる。 -</p> -<pre><code>size[4] Tclunk tag[2] fid[4] -size[4] Rclunk tag[2] -</code></pre> -<p> -いらなくなった<code>fid</code>を忘れる。一度忘れた<code>fid</code>は<code>walk</code>で別のファイルを取得する際に再利用できる。</p> - -<h3>remove</h3> -<p> -ファイルを削除する。 -</p> -<pre><code>size[4] Tremove tag[2] fid[4] -size[4] Rremove tag[2] -</code></pre> -<p> -<code>fid</code>と紐付いたサーバー上のファイルを削除する。<code>fid</code>は<code>clunk</code>したのと同様に忘れられる。</p> - -<h3>stat、wstat</h3> -<p> -ファイルの属性を読み書きする。 -</p> -<pre><code>size[4] Tstat tag[2] fid[4] -size[4] Rstat tag[2] stat[n] - -size[4] Twstat tag[2] fid[4] stat[n] -size[4] Rwstat tag[2] -</code></pre> -<p> -<code>stat</code>はファイルの属性を読む。読めた情報は<code>stat</code>として返される。<code>wstat</code>はファイルの属性を<code>stat</code>に変更する。<code>stat</code>はファイルの名前や<code>qid</code>、サイズ、作成日、更新日等の情報がバイト列になったものである。</p> - -<h2>Goによる実装</h2> -<h3>メッセージの構造体の定義</h3> -<p> -メッセージには<code>size</code>、<code>tag</code>等共通の要素がある。クライアントからメッセージを受けとる際、<code>size</code>を参照して何バイト読むか判断し、メッセージの<code>type</code>によりその後の処理を場合分けする。そのためにメッセージを<code>interface</code>として定義した: -</p> -<pre><code>// A Msg represents any kind of message of 9P. -// It defines methods for common fields. -// For each message type &lt;T&gt;, new&lt;T&gt;([]byte) function parses the byte array -// of 9P message into the corresponding message struct. -// For detailed information on each message, consult the documentation -// of 9P protocol. -type Msg interface { - // Size returns the size field of message. - // Size field holds the size of the message in bytes - // including the 4-byte size field itself. - Size() uint32 - // Type returns the type field of message. - Type() MsgType - // GetTag returns the Tag of message. - // Tag is the identifier of each message. - // The Get prefix is to avoid name confliction with the each - // message's Tag field. - GetTag() uint16 - // SetTag sets the Tag field of the message. - SetTag(uint16) - // Marshal convert Msg to byte array to be transmitted. - marshal() []byte - String() string -} -</code></pre> -<p> -<code>marshal()</code>はサーバーから返信を送る際にメッセージの構造体をバイト列にエンコードするための関数である。<code>String() string</code>はログ出力用。</p> -<p> -メッセージはバイト列として受け取った後、メッセージのタイプによって処理を分ける。その際、扱いやすいようにバイト列を各メッセージの構造体に変換する。変換前のバイト列から、各メッセージに共通のフィールドを取得できると便利なので、バイト列のまま<code>Msg</code>インターフェースを実装する<code>bufMsg</code>を定義した。名前はいまいち。</p> -<pre><code>type bufMsg []byte - -func (msg bufMsg) Size() uint32 { return gbit32(msg[0:4]) } -func (msg bufMsg) Type() MsgType { return MsgType(msg[4]) } -func (msg bufMsg) GetTag() uint16 { return gbit16(msg[5:7]) } -func (msg bufMsg) SetTag(t uint16) { pbit16(msg[5:7], t) } -func (msg bufMsg) marshal() []byte { return []byte(msg)[:msg.Size()] } -func (msg bufMsg) String() string { - switch msg.Type() { - case Tversion: - return newTVersion(msg).String() - /* 省略 */ - } -} -</code></pre> -<p> -<code>gbit32</code>は4バイトのリトルエンディアンの配列を整数に変換するもので、<code>pbit32</code>は逆に整数をリトルエンディアンのスライスに格納するものである。</p> -<p> -各メッセージはそれぞれのフィールドを構造体のフィールドとして定義したものである。例えば<code>tversion</code>に対応する構造体は以下の通り。</p> -<pre><code>type TVersion struct { - Tag uint16 - Msize uint32 - Version string -} -</code></pre> -<p> -バイト列から各メッセージの構造体に変換するために<code>new<i>メッセージタイプ</i></code>を定義した。 -</p> -<pre><code>func newTVersion(buf []byte) *TVersion { - msg := new(TVersion) - msg.Tag = gbit16(buf[5:7]) - msg.Msize = gbit32(buf[7:11]) - vs := gbit16(buf[11:13]) - msg.Version = string(buf[13 : 13+vs]) - return msg -} -</code></pre> - -<h3>メッセージのやりとり</h3> -<p> -まずは9Pメッセージを読む関数を実装する。バイト列が読めればいいので引数は<code>io.Reader</code>にした: -</p> -<pre><code>func readMsg(r io.Reader) ([]byte, error) { -</code></pre> -<p> -最初にメッセージのサイズを読む: -</p> -<pre><code> buf := make([]byte, 4) - read, err := r.Read(buf) - if err != nil { - if err == io.EOF { - return nil, err - } - return nil, fmt.Errorf(&quot;read size: %v&quot;, err) - } - if read != len(buf) { - return buf, fmt.Errorf(&quot;read size: invalid message.&quot;) - } - size := bufMsg(buf).Size() -</code></pre> -<p> -続いて読みこんだサイズに基づいてメッセージの残りの部分を読む。 -一回の<code>Read</code>で最後まで読めないことがあったので、 -全部読めるまで<code>Read</code>を繰り返す: -</p> -<pre><code> mbuf := make([]byte, size-4) - for read = 0; read &lt; int(size)-4; { - n, err := r.Read(mbuf[read:]) - if err != nil { - return buf, fmt.Errorf(&quot;read body: %v&quot;, err) - } - read += n - } - buf = append(buf, mbuf...) - return buf, nil -} -</code></pre> -<p> -読み込んだバイト列は<code>ReadMsg</code>関数でメッセージの構造体に変換する。</p> -<pre><code>RecvMsg(r io.Reader) (Msg, error) { - b, err := readMsg(r) - if err == io.EOF { - return nil, err - } else if err != nil { - return nil, fmt.Errorf(&quot;readMsg: %v&quot;, err) - } - return unmarshal(b) -} -</code></pre> -<p> -返信のメッセージはメッセージの構造体を<code>marshal</code>関数でバイト列に変換して<code>io.Writer</code>に書き込む。</p> -<pre><code>// SendMsg send a 9P message to w -func SendMsg(msg Msg, w io.Writer) error { - if _, err := w.Write(msg.marshal()); err != nil { - return fmt.Errorf(&quot;write: %v&quot;, err) - } - return nil -} -</code></pre> - -<h3>メインループ</h3> -<p> -ライブラリはメッセージを読み込むためのリスナーgoroutinと返信を書き込むためのレスポンダーgoroutine、そして各メッセージを処理するためのgoroutineを立ち上げて、チャネルでそれぞれを繋ぐ。リスナーがメッセージを読み込むと、<code>request</code>構造体に格納し、メッセージのタイプに応じて各goroutineに渡す。渡されたgoroutineはメッセージに応じた処理をし、返信のメッセージをリスポンダーgoroutineに渡す。リスポンダーgoroutineはメッセージをバイト列に変換して返信する。 -</p> -<p> -この設計がいいかどうかよく知らんけど、Go言語を触るからgoroutineとチャネルによるパイプラインを作ってみたかった。</p> - -<h3>各メッセージの処理</h3> -<p> -メッセージは<code>s<i>メッセージタイプ</i></code>goroutineで処理する。関数は<code>for</code>ループのなかでチャンネル<code>rc</code>から<code>request</code>が届くのを待つ。届いたリクエストには<code>ifcall</code>フィールドにクライアントからの<code>Msg</code>が含まれるので、この関数が担当するstructにキャストする。このキャストが失敗するのは明かなサーバーのバグなのでタイプアサーションはしない。</p> -<pre><code>func s<i>メッセージタイプ</i>(ctx context.Context, c *conn, rc &lt;-chan *request) { - for { - select { - case &lt;-ctx.Done(); - return - case r, ok := rc: - if !ok { - return - } - ifcall := r.ifcall.(*T<i>メッセージタイプ</i>) -</code></pre> -<p> -各種処理を完了したら、届いた<code>request</code>の<code>ofcall</code>に返信の<code>Msg</code>を格納して返信を担当するgoroutineに送る。<pre><code> select { - case c.respChan &lt;- r: - case &lt;-ctx.Done(): - return - } -</code></pre> -</p> -<h4>Version</h4> -<p> -クライアントから届いたバージョンの提案を見て、頭に"9P2000"があれば返信のバージョンを"9P2000"にする。現在定義されているバージョンは"9P2000"の他、Unix用に拡張した"9P2000.u"、Linux用に拡張した"9P2000.l"がある。基本的な昨日を実装することが目標なので、"9P2000"のみを受け付ける。</p> -<p> -Versionメッセージではメッセージの最大サイズも決定する。サーバーは予め8Kバイトを最大サイズにしている。クライアントがこれ以上のサイズを提案した場合、8Kにしてもらい、これ以下のサイズを提案した場合、そのサイズでメッセージをやりとりする。8Kバイトという既定値はplan9の実装を参考にした。このサイズである必然性はよく知らない。</p> -<pre><code> version := ifcall.Version - if strings.HasPrefix(version, &quot;9P2000&quot;) { - version = &quot;9P2000&quot; - } else { - version = &quot;unknown&quot; - } - msize := ifcall.Msize - if msize &gt; c.mSize() { - msize = c.mSize() - } - r.ofcall = &amp;RVersion{ - Msize: msize, - Version: version, - } - c.setMSize(r.ofcall.(*RVersion).Msize) -</code></pre> - -<h4>Auth</h4> -<p> -サーバーの認証は<code>Server</code>の<code>Auth</code>フィールドに認証のためのやりとりを待ち受ける関数を登録することで有効化できる。このフィールドが<code>nil</code>の場合、認証が必要ない旨のエラーを返す。</p> -<pre><code> if c.s.Auth == nil { - r.err = fmt.Errorf(&quot;authentication not required&quot;) - goto resp - } -</code></pre> -<p> -認証が必要な場合、クライアントから提案された<code>afid</code>をコネクションに登録して、<code>Server.Auth</code>を呼び出す。 -</p> - -<h4>attach</h4> -<p> -サーバーはまず認証が必要な場合認証が済んでいるか確認する。</p> -<pre><code> switch { - case c.s.Auth == nil &amp;&amp; ifcall.Afid == NOFID: - case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID: - r.err = ErrBotch - goto resp - case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID: - r.err = fmt.Errorf(&quot;authentication required&quot;) - goto resp - case c.s.Auth != nil &amp;&amp; ifcall.Afid != NOFID: - afid, ok := c.fPool.lookup(ifcall.Afid) - if !ok { - r.err = ErrUnknownFid - goto resp - } - af, ok := afid.file.(*AuthFile) - if !ok { - r.err = fmt.Errorf(&quot;not auth file&quot;) - goto resp - } - if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK { - r.err = fmt.Errorf(&quot;not authenticated&quot;) - goto resp - } - } -</code></pre> -<p> -次にクライアントが要求してきたファイルツリーがサーバーにあるかどうか確認し、クライアントが設定した<code>fid</code>にルートディレクトリを紐付ける。</p> -<pre><code> r.fid, err = c.fPool.add(ifcall.Fid) - if err != nil { - r.err = ErrDupFid - goto resp - } - r.fid.path = &quot;.&quot; - r.fid.uid = ifcall.Uname - fsys, ok = c.s.fsmap[ifcall.Aname] - if !ok { - r.err = fmt.Errorf(&quot;no such file system&quot;) - goto resp - } - r.fid.fs = fsys - fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;) - if err != nil { - r.err = fmt.Errorf(&quot;stat root: %v&quot;, err) - goto resp - } - r.fid.qidpath = fi.Sys().(*Stat).Qid.Path - r.ofcall = &amp;RAttach{ - Qid: fi.Sys().(*Stat).Qid, - } -</code></pre> - -<h4>flush</h4> -<p> -指定されたリクエストの<code>flush</code>メソッドを呼び出す。<code>flush</code>メソッドはリクエストの<code>done</code>チャンネルを閉じ、リクエストのタグを削除する。処理に時間のかかるものは、リクエストの<code>done</code>チャネルが閉じられたら、その処理を中止する。</p> -<pre><code>// flush cancels the request by calling r.cancel. -// It also delete the request from its pool. -func (r *request) flush() { - close(r.done) - r.pool.delete(r.tag) -} -</code></pre> - -<h4>walk</h4> -<p> -仕様に書かれている通りの条件を確認した後、ファイルツリーを順番に辿り、得られた<code>Qid</code>を記録してクライアントに返す。</p> -<pre><code> wqids = make([]Qid, 0, len(ifcall.Wnames)) - cwdp = oldFid.path - for _, name := range ifcall.Wnames { - cwdp = path.Clean(path.Join(cwdp, name)) - if cwdp == &quot;..&quot; { - cwdp = &quot;.&quot; // parent of the root is itself. - } - stat, err := fs.Stat(ExportFS{oldFid.fs}, cwdp) - if err != nil { - break - } - wqids = append(wqids, stat.Sys().(*Stat).Qid) - } - if len(wqids) == 0 { - newFid.qidpath = oldFid.qidpath - } else { - newFid.qidpath = wqids[len(wqids)-1].Path - } - newFid.path = cwdp - newFid.uid = oldFid.uid - newFid.fs = oldFid.fs - r.ofcall = &amp;RWalk{ - Qids: wqids, - } -</code></pre> - -<h4>open</h4> -<h4>create</h4> -<h4>read</h4> -<h4>write</h4> -<h4>clunk</h4> -<h4>remove</h4> -<h4>stat</h4> -<h4>wstat</h4> - </article> - - </main> - <footer> - <address>info(at)mtkn(dot)jp</address> - <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> - </footer> -</body> -</html> diff --git a/pub/draft/9p_go.html b/pub/draft/9p_go.html @@ -0,0 +1,334 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <link rel="stylesheet" type="text/css" href="/style.css"> + <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> + <title>9PのGoによる実装</title> +</head> +<body> + <header> + <a href="/">主頁</a> | + <a href="/about.html">自己紹介</a> | + <a href="/journal">日記</a> | + <a href="/farm">農業</a> | + <a href="/kitchen">台所</a> | + <a href="/computer">電算機</a> | + <a href="/poetry">詩</a> | + <a href="/books">本棚</a> | + <a href="/gallery">絵</a> | + <a href="https://git.mtkn.jp">Git</a> + </header> + <main> + <article> +<h1>9PのGoによる実装</h1> +<time>2024-12-18</time> + +<h2>メッセージの構造体の定義</h2> +<p> +メッセージには<code>size</code>、<code>tag</code>等共通の要素がある。クライアントからメッセージを受けとる際、<code>size</code>を参照して何バイト読むか判断し、メッセージの<code>type</code>によりその後の処理を場合分けする。そのためにメッセージを<code>interface</code>として定義した: +</p> +<pre><code>// A Msg represents any kind of message of 9P. +// It defines methods for common fields. +// For each message type &lt;T&gt;, new&lt;T&gt;([]byte) function parses the byte array +// of 9P message into the corresponding message struct. +// For detailed information on each message, consult the documentation +// of 9P protocol. +type Msg interface { + // Size returns the size field of message. + // Size field holds the size of the message in bytes + // including the 4-byte size field itself. + Size() uint32 + // Type returns the type field of message. + Type() MsgType + // GetTag returns the Tag of message. + // Tag is the identifier of each message. + // The Get prefix is to avoid name confliction with the each + // message's Tag field. + GetTag() uint16 + // SetTag sets the Tag field of the message. + SetTag(uint16) + // Marshal convert Msg to byte array to be transmitted. + marshal() []byte + String() string +} +</code></pre> +<p> +<code>marshal()</code>はサーバーから返信を送る際にメッセージの構造体をバイト列にエンコードするための関数である。<code>String() string</code>はログ出力用。</p> +<p> +メッセージはバイト列として受け取った後、メッセージのタイプによって処理を分ける。その際、扱いやすいようにバイト列を各メッセージの構造体に変換する。変換前のバイト列から、各メッセージに共通のフィールドを取得できると便利なので、バイト列のまま<code>Msg</code>インターフェースを実装する<code>bufMsg</code>を定義した。名前はいまいち。</p> +<pre><code>type bufMsg []byte + +func (msg bufMsg) Size() uint32 { return gbit32(msg[0:4]) } +func (msg bufMsg) Type() MsgType { return MsgType(msg[4]) } +func (msg bufMsg) GetTag() uint16 { return gbit16(msg[5:7]) } +func (msg bufMsg) SetTag(t uint16) { pbit16(msg[5:7], t) } +func (msg bufMsg) marshal() []byte { return []byte(msg)[:msg.Size()] } +func (msg bufMsg) String() string { + switch msg.Type() { + case Tversion: + return newTVersion(msg).String() + /* 省略 */ + } +} +</code></pre> +<p> +<code>gbit32</code>は4バイトのリトルエンディアンの配列を整数に変換するもので、<code>pbit32</code>は逆に整数をリトルエンディアンのスライスに格納するものである。</p> +<p> +各メッセージはそれぞれのフィールドを構造体のフィールドとして定義したものである。例えば<code>tversion</code>に対応する構造体は以下の通り。</p> +<pre><code>type TVersion struct { + Tag uint16 + Msize uint32 + Version string +} +</code></pre> +<p> +バイト列から各メッセージの構造体に変換するために<code>new<i>メッセージタイプ</i></code>を定義した。 +</p> +<pre><code>func newTVersion(buf []byte) *TVersion { + msg := new(TVersion) + msg.Tag = gbit16(buf[5:7]) + msg.Msize = gbit32(buf[7:11]) + vs := gbit16(buf[11:13]) + msg.Version = string(buf[13 : 13+vs]) + return msg +} +</code></pre> + +<h2>メッセージのやりとり</h2> +<p> +まずは9Pメッセージを読む関数を実装する。バイト列が読めればいいので引数は<code>io.Reader</code>にした: +</p> +<pre><code>func readMsg(r io.Reader) ([]byte, error) { +</code></pre> +<p> +最初にメッセージのサイズを読む: +</p> +<pre><code> buf := make([]byte, 4) + read, err := r.Read(buf) + if err != nil { + if err == io.EOF { + return nil, err + } + return nil, fmt.Errorf(&quot;read size: %v&quot;, err) + } + if read != len(buf) { + return buf, fmt.Errorf(&quot;read size: invalid message.&quot;) + } + size := bufMsg(buf).Size() +</code></pre> +<p> +続いて読みこんだサイズに基づいてメッセージの残りの部分を読む。 +一回の<code>Read</code>で最後まで読めないことがあったので、 +全部読めるまで<code>Read</code>を繰り返す: +</p> +<pre><code> mbuf := make([]byte, size-4) + for read = 0; read &lt; int(size)-4; { + n, err := r.Read(mbuf[read:]) + if err != nil { + return buf, fmt.Errorf(&quot;read body: %v&quot;, err) + } + read += n + } + buf = append(buf, mbuf...) + return buf, nil +} +</code></pre> +<p> +読み込んだバイト列は<code>ReadMsg</code>関数でメッセージの構造体に変換する。</p> +<pre><code>RecvMsg(r io.Reader) (Msg, error) { + b, err := readMsg(r) + if err == io.EOF { + return nil, err + } else if err != nil { + return nil, fmt.Errorf(&quot;readMsg: %v&quot;, err) + } + return unmarshal(b) +} +</code></pre> +<p> +返信のメッセージはメッセージの構造体を<code>marshal</code>関数でバイト列に変換して<code>io.Writer</code>に書き込む。</p> +<pre><code>// SendMsg send a 9P message to w +func SendMsg(msg Msg, w io.Writer) error { + if _, err := w.Write(msg.marshal()); err != nil { + return fmt.Errorf(&quot;write: %v&quot;, err) + } + return nil +} +</code></pre> + +<h2>メインループ</h2> +<p> +ライブラリはメッセージを読み込むためのリスナーgoroutinと返信を書き込むためのレスポンダーgoroutine、そして各メッセージを処理するためのgoroutineを立ち上げて、チャネルでそれぞれを繋ぐ。リスナーがメッセージを読み込むと、<code>request</code>構造体に格納し、メッセージのタイプに応じて各goroutineに渡す。渡されたgoroutineはメッセージに応じた処理をし、返信のメッセージをリスポンダーgoroutineに渡す。リスポンダーgoroutineはメッセージをバイト列に変換して返信する。 +</p> +<p> +この設計がいいかどうかよく知らんけど、Go言語を触るからgoroutineとチャネルによるパイプラインを作ってみたかった。</p> + +<h2>各メッセージの処理</h2> +<p> +メッセージは<code>s<i>メッセージタイプ</i></code>goroutineで処理する。関数は<code>for</code>ループのなかでチャンネル<code>rc</code>から<code>request</code>が届くのを待つ。届いたリクエストには<code>ifcall</code>フィールドにクライアントからの<code>Msg</code>が含まれるので、この関数が担当するstructにキャストする。このキャストが失敗するのは明かなサーバーのバグなのでタイプアサーションはしない。</p> +<pre><code>func s<i>メッセージタイプ</i>(ctx context.Context, c *conn, rc &lt;-chan *request) { + for { + select { + case &lt;-ctx.Done(); + return + case r, ok := rc: + if !ok { + return + } + ifcall := r.ifcall.(*T<i>メッセージタイプ</i>) +</code></pre> +<p> +各種処理を完了したら、届いた<code>request</code>の<code>ofcall</code>に返信の<code>Msg</code>を格納して返信を担当するgoroutineに送る。<pre><code> select { + case c.respChan &lt;- r: + case &lt;-ctx.Done(): + return + } +</code></pre> +</p> +<h3>Version</h3> +<p> +クライアントから届いたバージョンの提案を見て、頭に"9P2000"があれば返信のバージョンを"9P2000"にする。現在定義されているバージョンは"9P2000"の他、Unix用に拡張した"9P2000.u"、Linux用に拡張した"9P2000.l"がある。基本的な昨日を実装することが目標なので、"9P2000"のみを受け付ける。</p> +<p> +Versionメッセージではメッセージの最大サイズも決定する。サーバーは予め8Kバイトを最大サイズにしている。クライアントがこれ以上のサイズを提案した場合、8Kにしてもらい、これ以下のサイズを提案した場合、そのサイズでメッセージをやりとりする。8Kバイトという既定値はplan9の実装を参考にした。このサイズである必然性はよく知らない。</p> +<pre><code> version := ifcall.Version + if strings.HasPrefix(version, &quot;9P2000&quot;) { + version = &quot;9P2000&quot; + } else { + version = &quot;unknown&quot; + } + msize := ifcall.Msize + if msize &gt; c.mSize() { + msize = c.mSize() + } + r.ofcall = &amp;RVersion{ + Msize: msize, + Version: version, + } + c.setMSize(r.ofcall.(*RVersion).Msize) +</code></pre> + +<h3>Auth</h3> +<p> +サーバーの認証は<code>Server</code>の<code>Auth</code>フィールドに認証のためのやりとりを待ち受ける関数を登録することで有効化できる。このフィールドが<code>nil</code>の場合、認証が必要ない旨のエラーを返す。</p> +<pre><code> if c.s.Auth == nil { + r.err = fmt.Errorf(&quot;authentication not required&quot;) + goto resp + } +</code></pre> +<p> +認証が必要な場合、クライアントから提案された<code>afid</code>をコネクションに登録して、<code>Server.Auth</code>を呼び出す。 +</p> + +<h3>attach</h3> +<p> +サーバーはまず認証が必要な場合認証が済んでいるか確認する。</p> +<pre><code> switch { + case c.s.Auth == nil &amp;&amp; ifcall.Afid == NOFID: + case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID: + r.err = ErrBotch + goto resp + case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID: + r.err = fmt.Errorf(&quot;authentication required&quot;) + goto resp + case c.s.Auth != nil &amp;&amp; ifcall.Afid != NOFID: + afid, ok := c.fPool.lookup(ifcall.Afid) + if !ok { + r.err = ErrUnknownFid + goto resp + } + af, ok := afid.file.(*AuthFile) + if !ok { + r.err = fmt.Errorf(&quot;not auth file&quot;) + goto resp + } + if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK { + r.err = fmt.Errorf(&quot;not authenticated&quot;) + goto resp + } + } +</code></pre> +<p> +次にクライアントが要求してきたファイルツリーがサーバーにあるかどうか確認し、クライアントが設定した<code>fid</code>にルートディレクトリを紐付ける。</p> +<pre><code> r.fid, err = c.fPool.add(ifcall.Fid) + if err != nil { + r.err = ErrDupFid + goto resp + } + r.fid.path = &quot;.&quot; + r.fid.uid = ifcall.Uname + fsys, ok = c.s.fsmap[ifcall.Aname] + if !ok { + r.err = fmt.Errorf(&quot;no such file system&quot;) + goto resp + } + r.fid.fs = fsys + fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;) + if err != nil { + r.err = fmt.Errorf(&quot;stat root: %v&quot;, err) + goto resp + } + r.fid.qidpath = fi.Sys().(*Stat).Qid.Path + r.ofcall = &amp;RAttach{ + Qid: fi.Sys().(*Stat).Qid, + } +</code></pre> + +<h3>flush</h3> +<p> +指定されたリクエストの<code>flush</code>メソッドを呼び出す。<code>flush</code>メソッドはリクエストの<code>done</code>チャンネルを閉じ、リクエストのタグを削除する。処理に時間のかかるものは、リクエストの<code>done</code>チャネルが閉じられたら、その処理を中止する。</p> +<pre><code>// flush cancels the request by calling r.cancel. +// It also delete the request from its pool. +func (r *request) flush() { + close(r.done) + r.pool.delete(r.tag) +} +</code></pre> + +<h3>walk</h3> +<p> +仕様に書かれている通りの条件を確認した後、ファイルツリーを順番に辿り、得られた<code>Qid</code>を記録してクライアントに返す。</p> +<pre><code> wqids = make([]Qid, 0, len(ifcall.Wnames)) + cwdp = oldFid.path + for _, name := range ifcall.Wnames { + cwdp = path.Clean(path.Join(cwdp, name)) + if cwdp == &quot;..&quot; { + cwdp = &quot;.&quot; // parent of the root is itself. + } + stat, err := fs.Stat(ExportFS{oldFid.fs}, cwdp) + if err != nil { + break + } + wqids = append(wqids, stat.Sys().(*Stat).Qid) + } + if len(wqids) == 0 { + newFid.qidpath = oldFid.qidpath + } else { + newFid.qidpath = wqids[len(wqids)-1].Path + } + newFid.path = cwdp + newFid.uid = oldFid.uid + newFid.fs = oldFid.fs + r.ofcall = &amp;RWalk{ + Qids: wqids, + } +</code></pre> + +<h3>open</h3> +<h3>create</h3> +<h3>read</h3> +<h3>write</h3> +<h3>clunk</h3> +<h3>remove</h3> +<h3>stat</h3> +<h3>wstat</h3> + </article> + + </main> + <footer> + <address>info(at)mtkn(dot)jp</address> + <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> + </footer> +</body> +</html> diff --git a/pub/index.html b/pub/index.html @@ -49,13 +49,6 @@ <li>2023-10-24 <a href="/gallery/index.html">絵を追加した</a></li> <li>2023-10-07 <a href="/gallery/20231007.html">絵を描いた</a></li> <li>2023-08-10 <a href="/journal/posts/20230810.html">イソヒヨ</a></li> -<li>2023-08-03 <a href="/journal/posts/20230803.html">サマータイムレンダ読んだ</a></li> -<li>2023-08-02 <a href="/journal/posts/20230802.html">着物できた</a></li> -<li>2023-07-29 <a href="/journal/posts/20230729.html">無題</a></li> -<li>2023-07-26 <a href="/journal/posts/20230726.html">GOPL読んだ</a></li> -<li>2023-07-25 <a href="/journal/posts/20230725.html">水道と空調</a></li> -<li>2023-05-02 <a href="/computer/what-i-use.html">使用しているハードウェア、ソフトウェア</a></li> -<li>2023-04-25 <a href="/computer/rp2040_1.html">RP2040 SDKなしでLチカ</a></li> </ul> </article> diff --git a/pub/rss.xml b/pub/rss.xml @@ -5,10 +5,303 @@ <description>ウェブページの更新履歴</description> <language>ja-jp</language> <link>https://www.mtkn.jp</link> -<lastBuildDate>Wed, 11 Dec 2024 20:38:28 +0900</lastBuildDate> -<pubDate>Wed, 11 Dec 2024 20:38:28 +0900</pubDate> +<lastBuildDate>Wed, 18 Dec 2024 20:43:17 +0900</lastBuildDate> +<pubDate>Wed, 18 Dec 2024 20:43:17 +0900</pubDate> <docs>https://www.rssboard.org/rss-specification</docs> <item> +<title>9P</title> +<link>https://www.mtkn.jp/computer/9p.html</link> +<guid>https://www.mtkn.jp/computer/9p.html</guid> +<pubDate>Wed, 18 Dec 2024 00:00:00 +0900</pubDate> +<description><![CDATA[<h1>9P</h1> +<time>2024-10-27</time> +<h2>はじめに</h2> +<p> +9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。このプロトコルを使えば、ディスクに記録されたデータだけでなく、マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等もファイルとして扱える。Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOSのために設計された。 +</p> + +<p> +Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計されているので、この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。</p> + +<p> +便利そうなのでGo言語で実装してみた。 +</p> + +<h2>9Pプロトコル</h2> +<h3>概要</h3> +<p> +9Pの通信ではクライアントがサーバーに対してリクエストを送り、サーバーがそれに対してリプライを返すことを繰り返す。クライアントからのリクエストをTメッセージ、サーバーからのリプライをRメッセージと言う。Tメッセージにはファイルにアクセスするための色々なものが用意されている。例えばファイルを開くためのTopenや、開いたファイルに書き込むためのTwrite等である。それぞれのTメッセージには対応するRメッセージがある。Topenに対してRopen、Twriteに対してRwrite等である。</p> +<p> +通信は必要な情報をプロトコルの定める方法でバイト列に変換して行う。2バイト以上のデータはリトルエンディアンになるように配置する。テキストデータはUTF-8にエンコードする。テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。</p> +<p> +各メッセージは4バイトの整数から始まる。これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。次にメッセージの種類を記述する1バイトのデータが来る。次はメッセージのタグである。クライアントはサーバーからの返事を待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージに対する返事なのかを識別する必要がある。そのために使うのがタグである。クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに同じタグを付ける。その後ろには、メッセージの種類によって固有の情報が付けられる。</p> +<p> +以下は実際の9P通信のログである。実際はバイナリの通信だが、ログとして見易いように変換してある。また、先頭にくるメッセージサイズは省略してある。この例では、サーバーのルートディレクトリにある<code>hello</code>というファイルの内容を読みこんでいる。:</p> +<pre><code>&lt;-- Tversion Tag 65535 msize 8192 version '9P2000' +--&gt; Rversion Tag 65535 msize 8192 version '9P2000' +&lt;-- Tattach Tag 0 fid 0 afid -1 uname kenji aname +--&gt; Rattach Tag 0 qid (0000000000000000 0 d) +&lt;-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello +--&gt; Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 ) +&lt;-- Tstat Tag 0 fid 1 +--&gt; Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0 +&lt;-- Twalk Tag 0 fid 1 newfid 2 nwname 0 +--&gt; Rwalk Tag 0 nwqid 0 +&lt;-- Topen Tag 0 fid 2 mode 0x0 +--&gt; Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169 +&lt;-- Tread Tag 0 fid 2 offset 0 count 4096 +--&gt; Rread Tag 0 count 7 ' 776f726c 64210a' +&lt;-- Tread Tag 0 fid 2 offset 7 count 4096 +--&gt; Rread Tag 0 count 0 '' +&lt;-- Tclunk Tag 0 fid 2 +--&gt; Rclunk Tag 0 +</code></pre> +<p> +まず最初にクライアントがサーバーに対してTversionを送り、プロトコルのバージョンと、メッセージの最大サイズを交渉する。Tversionのタグは<code>65535</code>と決められている。<code>msize</code>の8192はメッセージの最大サイズ(バイト)で、<code>version</code>の9P2000がプロトコルのバージョンである。サーバーからRversionで同じ<code>msize</code>と<code>version</code>が返ってきたのでこのサイズとバージョンで以降の通信を行う。</p> +<p> +次のTattachで、クライアントがサーバーにルートディレクトリの<code>fid</code>を要求する。<code>afid</code>以降は認証用の情報である(後述)。<code>fid</code>はコネクションにおいてファイルと紐付けられる整数で、Unixにおけるファイルディスクリプタのようなものである。ファイルの読み書きはこの<code>fid</code>を使って行われる。サーバーからの<code>Rattach</code>にはルートディレクトリの<code>qid</code>が含まれる。これはサーバー上でファイルを一意に識別するもので、Unixにおけるinodeに相当するものである。以上でサーバーに繋いでセッションを確立できた。</p> +<p> +次にTwalkで目的のファイル(<code>hello</code>)までファイルツリーを辿る。ここでは起点として先程得られたルートディレクトリ(<code>fid = 1</code>)を起点として、このディレクトリ内の<code>hello</code>ファイルを要求して<code>fid = 2</code>を割り当てようとしている。サーバーはルートディレクトリ内に要求されたファイルが見付かったので、そのファイルの<code>qid</code>を返す。</p> +<p> +目的のファイルに<code>fid</code>が割り当てられたので、Tstatでこのファイルの属性を要求している。おそらくファイルのパーミッションを調べるためである。</p> +<p> +次にTopenを使ってこのファイルを開き、Treadを使って内容を読む。二回目のTreadに対して0バイトのデータが返ってきたので、ファイルはこれで終りである。</p> +<p> +最後に、必要なくなった<code>fid</code>はTclunkで捨てる。</p> + +<h3>List of all Message Types</h3> + +<table> +<tr> +<th>Tメッセージ</th> +<th>Rメッセージ</th> +<th>説明</th> +</tr> +<tr> +<td>Tversion</td> +<td>Rversion</td> +<td>バージョンの交渉</td> +</tr> +<tr> +<td>Tauth</td> +<td>Rauth</td> +<td>認証ファイルの取得</td> +</tr> +<tr> +<td></td> +<td>Rerror</td> +<td>エラー(Rメッセージのみ)</td> +</tr> +<tr> +<td>Tflush</td> +<td>Rflush</td> +<td>処理中のリクエストをキャンセル</td> +</tr> +<tr> +<td>Tattach</td> +<td>Rattach</td> +<td>ルートディレクトリの取得</td> +</tr> +<tr> +<td>Twalk</td> +<td>Rwalk</td> +<td>ファイルツリーを辿る</td> +</tr> +<tr> +<td>Topen</td> +<td>Ropen</td> +<td>ファイルを開く</td> +</tr> +<tr> +<td>Tcreate</td> +<td>Rcreate</td> +<td>ファイルの作成</td> +</tr> +<tr> +<td>Tread</td> +<td>Rread</td> +<td>ファイルを読む</td> +</tr> +<tr> +<td>Twrite</td> +<td>Rwrite</td> +<td>ファイルへ書き込む</td> +</tr> +<tr> +<td>Tclunk</td> +<td>Rclunk</td> +<td>fidの削除</td> +</tr> +<tr> +<td>Tremove</td> +<td>Rremove</td> +<td>ファイルを削除</td> +</tr> +<tr> +<td>Tstat</td> +<td>Rstat</td> +<td>ファイルの属性を取得</td> +</tr> +<tr> +<td>Twstat</td> +<td>Rwstat</td> +<td>ファイルの属性を変更</td> +</tr> +</table> + +<h2><code>fid</code>と<code>qid</code></h2> +<p> +<code>fid</code>はコネクションにおいてファイルを指定するために使われる整数で、Unixのファイルディスクリプタのようなものである。使用する整数はクライアントが指定できる。サーバーに接続する際にTattachにより、サーバーのルートディレクトリの<code>fid</code>が設定され、Twalkにより追加され、Tclunkにより削除される。</p> +<p> +<code>qid</code>はサーバーがファイルを一意に識別するためのもので、Unixのinodeに相当するものである。ただしinodeはファイルが削除されると再利用される可能性があるのに対し、<code>qid</code>は再利用できない。<code>qid</code>はこのファイルがディレクトリかどうか等を示す<code>type</code>、ファイルが変更されたときにインクリメントされる<code>vers</code>、そしてファイルを一意に表す<code>path</code>の3つの情報で構成される。</p> +<h2>認証</h2> +<p> +9Pにはクライアントの認証が組込まれていない。もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすればいちいちコードを修正してコンパイルしなおさなければいけないうえ、9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。認証に必要なやりとりは認証サーバーとクライアントが行い、9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することでクライアントの確認を行う。 +</p> +<p> +9PサーバーはTattachメッセージを受けとると、認証サーバーとのコネクションを確立する。その後<code>Rattach</code>メッセージで<code>afid</code>という特殊なFidをクライアントに返す。クライアントからこの<code>afid</code>に対して読み書きする命令が届くと、9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。つまりクライアントはこの<code>afid</code>を通じて認証サーバーと直接やりとりができる。クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報を<code>afid</code>に書き込んで認証する。このように認証をプロトコル自体から切り離すことで、認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。また、より強力な認証システムを組み込むのも楽である。 +</p> + +<h2>コネクションの共有</h2> +<p> +クライアントはサーバーと<code>version</code>メッセージを交すことでコネクションを確立する。コネクションの確立からの一連のやりとりをセッションという。ひとつのコネクションは複数のクライアントが共有できる。</p> +<p> +クライアントはTauthまたはTattachメッセージによりサーバーに<code>fid</code>を要求できる。このうちTauthにより得られた<code>fid</code>は認証以外には使えない。Tattachで得られた<code>fid</code>は、Twalkメッセージによりファイルツリーを辿るための起点として利用でき、このとき辿った先のファイルを示す<code>fid</code>が生成される。これ以外の方法で<code>fid</code>が増えることはない。つまり、サーバーはひとつのコネクションのなかで、から派生した<code>fid</code>を追跡することで、クライアントを区別できる。</p> + +<h2>メッセージ詳細</h2> +<p> +各メッセージの詳細を書く。それぞれ最初にメッセージの形式を書く。<code><i>field</i>[<i>n</i>]</code>は<code><i>field</i></code>という名前の<i>n</i>バイトのデータを表す(<i>n</i>は整数)。また、括弧の中が整数ではなくsになっている場合、そのフィールドは文字列のデータで、その前に文字列の長さを示す2バイトのデータが先行する。</p> +<h3>version</h3> +<p> +プロトコルのバージョンを交渉すし、コネクションを確立する。 +</p> +<pre><code>size[4] Tversion tag[2] msize[4] version[s] +size[4] Rversion tag[2] msize[4] version[s] +</code></pre> +<p> +クライアントは最初にサーバーにこのメッセージを送ってバージョンとメッセージの最大サイズ(<code>msize</code>)を交渉する。サーバーはクライアントから送られたバージョンとメッセージサイズに対応していれば、同じバージョンとサイズを送り返し、交渉成立である。</p> +<p> +サーバーとクライアントはこのメッセージを以ってコネクションを確立する。</p> + +<h3>attach、auth</h3> +<p> +コネクションを確立する。 +</p> +<pre><code>size[4] Tauth tag[2] afid[4] uname[s] aname[s] +size[4] Rauth tag[2] aqid[13] + +size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] +size[4] Rattach tag[2] qid[13] +</code></pre> +<p> +<code>attach</code>メッセージはサーバーのルートディレクトリを要求し、<code>fid</code>を割り当てる。認証が必要なサーバーであれば、先に<code>auth</code>メッセージで認証を済ませておき、<code>attach</code>メッセージの<code>afid</code>に、先程使用した<code>afid</code>をセットする。サーバーはこの<cdoe>afid</code>に紐付いている認証サーバーにクライアントが認証済みであるかを確認し、認証済みであればルートディレクトリの<code>qid</code>を返す。サーバーが複数のファイルツリーを公開している場合、<code>aname</code>で指定する。</p> +<p> +<code>auth</code>メッセージは認証サーバーとやりとりするための<code>fid</code>である<code>afid</code>を要求する。この<code>afid</code>を読み書きすることで認証サーバーとやりとりをし、自身を身分を証明する。認証が済んだらこの<code>afid</code>をセットした<code>attach</code>メッセージを送る。</p> + +<h3>error</h3> +<p> +エラーを返す。 +</p> +<pre><code>size[4] Rerror tag[2] ename[s] +</code></pre> +<p> +クライアントからの要求に対して、なにかエラーがおきたときにそのエラーを返すために使われる。そのためサーバーからクライアントへの<code>Rerror</code>はあるが逆向きのTメッセージはない。</p> + +<h3>flush</h3> +<p> +リクエストをキャンセルする。 +</p> +<pre><code>size[4] Tflush tag[2] oldtag[2] +size[4] Rflush tag[2] +</code></pre> +<p> +実行中の要求をキャンセルする。キャンセルしたいリクエストの<code>tag</code>を<code>oldtag</code>にセットする。 +</p> + +<h3>walk</h3> +<p> +ファイルツリーを移動する。 +</p> +<pre><code>size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) +size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13]) +</code></pre> +<p> +<code>fid</code>を起点に移動する。移動先のファイルを<code>newfid</code>にセットする。移動するディレクトリの数を<code>nwname</code>に、移動するディレクトリの名前を移動する順に<code>wname</code>にそれぞれセットする。例えばカレントディレクトリから<code>./dir1/dir2/</code>に移動する場合、<code>nwname</code>は<code>2</code>、<code>wname</code>は<code>{"dir1", "dir2"}</code>となる。<code>wname</code>の最後の要素はディレクトリでなくファイルでもいい。</p> +<p> +<code>wname</code>の最初の要素への移動が失敗した場合は<code>Rerror</code>が返される。それ以外の場合は<code>Rwalk</code>が返され、<code>qid</code>は移動が成功した順番にそのファイルの<code>qid</code>がセットされる。<code>nwname</code>と<code>nwqid</code>が一致した場合は最後まで移動できたことになる。</p> +<p> +<code>walk</code>は<code>fid</code>を増殖させる唯一の方法である。</p> + +<h3>open、create</h3> +<p> +<code>fid</code>を開いて(作成して)読み書きできる状態にする。 +</p> +<pre><code>size[4] Topen tag[2] fid[4] mode[1] +size[4] Ropen tag[2] qid[13] iounit[4] + +size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] +size[4] Rcreate tag[2] qid[13] iounit[4] +</code></pre> +<p> +<code>open</code>は<code>fid</code>と紐付いたファイルを開く。<code>mode</code>は読み込み専用の<code>OREAD</code>、書き込み専用の<code>OWRITE</code>、読み書きの<code>ORDWR</code>等である。ファイルを開く手順は以下の通り: +<ol> +<li><code>attach</code>でルートディレクトリの<code>fid</code>を取得</li> +<li>ルートディレクトリの<code>fid</code>から<code>walk</code>で開きたいファイルの<code>fid</code>を取得</li> +<li>その<code>fid</code>を<code>open</code>で開く</li> +</ol> +</p> +<p> +<code>create</code>は<code>fid</code>と紐付いたディレクトリに新しいファイルを作成する。<code>creat</code>ではなく<code>create</code>である。作成が成功したら<code>fid</code>は新しく作成されたファイルに紐付く。</p> + +<h3>read、write</h3> +<p> +ファイルを読み書きする。 +</p> +<pre><code>size[4] Tread tag[2] fid[4] offset[8] count[4] +size[4] Rread tag[2] count[4] data[count] + +size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] +size[4] Rwrite tag[2] count[4] +</code></pre> +<p> +<code>read</code>は<code>fid</code>の<code>offset</code>バイト目から<code>count</code>バイト読む。読んだデータと、読めたデータサイズが<code>data</code>、<code>count</code>ととして返される。<code>write</code>は<code>fid</code>の<code>offset</code>バイト目に<code>count</code>バイトのデータ<code>data</code>を書く。書きこめたサイズが<code>count</code>として返される。ファイルを読み書きするためにはあらかじめ<code>fid</code>を<code>open</code>する必要がある。</p> + +<h3>clunk</h3> +<p> +<code>fid</code>を忘れる。 +</p> +<pre><code>size[4] Tclunk tag[2] fid[4] +size[4] Rclunk tag[2] +</code></pre> +<p> +いらなくなった<code>fid</code>を忘れる。一度忘れた<code>fid</code>は<code>walk</code>で別のファイルを取得する際に再利用できる。</p> + +<h3>remove</h3> +<p> +ファイルを削除する。 +</p> +<pre><code>size[4] Tremove tag[2] fid[4] +size[4] Rremove tag[2] +</code></pre> +<p> +<code>fid</code>と紐付いたサーバー上のファイルを削除する。<code>fid</code>は<code>clunk</code>したのと同様に忘れられる。</p> + +<h3>stat、wstat</h3> +<p> +ファイルの属性を読み書きする。 +</p> +<pre><code>size[4] Tstat tag[2] fid[4] +size[4] Rstat tag[2] stat[n] + +size[4] Twstat tag[2] fid[4] stat[n] +size[4] Rwstat tag[2] +</code></pre> +<p> +<code>stat</code>はファイルの属性を読む。読めた情報は<code>stat</code>として返される。<code>wstat</code>はファイルの属性を<code>stat</code>に変更する。<code>stat</code>はファイルの名前や<code>qid</code>、サイズ、作成日、更新日等の情報がバイト列になったものである。</p> + +]]></description> +</item> +<item> <title>きのかわ弦楽合奏団 第5回コミュニティコンサート</title> <link>https://www.mtkn.jp/journal/posts/20241211.html</link> <guid>https://www.mtkn.jp/journal/posts/20241211.html</guid> diff --git a/pub/sitemap.xml b/pub/sitemap.xml @@ -1,5 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> +<url><loc>https://www.mtkn.jp/computer/</loc><lastmod>2024-12-18</lastmod></url> +<url><loc>https://www.mtkn.jp/computer/9p.html</loc><lastmod>2024-12-18</lastmod></url> <url><loc>https://www.mtkn.jp/journal/posts/20241211.html</loc><lastmod>2024-12-11</lastmod></url> <url><loc>https://www.mtkn.jp/journal/</loc><lastmod>2024-12-11</lastmod></url> <url><loc>https://www.mtkn.jp/</loc><lastmod>2024-12-11</lastmod></url> @@ -18,7 +20,6 @@ <url><loc>https://www.mtkn.jp/computer/rp2040_2.html</loc><lastmod>2024-02-27</lastmod></url> <url><loc>https://www.mtkn.jp/computer/rp2040_1.html</loc><lastmod>2024-02-25</lastmod></url> <url><loc>https://www.mtkn.jp/computer/mailserver.html</loc><lastmod>2024-02-24</lastmod></url> -<url><loc>https://www.mtkn.jp/computer/</loc><lastmod>2024-02-22</lastmod></url> <url><loc>https://www.mtkn.jp/computer/git_server.html</loc><lastmod>2024-02-15</lastmod></url> <url><loc>https://www.mtkn.jp/kitchen/recipe/miso.html</loc><lastmod>2024-02-09</lastmod></url> <url><loc>https://www.mtkn.jp/about.html</loc><lastmod>2024-01-23</lastmod></url>