美文网首页码农的世界
Ktor 从入门到放弃(六) WebSockets

Ktor 从入门到放弃(六) WebSockets

作者: 何晓杰Dev | 来源:发表于2019-03-23 12:49 被阅读103次

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

    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<UserSession, String>()
        private val members = ConcurrentHashMap<UserSession, MutableList<WebSocketSession>>()
        private val lastMessages = LinkedList<String>()
    }
    

    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<WebSocketSession>() }
        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

        本文链接:https://www.haomeiwen.com/subject/iqnjvqtx.html