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 個大問題,以及更多的細節問題:
- 建立連線
- 資料傳輸
- 連線的變動
當前,URLSession 底層就是使用 Network.framework 完成基礎連線的。特地查了一下,相關私有 API 是從 iOS 9 開始存在的。
在未來,Apple 希望你能夠將原來的 Socket API 全部替換為全新的 Network.framework。(iOS 又有人要了!)
Network.framework 的特點
- 智慧建立連線
- 經優化的資料傳輸
- 內建的安全加密
- 無縫相容行動網路
- 原生 Swift 支援???
開始你的第一次連線
Socket 主要使用的三種場景:遊戲聯機、流式視訊傳輸、線上聊天。
使用傳統 Socket 建立連線
- 使用
getaddrinfo()
查詢 DNS - 使用正確的地址族去呼叫
socket()
- 使用
setsockopt()
設定 socket 選項 - 呼叫
connect()
開始 TCP 連線 - 等待直到一個可寫入的事件回撥
使用 Network.framework 建立連線
- 使用
NWEndPoint
與NWParameters
建立連線 - 呼叫
connection.start()
- 等待連線進入
.ready
的狀態
連線的生命週期
在連線設定完畢以後,就會進入 準備 狀態。而針對移動裝置複雜的網路狀態,你需要更加智慧的建立連線。
而使用 Network.framework ,你可以十分簡單的對網路路徑進行配置,比如下面的例子中,指定了僅使用蜂窩網路、使用 IPv6 協議、與禁止代理。都僅是一行命令就完成了。特別當你需要為特定連線指定連線方式時,這個框架能極大提高你的效率。
在準備完畢以後,連線可能進入 等待 、就緒 或 失敗 狀態。當然在你取消連線時也會進入 取消 狀態。
案例:流式視訊傳輸
該案例使用 UDP 進行視訊的實時傳輸,出於簡化考慮,並未對視訊幀做任何編碼,直接把裸資料封包,並通過 UDP 傳輸。在接收端,解包資料並重新封裝為視訊幀,直接進行播放。案例中也使用了 Bonjour 服務來進行快速裝置配對連線。
在監聽端的程式碼異常簡單,甚至連 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 專題目錄