WebSocket-Swift
Starscream的使用
WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:WebSocket 是一种双向通信协议.
现在很多第三方比如:融云,环信,网易云,腾讯云等等,都可以满足即时通讯的功能。而且技术成熟。但是当由于特殊要求不能使用第三方时,就可以使用WebSocket建立长链接,实现即时通讯功能。
目前Swift版本的WebSocket最好用的框架就应该是daltoniam的Starscream了。
Starscream
库下载地址:Starscream
OC最好用的算是Facebook的SocketRocket,使用说明可以参考我的另一篇文章iOS WebSocket长链接
使用
一般一个项目在启动后的某个时机会启动创建一个长链接,如果多个地方需要,就可以封装为一个单例,全局使用。
可以使用podpod管理库, 在podfile中加入
pod 'Starscream', '~> 4.0.0'
在使用命令行工具cd到当前工程 安装
pod install
导入头文件import Starscream
,即可使用。
1.首先创建一个名为WebSocketManager的单例类
static let shard = WebSocketManager()
可以使用单例,也可以使用[alloc]init 根据情况自己选择
2.创建一个枚举,分别表示WebSocket的链接状态
enum WebSocketConnectType {
case closed //初始状态,未连接
case connect //已连接
case disconnect //连接后断开
case reconnecting //重连中...
}
3.创建连接
// MARK: - 公开方法,外部调用
func connectSocket(_ paremeters: Any?) {
paremeters可以是外部传入的参数。
4.接收消息
包括连接成功的消息,发送的文本消息,连接失败的消息,等等。
新版的库,将所有的消息接收都封装在了一个代理方法里面,老版的是分开的。老版本的代理我写在了最下面,可以参考一下,有助于理解。
遵循代理,实现代理,接收消息:
webSocket?.delegate = self
消息接收:
func didReceive(event: WebSocketEvent, client: WebSocket) {
//接收各种消息
}
5.关闭连接
/// 断开链接
func disconnect()
6.为保持长链接的连接状态,需要定时向后台发送消息,就是俗称的:心跳包。
需要创建一个定时器,固定时间发送ping消息。
7.重新连接
/// 重新连接
func reConnectSocket()
8.链接断开情况处理:
首先判断是否是主动断开,并且记录这个操作状态:
如果是主动断开就不作处理。
如果不是主动断开链接,需要做重新连接的逻辑.
/// 用于判断是否主动关闭长连接,如果是主动断开连接,连接失败的代理中,就不用执行 重新连接方法
private var isActivelyClose:Bool = false
代码如下:
//
// WebSocketManager.swift
// SwiftTools
//
// Created by 网易词典 on 2019/12/1.
// Copyright © 2019 xuanhe. All rights reserved.
//
import UIKit
import Starscream
// MARK: - WebSocket代理
//这里即设置代理,稍后还会发通知.使用情况不一样.
protocol WebSocketManagerDelegate: class {
/// 建立连接成功通知
func webSocketManagerDidConnect(manager: WebSocketManager)
/// 断开链接通知,参数 `isReconnecting` 表示是否处于等待重新连接状态。
func webSocketManagerDidDisconnect(manager: WebSocketManager, error: Error?)
/// 接收到消息后的回调(String)
func webSocketManagerDidReceiveMessage(manager: WebSocketManager, text: String)
/// 接收到消息后的回调(Data)
func webSocketManagerDidReceiveData(manager: WebSocketManager, data: Data)
}
enum WebSocketConnectType {
case closed //初始状态,未连接
case connect //已连接
case disconnect //连接后断开
case reconnecting //重连中...
}
class WebSocketManager: NSObject {
/// 单例,可以使用单例,也可以使用[alloc]init 根据情况自己选择
static let shard = WebSocketManager()
/// WebSocket对象
private var webSocket : WebSocket?
/// 是否连接
var isConnected : Bool = false
/// 代理
weak var delegate: WebSocketManagerDelegate?
private var heartbeatInterval: TimeInterval = 5
/// 重连次数
private var reConnectCount: Int = 0
//存储要发送给服务端的数据,本案例不实现此功能,如有需求自行实现
private var sendDataArray = [String]()
///心跳包定时器
var heartBeatTimer: Timer?
///网络监听定时器
var netWorkTimer:Timer?
var connectType : WebSocketConnectType = .closed
/// 用于判断是否主动关闭长连接,如果是主动断开连接,连接失败的代理中,就不用执行 重新连接方法
private var isActivelyClose:Bool = false
/// 当前是否有网络,👇👇👇👇👇应该由各自项目提供,本处为了方便,简历一个属性作为临时变量
private var isHaveNet:Bool = true
override init() {
// webSocket.advancedDelegate = self
}
// MARK: - 公开方法,外部调用
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()
// 自定义队列,一般不需要设置,默认主队列
//webSocket?.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")
}
/// 发送消息
func sendMessage(_ text: String) {
if self.isHaveNet {
// 有网络直接发消息
if self.connectType == .connect { //已经连接
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 {
// 无网络的时候的操作
//1.提示无网络
//2.存储消息
self.sendDataArray.append(text)
//等待来网
guard isActivelyClose else {
initNetWorkTestingTimer()
return
}
}
}
/// 断开链接
func disconnect() {
self.isActivelyClose = true
self.connectType = .disconnect
webSocket?.disconnect()
destoryHeartBeat()
destoryNetWorkStartTesting()
}
/// 重新连接
func reConnectSocket() {
if self.reConnectCount > 10 { //重连10次
self.reConnectCount = 0;
return
}
//重连10次,每两次间隔5s
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
if self.connectType == .reconnecting {
return
}
/// 连接
self.connectSocket(nil)
self.reConnectCount = self.reConnectCount + 1
}
}
// MARK: - 网络监听
func networkNotifation() {
//外部最好也传进来一个网络变化的通知
//当断开网络时候,不在进行重新连接
//当网络恢复的时候,重新连接,根据自己业务进行更新.
//更新网络状态
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的内容,和服务器商定"
if let data = text.data(using: String.Encoding.utf8) {
webSocket?.write(ping: data)
}
// 我在网上查阅资料显示,也可以使用webSocket?.write(string: "")
// 即: webSocket?.write(string: text)
// write方法中ping和text是一样的,只是传入的枚举不一样,可以参考源代码
}else{
// 发现没有连接,根据需求做判断
}
}
/// 没有网络的时候开始定时 -- 用于网络检测
@objc private func noNetWorkStartTesting() {
//有网络
if isHaveNet {//这里可以根据业务需要修改
//1.关闭网络监测定时器
destoryNetWorkStartTesting()
//2.重新连接
reConnectSocket()
}
}
//关闭心跳定时器
private func destoryHeartBeat() {
self.heartBeatTimer?.invalidate()
self.heartBeatTimer = nil
}
//关闭网络监测定时器
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)
_ = "连接成功,在这里处理成功后的逻辑,比如将发送失败的消息重新发送等等..."
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() //断开心跳定时器
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)
//当全局都需要数据时,这里使用通知.
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: - 老版本的代理,不要了
/// 👇👇👇👇👇👇👇👇👇👇
///都是分开的,现在都合成一个了,就是上面的didReceive,但是可以参考一下,理解逻辑,.知道哪些是重要的
/// 连接成功后的回调
func websocketDidConnect(socket: WebSocketClient) {
print(#function)
}
/// 断开连接后的回调
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
print(#function)
}
/// 接收到消息后的回调(String)
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print(#function)
}
/// 接收到消息后的回调(Data)
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
print(#function)
}
/// 👆👆👆👆👆👆👆👆👆👆👆
}
说明:
代理里面已经是全部的逻辑了,但是有一些细节需要特殊说明,也需要我们自行处理:
- 外部接收消息,是使用代理还是使用通知??
当我们的长链接只需要在某一个页面使用的时候,我们创建了一个代理,在实现页面需要实现代理即可。
当我们需要全局使用的时候,请使用通知,代理不准确。
下面代码两种方法,请使用其中一个:
delegate?.webSocketManagerDidReceiveMessage(manager: self, text: string)
//当全局都需要数据时,这里使用通知.
let dic = ["text" : string]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "webSocketManagerDidReceiveMessage"), object: dic)
- webSocket代理里面,我只写基本的处理逻辑,需要根据项目处理不同状态下的逻辑。
- 当消息发送失败的时候,消息处理逻辑我们没有实现完全。项目代码里面有写。
存储要发送给服务端的数据,本案例不实现此功能,如有需求自行实现
。做法就是我们把发送失败的消息存起来,链接成功后重新发送。 - 重连次数,我设置的是10次,可以自行修改
- 当前是否有网络的判断,我写了一个bool变量,需要替换成我们项目里面网络状态判断的相关代码。
- 可以自定义队列,一般不需要设置,默认主队列。我没有处理在其他队列情况的逻辑。
本文参考了文章:
SSRWebSocket