Guides: How to implement a chat with WebSockets

预计阅读时间:4分钟

在本教程中,您将学习如何使用Ktor制作聊天应用程序. 我们将使用WebSockets进行实时双向通信.

为此,我们将使用RoutingWebSocketsSessions功能.

这是一个高级教程,它假定您具有有关Ktor的一些基本知识,因此您应该首先遵循有关制作网站指南 .

目录:

Setting up the project

第一步是建立一个项目. 您可以按照快速入门指南进行操作,也可以使用以下表单创建表单:

the pre-configured generator form

Understanding WebSockets

WebSockets是HTTP的子协议. 它以带有升级请求标头的普通HTTP请求开始,并且连接切换为双向通信,而不是请求响应.

可以作为WebSocket协议的一部分发送的最小传输单位是Frame . WebSocket框架定义了可能是二进制或文本的类型,长度和有效负载. 在内部,这些帧可能在几个TCP数据包中透明发送.

您可以将框架视为WebSocket消息. 框架可以是以下类型:文本,二进制,关闭,ping和pong.

通常,您将处理TextBinary帧,而在大多数情况下,另一个将由Ktor处理(尽管您可以使用原始模式,在其中您可以自己处理这些额外的帧类型).

在其页面上,您可以阅读有关WebSockets功能的更多信息.

WebSocket route

第一步是为WebSocket创建路由. 在这种情况下,我们将定义/chat路由,但是首先,我们将使该路由充当" echo" WebSocket路由,该路由将向您发送与发送给它相同的文本消息.

webSocket路由旨在长期存在. 由于它是一个暂停块,并使用轻量级的Kotlin协程,因此很好,您可以一次处理数十万个连接(取决于机器和复杂性),同时保持代码易于读取和编写.

routing {
    webSocket("/chat") { // this: DefaultWebSocketSession
        while (true) {
            val frame = incoming.receive() // suspend
            when (frame) {
                is Frame.Text -> {
                    val text = frame.readText()
                    outgoing.send(Frame.Text(text)) // suspend
                }
            }
        }
    }
}

Keeping a set of opened connections

我们可以使用Set保留打开的连接列表. 我们可以使用简单的try...finally跟踪它们. 由于默认情况下Ktor是多线程的,因此我们应该使用线程安全的集合或将主体限制为具有newSingleThreadContext的单个线程 .

routing {
    val wsConnections = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
    
    webSocket("/chat") { // this: DefaultWebSocketSession
        wsConnections += this
        try {
            while (true) {
                val frame = incoming.receive()
                // ...
            }
        } finally {
            wsConnections -= this
        }
    }
}

Propagating a message among all the connections

现在我们有了一组连接,我们可以遍历它们并使用会话发送所需的帧. 每次用户发送消息时,我们都将传播到所有连接的客户端.

routing {
    val wsConnections = Collections.synchronizedSet(LinkedHashSet<DefaultWebSocketSession>())
    
    webSocket("/chat") { // this: DefaultWebSocketSession
        wsConnections += this
        try {
            while (true) {
                val frame = incoming.receive()
                when (frame) {
                    is Frame.Text -> {
                        val text = frame.readText()
                        // Iterate over all the connections
                        for (conn in wsConnections) {
                            conn.outgoing.send(Frame.Text(text))
                        }
                    }
                }
            }
        } finally {
            wsConnections -= this
        }
    }
}

Assigning names to users/connections

我们可能希望将一些信息(例如名称与关联的连接)相关联,我们可以创建一个包含WebSocketSession的对象并将其存储,如下所示:

class ChatClient(val session: DefaultWebSocketSession) {
    companion object { var lastId = AtomicInteger(0) }
    val id = lastId.getAndIncrement()
    val name = "user$id"
}

routing {
    val clients = Collections.synchronizedSet(LinkedHashSet<ChatClient>())
    
    webSocket("/chat") { // this: DefaultWebSocketSession
        val client = ChatClient(this)
        clients += client
        try {
            while (true) {
                val frame = incoming.receive()
                when (frame) {
                    is Frame.Text -> {
                        val text = frame.readText()
                        // Iterate over all the connections
                        val textToSend = "${client.name} said: $text"
                        for (other in clients.toList()) {
                            other.session.outgoing.send(Frame.Text(textToSend))
                        }
                    }
                }
            }
        } finally {
            clients -= client
        }
    }
}

Exercises

Creating a client

创建一个连接到该端点的JavaScript客户端,并将其与ktor一起使用.

JSON

使用kotlinx.serialization发送和接收VO

by  ICOPY.SITE