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