iOS 中使用 webSocket
是服務器和app之間的一種通信方式
webSocket 實現了服務端推機制(主動向客戶端發送消息)。新的 web 瀏覽器全都支持 WebSocket,這使得它的使用超級簡單。通過 WebSocket 能夠打開持久連接,大部分網絡都能輕松處理 WebSocket 連接。在 iOS 中使用 WebSocket 比較麻煩,你必須進行大量的設置,而且內置的 API 根本幫不上忙。這時 Starscream 出現了——這個小巧、易於使用的庫讓你所有的煩惱不翼而飛。
Client1 ——-> cloud ————>client2,3,4…
<——-返回ack <——-返回ack
一,基本使用
1根據url創建socket
var request = URLRequest(url: URL(string: "url")!)
request.timeoutInterval = 5//超時時間
socket = WebSocket(request: request)
socket.delegate = self//接收到消息走代理方法
2。發送消息
socket.write(string: sendStr)
3.接收消息,在代理方法中
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
//接收到字符串消息
}
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
printLog(“\(data)”)//接收到data消息
}
二 常見問題
1.如何確保client向特定的client發送消息
“\(storeID!)-\(deviceNumber)-\(deviceGlobalID!)”.uppercased() 這些標志客戶端的唯一性
發送消息時帶着要發送給哪些client(唯一標識性數組)發送給cloud,cloud根據要發送給的client數組向相應的client發送消息
/// 發送一條消息到指定的多個設備
///
/// - Parameters:
/// - deviceID: web socket 登陸名稱數組
/// - text: 要發送的文本
func sendTextTo(deviceIDs: [String], text: String) {
if socket == nil {
return
}
if socket.isConnected == false {
return
}
let cmdMessage = AldeloMessage(Type: 1, MsgGID: UUID().uuidString, Receivers: deviceIDs, Content: text, Time: nil, Publisher: nil, axOrderIDs: nil)
if let sendStr = AldeloMessage.toJsonString(messages: [cmdMessage]) {
socket.write(string: sendStr)
}
}
2.如何保持鏈接
十分鍾發送一次心跳包,app進入前台時,app斷網重連時,app失去web連接時,重新連接
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
do {
try reachability.startNotifier()
} catch {
print("Unable to start notifier")
}
reachability.whenReachable = { [weak self] reachability in
self?.reconnectTimes = 10
firstly {
after(seconds: 3)
}.done {
if self?.socket == nil {
return
}
self?.socket.connect()
}
}
if #available(iOS 10.0, *) {
timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { timer in
let now = Date().timeIntervalSince1970
let s = now - self.lastReceivedMessageTime
if s >= 600 && s <= 660 {
self.sendHeartBeat()
} else if s > 660 {
self.reconnect()
}
}
} else {
timer = Timer.scheduledTimer(timeInterval: 30, target: self, selector: #selector(handleTimer), userInfo: nil, repeats: true)
}
@objc func handleTimer(timer: Timer) {
let now = Date().timeIntervalSince1970
let s = now - self.lastReceivedMessageTime
if s >= 600 && s <= 660 {
self.sendHeartBeat()
} else if s > 660 {
self.reconnect()
}
}
@objc func appDidBecomeActive(_ application: UIApplication) {
firstly {
after(seconds: 3)
}.done {
if self.socket == nil {
return
}
if self.socket.isConnected == false && self.reachability.connection != .none {
self.socket.connect()
}
}
}
3.如何保證消息送達
client到cloud:client中維護一個message數據表(包括字段是否發送成功sent)cloud收到消息之后向client返回ack,client收到ack后將該條message標記為sent=1已發送
60秒client未收到ack,視為發送失敗,從新發送
cloud端message表中已經存在該條消息,則忽略,但是向客戶端client發送ack
cloud到clinet:client收到消息后向cloud返回ack,cloud收到ack標記消息為已發送成功, 60秒cloud未收到ack,視為發送失敗,從新發送
client端message表中已經存在該條消息,則忽略,但是向客戶端client發送ack
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
printLog("Web Socket receive \(text)")
lastReceivedMessageTime = Date().timeIntervalSince1970
if text == "$" {
printLog("Received Heart Beat!!!!")
return
}
guard let messageArray = AldeloMessage.from(jsonData: text.data(using: .utf8)!) else { return }
for message in messageArray {
if message.Type == 99 { //ACK
DBPool.write { db in
try? db.execute("Update AldeloMessageRecord Set sent = 1 Where messageGID = '\(message.MsgGID)'")
}
continue
}
//收到消息后回復ACK,這樣服務器會標記這條消息發送成功
sendACK(message: message)
//從收到的消息列表中對比msgid, 如果已經收到過,則忽略這條消息, 去重處理
var shouldReturn = false
DBQueue.inDatabase { db in
do {
if let count = try Int.fetchOne(db, "Select count(*) from AldeloMessageRecord where messageGID = '\(message.MsgGID)'"), count > 0 {
//數據庫里有這條消息,說明已經收到過,忽略掉
Log.shareInstance.log(message: "Websocket 收到重復消息,已忽略")
printLog("Websocket 收到重復消息,已忽略")
shouldReturn = true
}
} catch {
Log.shareInstance.log(message: "讀取數據庫錯誤")
printLog("讀取數據庫錯誤")
self.createTable()
}
}
if shouldReturn == true {
return
}
// if let count = DatabaseOption().intForSql("Select count(*) from AldeloMessageRecord where messageGID = '\(message.MsgGID)'"), count > 0 {
// //數據庫里有這條消息,說明已經收到過,忽略掉
// Log.shareInstance.log(message: "Websocket 收到重復消息,已忽略")
// printLog("Websocket 收到重復消息,已忽略")
// return
// }
//發完ACK將message存到數據庫
let aMessage = AldeloMessageRecord()
aMessage.messageGID = message.MsgGID
aMessage.type = message.Type
aMessage.time = message.Time
aMessage.publisher = message.Publisher
if message.Type == 1 { //text
guard let content = message.Content else { continue }
aMessage.message = message.Content
if content.hasPrefix("cmd::") {
let ar = content.components(separatedBy: "::")
var para: String? = nil
if ar.count == 3 {
para = ar[2]
}
let cmdString = "\(ar[0])::\(ar[1])".lowercased()
let command = AldeloCommand(rawValue: cmdString) ?? AldeloCommand.unknown
if command == .clinePrint {
if let ar = para?.components(separatedBy: ","), ar.count == 2 {
if let orderID = Int64(ar[0]), orderID > 0 {
gotPrintCommandBlock?([orderID],ar[1].boolValue(),true, message)
delegate?.receivedPrintCommand(axOrderIDs: [orderID], packingPrint: ar[1].boolValue(), isClientWebSocket: true)
}
}
} else {
gotCommandBlock?(command,para, message)
delegate?.receivedCommand(cmd: command,parameter: para, message: message)
}
} else {
gotMessageBlock?(message)
delegate?.receivedMessage(message: message)
}
} else if message.Type == 2 { //print
guard let orderIDs = message.axOrderIDs else { return }
aMessage.message = "\(orderIDs)"
gotPrintCommandBlock?(orderIDs,true,false,message)
delegate?.receivedPrintCommand(axOrderIDs: orderIDs, packingPrint: true, isClientWebSocket: false)
} else if message.Type == 3 { //QR payment
guard let content = message.Content else { continue }
aMessage.message = content
gotQRPaymentBlock?(content)
delegate?.receivedQRPayment(content: content)
} else if message.Type == 4 { //cloud 強制反激活
printLog("cloud 強制反激活 .....")
}
DBPool.write { db in
try? aMessage.insert(db)
}
}
}
starscream地址:https://github.com/daltoniam/starscream
