www.mtkn.jp

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

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