［iOS 12］Network FrameworkでUDPソケット通信

iOS 12で新規追加された**Network Frameworkを使ってUDPによるソケット通信を実装**してみました。

以前だと[`CFSocket`というCore FoundationのクラスでC言語ベースで実装する必要があった](https://qiita.com/Biacco/items/ed3bae426ceef0a85603)ところが、Networkフレームワークの登場によりSwiftでSwiftyに書けるようになります。

### 受信側の実装

`NWListener`というクラスを使って、UDPのListenerを実装します。


```swift
// 定数
let networkType = "_networkplayground._udp."
let networkDomain = "local"
```

```swift
private func startListener(name: String) {
    let udpParams = NWParameters.udp
    guard let listener = try! NWListener(parameters: udpParams) else { fatalError() }
    
    listener.service = NWListener.Service(name: name, type: networkType)

    let listnerQueue = 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)")
}

private func receive(on connection: NWConnection) {
    print("receive on connection: \(connection)")
    connection.receive { (data: Data?, contentContext: NWConnection.ContentContext?, aBool: Bool, error: NWError?) in
        
        if let data = data, let message = String(data: data, encoding: .utf8) {
            print("Received Message: \(message)")
        }

        if let error = error {
            print(error)
        } else {
            // エラーがなければこのメソッドを再帰的に呼ぶ
            self.receive(on: connection)
        }
    }   
}
```

- [NWListener](https://developer.apple.com/documentation/network/nwlistener)
- [NWListener.Service](https://developer.apple.com/documentation/network/nwlistener/service)
- [NWParameters](https://developer.apple.com/documentation/network/nwparameters)


### 送信側の実装

`NWConnection`というクラスを利用して、UDPでデータ送信のための準備を行います。（Connectionとは言ってるものの、UDPなのでTCPとは違ってハンドシェイクを行っての接続の確立、みたいなことはしない）

```swift
private var connection: NWConnection!

private func startConnection(to name: String) {
    let udpParams = NWParameters.udp
    // 送信先エンドポイント
    let endpoint = NWEndpoint.service(name: name, type: networkType, domain: networkDomain, interface: nil)
    connection = NWConnection(to: endpoint, using: udpParams)
    
    connection.stateUpdateHandler = { (state: NWConnection.State) in
        guard state != .ready { return }
        print("connection is ready")

        // do something
        ...
    }
    
    // コネクション開始
    let connectionQueue = DispatchQueue(label: "com.shu223.NetworkPlayground.sender")
    connection.start(queue: connectionQueue)
}

func send(message: String) {
    let data = message.data(using: .utf8)
    
    // 送信完了時の処理
    let completion = NWConnection.SendCompletion.contentProcessed { (error: NWError?) in
        print("送信完了")
    }

    // 送信
    connection.send(content: data, completion: completion)
}
```

- [NWConnection](https://developer.apple.com/documentation/network/nwconnection)
- [NWEndpoint](https://developer.apple.com/documentation/network/nwendpoint)


### 
### サービスを探索する

接続相手を見つけるため、Listenerがアドバタイズしているであろうサービス（`NWListener.Service`）を探索します。

- 初期化

```swift
let netServiceBrowser = NetServiceBrowser()
```

- `NetServiceBrowserDelegate`プロトコルのデリゲートメソッド群を実装
  - すべて`optional`
  - とりいそぎ動作確認したいだけであれば、`netServiceBrowserWillSearch(_:)`（探索スタートする前に呼ばれるのでちゃんと動いてることを確認できる）と、`netServiceBrowser(_:didFind:moreComing:)`（サービス発見したときに呼ばれる）を最低限実装しておけばOK

```swift
extension ViewController: NetServiceBrowserDelegate {
    // 探索スタートする前に呼ばれる
    func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) {
    }

    // サービスを発見したら呼ばれる
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        // 自分以外であれば送信開始
        guard service.name != myName else { return }
        startConnection(to: service.name)
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String : NSNumber]) {
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didFindDomain domainString: String, moreComing: Bool) {
    }
    
    func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) {
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
    }
    
    func netServiceBrowser(_ browser: NetServiceBrowser, didRemoveDomain domainString: String, moreComing: Bool) {
    }
}
```


- 探索開始

```swift
netServiceBrowser.delegate = self
netServiceBrowser.searchForServices(ofType: networkType, inDomain: networkDomain)
```

### その他

- 受信・送信両方の機能を1つのアプリに持たせる
  - つまり**どちらもがListenerになり、どちらもが送信側になりうる**

- アプリ起動時に受信を開始

```swift
startListener(name: myName)
```

- 送信ボタン

```swift
@IBAction func sendBtnTapped(_ sender: UIButton) {
    send(message: "hoge")
}
```


### 実行


- 2台のiOSデバイスを用意する
- 同じネットワークに接続する
- 同アプリを実行

以上で両デバイスで受信準備が完了し、相手を見つけて送信準備も完了（`NWConnection.State`が`.ready`）したら、送信ボタンを押すたびに相手にメッセージが飛ぶようになります。