WebSockets

WebSocket是建立在一个单独的TCP连接基础之上的全双工通讯协议,目前广泛被应用与各类浏览器和Web服务器。WebSocket协议使得浏览器和服务器之间的实时通信和通信连接维持成为可能。通信过程在TCP端口上完成,比如80端口或443端口。

WebSocket协议目前支持大多数主流浏览器,包括谷歌Chrome、微软Edge和IE、火狐、苹果Safari,以及Opera。WebSocket的实现需要网页程序和后台服务器的同时支持。

准备开始

请修改您的 Package.swift 文件并增加以下依存关系:

.Package(url:"https://github.com/PerfectlySoft/Perfect-WebSockets.git", majorVersion: 3)

保存好后就可以在您的源程序内使用WebSocket函数库了:

import PerfectWebSockets

最简单的应用可以在网页中实现,比如聊天室 —— 用户一旦输入了一些内容,其他在同一个网页上的人都能够同步看到这个结果。

以下代码展示了如何在路由/echo上增加WebSocket服务具柄:

var routes = Routes()

routes.add(method: .get, uri: "/echo", handler: {
        request, response in
    // 具柄创建后,您需要为具柄提供一个闭包便于回调:
    WebSocketHandler(handlerProducer: {
        (request: WebRequest, protocols: [String]) -> WebSocketSessionHandler? in

        // 检查客户端是否在请求我们的“echo“服务。
        guard protocols.contains("echo") else {
            return nil
        }

        // 返回真正的服务具柄
        return EchoHandler()
    }).handleRequest(request: request, response: response)
    }
)

处理 WebSocket 会话

WebSocket服务具柄必须要实现WebSocketSessionHandler会话控制协议。该协议需要一个函数用于处理请求和socket会话:

该函数会在WebSocket连接时调用,随后您就可以开始读写信息了。最开始的 WebRequest 对象用于控制会话,而消息通过WebSocket对象进行传输。

首先WebRequest对象用于启动会话。

消息则通过 WebSocket 对象返回。

调用 WebSocket.sendStringMessageWebSocket.sendBinaryMessage 给客户端发数据。 调用 WebSocket.readStringMessageWebSocket.readBinaryMessage 则用于接收从客户端发来的数据。

默认情况下,读操作会造成程序阻塞直到有消息到达或者网络错误发生。

设置读操作超时的方法是设置WebSocket.readTimeoutSeconds属性。

当会话结束则调用WebSocket.close()方法关闭连接。

请参考以下例子——在WebSocketSessionHandler会话控制具柄上扩展出的“回声”实例——运行这个例子,客户端socket发出的内容就会被原封不动地返回。

class EchoHandler: WebSocketSessionHandler {

    // 超级协议名称,我们自己定义的
    // 虽然是可选项,但是应该与客户端WebSocket创建的协议名称完全一致。
    let socketProtocol: String? = "echo"

    // 该函数在 WebSocketHandler 一旦建立连接后就会被调用。
    func handleSession(request: WebRequest, socket: WebSocket) {

        // 从客户端读取文字消息
        // 如果要读取二进制数据则用 `WebSocket.readBytesMessage` 方法
        socket.readStringMessage {
            // 回调函数的参数包括:
            //  string: 接收到的数据
            //  op:该消息的操作代码
            //  fin:数据已经读取完毕,如果是真就是读完了。
            //  (反之意味着数据只是被部分读取)
            string, op, fin in

            // 如果接收到的数据为空,那么有可能是超时,
            // 也有可能是因为网络异常,比如客户端掉线等等
            // 默认情况下是不会超时的。
            guard let string = string else {
                // 如果客户端窗口关闭这类的事件发生了,那么就应该在服务器端进行相应的关闭操作。
                socket.close()
                return
            }

            // 输出读取到的内容。
            print("已经获取了信息:\(string) \t操作码:\(op) \t是否全部读取:\(fin)")

            // 将收到的数据原封不动地返回给客户
            // 这里简单起见直接将final设置为真,意味着一次性把数据发完。
            // 但是WebSockets还可以选择一次只发送一个数据片段
            // 比如,如果传输一个大文件,比如视频文件,
            // 那么需要把数据分段发出,分段过程中final值为假。
            // 表明后续数据需要继续发出
            // 而后续内容将与已经发出的数据构成一个逻辑上的完整消息。
            // 如果是发送视频,那么必须是在最后一段数据发完时用上将这个标志变量设置为真。
            socket.sendStringMessage(string, final: true) {

                // 该回调函数会在消息发送完成后调用
                // 递归调用,继续下一个读取操作 —— 一直读下去
                self.handleSession(request, socket: socket)
            }
        }
    }
}

FastCGI 使用注意

WebSockets 目前支持独立运行的 Perfect HTTP 服务器,暂时无法支持 Perfect FastCGI 连接的集成

WebSocket 类

enum OpcodeType 操作代码

WebSocket 消息可以有下列几种类型:continuation(连续)、text(文本)、binary(二进制)、close(关闭)、ping or pong(乒乓测试),或者invalid(无效的类型)。

var readTimeoutSeconds 超时控制

如果要从当前socket 进行读取数据的操作,那么设置这个属性作为读操作的时间限制。如果属性被设置为NetEvent.noTimeout (-1),则程序会被阻塞并一直等待,不会发生超时的事件。

read message 读取消息

WebSocket有两种消息格式:文本消息或二进制消息。文本消息数据形式为字符串,而二进制消息数据格式为字节数组[UInt8]。

读取文本消息

public func readStringMessage(continuation: @escaping (String?, _ opcode: OpcodeType, _ final: Bool) -> ())

读取二进制消息

public func readBytesMessage(continuation: @escaping ([UInt8]?, _ opcode: OpcodeType, _ final: Bool) -> ())

上述两个方法的参数包括:

String / [UInt8]

收到的数据,文本或者二进制数组

opcode

操作代码。您可以参考这个代码用于更多更复杂的通信控制。

final

读取完成。该参数如果是真,表示数据已经全部收到,假表示部分收到(后面还有数据要发过来)。

send message 发送消息

同样也有两种方式发送消息:文本消息和二进制消息,区别就在于数据类型,分别是字符串和二进制数组。

发送文本消息

public func sendStringMessage(string: String, final: Bool, completion: @escaping () -> ())

发送二进制消息

public func sendBinaryMessage(bytes: [UInt8], final: Bool, completion: @escaping () -> ())

最后一个final参数用于表明该消息是全部还是部分,如果final为假则表示逻辑上还有后续消息会发给客户端,与之前已经发出的消息构成了一个逻辑整体,内容不可分割。

ping & pong 乒乓测试

Perfect WebSocket 函数库还提供了测试连接的简便函数,即乒乓函数。乒用于发起测试,乓则是在收到乒后返回一个应答。

下面是两个函数的定义:

/// 向客户发送乓消息:
public func sendPong(completion: @escaping () -> ())

/// 向客户发送乒消息
/// 并等待乓返回
public func sendPing(completion: @escaping () -> ())

func close() 关闭连接

所有读写操作完毕并不希望继续保持数据连接的情况下,请关闭WebSocket连接。

socket.close()

详细的WebSockets例子,请参见 Perfect-WebSocketsServer