8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-09-26

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

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

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

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

リポジトリ

Network.frameworkのNWProtocolQUICを使った実装

NWProtocolQUIC.Optionsの作成

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

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になると、コネクションが読み書きできるようになっています

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()を再帰呼び出しします

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に変換して送信します

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秒後にそれぞれメッセージを送ります

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を使用します

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"を設定します

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を受け取る

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を受け取る

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

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

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


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が提供されていると使いやすく、ありがたいです

8
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?