こんにちは.趣味の将棋がなかなか強くならなくて困ってます.勉強せずにすぐ対局してしまうのがダメなのはわかってますがつい対局してしまいます. 今回はKLab Expert Campに参加してTCP/IPプロトコルスタック自作に挑戦しました.

# 経緯

最初にこのイベントについて知ったのは昨年の第一回TCP/IPプロトコルスタック自作キャンプ (opens new window)に参加されていた参加者の方のツイートとブログでした.ちょうど僕もTCP/IPの自作を行う機運が高まっていたので次回があれば絶対参加するぞと思っていました.

# KLab Expert Camp(TCP/IPプロトコルスタック自作開発)

microps (opens new window)lectcp (opens new window)を開発されているpandax381 (opens new window)さんに直接解説やアドバイスをいただきながらTCP/IPを実装していくというものです.

基本コースと発展コースが用意されており基本コースは講義形式でmicropsを参考に実装を行い,発展コースは自身の持ち込んだテーマでTCP/IPに関する開発を行います. 僕はGo言語で自作TCP/IPをすでに開発中でTCPも少し動いていたので自作のプロトコルスタックにwindow制御などの機能追加をしたいと思っていました.

僕の自作TCP/IPのリポジトリはこちら

terassyi/gotcp - GitHub (opens new window)

# やったこと

やったことの概要は成果発表の資料にまとめてます.

成果発表スライドをチラッとみていただければ分かりますが期間中進捗はほぼほぼなしで永遠にデバッグしてました.とはいえ自分一人では気づかなかったバグや仕様の落とし穴などを指摘していただけたので貴重な経験でした.結果としてデバッグも進んだのでよかったです.

スライドで上げているバグとそれに対してやったことについて軽く紹介します.

# RFC793に書かれている仕様が現実の実装と異なる場合がある

これはPSHフラグ問題です.TCPは到着したセグメントを格納しておくバッファを持っています. そしてRFC793の仕様には以下のような記述があります.

There is a coupling between the push function and the use of buffers of data that cross the TCP/user interface. Each time a PUSH flag is associated with data placed into the receiving user’s buffer, the buffer is returned to the user for processing even if the buffer is not filled. If data arrives that fills the user’s buffer before a PUSH is seen, the data is passed to the user in buffer size units.

つまり,TCPはPSHフラグが検出される,もしくはバッファがいっぱいになったらアプリケーションにデータを投げるということになっています. ということで僕が当初書いていたコードがこちら.

セグメントの処理部.

if packet.Packet.Header.OffsetControlFlag.ControlFlag().Psh() {
	//c.readyQueue <- packet.Packet.Data
	c.rcvBuffer = append(c.rcvBuffer, packet.Packet.Data...)
	c.readyQueue <- c.rcvBuffer
	
} else {
	c.rcvBuffer = append(c.rcvBuffer, packet.Packet.Data...)
	if len(c.rcvBuffer) >= cap(c.rcvBuffer) {
		c.readyQueue <- c.rcvBuffer
}
// ...
1
2
3
4
5
6
7
8
9
10
11

Read処理部

func (c *Conn) read(b []byte) (int, error) {
	buf, ok := <-c.readyQueue
	if !ok {
		return 0, fmt.Errorf("failed to read")
	// ...
1
2
3
4
5

しかし現在のSocket APIは能動的にデータを上位層に渡す手段は持っていません.実際の実装ではシステムコールを使用してreadするのでアプリケーション側が任意のタイミングでデータを読み出します.つまり,PSHフラグは現在意味をなさず,TCPは到着したデータを単にバッファに詰め込んでアプリケーションからの読み出しを待ちます.

というわけで実装を変更しました.

// Do not check PSH flag.
l := len(packet.Packet.Data)
if len(c.rcvBuffer.buf)+l >= cap(c.rcvBuffer.buf) {
	c.rcvBuffer.init()
	c.tcb.rcv.WND = window
	c.rcvBuffer.buf = append(c.rcvBuffer.buf, packet.Packet.Data...)
	c.tcb.rcv.NXT = c.tcb.rcv.NXT + uint32(l)
	c.tcb.rcv.WND = c.tcb.rcv.WND - uint32(l)
	// ...
}
1
2
3
4
5
6
7
8
9
10

セグメント処理部では単に到着したデータをバッファに詰め込みます.

func (c *Conn) read(b []byte) (int, error) {
	if _, ok := <-c.rcvBuffer.readable; !ok {
		return 0, fmt.Errorf("failed to recv")
	}
	l := copy(b, c.rcvBuffer.buf)
	c.rcvBuffer.init()
	c.tcb.rcv.WND = window
	return l, nil
}
1
2
3
4
5
6
7
8
9

Read処理部ではバッファがreadableならバッファをコピーします.

RFC793は昔に策定された仕様なので実際の実装とは異なる場合があるようです.歴史を感じます.

# パケットがどこかへ消えてしまう(未解決問題)

ある程度大きなデータを送受信しようとすると途中でパケットが欠損してしまい応答ができなくなるという問題がありました. EthernetからTCPまで結構複雑にgoroutineとchannelを使用してデータを送受信しています. 非同期処理の中でデータが落ちてるのかなと思いデバッグしてましたが期間中に修正することはできず時間切れでした.

当時の僕

# 解決

期間中は間に合いませんでしたが原因はgoroutineとchannelのスイッチングの問題でうまく処理ができていないことのようでした.原因は詳しく調査できていませんが以下のようにchannel受信処理の前にsleepを挟むとうまく動作しました.

for {
	time.Sleep(time.Millisecond * 100)
	buf, ok := <-rcvQueue
	// ...
}
1
2
3
4
5

# まとめ

期間中はpandax381さんをはじめKLabの方々に様々なサポートをしていただきました.ありがとうございました.いただいたお菓子も非常に美味しかったです.結果としてほぼほぼ全期間デバッグで終わってしまいましたが一人では気づけない箇所に気づくことができたので非常に有意義でした.

詳しいTCP/IPの実装などはまた別に記事にできればなと思います. 貴重な機会をいただきましてありがとうございました.