Golang 實現 IM 服務之羣聊廣播

【導讀】本文詳細介紹了 go 語言實現羣聊、廣播服務的實戰。

其實從上學開始就一直想寫一個 im。最近深入 go,真是學會了太多,感覺人森雖然苦短,但是也不能只用 python。很多知識是不用編譯型語言無法瞭解的。

該來的還是會來,現在會一步一步用 go 把這個服務器完善起來 先從這個 demo 開始。

這個 demo 我們要求所有連上服務器的用戶都會知道有用戶的離開,有用戶的加入 (除了第一個加入的用戶),每個人說話就像聊天室一樣,房間裏的所有人都能看到。

由於接收 tcp 請求,get accept 的 conn 步驟都差不多所以先上 main 部分的代碼:

func main() {
    listener, err := net.Listen("tcp""0.0.0.0:8888")
    if err != nil {
        log.Fatal(err)
    }

    go broadcaster()

    for{
        conn, err := listener.Accept()
        if err != nil {
            fmt.Fprintf(os.Stdout, "you got something wrong %v", err)
            continue
        }
        go handleConn(conn)
    }
}

使用 net 包裏面提供的 Listen 監聽 tcp 來自 8888 端口的數據。並獲得一個 listener 對象。

發起一個 goroutine 用於消息廣播使用

然後進入監聽循環,使用 listener 對象提供的 Accept 方法來獲取連接。

每獲得一個連接就重新啓一個 goroutine 去 handle 這個鏈接。

main 裏面寫的代碼非常簡單,其實服務器要做的事情總結一下無非就是獲得 listener 對象,然後不停的獲取獲取鏈接上來的 conn 對象,然後把這些對象丟給處理鏈接函數去進行處理。在使用 handleConn 方法處理 conn 對象的時候,我們同樣對不同的鏈接都啓一個 goroutine 去併發處理每個 conn 這樣則無需等待。

用於要給在線的所有用戶發送消息,而不同的用戶的 conn 對象都在不同的 goroutine 裏面,我們很容易想到使用隊列這種東西來做消息的傳遞,但是 golang 裏面有 channel 來處理各不同 goroutine 之間的消息傳遞,所以在這個 demo 我選擇使用 channel 在各不同的 goroutine 中傳遞廣播消息。

先申明一些要用到的 channel

type client chan<- string  // send only channel

var (
    entering = make(chan client)
    leaving = make(chan client)
    messages = make(chan string)
)

這裏要注意一點的是,重新定義了一個 client 類型,他是一個單向 chennel,只能往裏面寫消息。

下面申請的 entering 和 leaving 都是 client 類型的 channel。

什麼意思呢?

就是說下面的 entering 和 leaving 都是裝 channel 的 channel。這裏有點繞要注意,裝 channel 的 channel 在 <- 的時候,會直接將 channel 對象裝進去。

這裏拓展開說說這個問題,以免下面的代碼難以理解,來看一個例子:

package main

import (
    "fmt"
    "time"
)

type client chan string
var entering = make(chan client)

func main() {
    ch := make(chan string)
    go func() {ch <- "那你很棒棒哦😯 "}()
    go func() {entering <- ch}()
    o := <-entering

    time.Sleep(2 * time.Second)
    fmt.Println(<-o)
}

這裏我們創建了一個 client 類型,他是一個 string 類型的 channel。

同時申明一個 entering,他是一個 client 類型的 channel。這裏也可以寫成 make(chan chan string) 但是寫成 client 更方便清晰有木有。

在執行 entering <- ch 的時候,並不是把 ch 裏面裝的 string 內容吐出去了,而是把自己裝進了 entering。

後面寫的都是在驗證這一行爲就不繼續贅述了。

繼續回來說 broadcaster 函數:

func broadcaster() {
    clients := make(map[client]bool) //all connected clients
    for {
        select {
        case msg := <- messages:
            // Broadcast incoming message to all
            // clients' outgoing message channels.
            for cli := range clients{
                cli <- msg
            }
        case cli := <- entering:
            clients[cli] = true
        case cli := <- leaving:
            delete(clients, cli)
            close(cli)
        }
    }
}

我們在 main 裏面使用 goroutine 開啓了一個 broadcaster 函數來負責廣播所有用戶發送的消息。

這裏使用一個字典來保存用戶 clients,字典的 key 是各連接申明的單向發隊列。

使用一個 select 開啓一個多路複用:

每當有廣播消息從 messages 發送進來,都會循環 cliens 對裏面的每個 channel 發消息。

每當有消息從 entering 裏面發送過來,就生成一個新的 key-value。相當於給 clients 裏面增加一個新的 client。

每當有消息從 leaving 裏面發送過來,就刪掉這個 key-value 對,並關閉對應的 channel。

最後我們來看 handleConn 函數里的邏輯:

func broadcaster() {
    clients := make(map[client]bool) //all connected clients
    for {
        select {
        case msg := <- messages:
            // Broadcast incoming message to all
            // clients' outgoing message channels.
            for cli := range clients{
                cli <- msg
            }
        case cli := <- entering:
            clients[cli] = true
        case cli := <- leaving:
            delete(clients, cli)
            close(cli)
        }
    }
}

爲每個過來處理的 conn 都創建一個新的 channel,開啓一個新的 goroutine 去把發送給這個 channel 的消息寫進 conn。

獲取連接過來的 ip 地址和端口號。

先把歡迎信息寫進 channel 返回給客戶端。

然後生成一條廣播消息寫進 messages 裏。

然後把這個 channel 加入到客戶端集合 也就是 entering <- ch

然後開始監聽客戶端往 conn 裏寫的數據,每掃描到一條就將這條消息發送到廣播 channel 中

如果客戶端關閉了標準輸入,那麼把隊列離開寫入 leaving 交給廣播函數去刪除這個客戶端並關閉這個客戶端。

廣播這個人的離開給所有人。

最後關閉這個客戶端的連接 Conn.Close()。

最後上 clientWriter 的代碼:

func clientWriter(conn net.Conn, ch <-chan string) {
    for msg := range ch {
        fmt.Fprintln(conn, msg) // NOTE: ignoring network errors
    }
}

沒什麼好說的,就是把每個發送過來的消息都寫入到 conn 中,沒有消息發過來的時候就阻塞。

其實看似簡單餓服務器做了一些細節上的處理,因爲 golang 中字典並不是併發安全的,所以只有一個 gonroutine 單獨幹這件事情,保證了其併發情況也安全。

關於併發安全這個話題,可以寫 n 篇文章來闡述其細節也不爲過,以後可能會有更多機會介紹到。

這麼看其實邏輯已經非常清楚了,後續我還會往這個服務器上加更多的功能,包括讓客戶端寫入自己的名字來替代現在用 ip 地址標記遠端連接的情況。

轉自:

cnblogs.com/piperck/p/6480198.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/prJU2Ie3mwD-8yV5hmZ_ZA