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