WWDC 2018:Network.framework 入門,現代化 Socket 編程的新選擇

WWDC18 Session 715 Introducing Network.framework: A modern alternative to Sockets編程

現代化的傳輸 API

提及 Socket ,我回頭望了一眼書架上厚厚的 UNIX 網絡編程 卷1: 套接字聯網 API(第 3 版) ,而她的姊妹進程間通訊我連塑封膜都沒拆開。的確,這套最先來自 BSD 的 API 很讓人頭疼。雖然她們依然是跨平臺程序的最佳選擇,可是我想應該沒有哪一個小夥伴在項目中會有勇氣從這些 API 開始構築,至少是 CFNetwork 或者 NSNetwork 中的現成接口。更通常性的是選一些面向對象的第三方庫,好比老牌的 CocoaAsyncSocket。固然做爲 Swift 老法師我也會推薦你看看 IBM 出品的 BlueSocket。swift

Socket 編程有不少須要解決的問題,最重要的 3 個大問題,以及更多的細節問題:安全

  • 創建鏈接
  • 數據傳輸
  • 鏈接的變更

當前,URLSession 底層就是使用 Network.framework 完成基礎鏈接的。特意查了一下,相關私有 API 是從 iOS 9 開始存在的。網絡

在將來,Apple 但願你可以將原來的 Socket API 所有替換爲全新的 Network.framework。(iOS 又有人要了!)閉包

Network.framework 的特色

  • 智能創建鏈接
  • 經優化的數據傳輸
  • 內建的安全加密
  • 無縫兼容移動網絡
  • 原生 Swift 支持🔥🔥🔥

開始你的第一次鏈接

Socket 主要使用的三種場景:遊戲聯機、流式視頻傳輸、在線聊天。app

使用傳統 Socket 創建鏈接

  1. 使用 getaddrinfo() 查詢 DNS
  2. 使用正確的地址族去調用 socket()
  3. 使用 setsockopt() 設置 socket 選項
  4. 調用 connect() 開始 TCP 鏈接
  5. 等待直到一個可寫入的事件回調

使用 Network.framework 創建鏈接

  1. 使用 NWEndPointNWParameters 建立鏈接
  2. 調用 connection.start()
  3. 等待鏈接進入 .ready 的狀態

對就是這麼簡單,徹底的原生 Swift 支持,又面向對象,又支持閉包。這樣的接口,你不心動麼?

鏈接的生命週期

在鏈接設置完畢之後,就會進入 準備 狀態。而針對移動設備複雜的網絡狀態,你須要更加智能的創建鏈接。框架

而使用 Network.framework ,你能夠十分簡單的對網絡路徑進行配置,好比下面的例子中,指定了僅使用蜂窩網絡、使用 IPv6 協議、與禁止代理。都僅是一行命令就完成了。特別當你須要爲特定鏈接指定鏈接方式時,這個框架能極大提升你的效率。socket

在準備完畢之後,鏈接可能進入 等待就緒失敗 狀態。固然在你取消鏈接時也會進入 取消 狀態。tcp

案例:流式視頻傳輸

該案例使用 UDP 進行視頻的實時傳輸,出於簡化考慮,並未對視頻幀作任何編碼,直接把裸數據封包,並經過 UDP 傳輸。在接收端,解包數據並從新封裝爲視頻幀,直接進行播放。案例中也使用了 Bonjour 服務來進行快速設備配對鏈接。ide

在監聽端的代碼異常簡單,甚至連 Bonjour 服務也已經整合好了。你要作的僅僅是指定 .udp 並指定正確的 Bonjour 服務名稱。

最佳的數據傳輸方式

數據的發送與接收

單幀發送

// Send a single frame
func sendFrame(_ connection: NWConnection, frame: Data) {
    // The .contentProcessed completion provides sender-side back-pressure
    connection.send(content: frame, completion: .contentProcessed { (sendError) in
        if let sendError = sendError {
            // Handle error in sending
        } else {
            // Send has been processed, send the next frame
            let nextFrame = generateNextFrame()
            sendFrame(connection, frame: nextFrame)
        }
    })
}
複製代碼

使用 batch 發送多個數據報

// Hint that multiple datagrams should be sent as one batch
connection.batch {
    for datagram in datagramArray {
        connection.send(content: datagramArray, completion: .contentProcessed { (error) in
            // Handle error in sending
        }
    })
}
複製代碼

在接收時,提供了方便的方法來讀取消息頭

// Read one header from the connection
func readHeader(connection: NWConnection) {
    // Read exactly the length of the header
    let headerLength: Int = 10
    connection.receive(minimumIncompleteLength: headerLength, maximumLength: headerLength) { (content, contentContext, isComplete, error) in
        if let error = error {
            // Handle error in reading
        } else {
         // Parse out body length
        readBody(connection, bodyLength: bodyLength)
        }
    }
}
// Follow the same pattern as readHeader() to read exactly the body length
func readBody(_ connection: NWConnection, bodyLength: Int) { ... }
複製代碼

高級選項

顯式擁塞通知(Explicit Congestion Notification)

在全部 TCP 鏈接中 ECN 是默認開啓的。

在 UDP 鏈接中爲每一個數據包標記 ECN 的方法:

let ipMetadata = NWProtocolIP.Metadata() 
ipMetadata.ecn = .ect0
let context = NWConnection.ContentContext(identifier: "ECN", metadata: [ ipMetadata ])
connection.send(content: datagram, contentContext: context, completion: .contentProcessed{..})
複製代碼

服務等級(網絡隊列優先級)

爲整個鏈接更改服務等級

let parameters = NWParameters.tls 
parameters.serviceClass = .background
複製代碼

爲每一個 UDP 數據包更改服務等級

let ipMetadata = NWProtocolIP.Metadata() 
ipMetadata.serviceClass = .signaling
let context = NWConnection.ContentContext(identifier: "Signaling", metadata: [ ipMetadata ])
connection.send(content: datagram, contentContext: context, completion: .contentProcessed{..})
複製代碼

快速鏈接(Fast Open Connections)

容許在鏈接上快速打開須要發送冪等數據

parameters.allowFastOpen = true
let connection = NWConnection(to: endpoint, using: parameters)
connection.send(content: initialData, completion: .idempotent) 
connection.start(queue: myQueue)
複製代碼

能夠手動啓用 TCP Fast Open 以經過 TFO 運行 TLS

let tcpOptions = NWProtocolTCP.Options() 
tcpOptions.enableFastOpen = true
複製代碼

容許失效的 DNS 查詢結果

主動使用失效的 DNS 查詢結果

parameters.expiredDNSBehavior = .allow
let connection = NWConnection(to: endpoint, using: parameters)
connection.start(queue: myQueue)
複製代碼

新的 DNS 查詢會同步進行

處理網絡鏈接的變更

開始鏈接

  • .waiting 狀態暗示鏈接還未創建
  • 避免在網絡鏈接開始前檢查可用性
  • 在須要時在 NWParameters 限制鏈接類型

處理網絡鏈接狀態的變化

主要是兩個狀態,一個是 isViable 當前鏈接是否可用,一個是 betterPathAvailable 是否有更佳的鏈接路徑。她們也都提供了相應的閉包來處理

// Handle connection viability
connection.viabilityUpdateHandler = { (isViable) in
    if (!isViable) {
        // Handle connection temporarily losing connectivity
    } else {
        // Handle connection return to connectivity
    }
}

// Handle better paths
connection.betterPathUpdateHandler = { (betterPathAvailable) in
    if (betterPathAvailable) {
        // Start a new connection if migration is possible
    } else {
        // Stop any attempts to migrate
    }
}
複製代碼

開始實踐

應避免的作法

不該繼續使用的接口

CoreFoundation 中 CFStream 綁定的相關方法及 CFSocket

Foundation 中與 NSStream 綁定、NSNetService 監聽、NSSocketPort 以及 SystemConfiguration 中的 SCNetworkReachability

推薦的接口

固然是 URLSession 和 Network.framework。

查看更多 WWDC 18 相關文章請前往 老司機x知識小集xSwiftGG WWDC 18 專題目錄

相關文章
相關標籤/搜索