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<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 從入門到放棄(七) 部署到生產環境》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容