Ktor 从入门到放弃(六) WebSockets

由于一些众所不知的原因,最近很忙,原本说好的这篇居然延了一周。另外,我从头对过去这一系列文章进行了复盘,采纳了一些意见并做了一些勘误。好了,下面进入正文。

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

Ktor 中,可以很方便的完成 WebSocket 的操作,需要的只是一点点代码,文本将带你一步步实现一个在线的聊天室。

首先,我们在服务器应用中加入 WebSocket 的支持,简单的 Gradle 引用即可:

compile "io.ktor:ktor-websockets:$ktor_version"

与之前所讲述的 FreeMarkar 或 Session 一样,WebSocket 也是使用插件形式安装的:

fun Application.main() {
    install(WebSockets) {
        pingPeriod = Duration.ofMinutes(1)
    }
}

此时我们的服务器就支持 WebSocket 了,可以进一步编写代码,现在来设计一个服务器,对于聊天室来说,只有几个简单的点,如用户加入,用户退出,收发消息,修改昵称,服务器端广播等,下面一个个来实现。

首先,做一些准备,在服务器类中加入一些必要的管理对象:

class ChatServer {
    private val memberNames = ConcurrentHashMap()
    private val members = ConcurrentHashMap>()
    private val lastMessages = LinkedList()
}

members 用于管理用户的连接会话,用于向指定用户发送消息。memberNames 用于管理用户在聊天室的昵称,lastMessages 用于向每个用户同步最新的消息。

此处使用 ConcurrentHashMap,是因为 HashMap 并非线程安全,而 HashTable 效率低下,然而在协程内往往会有非常激烈的线程竞争,因此在此处选用 ConcurrentHashMap 来解决问题。

接着完成用户的加入与离开,其实就是 Session 的加入与离开:

suspend fun memberJoin(member: UserSession, socket: WebSocketSession) {
    val name = memberNames.computeIfAbsent(member) { member.nickname }
    val list = members.computeIfAbsent(member) { CopyOnWriteArrayList() }
    list.add(socket)
    if (list.size == 1) {
        serverBroadcast("Member joined: $name.")
    }
    val messages = synchronized(lastMessages) { lastMessages.toList() }
    for (message in messages) {
        socket.send(Frame.Text(message))
    }
}

suspend fun memberLeft(member: UserSession, socket: WebSocketSession) {
    val connections = members[member]
    connections?.remove(socket)
    if (connections != null && connections.isEmpty()) {
        val name = memberNames.remove(member) ?: member
        serverBroadcast("Member left: $name.")
    }
}

然后就是消息的收发,对于服务器来说,其实是一个消息的中转站,它接收消息并且转发给相应的用户:

suspend fun receivedMessage(id: UserSession, command: String) {
        server.message(id, command)
}

suspend fun message(sender: UserSession, message: String) {
    val name = memberNames[sender] ?: sender.nickname
    val formatted = "[$name] $message"
    broadcast(sender.chatroomId, formatted)
    synchronized(lastMessages) {
        lastMessages.add(formatted)
        if (lastMessages.size > 100) {
            lastMessages.removeFirst()
        }
    }
}

此处有一个 chatroomId 的设定,是因为聊天室可能有很多个,而我们的消息却不可能永远处于全员广播的状态,有必要按聊天室来拆分具体的请求。此处我们将 chatroomId 放在 Session 里,然后可以方便的过滤与消息发送:

suspend fun broadcast(roomId: String, message: String) =
    members.filter { it.key.chatroomId == roomId }.values.forEach {
        it.send(Frame.Text(message))
    }

到此,聊天服务器类已经写好了,是不是很简单?接下去只要让 Ktor 服务器能够响应 WebSocket 请求:

private val server = ChatServer()
fun Application.main() {
    install(WebSockets) {
        pingPeriod = Duration.ofMinutes(1)
    }
    routing {
        webSocket("/ws") {
            val ses = session
            if (ses == null) {
                close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "No session"))
                return@webSocket
            }
            server.memberJoin(ses, this)
            try {
                incoming.consumeEach {
                    if (it is Frame.Text) {
                        server.receivedMessage(ses, it.readText())
                    }
                }
            } finally {
                server.memberLeft(ses, this)
            }
        }

    }
}

好了,到此为止,我们在服务器端的准备已经全部做好了。下面写一个简单的页面来完成消息的收发。


对于前端页面而言,其实并不关心后端是 Ktor 或者是别的,只需要后端支持的协议是标准的 WebSocket 即可,所以对于前端来说,基础代码几乎是固定的:

var socket = null;

function connect() {
    socket = new WebSocket("ws://" + window.location.host + "/ws");
    socket.onclose = function(e) {
        setTimeout(connect, 5000);
    };
    socket.onmessage = function(e) {
        received(e.data.toString());
    };
}

function received(message) {
    // TODO: received message
}

其实就是那么简单的,通过 javascript 代码来建立一个 WebSocket 连接即可。

来个动图可以更好的看到效果:

效果图

好了,本篇又要结束了,似乎这篇讲的东西还是比较多的,代码片段或许不怎么容易理解,我特地写了一个 demo 程序供各位参考,点击访问我的 Github


下一篇预告:《Ktor 从入门到放弃(七) 部署到生产环境》

你可能感兴趣的:(Ktor 从入门到放弃(六) WebSockets)