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

劉棟發表於2019-03-02

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。

Socket 程式設計有很多需要解決的問題,最重要的 3 個大問題,以及更多的細節問題:

  • 建立連線
    WWDC 2018:Network.framework 入門,現代化 Socket 程式設計的新選擇
  • 資料傳輸
    WWDC 2018:Network.framework 入門,現代化 Socket 程式設計的新選擇
  • 連線的變動
    WWDC 2018:Network.framework 入門,現代化 Socket 程式設計的新選擇

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

在未來,Apple 希望你能夠將原來的 Socket API 全部替換為全新的 Network.framework。(iOS 又有人要了!)

Network.framework 的特點

  • 智慧建立連線
  • 經優化的資料傳輸
  • 內建的安全加密
  • 無縫相容行動網路
  • 原生 Swift 支援???

開始你的第一次連線

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

使用傳統 Socket 建立連線

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

使用 Network.framework 建立連線

  1. 使用 NWEndPointNWParameters 建立連線
  2. 呼叫 connection.start()
  3. 等待連線進入 .ready 的狀態
WWDC 2018:Network.framework 入門,現代化 Socket 程式設計的新選擇

對就是這麼簡單,完全的原生 Swift 支援,又物件導向,又支援閉包。這樣的介面,你不心動麼?

連線的生命週期

在連線設定完畢以後,就會進入 準備 狀態。而針對移動裝置複雜的網路狀態,你需要更加智慧的建立連線。

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

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

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

在準備完畢以後,連線可能進入 等待就緒失敗 狀態。當然在你取消連線時也會進入 取消 狀態。

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

案例:流式視訊傳輸

該案例使用 UDP 進行視訊的實時傳輸,出於簡化考慮,並未對視訊幀做任何編碼,直接把裸資料封包,並通過 UDP 傳輸。在接收端,解包資料並重新封裝為視訊幀,直接進行播放。案例中也使用了 Bonjour 服務來進行快速裝置配對連線。

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

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

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

最佳的資料傳輸方式

資料的傳送與接收

單幀傳送

// 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
    }
}
複製程式碼

開始實踐

應避免的做法

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

不應繼續使用的介面

CoreFoundation 中 CFStream 繫結的相關方法及 CFSocket

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

Foundation 中與 NSStream 繫結、NSNetService 監聽、NSSocketPort 以及 SystemConfiguration 中的 SCNetworkReachability

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

推薦的介面

當然是 URLSession 和 Network.framework。

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

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

相關文章