9p_go.html (12673B)
1 <h1>9PのGoによる実装</h1> 2 <time>2024-12-18</time> 3 4 <h2>メッセージの構造体の定義</h2> 5 <p> 6 メッセージには<code>size</code>、<code>tag</code>等共通の要素がある。\ 7 クライアントからメッセージを受けとる際、\ 8 <code>size</code>を参照して何バイト読むか判断し、\ 9 メッセージの<code>type</code>によりその後の処理を場合分けする。\ 10 そのためにメッセージを<code>interface</code>として定義した: 11 </p> 12 <pre><code>\ 13 // A Msg represents any kind of message of 9P. 14 // It defines methods for common fields. 15 // For each message type <T>, new<T>([]byte) function parses the byte array 16 // of 9P message into the corresponding message struct. 17 // For detailed information on each message, consult the documentation 18 // of 9P protocol. 19 type Msg interface { 20 // Size returns the size field of message. 21 // Size field holds the size of the message in bytes 22 // including the 4-byte size field itself. 23 Size() uint32 24 // Type returns the type field of message. 25 Type() MsgType 26 // GetTag returns the Tag of message. 27 // Tag is the identifier of each message. 28 // The Get prefix is to avoid name confliction with the each 29 // message's Tag field. 30 GetTag() uint16 31 // SetTag sets the Tag field of the message. 32 SetTag(uint16) 33 // Marshal convert Msg to byte array to be transmitted. 34 marshal() []byte 35 String() string 36 } 37 </code></pre> 38 <p> 39 <code>marshal()</code>はサーバーから返信を送る際に\ 40 メッセージの構造体をバイト列にエンコードするための関数である。\ 41 <code>String() string</code>はログ出力用。\ 42 </p> 43 <p> 44 メッセージはバイト列として受け取った後、\ 45 メッセージのタイプによって処理を分ける。\ 46 その際、扱いやすいようにバイト列を各メッセージの構造体に\ 47 変換する。\ 48 変換前のバイト列から、各メッセージに共通のフィールドを\ 49 取得できると便利なので、\ 50 バイト列のまま<code>Msg</code>インターフェースを実装する\ 51 <code>bufMsg</code>を定義した。\ 52 名前はいまいち。\ 53 </p> 54 <pre><code>\ 55 type bufMsg []byte 56 57 func (msg bufMsg) Size() uint32 { return gbit32(msg[0:4]) } 58 func (msg bufMsg) Type() MsgType { return MsgType(msg[4]) } 59 func (msg bufMsg) GetTag() uint16 { return gbit16(msg[5:7]) } 60 func (msg bufMsg) SetTag(t uint16) { pbit16(msg[5:7], t) } 61 func (msg bufMsg) marshal() []byte { return []byte(msg)[:msg.Size()] } 62 func (msg bufMsg) String() string { 63 switch msg.Type() { 64 case Tversion: 65 return newTVersion(msg).String() 66 /* 省略 */ 67 } 68 } 69 </code></pre> 70 <p> 71 <code>gbit32</code>は4バイトのリトルエンディアンの配列を\ 72 整数に変換するもので、\ 73 <code>pbit32</code>は逆に整数をリトルエンディアンのスライスに\ 74 格納するものである。\ 75 </p> 76 <p> 77 各メッセージはそれぞれのフィールドを構造体のフィールドとして\ 78 定義したものである。\ 79 例えば<code>tversion</code>に対応する構造体は以下の通り。\ 80 </p> 81 <pre><code>\ 82 type TVersion struct { 83 Tag uint16 84 Msize uint32 85 Version string 86 } 87 </code></pre> 88 <p> 89 バイト列から各メッセージの構造体に変換するために\ 90 <code>new<i>メッセージタイプ</i></code>\ 91 を定義した。 92 </p> 93 <pre><code>\ 94 func newTVersion(buf []byte) *TVersion { 95 msg := new(TVersion) 96 msg.Tag = gbit16(buf[5:7]) 97 msg.Msize = gbit32(buf[7:11]) 98 vs := gbit16(buf[11:13]) 99 msg.Version = string(buf[13 : 13+vs]) 100 return msg 101 } 102 </code></pre> 103 104 <h2>メッセージのやりとり</h2> 105 <p> 106 まずは9Pメッセージを読む関数を実装する。\ 107 バイト列が読めればいいので引数は<code>io.Reader</code>にした: 108 </p> 109 <pre><code>\ 110 func readMsg(r io.Reader) ([]byte, error) { 111 </code></pre> 112 <p> 113 最初にメッセージのサイズを読む: 114 </p> 115 <pre><code> buf := make([]byte, 4) 116 read, err := r.Read(buf) 117 if err != nil { 118 if err == io.EOF { 119 return nil, err 120 } 121 return nil, fmt.Errorf("read size: %v", err) 122 } 123 if read != len(buf) { 124 return buf, fmt.Errorf("read size: invalid message.") 125 } 126 size := bufMsg(buf).Size() 127 </code></pre> 128 <p> 129 続いて読みこんだサイズに基づいてメッセージの残りの部分を読む。 130 一回の<code>Read</code>で最後まで読めないことがあったので、 131 全部読めるまで<code>Read</code>を繰り返す: 132 </p> 133 <pre><code> mbuf := make([]byte, size-4) 134 for read = 0; read < int(size)-4; { 135 n, err := r.Read(mbuf[read:]) 136 if err != nil { 137 return buf, fmt.Errorf("read body: %v", err) 138 } 139 read += n 140 } 141 buf = append(buf, mbuf...) 142 return buf, nil 143 } 144 </code></pre> 145 <p> 146 読み込んだバイト列は<code>ReadMsg</code>関数で\ 147 メッセージの構造体に変換する。\ 148 </p> 149 <pre><code>\ 150 RecvMsg(r io.Reader) (Msg, error) { 151 b, err := readMsg(r) 152 if err == io.EOF { 153 return nil, err 154 } else if err != nil { 155 return nil, fmt.Errorf("readMsg: %v", err) 156 } 157 return unmarshal(b) 158 } 159 </code></pre> 160 <p> 161 返信のメッセージはメッセージの構造体を<code>marshal</code>関数\ 162 でバイト列に変換して<code>io.Writer</code>に書き込む。\ 163 </p> 164 <pre><code>\ 165 // SendMsg send a 9P message to w 166 func SendMsg(msg Msg, w io.Writer) error { 167 if _, err := w.Write(msg.marshal()); err != nil { 168 return fmt.Errorf("write: %v", err) 169 } 170 return nil 171 } 172 </code></pre> 173 174 <h2>メインループ</h2> 175 <p> 176 ライブラリはメッセージを読み込むためのリスナーgoroutinと\ 177 返信を書き込むためのレスポンダーgoroutine、\ 178 そして各メッセージを処理するためのgoroutineを立ち上げて、\ 179 チャネルでそれぞれを繋ぐ。\ 180 リスナーがメッセージを読み込むと、<code>request</code>構造体に格納し、\ 181 メッセージのタイプに応じて各goroutineに渡す。\ 182 渡されたgoroutineはメッセージに応じた処理をし、\ 183 返信のメッセージをリスポンダーgoroutineに渡す。\ 184 リスポンダーgoroutineはメッセージをバイト列に変換して返信する。 185 </p> 186 <p> 187 この設計がいいかどうかよく知らんけど、\ 188 Go言語を触るからgoroutineとチャネルによるパイプラインを作ってみたかった。\ 189 </p> 190 191 <h2>各メッセージの処理</h2> 192 <p> 193 メッセージは<code>s<i>メッセージタイプ</i></code>goroutineで処理する。\ 194 関数は<code>for</code>ループのなかでチャンネル<code>rc</code>から\ 195 <code>request</code>が届くのを待つ。\ 196 届いたリクエストには<code>ifcall</code>フィールドにクライアントからの\ 197 <code>Msg</code>が含まれるので、\ 198 この関数が担当するstructにキャストする。\ 199 このキャストが失敗するのは明かなサーバーのバグなのでタイプアサーションはしない。\ 200 </p> 201 <pre><code>\ 202 func s<i>メッセージタイプ</i>(ctx context.Context, c *conn, rc <-chan *request) { 203 for { 204 select { 205 case <-ctx.Done(); 206 return 207 case r, ok := rc: 208 if !ok { 209 return 210 } 211 ifcall := r.ifcall.(*T<i>メッセージタイプ</i>) 212 </code></pre> 213 <p> 214 各種処理を完了したら、届いた<code>request</code>の<code>ofcall</code>に\ 215 返信の<code>Msg</code>を格納して返信を担当するgoroutineに送る。\ 216 <pre><code> select { 217 case c.respChan <- r: 218 case <-ctx.Done(): 219 return 220 } 221 </code></pre> 222 </p> 223 <h3>Version</h3> 224 <p> 225 クライアントから届いたバージョンの提案を見て、頭に"9P2000"があれば\ 226 返信のバージョンを"9P2000"にする。\ 227 現在定義されているバージョンは"9P2000"の他、\ 228 Unix用に拡張した"9P2000.u"、Linux用に拡張した"9P2000.l"がある。\ 229 基本的な昨日を実装することが目標なので、"9P2000"のみを受け付ける。\ 230 </p> 231 <p> 232 Versionメッセージではメッセージの最大サイズも決定する。\ 233 サーバーは予め8Kバイトを最大サイズにしている。\ 234 クライアントがこれ以上のサイズを提案した場合、8Kにしてもらい、\ 235 これ以下のサイズを提案した場合、そのサイズでメッセージをやりとりする。\ 236 8Kバイトという既定値はplan9の実装を参考にした。\ 237 このサイズである必然性はよく知らない。\ 238 </p> 239 <pre><code> version := ifcall.Version 240 if strings.HasPrefix(version, "9P2000") { 241 version = "9P2000" 242 } else { 243 version = "unknown" 244 } 245 msize := ifcall.Msize 246 if msize > c.mSize() { 247 msize = c.mSize() 248 } 249 r.ofcall = &RVersion{ 250 Msize: msize, 251 Version: version, 252 } 253 c.setMSize(r.ofcall.(*RVersion).Msize) 254 </code></pre> 255 256 <h3>Auth</h3> 257 <p> 258 サーバーの認証は<code>Server</code>の<code>Auth</code>フィールドに\ 259 認証のためのやりとりを待ち受ける関数を登録することで有効化できる。\ 260 このフィールドが<code>nil</code>の場合、認証が必要ない旨のエラーを返す。\ 261 </p> 262 <pre><code> if c.s.Auth == nil { 263 r.err = fmt.Errorf("authentication not required") 264 goto resp 265 } 266 </code></pre> 267 <p> 268 認証が必要な場合、クライアントから提案された<code>afid</code>をコネクションに\ 269 登録して、<code>Server.Auth</code>を呼び出す。 270 </p> 271 272 <h3>attach</h3> 273 <p> 274 サーバーはまず認証が必要な場合認証が済んでいるか確認する。\ 275 </p> 276 <pre><code> switch { 277 case c.s.Auth == nil && ifcall.Afid == NOFID: 278 case c.s.Auth == nil && ifcall.Afid != NOFID: 279 r.err = ErrBotch 280 goto resp 281 case c.s.Auth != nil && ifcall.Afid == NOFID: 282 r.err = fmt.Errorf("authentication required") 283 goto resp 284 case c.s.Auth != nil && ifcall.Afid != NOFID: 285 afid, ok := c.fPool.lookup(ifcall.Afid) 286 if !ok { 287 r.err = ErrUnknownFid 288 goto resp 289 } 290 af, ok := afid.file.(*AuthFile) 291 if !ok { 292 r.err = fmt.Errorf("not auth file") 293 goto resp 294 } 295 if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK { 296 r.err = fmt.Errorf("not authenticated") 297 goto resp 298 } 299 } 300 </code></pre> 301 <p> 302 次にクライアントが要求してきたファイルツリーがサーバーにあるかどうか確認し、\ 303 クライアントが設定した<code>fid</code>にルートディレクトリを紐付ける。\ 304 </p> 305 <pre><code> r.fid, err = c.fPool.add(ifcall.Fid) 306 if err != nil { 307 r.err = ErrDupFid 308 goto resp 309 } 310 r.fid.path = "." 311 r.fid.uid = ifcall.Uname 312 fsys, ok = c.s.fsmap[ifcall.Aname] 313 if !ok { 314 r.err = fmt.Errorf("no such file system") 315 goto resp 316 } 317 r.fid.fs = fsys 318 fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, ".") 319 if err != nil { 320 r.err = fmt.Errorf("stat root: %v", err) 321 goto resp 322 } 323 r.fid.qidpath = fi.Sys().(*Stat).Qid.Path 324 r.ofcall = &RAttach{ 325 Qid: fi.Sys().(*Stat).Qid, 326 } 327 </code></pre> 328 329 <h3>flush</h3> 330 <p> 331 指定されたリクエストの<code>flush</code>メソッドを呼び出す。\ 332 <code>flush</code>メソッドはリクエストの<code>done</code>チャンネルを閉じ、\ 333 リクエストのタグを削除する。\ 334 処理に時間のかかるものは、リクエストの<code>done</code>チャネルが閉じられたら、\ 335 その処理を中止する。\ 336 </p> 337 <pre><code>\ 338 // flush cancels the request by calling r.cancel. 339 // It also delete the request from its pool. 340 func (r *request) flush() { 341 close(r.done) 342 r.pool.delete(r.tag) 343 } 344 </code></pre> 345 346 <h3>walk</h3> 347 <p> 348 仕様に書かれている通りの条件を確認した後、\ 349 ファイルツリーを順番に辿り、得られた<code>Qid</code>を\ 350 記録してクライアントに返す。\ 351 </p> 352 <pre><code> wqids = make([]Qid, 0, len(ifcall.Wnames)) 353 cwdp = oldFid.path 354 for _, name := range ifcall.Wnames { 355 cwdp = path.Clean(path.Join(cwdp, name)) 356 if cwdp == ".." { 357 cwdp = "." // parent of the root is itself. 358 } 359 stat, err := fs.Stat(ExportFS{oldFid.fs}, cwdp) 360 if err != nil { 361 break 362 } 363 wqids = append(wqids, stat.Sys().(*Stat).Qid) 364 } 365 if len(wqids) == 0 { 366 newFid.qidpath = oldFid.qidpath 367 } else { 368 newFid.qidpath = wqids[len(wqids)-1].Path 369 } 370 newFid.path = cwdp 371 newFid.uid = oldFid.uid 372 newFid.fs = oldFid.fs 373 r.ofcall = &RWalk{ 374 Qids: wqids, 375 } 376 </code></pre> 377 378 <h3>open</h3> 379 <h3>create</h3> 380 <h3>read</h3> 381 <h3>write</h3> 382 <h3>clunk</h3> 383 <h3>remove</h3> 384 <h3>stat</h3> 385 <h3>wstat</h3>