Action Cable Swift 是用于Action Cable Rails 5的一个客户端库,它使您轻松地向您的应用添加实时功能。这个Swift客户端受 "Swift-ActionCableClient" 启发,但现在不再支持,我创建了Action-Cable-Swift。
WebSockets客户端现在也已从客户端中分离。
安装
要安装,只需简单地
Swift包管理器
将以下行添加到您的Package.swift
中
// ...
.package(name: "ActionCableSwift", url: "https://github.com/nerzh/Action-Cable-Swift.git", from: "0.3.0"),
targets: [
.target(
name: "YourPackageName",
dependencies: [
.product(name: "ActionCableSwift", package: "ActionCableSwift")
])
// ...
Cocoa Pods
将以下行添加到您的 Podfile
:
pod 'ActionCableSwift'
然后您可以导入 ActionCableSwift
import ActionCableSwift
使用方法
ACWebSocketProtocol
协议。
您的 WebSocketService 应实现 Websocket-kit 一起使用
与Swift Server Work Group (SSWG) 的酷快替代方案,名为 Websocket-kit。
我强烈建议不要使用 Starscream 来实现 WebSocket,因为它们有一个奇怪的实现方式,不允许在断开连接后方便地重新连接到远程服务器。还有一个来自Websocket-kit 是基于 Swift-NIO 构建的 SPM(Swift Package Manager)客户端库
// ...
dependencies: [
.package(name: "ActionCableSwift", url: "https://github.com/nerzh/Action-Cable-Swift.git", from: "0.3.0"),
.package(name: "websocket-kit", url: "https://github.com/vapor/websocket-kit.git", .upToNextMinor(from: "2.0.0"))
],
targets: [
.target(
name: "YourPackageName",
dependencies: [
.product(name: "ActionCableSwift", package: "ActionCableSwift"),
.product(name: "WebSocketKit", package: "websocket-kit")
])
// ...
剧透:基于 Websocket-kit (Swift-NIO) 的 WSS 推荐实现
这是一个用于线程安全访问 WebSocket 实例的 propertyWrapper
import Foundation
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let lock = NSLock()
init(wrappedValue value: Value) {
self.value = value
}
var wrappedValue: Value {
get { return load() }
set { store(newValue: newValue) }
}
func load() -> Value {
lock.lock()
defer { lock.unlock() }
return value
}
mutating func store(newValue: Value) {
lock.lock()
defer { lock.unlock() }
value = newValue
}
}
这是 WSS 的实现
import NIO
import NIOHTTP1
import NIOWebSocket
import WebSocketKit
final class WSS: ACWebSocketProtocol {
var url: URL
private var eventLoopGroup: EventLoopGroup
@Atomic var ws: WebSocket?
init(stringURL: String, coreCount: Int = System.coreCount) {
url = URL(string: stringURL)!
eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: coreCount)
}
var onConnected: ((_ headers: [String : String]?) -> Void)?
var onDisconnected: ((_ reason: String?) -> Void)?
var onCancelled: (() -> Void)?
var onText: ((_ text: String) -> Void)?
var onBinary: ((_ data: Data) -> Void)?
var onPing: (() -> Void)?
var onPong: (() -> Void)?
func connect(headers: [String : String]?) {
var httpHeaders: HTTPHeaders = .init()
headers?.forEach({ (name, value) in
httpHeaders.add(name: name, value: value)
})
let promise: EventLoopPromise<Void> = eventLoopGroup.next().makePromise(of: Void.self)
WebSocket.connect(to: url.absoluteString,
headers: httpHeaders,
on: eventLoopGroup
) { ws in
self.ws = ws
ws.onPing { [weak self] (ws) in
self?.onPing?()
}
ws.onPong { [weak self] (ws) in
self?.onPong?()
}
ws.onClose.whenComplete { [weak self] (result) in
switch result {
case .success:
self?.onDisconnected?(nil)
self?.onCancelled?()
case let .failure(error):
self?.onDisconnected?(error.localizedDescription)
self?.onCancelled?()
}
}
ws.onText { (ws, text) in
self.onText?(text)
}
ws.onBinary { (ws, buffer) in
var data: Data = Data()
data.append(contentsOf: buffer.readableBytesView)
self.onBinary?(data)
}
}.cascade(to: promise)
promise.futureResult.whenSuccess { [weak self] (_) in
guard let self = self else { return }
self.onConnected?(nil)
}
}
func disconnect() {
ws?.close(promise: nil)
}
func send(data: Data) {
ws?.send([UInt8](data))
}
func send(data: Data, _ completion: (() -> Void)?) {
let promise: EventLoopPromise<Void>? = ws?.eventLoop.next().makePromise(of: Void.self)
ws?.send([UInt8](data), promise: promise)
promise?.futureResult.whenComplete { (_) in
completion?()
}
}
func send(text: String) {
ws?.send(text)
}
func send(text: String, _ completion: (() -> Void)?) {
let promise: EventLoopPromise<Void>? = ws?.eventLoop.next().makePromise(of: Void.self)
ws?.send(text, promise: promise)
promise?.futureResult.whenComplete { (_) in
completion?()
}
}
}
与 Starscream 一起使用
pod 'Starscream', '~> 4.0.0'
剧透:如果你想继续使用 "Starscream",则可以复制此代码用于 WebSocket 客户端
import Foundation
import Starscream
class WSS: ACWebSocketProtocol, WebSocketDelegate {
var url: URL
var ws: WebSocket
init(stringURL: String) {
url = URL(string: stringURL)!
ws = WebSocket(request: URLRequest(url: url))
ws.delegate = self
}
var onConnected: ((_ headers: [String : String]?) -> Void)?
var onDisconnected: ((_ reason: String?) -> Void)?
var onCancelled: (() -> Void)?
var onText: ((_ text: String) -> Void)?
var onBinary: ((_ data: Data) -> Void)?
var onPing: (() -> Void)?
var onPong: (() -> Void)?
func connect(headers: [String : String]?) {
ws.request.allHTTPHeaderFields = headers
ws.connect()
}
func disconnect() {
ws.disconnect()
}
func send(data: Data) {
ws.write(data: data)
}
func send(data: Data, _ completion: (() -> Void)?) {
ws.write(data: data, completion: completion)
}
func send(text: String) {
ws.write(string: text)
}
func send(text: String, _ completion: (() -> Void)?) {
ws.write(string: text, completion: completion)
}
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
onConnected?(headers)
case .disconnected(let reason, let code):
onDisconnected?(reason)
case .text(let string):
onText?(string)
case .binary(let data):
onBinary?(data)
case .ping(_):
onPing?()
case .pong(_):
onPong?()
case .cancelled:
onCancelled?()
default: break
}
}
}
下一步:使用 ActionCableSwift
import ActionCableSwift
/// web socket client
let ws = WSS(stringURL: "ws://:3334/cable")
/// action cable client
var client = ACClient(ws: ws)
/// pass headers to connect
client.headers = ["COOKIE": "Value"]
/// make channel
/// buffering - buffering messages if disconnect and flush after reconnect
var options = ACChannelOptions(buffering: true, autoSubscribe: true)
let channel = client.makeChannel(name: "RoomChannel", options: options)
channel.addOnSubscribe { (channel, optionalMessage) in
print(optionalMessage)
}
channel.addOnMessage { (channel, optionalMessage) in
print(optionalMessage)
}
channel.addOnPing { (channel, optionalMessage) in
print("ping")
}
/// Connect
client.connect()
手动携带参数订阅频道
client.addOnConnected { (headers) in
/// without params
try? channel.subscribe()
/// with params
try? channel.subscribe(params: ["Key": "Value"])
}
频道回调
func addOnMessage(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnSubscribe(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnUnsubscribe(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnRejectSubscription(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
func addOnPing(_ handler: @escaping (_ channel: ACChannel, _ message: ACMessage?) -> Void)
在频道上执行操作
// Send an action
channel.addOnSubscribe { (channel, optionalMessage) in
try? channel.sendMessage(actionName: "speak", params: ["test": 10101010101])
}
认证 & 表头
client.headers = [
"Authorization": "sometoken"
]
需求
任何WebSocket库,例如
作者
我
许可证
ActionCableSwift采用MIT许可证。有关更多信息,请参阅LICENSE文件。