CFSocket を使って iOS で socket 通信した話

iOS + Swift では概ね抽象度の高い API を使ってネットワーク処理を行うことが多い(し、[Apple も推奨している](https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/SocketsAndStreams/SocketsAndStreams.html))のですが、それでも Socket 通信が必要だったりする場合もあるかと思います。ので試しました。

Core Foundation を触ることになるので、結果として C と Swift の相互運用、特に UnsafePointer についてもぶつかることがあったのですが、これは非常にいい記事があるので、そちらを参考にしてください。 [素晴らしいSwiftのポインタ型の解説](http://qiita.com/omochimetaru/items/c95e0d36ae7f1b1a9052)

サンプルコードではエラー処理などは端折っていたりするので、その点はご容赦ください。

# CFSocket or POSIX

[Apple のドキュメント](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/UsingSocketsandSocketStreams.html)によると、Socket 通信には CFSocket と POSIX の socket API が利用できるようです。今回はドキュメントに従って CFSocket を利用して実装してみます。

# CFSocket の作成

CFSocket を作成するには

- ```func CFSocketCreate(CFAllocator!, Int32, Int32, Int32, CFOptionFlags, CFSocketCallBack!, UnsafePointer<CFSocketContext>!)```
- ```func CFSocketCreateConnectedToSocketSignature(CFAllocator!, UnsafePointer<CFSocketSignature>!, CFOptionFlags, CFSocketCallBack!, UnsafePointer<CFSocketContext>!, CFTimeInterval)```
- ```func CFSocketCreateWithNative(CFAllocator!, CFSocketNativeHandle, CFOptionFlags, CFSocketCallBack!, UnsafePointer<CFSocketContext>!)```
- ```func CFSocketCreateWithSocketSignature(CFAllocator!, UnsafePointer<CFSocketSignature>!, CFOptionFlags, CFSocketCallBack!, UnsafePointer<CFSocketContext>!)```

の4つの関数がありますが、今回は一番オーソドックスな ```CFSocketCreate(CFAllocator!, Int32, Int32, Int32, CFOptionFlags, CFSocketCallBack!, UnsafePointer<CFSocketContext>!)``` を使います。ちなみにここで言う ```SocketSignature``` とは、接続先のアドレスと接続プロトコルなどをまとめた構造体のことで、Socket の作成と接続設定を同時に行うことができます。

最初に自分はまずここで驚いたのですが、Core Foundation はC言語による実装のため、API はすべて関数になります。

## CFSocketCreate のシグネチャと必要な要素

CFSocketCreate のシグネチャをみてみましょう。

```c
func CFSocketCreate(_ allocator: CFAllocator!, 
                  _ protocolFamily: Int32, 
                  _ socketType: Int32, 
                  _ protocol: Int32, 
                  _ callBackTypes: CFOptionFlags, 
                  _ callout: CFSocketCallBack!, 
                  _ context: UnsafePointer<CFSocketContext>!) -> CFSocket!
```

### allocator

allocator は NULL を指定するとデフォルトの Memory Allocator が利用されるので、NULL でよいです。

### protocolFamily

プロトコルを指定します。IPv4 であれば ```PF_INET```、IPv6 であれば ```PF_INET6``` になります。定義は ```Darwin.POSIX.sys.socket``` にあります。

### socketType

Socket のタイプですが、実質的には TCP/UDP の選択に使います。TCPであれば ```SOCK_STREAM``` 、UDP であれば ```SOCK_DGRAM``` を指定します。定義は同様に ```Darwin.POSIX.sys.socket``` にあります。

### protocol

こちらでもプロトコルとして TCP/UDP が選択できますが、他にもたくさん ```Darwin.POSIX.netinet.in```
 に定義されているようです。TCPであれば ```IPPROTO_TCP``` 、UDPであれば ```IPPROTO_UDP``` を指定します。

### callBackTypes

Callback (callout) が受け取るコールバックの種類を設定します。

- ```CFSocketCallBackType.readCallBack.rawValue```
- ```CFSocketCallBackType.acceptCallBack.rawValue```
- ```CFSocketCallBackType.dataCallBack.rawValue```
- ```CFSocketCallBackType.connectCallBack.rawValue```
- ```CFSocketCallBackType.writeCallBack.rawValue```

それぞれの論理和をとることで、複数のコールバックを受け取ることができます。

### callout

callout は ```CFSocketCallBack``` 型のコールバック関数を指定します。 ```CFSocketCallBack``` のシグネチャは

```swift
public typealias CFSocketCallBack = @convention(c) (CFSocket?, CFSocketCallBackType, CFData?, UnsafeRawPointer?, UnsafeMutableRawPointer?) -> Swift.Void
```

となっており、上記のシグネチャの関数ポインタを渡せばよいです。ただし、ここで渡すのはC言語型の関数ポインタであり( ```@convention(c)``` )、func キーワードで定義した Swift 関数/メソッドないしは、クロージャリテラルのみが受け入れられます。そしてスコープの変数をキャプチャすることは___できません___。

```swift
// OK
func callout_ok(sock: CFSocket?, callbackType: CFSocketCallBackType, address: CFData?, data: UnsafeRawPointer?, info: UnsafeMutableRawPointer?) -> Void {
  ...
}

// NG
var callout_ng = {(sock: CFSocket?, callbackType: CFSocketCallBackType, address: CFData?, data: UnsafeRawPointer?, info: UnsafeMutableRawPointer?) -> Void in
  ...
}

// NG
func callout_ng2(sock: CFSocket?, callbackType: CFSocketCallBackType, address: CFData?, data: UnsafeRawPointer?, info: UnsafeMutableRawPointer?) -> Void {
  print(self.hoge) // キャプチャはできない
  ...
}
```

キャプチャができないため、コールバックを受けても基本的には副作用がなにも起こせないように見えますが、次の引数である context でアクセス手段が提供されます。そのため、実装の詳細は次の context を説明してからにします。

### context

```UnsafePointer<CFSocketContext>!``` 型の引数です。この context は前述の通りコールバック関数と深い関係があります。まず ```CFSocketContext``` の定義をみてみます。

```c
typedef struct {
    CFIndex	version;
    void *	info;
    const void *(*retain)(const void *info);
    void	(*release)(const void *info);
    CFStringRef	(*copyDescription)(const void *info);
} CFSocketContext
```

この構造体の中で重要なのが ```void *info``` です。Apple のドキュメントを引用します。

> var info: UnsafeMutableRawPointer!
> 
> An arbitrary pointer to program-defined data, which can be associated with the CFSocket object at creation time. __This pointer is passed to all the callbacks defined in the context.__

_強調は筆者によるもの_

ここで重要なのが、This pointer is passed to all the callbacks defined in the context. というところです。つまりこの ```info``` が ```CFSocketCallBack``` 型の最後の引数 ```info``` として渡されます。コールバック関数は、この参照を通してアプリケーションの実装にアクセスすることができるというわけです。

```info``` の Swift から見た型は ```UnsafeMutableRawPointer``` で、いわゆる Opaque なポインタとなっており、実装については任意に決めることができます。

その他の変数に関しては…

- ```version``` は必ず ```0``` を代入することになっています。
- ```retain/release``` には ```info``` に関する retain/release コールバックを渡すことができます。```info``` が適切にメモリ管理できているのであれば ```nil``` にすることが可能です。
- ```copyDescription``` にはデバッグ用の ```info``` を説明する文章が入れられます。```nil``` にすることが可能です。

これらを構成することで、CFSocketを生成することができます。

## 実装

以上をもとに、実装を行います。今回は UDP IPv6 での送受信を行うことにします。

```swift
class SocketContext { // info に渡すデータ構造(今回はクラス)
  var onData: ((NSData) -> Void)?
}

class ViewController: UIViewController {

  var socketContext = SocketContext() // field として保持しておく

  func openSocket() -> CFSocket {
    socketContext.onData = { [weak self] (x: NSData) in
      DispatchQueue.main.async {
        print("receive!")
      }
    }

    var cfSocketContext = CFSocketContext(version: 0,
                                            info: &socketContext,
                                            retain: nil,
                                            release: nil,
                                            copyDescription: nil)

    func callout(sock: CFSocket?, callbackType: CFSocketCallBackType, address: CFData?,
                   _data: UnsafeRawPointer?, _info: UnsafeMutableRawPointer?) -> Void
    {
      switch callbackType {
      case CFSocketCallBackType.dataCallBack:
        let info = _info!.assumingMemoryBound(to: SocketContext.self).pointee
        let data = Unmanaged<CFData>.fromOpaque(_data!).takeUnretainedValue()
        info.onData?(data)
      default:
        break
      }
    }

    return CFSocketCreate(kCFAllocatorDefault, PF_INET6, SOCK_DGRAM, IPPROTO_UDP,
                            CFSocketCallBackType.dataCallBack.rawValue,
                            callout,
                            &cfSocketContext)
  }
}
```

これでまず CFSocket を生成することができました。

# Socket を設定して受信準備をする

生成した CFSocket に対して、パケットを受信できるように設定します。

## インターフェースとスコープIDを取得する

設定する前に、バインド対象のインターフェースと、IPv6 の場合スコープ ID というものが必要になります。IPv6 の scope ID について詳しくは説明しませんが、送受信に利用するインターフェースを特定するために必要な ID になります。

```swift
func getAddress(ifname: String) -> (String, Int)? {
  var address : String?

  // インターフェース一覧の取得
  var ifaddrMemory : UnsafeMutablePointer<ifaddrs>?
  guard getifaddrs(&ifaddrMemory) == 0 else {
    return nil
  }
  defer {
    freeifaddrs(ifaddrMemory)
  }
    
  let addrs: [UnsafeMutablePointer<ifaddrs>] = ifaddrMemory.map {
      Array(sequence(first: $0) { $0.pointee.ifa_next })
  } ?? []
        
  let ifap: UnsafeMutablePointer<ifaddrs>? = addrs.filter {
    let addrFamily = $0.pointee.ifa_addr.pointee.sa_family
    return addrFamily == UInt8(AF_INET) || addrFamily == UInt8(AF_INET6)
  }.filter {
    return String(cString: $0.pointee.ifa_name) == ifname
  }.first
        
  guard let ifa = ifap else {
    return nil
  }

  let adx = UnsafePointer<sockaddr>(ifa.pointee.ifa_addr)!
  let ad6 = UnsafePointer<sockaddr_in6>(OpaquePointer(adx))!

  // CString に変換する
  var hostname = [CChar](repeating: 0, count: Int(NI_MAXHOST))
  guard getnameinfo(ifa.pointee.ifa_addr,
                      socklen_t(ifa.pointee.ifa_addr.pointee.sa_len),
                      &hostname,
                      socklen_t(hostname.count),
                      nil,
                      socklen_t(0),
                      NI_NUMERICHOST) == 0 else
  {
    return nil
  }

  return (String(cString: hostname), Int(ad6.pointee.sin6_scope_id))
}
```

## sockaddr 構造体を使って CFSocket をバインドする

CFSocket をポートにバインドするには ```CFSocketSetAddress(_ s: CFSocket!, _ address: CFData!) -> CFSocketError``` を使います。CFData 型の ```address``` には、プロトコルに応じて ```sockaddr_in``` 構造体もしくは ```sockaddr_in6``` 構造体を渡します。

```swift
func bindSocket(socket :CFSocket, port: Int) {
  // インターフェース名が en0 のインターフェースに振られているアドレスとスコープIDを取得する
  guard let (addrWithInterface, scopeId) = getAddress(ifname: "en0") else {
          print("Get address failed")
          return
  }

  // IPv6 の場合インターフェース名が %インターフェース名 でくっつく？ので切り取る
  let addr = addrWithInterface.components(separatedBy: "%").first
        
  var sin6 = sockaddr_in6() // sockaddr_in6 構造体の確保
  sin6.sin6_len = __uint8_t(MemoryLayout<sockaddr_in6>.size) // 構造体のサイズ
  sin6.sin6_family = sa_family_t(AF_INET6)                   // プロトコル
  sin6.sin6_port = in_port_t(UInt16(port).bigEndian)         // バインドするポート
  sin6.sin6_scope_id = __uint32_t(scopeId)                   // スコープID
  sin6.sin6_flowinfo = __uint32_t(0)
  
  // String を CString として構造体に設定
  guard addr?.withCString({ inet_pton(AF_INET6, $0, &sin6.sin6_addr) }) == 1 else {
    print("Set IPv6 address failed")
    return
  }
        
  CFSocketSetAddress(socket, NSData(bytes: &sin6, length: MemoryLayout<sockaddr_in6>.size) as CFData)
}
```

## Socket を RunLoop に登録して Listen する

最後に、設定が完了した CFSocket を RunLoop に登録して、パケットを待ちます。

```swift
socketThread = Thread.init{
  CFRunLoopAddSource(CFRunLoopGetCurrent(),
                       CFSocketCreateRunLoopSource(nil, socket, 1),
                       CFRunLoopMode.defaultMode)

  RunLoop.current.run()
}

socketThread!.start()
```

これで、バインドしたポートに対して UDP でパケットを送信すると ```receive!``` とコンソールに表示されるはずです。

# Socket を使って送信する

送信はもっと簡単で、宛先アドレスとスコープIDを設定した ```sockaddr``` 構造体を作って ```CFSocketSendData(_ s: CFSocket!, _ address: CFData!, _ data: CFData!, _ timeout: CFTimeInterval) -> CFSocketError``` を呼び出すだけです。

```swift
func send(socket: CFSocket, data: CFData, sendTo: String, port: Int) {
  guard let (addrWithInterface, scopeId) = getAddress(ifname: "en0") else {
    print("Get address failed")
      return
    }
       
  var sin6 = sockaddr_in6()
  sin6.sin6_len = __uint8_t(MemoryLayout<sockaddr_in6>.size)
  sin6.sin6_family = sa_family_t(AF_INET6)
  sin6.sin6_port = in_port_t(UInt16(port).bigEndian)
  sin6.sin6_scope_id = __uint32_t(scopeId)
  sin6.sin6_flowinfo = __uint32_t(0)

  guard addr.withCString({ inet_pton(AF_INET6, $0, &sin6.sin6_addr) }) == 1 else {
    print("Set IPv6 address failed")
    return
  }

  CFSocketSendData(socket,
                     NSData(bytes: &sin6, length: MemoryLayout<sockaddr_in6>.size) as CFData,
                     data,
                     1)
}
```

# まとめ

以上、後半がやや駆け足になってしまいましたが、iOS で socket 通信をするために最低限必要そうな情報をまとめました。Swift からC言語の API に触れる際にいろいろお作法が違うために苦労しましたが、Swift のポインタ操作について理解できれば思ったより難しいことはないかもしれません。

Swift のポインターの世界については、また別記事でまとめたいと思います。

おしまい

# 参考

[Networking Programing Topics - Apple Developer](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Introduction/Introduction.html)