www.mtkn.jp

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

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 &lt;T&gt;, new&lt;T&gt;([]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(&quot;read size: %v&quot;, err)
     95 	}
     96 	if read != len(buf) {
     97 		return buf, fmt.Errorf(&quot;read size: invalid message.&quot;)
     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 &lt; int(size)-4; {
    108 		n, err := r.Read(mbuf[read:])
    109 		if err != nil {
    110 			return buf, fmt.Errorf(&quot;read body: %v&quot;, 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(&quot;readMsg: %v&quot;, 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(&quot;write: %v&quot;, 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 &lt;-chan *request) {
    152 	for {
    153 		select {
    154 		case &lt;-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 &lt;- r:
    165 			case &lt;-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, &quot;9P2000&quot;) {
    177 				version = &quot;9P2000&quot;
    178 			} else {
    179 				version = &quot;unknown&quot;
    180 			}
    181 			msize := ifcall.Msize
    182 			if msize &gt; c.mSize() {
    183 				msize = c.mSize()
    184 			}
    185 			r.ofcall = &amp;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(&quot;authentication not required&quot;)
    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 &amp;&amp; ifcall.Afid == NOFID:
    209 			case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID:
    210 				r.err = ErrBotch
    211 				goto resp
    212 			case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID:
    213 				r.err = fmt.Errorf(&quot;authentication required&quot;)
    214 				goto resp
    215 			case c.s.Auth != nil &amp;&amp; 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(&quot;not auth file&quot;)
    224 					goto resp
    225 				}
    226 				if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK {
    227 					r.err = fmt.Errorf(&quot;not authenticated&quot;)
    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 = &quot;.&quot;
    240 			r.fid.uid = ifcall.Uname
    241 			fsys, ok = c.s.fsmap[ifcall.Aname]
    242 			if !ok {
    243 				r.err = fmt.Errorf(&quot;no such file system&quot;)
    244 				goto resp
    245 			}
    246 			r.fid.fs = fsys
    247 			fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;)
    248 			if err != nil {
    249 				r.err = fmt.Errorf(&quot;stat root: %v&quot;, err)
    250 				goto resp
    251 			}
    252 			r.fid.qidpath = fi.Sys().(*Stat).Qid.Path
    253 			r.ofcall = &amp;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 == &quot;..&quot; {
    277 					cwdp = &quot;.&quot; // 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 = &amp;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