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

iOS + Swift では概ね抽象度の高い API を使ってネットワーク処理を行うことが多い(し、Apple も推奨している)のですが、それでも Socket 通信が必要だったりする場合もあるかと思います。ので試しました。

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

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

CFSocket or POSIX

Apple のドキュメントによると、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 のシグネチャをみてみましょう。

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 のシグネチャは

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

となっており、上記のシグネチャの関数ポインタを渡せばよいです。ただし、ここで渡すのはC言語型の関数ポインタであり( @convention(c) )、func キーワードで定義した 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 の定義をみてみます。

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. というところです。つまりこの infoCFSocketCallBack 型の最後の引数 info として渡されます。コールバック関数は、この参照を通してアプリケーションの実装にアクセスすることができるというわけです。

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

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

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

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

実装

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

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 になります。

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 構造体を渡します。

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 に登録して、パケットを待ちます。

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 を呼び出すだけです。

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


『 Swift 』Article List