WebSocket-Swift
Starscream的使用
WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更好的節(jié)省服務(wù)器資源和帶寬并達(dá)到實(shí)時(shí)通訊,它建立在 TCP 之上,同 HTTP 一樣通過(guò) TCP 來(lái)傳輸數(shù)據(jù),但是它和 HTTP 最大不同是:WebSocket 是一種雙向通信協(xié)議.
現(xiàn)在很多第三方比如:融云,環(huán)信,網(wǎng)易云,騰訊云等等,都可以滿(mǎn)足即時(shí)通訊的功能。而且技術(shù)成熟。但是當(dāng)由于特殊要求不能使用第三方時(shí),就可以使用WebSocket建立長(zhǎng)鏈接,實(shí)現(xiàn)即時(shí)通訊功能。
目前Swift版本的WebSocket最好用的框架就應(yīng)該是daltoniam的Starscream了。
Starscream庫(kù)下載地址:Starscream
OC最好用的算是Facebook的SocketRocket,使用說(shuō)明可以參考我的另一篇文章iOS WebSocket長(zhǎng)鏈接
使用
一般一個(gè)項(xiàng)目在啟動(dòng)后的某個(gè)時(shí)機(jī)會(huì)啟動(dòng)創(chuàng)建一個(gè)長(zhǎng)鏈接,如果多個(gè)地方需要,就可以封裝為一個(gè)單例,全局使用。
可以使用podpod管理庫(kù), 在podfile中加入
pod 'Starscream', '~> 4.0.0'
在使用命令行工具cd到當(dāng)前工程 安裝
pod install
導(dǎo)入頭文件import Starscream,即可使用。
1.首先創(chuàng)建一個(gè)名為WebSocketManager的單例類(lèi)
static let shard = WebSocketManager()
可以使用單例,也可以使用[alloc]init 根據(jù)情況自己選擇
2.創(chuàng)建一個(gè)枚舉,分別表示W(wǎng)ebSocket的鏈接狀態(tài)
enum WebSocketConnectType {
case closed //初始狀態(tài),未連接
case connect //已連接
case disconnect //連接后斷開(kāi)
case reconnecting //重連中...
}
3.創(chuàng)建連接
// MARK: - 公開(kāi)方法,外部調(diào)用
func connectSocket(_ paremeters: Any?) {
paremeters可以是外部傳入的參數(shù)。
4.接收消息
包括連接成功的消息,發(fā)送的文本消息,連接失敗的消息,等等。
新版的庫(kù),將所有的消息接收都封裝在了一個(gè)代理方法里面,老版的是分開(kāi)的。老版本的代理我寫(xiě)在了最下面,可以參考一下,有助于理解。
遵循代理,實(shí)現(xiàn)代理,接收消息:
webSocket?.delegate = self
消息接收:
func didReceive(event: WebSocketEvent, client: WebSocket) {
//接收各種消息
}
5.關(guān)閉連接
/// 斷開(kāi)鏈接
func disconnect()
6.為保持長(zhǎng)鏈接的連接狀態(tài),需要定時(shí)向后臺(tái)發(fā)送消息,就是俗稱(chēng)的:心跳包。
需要?jiǎng)?chuàng)建一個(gè)定時(shí)器,固定時(shí)間發(fā)送ping消息。
7.重新連接
/// 重新連接
func reConnectSocket()
8.鏈接斷開(kāi)情況處理:
首先判斷是否是主動(dòng)斷開(kāi),并且記錄這個(gè)操作狀態(tài):
如果是主動(dòng)斷開(kāi)就不作處理。
如果不是主動(dòng)斷開(kāi)鏈接,需要做重新連接的邏輯.
/// 用于判斷是否主動(dòng)關(guān)閉長(zhǎng)連接,如果是主動(dòng)斷開(kāi)連接,連接失敗的代理中,就不用執(zhí)行 重新連接方法
private var isActivelyClose:Bool = false
代碼如下:
//
// WebSocketManager.swift
// SwiftTools
//
// Created by 網(wǎng)易詞典 on 2019/12/1.
// Copyright ? 2019 xuanhe. All rights reserved.
//
import UIKit
import Starscream
// MARK: - WebSocket代理
//這里即設(shè)置代理,稍后還會(huì)發(fā)通知.使用情況不一樣.
protocol WebSocketManagerDelegate: class {
/// 建立連接成功通知
func webSocketManagerDidConnect(manager: WebSocketManager)
/// 斷開(kāi)鏈接通知,參數(shù) `isReconnecting` 表示是否處于等待重新連接狀態(tài)。
func webSocketManagerDidDisconnect(manager: WebSocketManager, error: Error?)
/// 接收到消息后的回調(diào)(String)
func webSocketManagerDidReceiveMessage(manager: WebSocketManager, text: String)
/// 接收到消息后的回調(diào)(Data)
func webSocketManagerDidReceiveData(manager: WebSocketManager, data: Data)
}
enum WebSocketConnectType {
case closed //初始狀態(tài),未連接
case connect //已連接
case disconnect //連接后斷開(kāi)
case reconnecting //重連中...
}
class WebSocketManager: NSObject {
/// 單例,可以使用單例,也可以使用[alloc]init 根據(jù)情況自己選擇
static let shard = WebSocketManager()
/// WebSocket對(duì)象
private var webSocket : WebSocket?
/// 是否連接
var isConnected : Bool = false
/// 代理
weak var delegate: WebSocketManagerDelegate?
private var heartbeatInterval: TimeInterval = 5
/// 重連次數(shù)
private var reConnectCount: Int = 0
//存儲(chǔ)要發(fā)送給服務(wù)端的數(shù)據(jù),本案例不實(shí)現(xiàn)此功能,如有需求自行實(shí)現(xiàn)
private var sendDataArray = [String]()
///心跳包定時(shí)器
var heartBeatTimer: Timer?
///網(wǎng)絡(luò)監(jiān)聽(tīng)定時(shí)器
var netWorkTimer:Timer?
var connectType : WebSocketConnectType = .closed
/// 用于判斷是否主動(dòng)關(guān)閉長(zhǎng)連接,如果是主動(dòng)斷開(kāi)連接,連接失敗的代理中,就不用執(zhí)行 重新連接方法
private var isActivelyClose:Bool = false
/// 當(dāng)前是否有網(wǎng)絡(luò),??????????應(yīng)該由各自項(xiàng)目提供,本處為了方便,簡(jiǎn)歷一個(gè)屬性作為臨時(shí)變量
private var isHaveNet:Bool = true
override init() {
// webSocket.advancedDelegate = self
}
// MARK: - 公開(kāi)方法,外部調(diào)用
func connectSocket(_ paremeters: Any?) {
guard let url = URL(string: "http://localhost:8888") else {
return
}
self.isActivelyClose = false
var request = URLRequest(url: url)
request.timeoutInterval = 5
//添加頭信息
request.setValue("headers", forHTTPHeaderField: "Cookie")
request.setValue("CustomeDeviceInfo", forHTTPHeaderField: "DeviceInfo")
webSocket = WebSocket(request: request)
webSocket?.delegate = self
webSocket?.connect()
// 自定義隊(duì)列,一般不需要設(shè)置,默認(rèn)主隊(duì)列
//webSocket?.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")
}
/// 發(fā)送消息
func sendMessage(_ text: String) {
if self.isHaveNet {
// 有網(wǎng)絡(luò)直接發(fā)消息
if self.connectType == .connect { //已經(jīng)連接
self.webSocket?.write(string: text)
}else if self.connectType == .reconnecting {
self.sendDataArray.append(text)
}else if self.connectType == .disconnect {
reConnectSocket()
}else{
self.sendDataArray.append(text)
}
} else {
// 無(wú)網(wǎng)絡(luò)的時(shí)候的操作
//1.提示無(wú)網(wǎng)絡(luò)
//2.存儲(chǔ)消息
self.sendDataArray.append(text)
//等待來(lái)網(wǎng)
guard isActivelyClose else {
initNetWorkTestingTimer()
return
}
}
}
/// 斷開(kāi)鏈接
func disconnect() {
self.isActivelyClose = true
self.connectType = .disconnect
webSocket?.disconnect()
destoryHeartBeat()
destoryNetWorkStartTesting()
}
/// 重新連接
func reConnectSocket() {
if self.reConnectCount > 10 { //重連10次
self.reConnectCount = 0;
return
}
//重連10次,每?jī)纱伍g隔5s
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
if self.connectType == .reconnecting {
return
}
/// 連接
self.connectSocket(nil)
self.reConnectCount = self.reConnectCount + 1
}
}
// MARK: - 網(wǎng)絡(luò)監(jiān)聽(tīng)
func networkNotifation() {
//外部最好也傳進(jìn)來(lái)一個(gè)網(wǎng)絡(luò)變化的通知
//當(dāng)斷開(kāi)網(wǎng)絡(luò)時(shí)候,不在進(jìn)行重新連接
//當(dāng)網(wǎng)絡(luò)恢復(fù)的時(shí)候,重新連接,根據(jù)自己業(yè)務(wù)進(jìn)行更新.
//更新網(wǎng)絡(luò)狀態(tài)
isHaveNet = false
}
// MARK: - 私有方法
/// 初始化心跳
private func initHeartBeat() {
if self.heartBeatTimer != nil {
return
}
self.heartBeatTimer = Timer(timeInterval: 1, target: self, selector: #selector(sendHeartBeat), userInfo: nil, repeats: true)
RunLoop.current.add(self.heartBeatTimer!, forMode: RunLoop.Mode.common)
}
private func initNetWorkTestingTimer() {
if self.netWorkTimer != nil {
return
}
self.netWorkTimer = Timer(timeInterval: 5, target: self, selector: #selector(noNetWorkStartTesting), userInfo: nil, repeats: true)
RunLoop.current.add(self.netWorkTimer!, forMode: RunLoop.Mode.common)
}
/// 心跳
@objc private func sendHeartBeat() {
if self.isConnected {
let text = "ping的內(nèi)容,和服務(wù)器商定"
if let data = text.data(using: String.Encoding.utf8) {
webSocket?.write(ping: data)
}
// 我在網(wǎng)上查閱資料顯示,也可以使用webSocket?.write(string: "")
// 即: webSocket?.write(string: text)
// write方法中ping和text是一樣的,只是傳入的枚舉不一樣,可以參考源代碼
}else{
// 發(fā)現(xiàn)沒(méi)有連接,根據(jù)需求做判斷
}
}
/// 沒(méi)有網(wǎng)絡(luò)的時(shí)候開(kāi)始定時(shí) -- 用于網(wǎng)絡(luò)檢測(cè)
@objc private func noNetWorkStartTesting() {
//有網(wǎng)絡(luò)
if isHaveNet {//這里可以根據(jù)業(yè)務(wù)需要修改
//1.關(guān)閉網(wǎng)絡(luò)監(jiān)測(cè)定時(shí)器
destoryNetWorkStartTesting()
//2.重新連接
reConnectSocket()
}
}
//關(guān)閉心跳定時(shí)器
private func destoryHeartBeat() {
self.heartBeatTimer?.invalidate()
self.heartBeatTimer = nil
}
//關(guān)閉網(wǎng)絡(luò)監(jiān)測(cè)定時(shí)器
private func destoryNetWorkStartTesting() {
self.netWorkTimer?.invalidate()
self.netWorkTimer = nil
}
}
extension WebSocketManager: WebSocketDelegate{
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
isConnected = true
delegate?.webSocketManagerDidConnect(manager: self)
_ = "連接成功,在這里處理成功后的邏輯,比如將發(fā)送失敗的消息重新發(fā)送等等..."
print("websocket is connected: \(headers)")
break
case .disconnected(let reason, let code):
isConnected = false
let error = NSError(domain: reason, code: Int(code), userInfo: nil) as Error
delegate?.webSocketManagerDidDisconnect(manager: self, error: error)
self.connectType = .disconnect
if self.isActivelyClose {
self.connectType = .closed
} else {
self.connectType = .disconnect
destoryHeartBeat() //斷開(kāi)心跳定時(shí)器
if self.isHaveNet {
reConnectSocket() //重新連接
} else {
initNetWorkTestingTimer()
}
}
print("websocket is disconnected: \(reason) with code: \(code)")
break
case .text(let string):
delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
//當(dāng)全局都需要數(shù)據(jù)時(shí),這里使用通知.
let dic = ["text" : string]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveMessage"), object: dic)
print("Received text: \(string)")
break
case .binary(let data):
print("Received data: \(data.count)")
break
case .ping(_):
print("ping")
break
case .pong(_):
print("pong")
break
case .viabilityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
isConnected = false
case .error(let error):
isConnected = false
handleError(error)
}
}
// custom
func handleError(_ error: Error?) {
if let e = error as? WSError {
print("websocket encountered an error: \(e.message)")
} else if let e = error {
print("websocket encountered an error: \(e.localizedDescription)")
} else {
print("websocket encountered an error")
}
}
// MARK: - 老版本的代理,不要了
/// ????????????????????
///都是分開(kāi)的,現(xiàn)在都合成一個(gè)了,就是上面的didReceive,但是可以參考一下,理解邏輯,.知道哪些是重要的
/// 連接成功后的回調(diào)
func websocketDidConnect(socket: WebSocketClient) {
print(#function)
}
/// 斷開(kāi)連接后的回調(diào)
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
print(#function)
}
/// 接收到消息后的回調(diào)(String)
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print(#function)
}
/// 接收到消息后的回調(diào)(Data)
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
print(#function)
}
/// ??????????????????????
}
說(shuō)明:
代理里面已經(jīng)是全部的邏輯了,但是有一些細(xì)節(jié)需要特殊說(shuō)明,也需要我們自行處理:
- 外部接收消息,是使用代理還是使用通知??
當(dāng)我們的長(zhǎng)鏈接只需要在某一個(gè)頁(yè)面使用的時(shí)候,我們創(chuàng)建了一個(gè)代理,在實(shí)現(xiàn)頁(yè)面需要實(shí)現(xiàn)代理即可。
當(dāng)我們需要全局使用的時(shí)候,請(qǐng)使用通知,代理不準(zhǔn)確。
下面代碼兩種方法,請(qǐng)使用其中一個(gè):
delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
//當(dāng)全局都需要數(shù)據(jù)時(shí),這里使用通知.
let dic = ["text" : string]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveMessage"), object: dic)
- webSocket代理里面,我只寫(xiě)基本的處理邏輯,需要根據(jù)項(xiàng)目處理不同狀態(tài)下的邏輯。
- 當(dāng)消息發(fā)送失敗的時(shí)候,消息處理邏輯我們沒(méi)有實(shí)現(xiàn)完全。項(xiàng)目代碼里面有寫(xiě)。
存儲(chǔ)要發(fā)送給服務(wù)端的數(shù)據(jù),本案例不實(shí)現(xiàn)此功能,如有需求自行實(shí)現(xiàn)。做法就是我們把發(fā)送失敗的消息存起來(lái),鏈接成功后重新發(fā)送。 - 重連次數(shù),我設(shè)置的是10次,可以自行修改
- 當(dāng)前是否有網(wǎng)絡(luò)的判斷,我寫(xiě)了一個(gè)bool變量,需要替換成我們項(xiàng)目里面網(wǎng)絡(luò)狀態(tài)判斷的相關(guān)代碼。
- 可以自定義隊(duì)列,一般不需要設(shè)置,默認(rèn)主隊(duì)列。我沒(méi)有處理在其他隊(duì)列情況的邏輯。
本文參考了文章:
SSRWebSocket