www.mtkn.jp

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

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 &lt;T&gt;, new&lt;T&gt;([]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(&quot;read size: %v&quot;, err)
    115 	}
    116 	if read != len(buf) {
    117 		return buf, fmt.Errorf(&quot;read size: invalid message.&quot;)
    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 &lt; int(size)-4; {
    128 		n, err := r.Read(mbuf[read:])
    129 		if err != nil {
    130 			return buf, fmt.Errorf(&quot;read body: %v&quot;, 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(&quot;readMsg: %v&quot;, 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(&quot;write: %v&quot;, 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 &lt;-chan *request) {
    172 	for {
    173 		select {
    174 		case &lt;-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 &lt;- r:
    185 			case &lt;-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, &quot;9P2000&quot;) {
    197 				version = &quot;9P2000&quot;
    198 			} else {
    199 				version = &quot;unknown&quot;
    200 			}
    201 			msize := ifcall.Msize
    202 			if msize &gt; c.mSize() {
    203 				msize = c.mSize()
    204 			}
    205 			r.ofcall = &amp;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(&quot;authentication not required&quot;)
    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 &amp;&amp; ifcall.Afid == NOFID:
    229 			case c.s.Auth == nil &amp;&amp; ifcall.Afid != NOFID:
    230 				r.err = ErrBotch
    231 				goto resp
    232 			case c.s.Auth != nil &amp;&amp; ifcall.Afid == NOFID:
    233 				r.err = fmt.Errorf(&quot;authentication required&quot;)
    234 				goto resp
    235 			case c.s.Auth != nil &amp;&amp; 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(&quot;not auth file&quot;)
    244 					goto resp
    245 				}
    246 				if af.Uname != ifcall.Uname || af.Aname != ifcall.Aname || !af.AuthOK {
    247 					r.err = fmt.Errorf(&quot;not authenticated&quot;)
    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 = &quot;.&quot;
    260 			r.fid.uid = ifcall.Uname
    261 			fsys, ok = c.s.fsmap[ifcall.Aname]
    262 			if !ok {
    263 				r.err = fmt.Errorf(&quot;no such file system&quot;)
    264 				goto resp
    265 			}
    266 			r.fid.fs = fsys
    267 			fi, err = fs.Stat(ExportFS{c.s.fsmap[ifcall.Aname]}, &quot;.&quot;)
    268 			if err != nil {
    269 				r.err = fmt.Errorf(&quot;stat root: %v&quot;, err)
    270 				goto resp
    271 			}
    272 			r.fid.qidpath = fi.Sys().(*Stat).Qid.Path
    273 			r.ofcall = &amp;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 == &quot;..&quot; {
    297 					cwdp = &quot;.&quot; // 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 = &amp;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>