www.mtkn.jp

Manuscripts for my personal webpage.
git clone https://git.mtkn.jp/www.mtkn.jp
Log | Files | Refs | LICENSE

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 &lt;T&gt;, new&lt;T&gt;([]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(&quot;read size: %v&quot;, err)
    122 	}
    123 	if read != len(buf) {
    124 		return buf, fmt.Errorf(&quot;read size: invalid message.&quot;)
    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 &lt; int(size)-4; {
    135 		n, err := r.Read(mbuf[read:])
    136 		if err != nil {
    137 			return buf, fmt.Errorf(&quot;read body: %v&quot;, 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(&quot;readMsg: %v&quot;, 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(&quot;write: %v&quot;, 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 &lt;-chan *request) {
    203 	for {
    204 		select {
    205 		case &lt;-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 &lt;- r:
    218 			case &lt;-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, &quot;9P2000&quot;) {
    241 				version = &quot;9P2000&quot;
    242 			} else {
    243 				version = &quot;unknown&quot;
    244 			}
    245 			msize := ifcall.Msize
    246 			if msize &gt; c.mSize() {
    247 				msize = c.mSize()
    248 			}
    249 			r.ofcall = &amp;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(&quot;authentication not required&quot;)
    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 &amp;&amp; ifcall.Afid == NOFID:
    278 			case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID:
    279 				r.err = ErrBotch
    280 				goto resp
    281 			case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID:
    282 				r.err = fmt.Errorf(&quot;authentication required&quot;)
    283 				goto resp
    284 			case c.s.Auth != nil &amp;&amp; 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(&quot;not auth file&quot;)
    293 					goto resp
    294 				}
    295 				if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK {
    296 					r.err = fmt.Errorf(&quot;not authenticated&quot;)
    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 = &quot;.&quot;
    311 			r.fid.uid = ifcall.Uname
    312 			fsys, ok = c.s.fsmap[ifcall.Aname]
    313 			if !ok {
    314 				r.err = fmt.Errorf(&quot;no such file system&quot;)
    315 				goto resp
    316 			}
    317 			r.fid.fs = fsys
    318 			fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;)
    319 			if err != nil {
    320 				r.err = fmt.Errorf(&quot;stat root: %v&quot;, err)
    321 				goto resp
    322 			}
    323 			r.fid.qidpath = fi.Sys().(*Stat).Qid.Path
    324 			r.ofcall = &amp;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 == &quot;..&quot; {
    357 					cwdp = &quot;.&quot; // 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 = &amp;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>