Network.frameworkでiOSとquic-goでQUICで双方向ストリーム

# Network.frameworkでiOSとquic-goでQUICで双方向ストリームしてみた

iOS15からiOSがQUICが使えるようになりました🎉🎉🎉🎉🎉🎉🎉
WWDC2021での発表はこちら

https://developer.apple.com/videos/play/wwdc2021/10094

この中で紹介されているのですが、URLSessionがHTTP/3をサポートするので、HTTPリクエストは複雑な事をしなくても QUICが使えそうです
また、Network.frameworkにNWProtocolQUICが新しく入っており、ほかのTCPやUDPのソケットと同じインタフェースでQUICのストリームを扱えるようになったようです

https://developer.apple.com/documentation/network/nwprotocolquic

そこで、今回はNetwork.frameworkを使って、iOSとquic-goでQUICの双方向ストリームを試してみました

# リポジトリ

https://github.com/fuziki/swift-quic-examples

# Network.frameworkのNWProtocolQUICを使った実装
## NWProtocolQUIC.Optionsの作成

application protocolsは"echo"で作成します
(作成するサーバ側と揃えます)
directionを双方に設定しておきます
今回はオレオレ証明書を利用するので、sec_protocol_options_set_verify_blockを使って全信頼します

```swift
let options = NWProtocolQUIC.Options(alpn: ["echo"])
options.direction = .bidirectional
let securityProtocolOptions: sec_protocol_options_t = options.securityProtocolOptions
sec_protocol_options_set_verify_block(securityProtocolOptions,
                                      { (_: sec_protocol_metadata_t,
                                         _: sec_trust_t,
                                         complete: @escaping sec_protocol_verify_complete_t) in
    // とりあえず OK
    complete(true)
}, DispatchQueue.main)
let parameters = NWParameters(quic: options)
```

## NWConnection の作成
先ほど作成した parametersを使ってNWConnectionを作成します
stateUpdateHandlerでstateが.readyになると、コネクションが読み書きできるようになっています

```swift
connection = NWConnection(host: "xxx.xxx.xxx.xxx", port: xxxx, using: parameters)
subscribe() // 後述
connection.stateUpdateHandler = { [weak self] (state: NWConnection.State) in
    print("state: \(state)")
    switch state {
    case .ready:
        print("connected!")
    default:
        break
    }
}
connection.start(queue: DispatchQueue.main)
```

## NWConnection のデータの受信
上の subscribe()を実装します
受け取ったcontentは今回は文字列決め打ちで表示します
receiveのクロージャは1度呼ばれると外されるので、subscribe()を再帰呼び出しします

```swift
func subscribe() {
    connection.receive(minimumIncompleteLength: 1, maximumLength: 128)
    { [weak self] (content: Data?, contentContext: NWConnection.ContentContext?, isComplete: Bool, error: NWError?) in
        print("receive: \(content.flatMap { String(data: $0, encoding: .utf8) } ?? "null data")")
        self?.subscribe()
    }
}
```

## NWConnection のデータの送信

送信用の関数を実装します
文字列をDataに変換して送信します

```swift
func send(message: String) {
    let completion: NWConnection.SendCompletion = .contentProcessed { (error: Error?) in
        print("send error: \(String(describing: error))")
    }
    print("send message: \(message)")
    connection.send(content: message.data(using: .utf8)!,
                    contentContext: .defaultMessage,
                    isComplete: true,
                    completion: completion)
}
```

先ほどのstateUpdateHandlerにて、.readyになったら、2秒後と4秒後にそれぞれメッセージを送ります

```swift
connection.stateUpdateHandler = { [weak self] (state: NWConnection.State) in
    print("state: \(state)")
    switch state {
    case .ready:
        print("connected!")
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self?.send(message: "first message")
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 4) {
            self?.send(message: "second message")
        }
    default:
        break
    }
}
```

swiftの実装は以上です

# quic-goを使ったエコーサーバの実装

クライアントから送信された文字列をそのまま送り返すエコーサーバを実装します
quic-go と、証明書の作成のために、insecureを使用します

```go
package main

import (
	"context"
	"crypto/tls"
	"log"

	"github.com/alta/insecure"
	quic "github.com/lucas-clemente/quic-go"
)
func main() {
	err := serve("xxx.xxx.xxx.xxx:xxxx")
	if err != nil {
		log.Fatal(err)
	}
}
func serve(addr string) error {
    // ここに実装していく
}
```

## quic.Listenerの作成

serve関数を実装します
insecureを使って証明書を作成します
application protocolsはクライアントと同じ"echo"を設定します

```go
cert, err := insecure.Cert()
if err != nil {
	return err
}
tlsConfig := &tls.Config{
	Certificates: []tls.Certificate{cert},
	NextProtos:   []string{"echo"},
}
quicConfig := &quic.Config{
	EnableDatagrams: false,
}
listener, err := quic.ListenAddr(addr, tlsConfig, quicConfig)
if err != nil {
	return err
}
```

quic.Sessionを受け取る

```go
for {
	sess, err := listener.Accept(context.Background())
	if err != nil {
		return err
	}
	go func(sess quic.Session) {
		log.Printf("sess: %#v", sess)
		// ここに実装していく
	}(sess)
}
```

quic.Streamを受け取る

```go
log.Printf("sess: %#v", sess)

stream, err := sess.AcceptStream(context.Background())
if err != nil {
	panic(err)
}
log.Printf("stream: %#v", stream)

```

Streamからバイトを受け取ったら、同じ内容を書き込みます

```go

for {
	bytes := make([]byte, 32)
	read, err := stream.Read(bytes)
	log.Printf("read: %#v, %v, %v", read, bytes, err)
	if err != nil {
		break
	}

	write, err := stream.Write(bytes[0:read])
	log.Printf("write: %#v, %v", write, err)
	if err != nil {
		break
	}
}
```

goの実装は以上です

# 動作確認
## Swiftの実行結果
送ったメッセージと同じ内容が送り返されていることが分かります

```
state: preparing
state: ready
quic connected!
send message: first message
send error: nil
receive: first message
send message: second message
send error: nil
receive: second message
```

## Goの実行結果

readした内容をそのままwriteできていることが分かります

```
sess: &quic.session{handshakeDestConnID:(中略)}
stream: &quic.stream{receiveStream:(中略)}
read: 13, [102 105 114 115 116 32 109 101 115 115 97 103 101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], <nil>
write: 13, <nil>
read: 14, [115 101 99 111 110 100 32 109 101 115 115 97 103 101 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0], <nil>
write: 14, <nil>
```

# まとめ
iOS15で新しく Network.frameworkに入ったNWProtocolQUICを使って、quic-goで建てたサーバとQUICで双方向ストリームしてみました
Network.frameworkを利用すると、websocketやtcp、udpなどと同じ感じでQUICを利用できました
高度に抽象化されているAPIが提供されていると使いやすく、ありがたいです