9p.html (32610B)
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width,initial-scale=1"> 6 <link rel="stylesheet" type="text/css" href="/style.css"> 7 <link rel="icon" type="image/x-icon" href="/pics/favicon.ico"> 8 <title>9P</title> 9 </head> 10 <body> 11 <header> 12 <a href="/">主頁</a> | 13 <a href="/about.html">自己紹介</a> | 14 <a href="/journal">日記</a> | 15 <a href="/farm">農業</a> | 16 <a href="/kitchen">台所</a> | 17 <a href="/computer">電算機</a> | 18 <a href="/poetry">詩</a> | 19 <a href="/books">本棚</a> | 20 <a href="/gallery">絵</a> | 21 <a href="https://git.mtkn.jp">Git</a> 22 </header> 23 <main> 24 <article> 25 <h1>9P</h1> 26 <time>2024-10-27</time> 27 <h2>はじめに</h2> 28 <p> 29 9Pはコンピュータ上の色々なものをファイルとして扱うためのプロトコルである。このプロトコルを使えば、ディスクに記録されたデータだけでなく、マウスやキーボードといった入力機器や、ネットワーク、プロセスの情報等もファイルとして扱える。Unixの後継OSとして米AT&Tのベル研究所で開発されたPlan9というOSのためのプロトコルである。 30 </p> 31 32 <p> 33 Plan9というOSはネットワークを介して複数のコンピュータを繋いで使うように設計されているので、この9Pもローカルだけでなくネットワーク越しに使うことを前提にしている。</p> 34 35 <p> 36 便利そうなのでGo言語で実装してみた。 37 </p> 38 39 <h2>9Pプロトコル</h2> 40 <h3>概要</h3> 41 <p> 42 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> 43 <p> 44 通信は必要な情報をプロトコルの定める方法でバイト列に変換して送受信する。2バイト以上のデータはリトルエンディアンになるように配置する。テキストデータはUTF-8にエンコードする。テキストデータは長さの情報も一緒に送るので最後のヌル文字は含めない。</p> 45 <p> 46 各メッセージは4バイトのサイズから始まる。これは、この4バイトを含むメッセージの長さをバイト単位で示したものである。次にメッセージの種類を記述する1バイトのデータが来る。次はメッセージのタグである。クライアントはサーバーからの返事を待たずに別のメッセージを送れるので、サーバーからのRメッセージがどのTメッセージに対する返事なのかを識別する必要がある。そのために使うのがタグである。クライアントがTメッセージを送る際にタグを付け、サーバーはそれに対するRメッセージに同じタグを付ける。その後ろには、メッセージの種類によって固有の情報が付けられる。</p> 47 <p> 48 以下は実際の9P通信のログである。実際はバイナリの通信だが、ログとして見易いように変換してある。また、先頭にくるメッセージサイズは省略してある。この例では、サーバーのルートディレクトリにある<code>hello</code>というファイルの内容を読みこんでいる。:</p> 49 <pre><code><-- Tversion Tag 65535 msize 8192 version '9P2000' 50 --> Rversion Tag 65535 msize 8192 version '9P2000' 51 <-- Tattach Tag 0 fid 0 afid -1 uname kenji aname 52 --> Rattach Tag 0 qid (0000000000000000 0 d) 53 <-- Twalk Tag 0 fid 0 newfid 1 nwname 1 0:hello 54 --> Rwalk Tag 0 nwqid 1 0:(0000000000000001 0 ) 55 <-- Tstat Tag 0 fid 1 56 --> Rstat Tag 0 stat 'hello' 'kenji' 'kenji' '' q (0000000000000001 0 ) m 0644 at 1730004808 mt 1730004808 l 7 t 0 d 0 57 <-- Twalk Tag 0 fid 1 newfid 2 nwname 0 58 --> Rwalk Tag 0 nwqid 0 59 <-- Topen Tag 0 fid 2 mode 0x0 60 --> Ropen Tag 0 qid (0000000000000001 0 ) iounit 8169 61 <-- Tread Tag 0 fid 2 offset 0 count 4096 62 --> Rread Tag 0 count 7 ' 776f726c 64210a' 63 <-- Tread Tag 0 fid 2 offset 7 count 4096 64 --> Rread Tag 0 count 0 '' 65 <-- Tclunk Tag 0 fid 2 66 --> Rclunk Tag 0 67 </code></pre> 68 <p> 69 まず最初にクライアントがサーバーに対して<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> 70 <p> 71 次の<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> 72 <p> 73 次に<code>Twalk</code>で目的のファイル(<code>hello</code>)までファイルツリーを辿る。ここでは起点として先程得られたルートディレクトリ(<code>fid = 1</code>)を起点として、このディレクトリ内の<code>hello</code>ファイルを要求して<code>fid = 2</code>を割り当てようとしている。サーバーはルートディレクトリ内に要求されたファイルが見付かったので、そのファイルの<code>qid</code>を返す。</p> 74 <p> 75 目的のファイルに<code>fid</code>が割り当てられたので、<code>Tstat</code>でこのファイルの属性を要求している。おそらくファイルのパーミッションを調べるためである。</p> 76 <p> 77 次に<code>Topen</code>を使ってこのファイルを開き、<code>Tread</code>を使って内容を読む。二回目の<code>Tread</code>に対して0バイトのデータが返ってきたので、ファイルはこれで終りである。</p> 78 <p> 79 最後に、必要なくなった<code>fid</code>は<code>Tclunk</code>で捨てる。</p> 80 81 <h3>List of all Message Types</h3> 82 83 <table> 84 <tr> 85 <th>Tメッセージ</th> 86 <th>Rメッセージ</th> 87 <th>説明</th> 88 </tr> 89 <tr> 90 <td>Tversion</td> 91 <td>Rversion</td> 92 <td>バージョンの交渉</td> 93 </tr> 94 <tr> 95 <td>Tauth</td> 96 <td>Rauth</td> 97 <td>認証ファイルの取得</td> 98 </tr> 99 <tr> 100 <td></td> 101 <td>Rerror</td> 102 <td>エラー(Rメッセージのみ)</td> 103 </tr> 104 <tr> 105 <td>Tflush</td> 106 <td>Rflush</td> 107 <td>処理中のリクエストをキャンセル</td> 108 </tr> 109 <tr> 110 <td>Tattach</td> 111 <td>Rattach</td> 112 <td>ルートディレクトリの取得</td> 113 </tr> 114 <tr> 115 <td>Twalk</td> 116 <td>Rwalk</td> 117 <td>ファイルツリーを辿る</td> 118 </tr> 119 <tr> 120 <td>Topen</td> 121 <td>Ropen</td> 122 <td>ファイルを開く</td> 123 </tr> 124 <tr> 125 <td>Tcreate</td> 126 <td>Rcreate</td> 127 <td>ファイルの作成</td> 128 </tr> 129 <tr> 130 <td>Tread</td> 131 <td>Rread</td> 132 <td>ファイルを読む</td> 133 </tr> 134 <tr> 135 <td>Twrite</td> 136 <td>Rwrite</td> 137 <td>ファイルへ書き込む</td> 138 </tr> 139 <tr> 140 <td>Tclunk</td> 141 <td>Rclunk</td> 142 <td>fidの削除</td> 143 </tr> 144 <tr> 145 <td>Tremove</td> 146 <td>Rremove</td> 147 <td>ファイルを削除</td> 148 </tr> 149 <tr> 150 <td>Tstat</td> 151 <td>Rstat</td> 152 <td>ファイルの属性を取得</td> 153 </tr> 154 <tr> 155 <td>Twstat</td> 156 <td>Rwstat</td> 157 <td>ファイルの属性を変更</td> 158 </tr> 159 </table> 160 161 <h2><code>fid</code>と<code>qid</code></h2> 162 <p> 163 <code>fid</code>はコネクションにおいてファイルを指定するために使われる整数で、Unixのファイルディスクリプタのようなものである。使用する整数はクライアントが指定できる。サーバーに接続する際に<code>Tattach</code>により、サーバーのルートディレクトリの<code>fid</code>が設定され、<code>Twalk</code>により追加され、<code>Tclunk</code>により削除される。</p> 164 <p> 165 <code>qid</code>はサーバーがファイルを一意に識別するためのもので、Unixのinodeに相当するものである。ただしinodeはファイルが削除されると再利用される可能性があるのに対し、<code>qid</code>は再利用できない。<code>qid</code>はこのファイルがディレクトリかどうか等を示す<code>type</code>、ファイルが変更されたときにインクリメントされる<code>vers</code>、そしてファイルを一意に表す<code>path</code>の3つの情報で構成される。</p> 166 <h2>認証</h2> 167 <p> 168 9Pにはクライアントの認証が組込まれていない。もともとはあったようだが、認証のアルゴリズムに欠陥が見付かったりすればいちいちコードを修正してコンパイルしなおさなければいけないうえ、9Pプロトコル自体も変更しないといけないので、外部に切りだすことになった。認証に必要なやりとりは認証サーバーとクライアントが行い、9Pサーバーは認証が成功したかどうかの情報を認証サーバーに確認することでクライアントの確認を行う。 169 </p> 170 <p> 171 9Pサーバーは<code>Tattach</code>メッセージを受けとると、認証サーバーとのコネクションを確立する。その後<code>Rattach</code>メッセージで<code>afid</code>という特殊なFidをクライアントに返す。クライアントからこの<code>afid</code>に対して読み書きする命令が届くと、9Pサーバーはこの読み書きを認証サーバーとのコネクションに横流しする。つまりクライアントはこの<code>afid</code>を通じて認証サーバーと直接やりとりができる。クライアントはユーザー名やパスワード等(パスワード認証の場合)の情報を<code>afid</code>に書き込んで認証する。このように認証をプロトコル自体から切り離すことで、認証に関するバグが見付かったとしても、認証サーバーを修正するだけでいい。また、より強力な認証システムを組み込むのも楽である。 172 </p> 173 174 <h2>コネクションの共有</h2> 175 <p> 176 クライアントはサーバーと<code>version</code>メッセージを交すことでコネクションを確立する。コネクションの確立からの一連のやりとりをセッションという。ひとつのコネクションは複数のクライアントが共有できる。</p> 177 <p> 178 クライアントは<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> 179 180 <h2>メッセージ詳細</h2> 181 <p> 182 各メッセージの詳細を書く。それぞれ最初にメッセージの形式を書く。<code><i>field</i>[<i>n</i>]</code>は<code><i>field</i></code>という名前の<i>n</i>バイトのデータを表す(<i>n</i>は整数)。また、括弧の中が整数ではなくsになっている場合、そのフィールドは文字列のデータで、その前に文字列の長さを示す2バイトのデータが先行する。</p> 183 <h3>version</h3> 184 <p> 185 プロトコルのバージョンを交渉すし、コネクションを確立する。 186 </p> 187 <pre><code>size[4] Tversion tag[2] msize[4] version[s] 188 size[4] Rversion tag[2] msize[4] version[s] 189 </code></pre> 190 <p> 191 クライアントは最初にサーバーにこのメッセージを送ってバージョンとメッセージの最大サイズ(<code>msize</code>)を交渉する。サーバーはクライアントから送られたバージョンとメッセージサイズに対応していれば、同じバージョンとサイズを送り返し、交渉成立である。</p> 192 <p> 193 サーバーとクライアントはこのメッセージを以ってコネクションを確立する。</p> 194 195 <h3>attach、auth</h3> 196 <p> 197 コネクションを確立する。 198 </p> 199 <pre><code>size[4] Tauth tag[2] afid[4] uname[s] aname[s] 200 size[4] Rauth tag[2] aqid[13] 201 202 size[4] Tattach tag[2] fid[4] afid[4] uname[s] aname[s] 203 size[4] Rattach tag[2] qid[13] 204 </code></pre> 205 <p> 206 <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> 207 <p> 208 <code>auth</code>メッセージは認証サーバーとやりとりするための<code>fid</code>である<code>afid</code>を要求する。この<code>afid</code>を読み書きすることで認証サーバーとやりとりをし、自身を身分を証明する。認証が済んだらこの<code>afid</code>をセットした<code>attach</code>メッセージを送る。</p> 209 210 <h3>error</h3> 211 <p> 212 エラーを返す。 213 </p> 214 <pre><code>size[4] Rerror tag[2] ename[s] 215 </code></pre> 216 <p> 217 クライアントからの要求に対して、なにかエラーがおきたときにそのエラーを返すために使われる。そのためサーバーからクライアントへの<code>Rerror</code>はあるが逆向きのTメッセージはない。</p> 218 219 <h3>flush</h3> 220 <p> 221 リクエストをキャンセルする。 222 </p> 223 <pre><code>size[4] Tflush tag[2] oldtag[2] 224 size[4] Rflush tag[2] 225 </code></pre> 226 <p> 227 実行中の要求をキャンセルする。キャンセルしたいリクエストの<code>tag</code>を<code>oldtag</code>にセットする。 228 </p> 229 230 <h3>walk</h3> 231 <p> 232 ファイルツリーを移動する。 233 </p> 234 <pre><code>size[4] Twalk tag[2] fid[4] newfid[4] nwname[2] nwname*(wname[s]) 235 size[4] Rwalk tag[2] nwqid[2] nwqid*(qid[13]) 236 </code></pre> 237 <p> 238 <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> 239 <p> 240 <code>wname</code>の最初の要素への移動が失敗した場合は<code>Rerror</code>が返される。それ以外の場合は<code>Rwalk</code>が返され、<code>qid</code>は移動が成功した順番にそのファイルの<code>qid</code>がセットされる。<code>nwname</code>と<code>nwqid</code>が一致した場合は最後まで移動できたことになる。</p> 241 <p> 242 <code>walk</code>は<code>fid</code>を増殖させる唯一の方法である。</p> 243 244 <h3>open、create</h3> 245 <p> 246 <code>fid</code>を開いて(作成して)読み書きできる状態にする。 247 </p> 248 <pre><code>size[4] Topen tag[2] fid[4] mode[1] 249 size[4] Ropen tag[2] qid[13] iounit[4] 250 251 size[4] Tcreate tag[2] fid[4] name[s] perm[4] mode[1] 252 size[4] Rcreate tag[2] qid[13] iounit[4] 253 </code></pre> 254 <p> 255 <code>open</code>は<code>fid</code>と紐付いたファイルを開く。<code>mode</code>は読み込み専用の<code>OREAD</code>、書き込み専用の<code>OWRITE</code>、読み書きの<code>ORDWR</code>等である。ファイルを開く手順は以下の通り: 256 <ol> 257 <li><code>attach</code>でルートディレクトリの<code>fid</code>を取得</li> 258 <li>ルートディレクトリの<code>fid</code>から<code>walk</code>で開きたいファイルの<code>fid</code>を取得</li> 259 <li>その<code>fid</code>を<code>open</code>で開く</li> 260 </ol> 261 </p> 262 <p> 263 <code>create</code>は<code>fid</code>と紐付いたディレクトリに新しいファイルを作成する。<code>creat</code>ではなく<code>create</code>である。作成が成功したら<code>fid</code>は新しく作成されたファイルに紐付く。</p> 264 265 <h3>read、write</h3> 266 <p> 267 ファイルを読み書きする。 268 </p> 269 <pre><code>size[4] Tread tag[2] fid[4] offset[8] count[4] 270 size[4] Rread tag[2] count[4] data[count] 271 272 size[4] Twrite tag[2] fid[4] offset[8] count[4] data[count] 273 size[4] Rwrite tag[2] count[4] 274 </code></pre> 275 <p> 276 <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> 277 278 <h3>clunk</h3> 279 <p> 280 <code>fid</code>を忘れる。 281 </p> 282 <pre><code>size[4] Tclunk tag[2] fid[4] 283 size[4] Rclunk tag[2] 284 </code></pre> 285 <p> 286 いらなくなった<code>fid</code>を忘れる。一度忘れた<code>fid</code>は<code>walk</code>で別のファイルを取得する際に再利用できる。</p> 287 288 <h3>remove</h3> 289 <p> 290 ファイルを削除する。 291 </p> 292 <pre><code>size[4] Tremove tag[2] fid[4] 293 size[4] Rremove tag[2] 294 </code></pre> 295 <p> 296 <code>fid</code>と紐付いたサーバー上のファイルを削除する。<code>fid</code>は<code>clunk</code>したのと同様に忘れられる。</p> 297 298 <h3>stat、wstat</h3> 299 <p> 300 ファイルの属性を読み書きする。 301 </p> 302 <pre><code>size[4] Tstat tag[2] fid[4] 303 size[4] Rstat tag[2] stat[n] 304 305 size[4] Twstat tag[2] fid[4] stat[n] 306 size[4] Rwstat tag[2] 307 </code></pre> 308 <p> 309 <code>stat</code>はファイルの属性を読む。読めた情報は<code>stat</code>として返される。<code>wstat</code>はファイルの属性を<code>stat</code>に変更する。<code>stat</code>はファイルの名前や<code>qid</code>、サイズ、作成日、更新日等の情報がバイト列になったものである。</p> 310 311 <h2>Goによる実装</h2> 312 <h3>メッセージの構造体の定義</h3> 313 <p> 314 メッセージには<code>size</code>、<code>tag</code>等共通の要素がある。クライアントからメッセージを受けとる際、<code>size</code>を参照して何バイト読むか判断し、メッセージの<code>type</code>によりその後の処理を場合分けする。そのためにメッセージを<code>interface</code>として定義した: 315 </p> 316 <pre><code>// A Msg represents any kind of message of 9P. 317 // It defines methods for common fields. 318 // For each message type <T>, new<T>([]byte) function parses the byte array 319 // of 9P message into the corresponding message struct. 320 // For detailed information on each message, consult the documentation 321 // of 9P protocol. 322 type Msg interface { 323 // Size returns the size field of message. 324 // Size field holds the size of the message in bytes 325 // including the 4-byte size field itself. 326 Size() uint32 327 // Type returns the type field of message. 328 Type() MsgType 329 // GetTag returns the Tag of message. 330 // Tag is the identifier of each message. 331 // The Get prefix is to avoid name confliction with the each 332 // message's Tag field. 333 GetTag() uint16 334 // SetTag sets the Tag field of the message. 335 SetTag(uint16) 336 // Marshal convert Msg to byte array to be transmitted. 337 marshal() []byte 338 String() string 339 } 340 </code></pre> 341 <p> 342 <code>marshal()</code>はサーバーから返信を送る際にメッセージの構造体をバイト列にエンコードするための関数である。<code>String() string</code>はログ出力用。</p> 343 <p> 344 メッセージはバイト列として受け取った後、メッセージのタイプによって処理を分ける。その際、扱いやすいようにバイト列を各メッセージの構造体に変換する。変換前のバイト列から、各メッセージに共通のフィールドを取得できると便利なので、バイト列のまま<code>Msg</code>インターフェースを実装する<code>bufMsg</code>を定義した。名前はいまいち。</p> 345 <pre><code>type bufMsg []byte 346 347 func (msg bufMsg) Size() uint32 { return gbit32(msg[0:4]) } 348 func (msg bufMsg) Type() MsgType { return MsgType(msg[4]) } 349 func (msg bufMsg) GetTag() uint16 { return gbit16(msg[5:7]) } 350 func (msg bufMsg) SetTag(t uint16) { pbit16(msg[5:7], t) } 351 func (msg bufMsg) marshal() []byte { return []byte(msg)[:msg.Size()] } 352 func (msg bufMsg) String() string { 353 switch msg.Type() { 354 case Tversion: 355 return newTVersion(msg).String() 356 /* 省略 */ 357 } 358 } 359 </code></pre> 360 <p> 361 <code>gbit32</code>は4バイトのリトルエンディアンの配列を整数に変換するもので、<code>pbit32</code>は逆に整数をリトルエンディアンのスライスに格納するものである。</p> 362 <p> 363 各メッセージはそれぞれのフィールドを構造体のフィールドとして定義したものである。例えば<code>tversion</code>に対応する構造体は以下の通り。</p> 364 <pre><code>type TVersion struct { 365 Tag uint16 366 Msize uint32 367 Version string 368 } 369 </code></pre> 370 <p> 371 バイト列から各メッセージの構造体に変換するために<code>new<i>メッセージタイプ</i></code>を定義した。 372 </p> 373 <pre><code>func newTVersion(buf []byte) *TVersion { 374 msg := new(TVersion) 375 msg.Tag = gbit16(buf[5:7]) 376 msg.Msize = gbit32(buf[7:11]) 377 vs := gbit16(buf[11:13]) 378 msg.Version = string(buf[13 : 13+vs]) 379 return msg 380 } 381 </code></pre> 382 383 <h3>メッセージのやりとり</h3> 384 <p> 385 まずは9Pメッセージを読む関数を実装する。バイト列が読めればいいので引数は<code>io.Reader</code>にした: 386 </p> 387 <pre><code>func readMsg(r io.Reader) ([]byte, error) { 388 </code></pre> 389 <p> 390 最初にメッセージのサイズを読む: 391 </p> 392 <pre><code> buf := make([]byte, 4) 393 read, err := r.Read(buf) 394 if err != nil { 395 if err == io.EOF { 396 return nil, err 397 } 398 return nil, fmt.Errorf("read size: %v", err) 399 } 400 if read != len(buf) { 401 return buf, fmt.Errorf("read size: invalid message.") 402 } 403 size := bufMsg(buf).Size() 404 </code></pre> 405 <p> 406 続いて読みこんだサイズに基づいてメッセージの残りの部分を読む。 407 一回の<code>Read</code>で最後まで読めないことがあったので、 408 全部読めるまで<code>Read</code>を繰り返す: 409 </p> 410 <pre><code> mbuf := make([]byte, size-4) 411 for read = 0; read < int(size)-4; { 412 n, err := r.Read(mbuf[read:]) 413 if err != nil { 414 return buf, fmt.Errorf("read body: %v", err) 415 } 416 read += n 417 } 418 buf = append(buf, mbuf...) 419 return buf, nil 420 } 421 </code></pre> 422 <p> 423 読み込んだバイト列は<code>ReadMsg</code>関数でメッセージの構造体に変換する。</p> 424 <pre><code>RecvMsg(r io.Reader) (Msg, error) { 425 b, err := readMsg(r) 426 if err == io.EOF { 427 return nil, err 428 } else if err != nil { 429 return nil, fmt.Errorf("readMsg: %v", err) 430 } 431 return unmarshal(b) 432 } 433 </code></pre> 434 <p> 435 返信のメッセージはメッセージの構造体を<code>marshal</code>関数でバイト列に変換して<code>io.Writer</code>に書き込む。</p> 436 <pre><code>// SendMsg send a 9P message to w 437 func SendMsg(msg Msg, w io.Writer) error { 438 if _, err := w.Write(msg.marshal()); err != nil { 439 return fmt.Errorf("write: %v", err) 440 } 441 return nil 442 } 443 </code></pre> 444 445 <h3>メインループ</h3> 446 <p> 447 ライブラリはメッセージを読み込むためのリスナーgoroutinと返信を書き込むためのレスポンダーgoroutine、そして各メッセージを処理するためのgoroutineを立ち上げて、チャネルでそれぞれを繋ぐ。リスナーがメッセージを読み込むと、<code>request</code>構造体に格納し、メッセージのタイプに応じて各goroutineに渡す。渡されたgoroutineはメッセージに応じた処理をし、返信のメッセージをリスポンダーgoroutineに渡す。リスポンダーgoroutineはメッセージをバイト列に変換して返信する。 448 </p> 449 <p> 450 この設計がいいかどうかよく知らんけど、Go言語を触るからgoroutineとチャネルによるパイプラインを作ってみたかった。</p> 451 452 <h3>各メッセージの処理</h3> 453 <p> 454 メッセージは<code>s<i>メッセージタイプ</i></code>goroutineで処理する。関数は<code>for</code>ループのなかでチャンネル<code>rc</code>から<code>request</code>が届くのを待つ。届いたリクエストには<code>ifcall</code>フィールドにクライアントからの<code>Msg</code>が含まれるので、この関数が担当するstructにキャストする。このキャストが失敗するのは明かなサーバーのバグなのでタイプアサーションはしない。</p> 455 <pre><code>func s<i>メッセージタイプ</i>(ctx context.Context, c *conn, rc <-chan *request) { 456 for { 457 select { 458 case <-ctx.Done(); 459 return 460 case r, ok := rc: 461 if !ok { 462 return 463 } 464 ifcall := r.ifcall.(*T<i>メッセージタイプ</i>) 465 </code></pre> 466 <p> 467 各種処理を完了したら、届いた<code>request</code>の<code>ofcall</code>に返信の<code>Msg</code>を格納して返信を担当するgoroutineに送る。<pre><code> select { 468 case c.respChan <- r: 469 case <-ctx.Done(): 470 return 471 } 472 </code></pre> 473 </p> 474 <h4>Version</h4> 475 <p> 476 クライアントから届いたバージョンの提案を見て、頭に"9P2000"があれば返信のバージョンを"9P2000"にする。現在定義されているバージョンは"9P2000"の他、Unix用に拡張した"9P2000.u"、Linux用に拡張した"9P2000.l"がある。基本的な昨日を実装することが目標なので、"9P2000"のみを受け付ける。</p> 477 <p> 478 Versionメッセージではメッセージの最大サイズも決定する。サーバーは予め8Kバイトを最大サイズにしている。クライアントがこれ以上のサイズを提案した場合、8Kにしてもらい、これ以下のサイズを提案した場合、そのサイズでメッセージをやりとりする。8Kバイトという既定値はplan9の実装を参考にした。このサイズである必然性はよく知らない。</p> 479 <pre><code> version := ifcall.Version 480 if strings.HasPrefix(version, "9P2000") { 481 version = "9P2000" 482 } else { 483 version = "unknown" 484 } 485 msize := ifcall.Msize 486 if msize > c.mSize() { 487 msize = c.mSize() 488 } 489 r.ofcall = &RVersion{ 490 Msize: msize, 491 Version: version, 492 } 493 c.setMSize(r.ofcall.(*RVersion).Msize) 494 </code></pre> 495 496 <h4>Auth</h4> 497 <p> 498 サーバーの認証は<code>Server</code>の<code>Auth</code>フィールドに認証のためのやりとりを待ち受ける関数を登録することで有効化できる。このフィールドが<code>nil</code>の場合、認証が必要ない旨のエラーを返す。</p> 499 <pre><code> if c.s.Auth == nil { 500 r.err = fmt.Errorf("authentication not required") 501 goto resp 502 } 503 </code></pre> 504 <p> 505 認証が必要な場合、クライアントから提案された<code>afid</code>をコネクションに登録して、<code>Server.Auth</code>を呼び出す。 506 </p> 507 508 <h4>attach</h4> 509 <p> 510 サーバーはまず認証が必要な場合認証が済んでいるか確認する。</p> 511 <pre><code> switch { 512 case c.s.Auth == nil && ifcall.Afid == NOFID: 513 case c.s.Auth == nil && ifcall.Afid != NOFID: 514 r.err = ErrBotch 515 goto resp 516 case c.s.Auth != nil && ifcall.Afid == NOFID: 517 r.err = fmt.Errorf("authentication required") 518 goto resp 519 case c.s.Auth != nil && ifcall.Afid != NOFID: 520 afid, ok := c.fPool.lookup(ifcall.Afid) 521 if !ok { 522 r.err = ErrUnknownFid 523 goto resp 524 } 525 af, ok := afid.file.(*AuthFile) 526 if !ok { 527 r.err = fmt.Errorf("not auth file") 528 goto resp 529 } 530 if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK { 531 r.err = fmt.Errorf("not authenticated") 532 goto resp 533 } 534 } 535 </code></pre> 536 <p> 537 次にクライアントが要求してきたファイルツリーがサーバーにあるかどうか確認し、クライアントが設定した<code>fid</code>にルートディレクトリを紐付ける。</p> 538 <pre><code> r.fid, err = c.fPool.add(ifcall.Fid) 539 if err != nil { 540 r.err = ErrDupFid 541 goto resp 542 } 543 r.fid.path = "." 544 r.fid.uid = ifcall.Uname 545 fsys, ok = c.s.fsmap[ifcall.Aname] 546 if !ok { 547 r.err = fmt.Errorf("no such file system") 548 goto resp 549 } 550 r.fid.fs = fsys 551 fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, ".") 552 if err != nil { 553 r.err = fmt.Errorf("stat root: %v", err) 554 goto resp 555 } 556 r.fid.qidpath = fi.Sys().(*Stat).Qid.Path 557 r.ofcall = &RAttach{ 558 Qid: fi.Sys().(*Stat).Qid, 559 } 560 </code></pre> 561 562 <h4>flush</h4> 563 <p> 564 指定されたリクエストの<code>flush</code>メソッドを呼び出す。<code>flush</code>メソッドはリクエストの<code>done</code>チャンネルを閉じ、リクエストのタグを削除する。処理に時間のかかるものは、リクエストの<code>done</code>チャネルが閉じられたら、その処理を中止する。</p> 565 <pre><code>// flush cancels the request by calling r.cancel. 566 // It also delete the request from its pool. 567 func (r *request) flush() { 568 close(r.done) 569 r.pool.delete(r.tag) 570 } 571 </code></pre> 572 573 <h4>walk</h4> 574 <p> 575 仕様に書かれている通りの条件を確認した後、ファイルツリーを順番に辿り、得られた<code>Qid</code>を記録してクライアントに返す。</p> 576 <pre><code> wqids = make([]Qid, 0, len(ifcall.Wnames)) 577 cwdp = oldFid.path 578 for _, name := range ifcall.Wnames { 579 cwdp = path.Clean(path.Join(cwdp, name)) 580 if cwdp == ".." { 581 cwdp = "." // parent of the root is itself. 582 } 583 stat, err := fs.Stat(ExportFS{oldFid.fs}, cwdp) 584 if err != nil { 585 break 586 } 587 wqids = append(wqids, stat.Sys().(*Stat).Qid) 588 } 589 if len(wqids) == 0 { 590 newFid.qidpath = oldFid.qidpath 591 } else { 592 newFid.qidpath = wqids[len(wqids)-1].Path 593 } 594 newFid.path = cwdp 595 newFid.uid = oldFid.uid 596 newFid.fs = oldFid.fs 597 r.ofcall = &RWalk{ 598 Qids: wqids, 599 } 600 </code></pre> 601 602 <h4>open</h4> 603 <h4>create</h4> 604 <h4>read</h4> 605 <h4>write</h4> 606 <h4>clunk</h4> 607 <h4>remove</h4> 608 <h4>stat</h4> 609 <h4>wstat</h4> 610 </article> 611 612 </main> 613 <footer> 614 <address>info(at)mtkn(dot)jp</address> 615 <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" rel="license noopener noreferrer">CC0 1.0</a> 616 </footer> 617 </body> 618 </html>