iOS 12で新規追加されたNetwork Frameworkを使って、UDPによるソケット通信を実装してみました。
以前だとCFSocket
というCore FoundationのクラスでC言語ベースで実装する必要があったところが、Networkフレームワークの登場によりSwiftでSwiftyに書けるようになります。
受信側の実装
NWListener
というクラスを使って、UDPのListenerを実装します。
// 定数letnetworkType="_networkplayground._udp."letnetworkDomain="local"
privatefuncstartListener(name:String) { letudpParams= NWParameters.udp guardletlistener= try! NWListener(parameters:udpParams) else { fatalError() } listener.service = NWListener.Service(name:name, type:networkType) letlistnerQueue= DispatchQueue(label:"com.shu223.NetworkPlayground.listener") // 新しいコネクション受診時の処理 listener.newConnectionHandler = { [unowned self] (connection:NWConnection) in connection.start(queue:listnerQueue) self.receive(on:connection) } // Listener開始 listener.start(queue:listnerQueue) print("Start Listening as \(listener.service!.name)") } privatefuncreceive(on connection:NWConnection) { print("receive on connection: \(connection)") connection.receive { (data:Data?, contentContext:NWConnection.ContentContext?, aBool:Bool, error:NWError?) inifletdata= data, letmessage= String(data:data, encoding: .utf8) { print("Received Message: \(message)") } ifleterror= error { print(error) } else { // エラーがなければこのメソッドを再帰的に呼ぶself.receive(on:connection) } } }
送信側の実装
NWConnection
というクラスを利用して、UDPでデータ送信のための準備を行います。(Connectionとは言ってるものの、UDPなのでTCPとは違ってハンドシェイクを行っての接続の確立、みたいなことはしない)
privatevarconnection:NWConnection!privatefuncstartConnection(to name:String) { letudpParams= NWParameters.udp // 送信先エンドポイントletendpoint= NWEndpoint.service(name:name, type:networkType, domain:networkDomain, interface:nil) connection = NWConnection(to:endpoint, using:udpParams) connection.stateUpdateHandler = { (state:NWConnection.State) inguard state != .ready else { return } print("connection is ready") // do something... } // コネクション開始letconnectionQueue= DispatchQueue(label:"com.shu223.NetworkPlayground.sender") connection.start(queue:connectionQueue) } funcsend(message:String) { letdata= message.data(using: .utf8) // 送信完了時の処理letcompletion= NWConnection.SendCompletion.contentProcessed { (error:NWError?) in print("送信完了") } // 送信 connection.send(content:data, completion:completion) }
サービスを探索する
接続相手を見つけるため、Listenerがアドバタイズしているであろうサービス(NWListener.Service
)を探索します。
初期化
letnetServiceBrowser= NetServiceBrowser()
NetServiceBrowserDelegate
を実装
- すべて
optional
- とりいそぎ動作確認したいだけであれば、
netServiceBrowserWillSearch(_:)
(探索スタートする前に呼ばれるのでちゃんと動いてることを確認できる)と、netServiceBrowser(_:didFind:moreComing:)
(サービス発見したときに呼ばれる)を最低限実装しておけばOK
extensionViewController:NetServiceBrowserDelegate { // 探索スタートする前に呼ばれるfuncnetServiceBrowserWillSearch(_ browser:NetServiceBrowser) { } // サービスを発見したら呼ばれるfuncnetServiceBrowser(_ browser:NetServiceBrowser, didFind service:NetService, moreComing:Bool) { // 自分以外であれば送信開始guard service.name != myName else { return } startConnection(to:service.name) } funcnetServiceBrowser(_ browser:NetServiceBrowser, didNotSearch errorDict:[String : NSNumber]) { } funcnetServiceBrowser(_ browser:NetServiceBrowser, didFindDomain domainString:String, moreComing:Bool) { } funcnetServiceBrowserDidStopSearch(_ browser:NetServiceBrowser) { } funcnetServiceBrowser(_ browser:NetServiceBrowser, didRemove service:NetService, moreComing:Bool) { } funcnetServiceBrowser(_ browser:NetServiceBrowser, didRemoveDomain domainString:String, moreComing:Bool) { } }
探索開始
netServiceBrowser.delegate =self netServiceBrowser.searchForServices(ofType:networkType, inDomain:networkDomain)
その他
受信・送信両方の機能を1つのアプリに持たせる
- つまりどちらもがListenerになり、どちらもが送信側になりうる
アプリ起動時に受信を開始
startListener(name:myName)
- 送信ボタン
@IBActionfuncsendBtnTapped(_ sender:UIButton) { send(message:"hoge") }
実行
以上で両デバイスで受信準備が完了し、相手を見つけて送信準備も完了(NWConnection.State
が.ready
)したら、送信ボタンを押すたびに相手にメッセージが飛ぶようになります。
Special Thanks
本実装はSwift Islandというカンファレンスでの Roy Marmelstein氏のワークショップで学んだ実装を自分なりに咀嚼して(復習・覚書として)書き直したものです。
WWDC18の当該セッションもまだ見てなくて、たぶんワークショップに参加しなかったら自分では当面さわらなかったんじゃないかと思うフレームワークなのですが、こうして自分でやってみると(CF
時代の不慣れなC言語による難しさ成分が取り除かれているため)意外と扱いやすいことがわかってよかったです。もうちょっとネットワークプロトコルの気持ちがわかりたいなと思ってた頃なので、引き続きさわっていきたい所存です。