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