Mihael Isaev

MIT License Swift 5.2 Swift.Stream


通过便捷的观察者接收和发送 websocket 消息。甚至可以在不同的端点上设置多个观察者!

💡观察者类型:经典型、声明型、可绑定型。请阅读以下内容以了解所有这些类型。

专为 Vapor4 构建。

💡Vapor3 版本可在 vapor3 分支和 1.0.0 标签中找到

如果您有改进此软件包的好主意,请在 Vapor 的 Discord 聊天中联系我 (@iMike#3049),或者直接发送 pull request。

通过 Swift Package Manager 安装

编辑您的 Package.swift

//add this repo to dependencies
.package(url: "https://github.com/MihaelIsaev/AwesomeWS.git", from: "2.0.0")
//and don't forget about targets
.target(name: "App", dependencies: [
    .product(name: "WS", package: "AwesomeWS"),
    .product(name: "Vapor", package: "vapor")
]),

它是如何工作的?

声明型观察者

WS 库具有 .default WSID,它代表 DeclarativeObserver

💡您可以使用另一种类型的观察者和您的自定义类来声明您自己的 WSID。

您可以这样轻松地开始使用它

app.ws.build(.default).serve()

在这种情况下,它将开始在 / 监听 websocket 连接,但您可以在调用 .serve() 之前更改它

app.ws.build(.default).at("ws").serve()

现在它正在 /ws 监听

您还可以使用中间件保护您的 websocket 端点,例如,您可以在建立连接之前检查身份验证。

app.ws.build(.default).at("ws").middlewares(AuthMiddleware()).serve()

好的,看起来不错,但是如何处理传入的数据呢?

由于我们使用代表 Declarative 观察者的 .default WSID,我们可以像这样处理传入的数据

app.ws.build(.default).at("ws").middlewares(AuthMiddleware()).serve().onOpen { client in
    print("client just connected \(client.id)")
}.onText { client, text in
    print("client \(client.id) text: \(text)")
}

还有以下处理程序可用:onCloseonPingonPongonBinaryonByteBuffer

💡设置 app.logger.logLevel = .infoapp.logger.logLevel = .debug 以查看有关连接的更多信息

经典型观察者

您应该创建一个从 ClassicObserver 继承的新类

import WS

class MyClassicWebSocket: ClassicObserver {
    override func on(open client: AnyClient) {}
    override func on(close client: AnyClient) {}

    override func on(text: String, client: AnyClient) {}
    /// also you can override: `on(ping:)`, `on(pong:)`, `on(binary:)`, `on(byteBuffer:)`
}

并且您必须为其声明一个 WSID

extension WSID {
    static var myClassic: WSID<MyClassicWebSocket> { .init() }
}

然后开始服务它

app.ws.build(.myClassic).at("ws").serve()

可绑定型观察者

这种观察者旨在以特殊格式发送和接收事件,例如 JSON

{ "event": "<event name>", "payload": <anything> }

或者只是

{ "event": "<event name>" }

💡默认情况下,lib 使用 JSONEncoderJSONDecoder,但您可以在 setup 方法中使用任何其他编码器/解码器替换它们。

首先,在 EID 扩展中声明任何可能的事件,如下所示

struct Hello: Codable {
  let firstName, lastName: String
}
struct Bye: Codable {
  let firstName, lastName: String
}
extension EID {
    static var hello: EID<Hello> { .init("hello") }
    static var bye: EID<Bye> { .init("bye") }
    // Use `EID<Nothing>` if you don't want any payload
}

然后创建您的自定义可绑定观察者类

class MyBindableWebsocket: BindableObserver {
    // register all EIDs here
    override func setup() {
        bind(.hello, hello)
        bind(.bye, bye)
        // optionally setup here custom encoder/decoder
        encoder = JSONEncoder() // e.g. with custom `dateEncodingStrategy`
        decoder = JSONDecoder() // e.g. with custom `dateDecodingStrategy`
    }

    // hello EID handler
    func hello(client: AnyClient, payload: Hello) {
        print("Hello \(payload.firstName) \(payload.lastName)")
    }

    // bye EID handler
    func bye(client: AnyClient, payload: Bye) {
        print("Bye \(payload.firstName) \(payload.lastName)")
    }
}

声明一个 WSID

extension WSID {
    static var myBindable: WSID<MyBindableWebsocket> { .init() }
}

然后开始服务它

app.ws.build(.myBindable).at("ws").serve()

💡在这里,您还可以提供自定义编码器/解码器,例如 app.ws.build(.myBindable).at("ws").encoder(JSONEncoder()).encoder(JSONDecoder()).serve()

如何发送数据

数据发送通过具有多种方法的 Sendable 协议工作

.send(text: <StringProtocol>) // send message with text
.send(bytes: <[UInt8]>) // send message with bytes
.send(data: <Data>) // send message with binary data
.send(model: <Encodable>) // send message with Encodable model
.send(model: <Encodable>, encoder: Encoder)
.send(event: <EID>) // send bindable event
.send(event: <EID>, payload: T?)

所有这些方法都返回 EventLoopFuture<Void>

使用上面列出的方法,您可以将消息发送到一个或多个客户端。

例如,在 on(open:)on(text:) 中发送给一个客户端

client.send(...)

发送给所有客户端

client.broadcast.send(...)
client.broadcast.exclude(client).send(...) // excluding himself
req.ws(.mywsid).broadcast.send(...)

发送给频道中的客户端

client.broadcast.channels("news", "updates").send(...)
req.ws(.mywsid).broadcast.channels("news", "updates").send(...)

发送给自定义过滤的客户端

例如,您想查找当前用户的所有 ws 连接,以便向其所有设备发送消息

req.ws(.mywsid).broadcast.filter { client in
    req.headers[.authorization].first == client.originalRequest.headers[.authorization].first
}.send(...)

广播

您可以在 app.ws.observer(.mywsid)req.ws(.mywsid).broadcastclient.broadcast 上访问 broadcast 对象。

此对象是一个构建器,因此使用它您应该像这样过滤收件人 client.broadcast.one(...).two(...).three(...).send()

可用方法

.encoder(Encoder) // set custom data encoder
.exclude([AnyClient]) // exclude provided clients from clients
.filter((AnyClient) -> Bool) // filter clients by closure result
.channels([String]) // filter clients by provided channels
.subscribe([String]) // subscribe filtered clients to channels
.unsubscribe([String]) // unsubscribe filtered clients from channels
.disconnect() // disconnect filtered clients
.send(...) // send message to filtered clients
.count // number of filtered clients

频道

订阅

client.subscribe(to: ..., on: eventLoop) // will subscribe client to provided channels

要订阅 newsupdates,请像这样调用它 client.subscribe(to: "news", "updates")

取消订阅

client.unsubscribe(from: ..., on: eventLoop) // will unsubscribe client from provided channels

列表

client.channels // will return a list of client channels

默认值

如果您的应用程序中只有一个观察者,您可以将其设置为默认值。它将使您能够使用它而无需一直提供其 WSID,因此您只需调用 req.ws() 而不是 req.ws(.mywsid)

// configure.swift

app.ws.setDefault(.myBindable)

您还可以为所有观察者设置自定义编码器/解码器

// configure.swift

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .secondsSince1970
app.ws.encoder = encoder

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
app.ws.decoder = decoder

客户端

正如您在每个处理程序中可能看到的那样,您始终拥有 client 对象。此对象符合 AnyClient 协议,该协议包含内部有用的内容

变量

一致性

原始请求使您能够例如确定连接的用户

let user = try client.originalRequest.requireAuthenticated(User.self)

如何实现自动 ping?

请查看 此 Gist 示例

如何从 iOS、macOS 等连接?

您可以从 iOS13 开始使用纯 URLSession websockets 功能,或者例如您可以使用我的 CodyFire lib 或经典的 Starscream lib

如何从 Android 连接?

使用任何支持纯 websockets 协议的 lib,例如不是 SocketIO,因为它使用自己的协议。

示例

遗憾的是,目前还没有 Vapor 4 的示例。

联系方式

请随时在 Vapor 的 Discord 中联系我,我的昵称是 iMike#3049

贡献

欢迎大家贡献!