ActionCableSwift 0.3.2

ActionCableSwift 0.3.2

Oleh Hudeichuk维护。



ActionCableSwift

Action Cable Swift Tweet

SPM Action Cable Swift Cocoa Pods

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

使用方法


您的 WebSocketService 应实现 ACWebSocketProtocol 协议。


Websocket-kit 一起使用

我强烈建议不要使用 Starscream 来实现 WebSocket,因为它们有一个奇怪的实现方式,不允许在断开连接后方便地重新连接到远程服务器。还有一个来自 Swift Server Work Group (SSWG) 的酷快替代方案,名为 Websocket-kit

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库,例如

Websocket-kit

Starscream

作者

许可证

ActionCableSwift采用MIT许可证。有关更多信息,请参阅LICENSE文件。