最近在接觸即時通訊相關的開發內容,撇除使用第三方服務的串接, 大多數都是建議使用 Socket 來和伺服器端進行連接和溝通。 而研究了一下,在 TCP / IP 架構下,sockets 可以分為兩種
- Datagram sockets(connectionless)
- Stream sockets(connection-oriented)
Datagram sockets(connectionless)
Datagram sockets 是使用 UDP 封包來進行傳送, 其主要的特色是速度快但不能保證資料的完整性以及次序有可能會有誤; 所以大多是使用在廣播資訊或是傳送一些較不是那麼重要的內容。
Stream sockets(connection-oriented)
而 Stream sockets 則是使用 TCP 封包來傳遞, 其因為會先需要確認 Server 和 Client 兩者連接狀態後再傳遞, 故速度較慢一些,但至少能確認資料的有序性以及完整度。
簡單的總結
以中華郵政的觀點來看,以結果來區分的話, 我們可以簡略地將 UDP 視為平信、TCP 視為掛號信。 而即時通訊的部分,依照上述的特色來看,就得選擇使用 TCP 的方式來進行溝通。
Server 和 Client 的溝通流程
首先,伺服器端和用戶端兩者必須使用同一類的封包才能互相通訊, 意思便是指 Server 建立了一個 UDP Socket,Client 也必須使用 UDP Socket 才能兩者打通。 而 Socket name 會需要包含 IP、Port、以及使用哪種協定; 當 Client 端的 Socket 成功聯繫上 Server 端的 Socket 時, 這兩者便形成一組 association。
iOS 上的 Socket 實作方式
我這邊是使用第三方套件的方式來協助實作,Github 上有不少相關的開源碼可以使用。
- socket.io
- CocoaAsyncSocket
這篇就先以 CocoaAsyncSocket 作為內容主軸來當後面的說明。 比照上方的流程圖,首先我們要先建立一個 socket,再來進行 connect 的動作, 之後相關的 callback,會以 delegate 的方式來做處理。
import CocoaAsyncSocket
let socket = GCDAsyncSocket(delegate: <#T##GCDAsyncSocketDelegate?#>, delegateQueue: <#T##DispatchQueue?#>)
do {
try socket.connect(toHost: Host, onPort: Port)
} catch {
print(error)
}
// GCDAsyncSocketDelegate
func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
// 連接上後,需要執行 readData 等候 Server 執行 writeData 的動作
sock.readData(withTimeout: -1, tag: 0)
}
func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
// 讀完資料後,還是需要再執行一次 readData,來等候下一次的資料傳輸過來
sock.readData(withTimeout: -1, tag: 0)
}
func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {
}
func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
}
不過這邊要比較注意到的是,會需要主動的執行 readData 的動作, 來告知底層,目前可以讀取傳輸進來的資料。
在 RxSwift 內的實作方式 讓 socket 的動作和事件是可被訂閱的,參考了一些實作的方式, 讓 CocoaAsyncSocket 和 RxSwift 結合在一塊:
//
// RxCocoaAsyncSocket.swift
//
// Created by 家齊 on 2017/7/14.
// Copyright © 2017年 張家齊. All rights reserved.
//
import Foundation
import RxSwift
import RxCocoa
import CocoaAsyncSocket
enum SocketEvent {
case connected
case disconnected(Error?)
case data(Data)
}
class RxCocoaAsyncSocketDelegateProxy: DelegateProxy {
fileprivate let subject = PublishSubject<SocketEvent>()
fileprivate weak var delegate: GCDAsyncSocketDelegate?
required init(parentObject: AnyObject) {
let socket = parentObject as? GCDAsyncSocket
delegate = socket?.delegate
super.init(parentObject: parentObject)
}
deinit {
subject.onCompleted()
}
}
extension RxCocoaAsyncSocketDelegateProxy: GCDAsyncSocketDelegate {
func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
subject.onNext(.connected)
sock.readData(withTimeout: -1, tag: 0)
}
func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
sock.readData(withTimeout: -1, tag: 0)
subject.onNext(.data(data))
}
func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {
subject.onNext(.disconnected(err))
}
func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
}
}
extension RxCocoaAsyncSocketDelegateProxy: DelegateProxyType {
static func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
guard let socket = object as? GCDAsyncSocket else {
return
}
socket.delegate = delegate as? GCDAsyncSocketDelegate
}
static func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
let socket = object as? GCDAsyncSocket
return socket?.delegate
}
}
extension Reactive where Base : GCDAsyncSocket {
var response: Observable<SocketEvent> {
return RxCocoaAsyncSocketDelegateProxy.proxyForObject(base).subject
}
var connected: Observable<Bool> {
return response.filter({
event -> Bool in
switch event {
case .connected, .disconnected:
return true
default:
return false
}
}).map({
event in
switch event {
case .connected:
return true
default:
return false
}
})
}
var json: Observable<[String: Any]?> {
return response.filter({
event -> Bool in
switch event {
case .data:
return true
default:
return false
}
}).map({
event in
switch event {
case .data(let data):
return (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any]
default:
return nil
}
})
}
}
有興趣的人可以參考參考!