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